Public File Sharing (#72)

* Public File Sharing

This implements public file sharing (read-only) with and without passwords (#57).

It also fixes a problem with filenames including special characters like `#` not working properly (#71).

You can share a directory or a single file, by using the new share icon on the right of the directories/files, and click on it to manage an existing file share (setting a new password, or deleting the file share).

There is some other minor cleanup and other copy updates in the README.

Closes #57
Fixes #71

* Hide UI elements when sharing isn't allowed
This commit is contained in:
Bruno Bernardino
2025-06-20 12:04:16 +01:00
committed by GitHub
parent c7d6b8077b
commit 7fac7febcf
29 changed files with 1541 additions and 155 deletions

View File

@@ -1,3 +1,5 @@
import { join } from 'std/path/join.ts';
import { Directory, DirectoryFile } from '/lib/types.ts';
import { humanFileSize, TRASH_PATH } from '/lib/utils/files.ts';
@@ -14,8 +16,11 @@ interface ListFilesProps {
onClickOpenMoveFile?: (parentPath: string, name: string) => void;
onClickDeleteDirectory?: (parentPath: string, name: string) => Promise<void>;
onClickDeleteFile?: (parentPath: string, name: string) => Promise<void>;
onClickCreateShare?: (filePath: string) => void;
onClickOpenManageShare?: (fileShareId: string) => void;
isShowingNotes?: boolean;
isShowingPhotos?: boolean;
fileShareId?: string;
}
export default function ListFiles(
@@ -32,8 +37,11 @@ export default function ListFiles(
onClickOpenMoveFile,
onClickDeleteDirectory,
onClickDeleteFile,
onClickCreateShare,
onClickOpenManageShare,
isShowingNotes,
isShowingPhotos,
fileShareId,
}: ListFilesProps,
) {
const dateFormat = new Intl.DateTimeFormat('en-GB', {
@@ -45,7 +53,7 @@ export default function ListFiles(
minute: '2-digit',
});
let routePath = 'files';
let routePath = fileShareId ? `file-share/${fileShareId}` : 'files';
let itemSingleLabel = 'file';
let itemPluralLabel = 'files';
@@ -81,7 +89,8 @@ export default function ListFiles(
<thead>
<tr class='border-b border-slate-600'>
{(directories.length === 0 && files.length === 0) ||
(typeof onClickChooseFile === 'undefined' && typeof onClickChooseDirectory === 'undefined')
(typeof onClickChooseFile === 'undefined' && typeof onClickChooseDirectory === 'undefined') ||
fileShareId
? null
: (
<th scope='col' class='pl-6 pr-2 font-medium text-white w-3'>
@@ -98,7 +107,9 @@ export default function ListFiles(
{isShowingNotes || isShowingPhotos
? null
: <th scope='col' class='px-6 py-4 font-medium text-white w-32'>Size</th>}
{isShowingPhotos ? null : <th scope='col' class='px-6 py-4 font-medium text-white w-20'></th>}
{isShowingPhotos || fileShareId
? null
: <th scope='col' class='px-6 py-4 font-medium text-white w-24'></th>}
</tr>
</thead>
<tbody class='divide-y divide-slate-600 border-t border-slate-600'>
@@ -107,7 +118,7 @@ export default function ListFiles(
return (
<tr class='bg-slate-700 hover:bg-slate-600 group'>
{typeof onClickChooseDirectory === 'undefined' ? null : (
{typeof onClickChooseDirectory === 'undefined' || fileShareId ? null : (
<td class='gap-3 pl-6 pr-2 py-4'>
{fullPath === TRASH_PATH ? null : (
<input
@@ -124,7 +135,7 @@ export default function ListFiles(
)}
<td class='flex gap-3 px-6 py-4'>
<a
href={`/${routePath}?path=${fullPath}`}
href={`/${routePath}?path=${encodeURIComponent(fullPath)}`}
class='flex items-center font-normal text-white'
>
<img
@@ -146,13 +157,13 @@ export default function ListFiles(
-
</td>
)}
{isShowingPhotos ? null : (
{isShowingPhotos || fileShareId ? null : (
<td class='px-6 py-4'>
{(fullPath === TRASH_PATH || typeof onClickOpenRenameDirectory === 'undefined' ||
typeof onClickOpenMoveDirectory === 'undefined')
? null
: (
<section class='flex items-center justify-end w-20'>
<section class='flex items-center justify-end w-24'>
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickOpenRenameDirectory(directory.parent_path, directory.directory_name)}
@@ -168,7 +179,8 @@ export default function ListFiles(
</span>
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickOpenMoveDirectory(directory.parent_path, directory.directory_name)}
onClick={() =>
onClickOpenMoveDirectory(directory.parent_path, directory.directory_name)}
>
<img
src='/images/move.svg'
@@ -181,7 +193,7 @@ export default function ListFiles(
</span>
{typeof onClickDeleteDirectory === 'undefined' ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickDeleteDirectory(directory.parent_path, directory.directory_name)}
>
<img
@@ -194,6 +206,36 @@ export default function ListFiles(
/>
</span>
)}
{typeof onClickCreateShare === 'undefined' || directory.file_share_id ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickCreateShare(join(directory.parent_path, directory.directory_name))}
>
<img
src='/images/share.svg'
class='white drop-shadow-md'
width={18}
height={18}
alt='Create public share link'
title='Create public share link'
/>
</span>
)}
{typeof onClickOpenManageShare === 'undefined' || !directory.file_share_id ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickOpenManageShare(directory.file_share_id!)}
>
<img
src='/images/share.svg'
class='white drop-shadow-md'
width={18}
height={18}
alt='Manage public share link'
title='Manage public share link'
/>
</span>
)}
</section>
)}
</td>
@@ -203,7 +245,7 @@ export default function ListFiles(
})}
{files.map((file) => (
<tr class='bg-slate-700 hover:bg-slate-600 group'>
{typeof onClickChooseFile === 'undefined' ? null : (
{typeof onClickChooseFile === 'undefined' || fileShareId ? null : (
<td class='gap-3 pl-6 pr-2 py-4'>
<input
class='w-3 h-3 cursor-pointer text-[#51A4FB] bg-slate-100 border-slate-300 rounded dark:bg-slate-700 dark:border-slate-600'
@@ -219,7 +261,9 @@ export default function ListFiles(
)}
<td class='flex gap-3 px-6 py-4'>
<a
href={`/${routePath}/open/${file.file_name}?path=${file.parent_path}`}
href={`/${routePath}/open/${encodeURIComponent(file.file_name)}?path=${
encodeURIComponent(file.parent_path)
}`}
class='flex items-center font-normal text-white'
target='_blank'
rel='noopener noreferrer'
@@ -243,9 +287,9 @@ export default function ListFiles(
{humanFileSize(file.size_in_bytes)}
</td>
)}
{isShowingPhotos ? null : (
{isShowingPhotos || fileShareId ? null : (
<td class='px-6 py-4'>
<section class='flex items-center justify-end w-20'>
<section class='flex items-center justify-end w-24'>
{typeof onClickOpenRenameFile === 'undefined' ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
@@ -278,7 +322,7 @@ export default function ListFiles(
)}
{typeof onClickDeleteFile === 'undefined' ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickDeleteFile(file.parent_path, file.file_name)}
>
<img
@@ -291,6 +335,36 @@ export default function ListFiles(
/>
</span>
)}
{typeof onClickCreateShare === 'undefined' || file.file_share_id ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickCreateShare(join(file.parent_path, file.file_name))}
>
<img
src='/images/share.svg'
class='white drop-shadow-md'
width={18}
height={18}
alt='Create public share link'
title='Create public share link'
/>
</span>
)}
{typeof onClickOpenManageShare === 'undefined' || !file.file_share_id ? null : (
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
onClick={() => onClickOpenManageShare(file.file_share_id!)}
>
<img
src='/images/share.svg'
class='white drop-shadow-md'
width={18}
height={18}
alt='Manage public share link'
title='Manage public share link'
/>
</span>
)}
</section>
</td>
)}