Support downloading directories as a zip archive (#106)
* Add directory download as zip feature Implements the ability for users to download directories as zip files if enabled in config. Adds a new API route for directory zipping, updates UI components to show a download button for directories, and introduces related config and type changes. Also includes a new download icon. * Windows path bugfix * Include empty directories in zip archive * Address feedback - `isDirectoryDownloadsAllowed` -> `areDirectoryDownloadsAllowed` - send `parentPath` & `name` to API instead of resolving `fullPath` on client - call `ensureUserPathIsValidAndSecurelyAccessible` before zipping - set config `allowDirectoryDownloads` default to `false` - add `zip` to Dockerfile and replace in-house zip algorithm - replace `download.svg` with heroicon's `arrow-down-tray` - `replace` with glob -> `replaceAll` with string * Cleanup apt-get command * Remove unused zip archive and directory functions
This commit is contained in:
@@ -18,6 +18,7 @@ interface ListFilesProps {
|
||||
onClickDeleteFile?: (parentPath: string, name: string) => Promise<void>;
|
||||
onClickCreateShare?: (filePath: string) => void;
|
||||
onClickOpenManageShare?: (fileShareId: string) => void;
|
||||
onClickDownloadDirectory?: (parentPath: string, name: string) => void;
|
||||
isShowingNotes?: boolean;
|
||||
isShowingPhotos?: boolean;
|
||||
fileShareId?: string;
|
||||
@@ -39,6 +40,7 @@ export default function ListFiles(
|
||||
onClickDeleteFile,
|
||||
onClickCreateShare,
|
||||
onClickOpenManageShare,
|
||||
onClickDownloadDirectory,
|
||||
isShowingNotes,
|
||||
isShowingPhotos,
|
||||
fileShareId,
|
||||
@@ -165,10 +167,26 @@ export default function ListFiles(
|
||||
typeof onClickOpenMoveDirectory === 'undefined')
|
||||
? null
|
||||
: (
|
||||
<section class='flex items-center justify-end w-24'>
|
||||
<section class='flex items-center justify-end w-32'>
|
||||
{typeof onClickDownloadDirectory === 'undefined' ? null : (
|
||||
<span
|
||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||
onClick={() => onClickDownloadDirectory(directory.parent_path, directory.directory_name)}
|
||||
>
|
||||
<img
|
||||
src='/images/download.svg'
|
||||
class='white drop-shadow-md'
|
||||
width={18}
|
||||
height={18}
|
||||
alt='Download directory as zip'
|
||||
title='Download directory as zip'
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||
onClick={() => onClickOpenRenameDirectory(directory.parent_path, directory.directory_name)}
|
||||
onClick={() =>
|
||||
onClickOpenRenameDirectory(directory.parent_path, directory.directory_name)}
|
||||
>
|
||||
<img
|
||||
src='/images/rename.svg'
|
||||
@@ -181,8 +199,7 @@ 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'
|
||||
|
||||
@@ -48,6 +48,7 @@ interface MainFilesProps {
|
||||
initialPath: string;
|
||||
baseUrl: string;
|
||||
isFileSharingAllowed: boolean;
|
||||
areDirectoryDownloadsAllowed: boolean;
|
||||
fileShareId?: string;
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ export default function MainFiles(
|
||||
initialPath,
|
||||
baseUrl,
|
||||
isFileSharingAllowed,
|
||||
areDirectoryDownloadsAllowed,
|
||||
fileShareId,
|
||||
}: MainFilesProps,
|
||||
) {
|
||||
@@ -411,6 +413,21 @@ export default function MainFiles(
|
||||
moveDirectoryOrFileModal.value = null;
|
||||
}
|
||||
|
||||
function onClickDownloadDirectory(parentPath: string, name: string) {
|
||||
// Create download URL with proper path encoding
|
||||
const downloadUrl = `/api/files/download-directory?parentPath=${encodeURIComponent(parentPath)}&name=${
|
||||
encodeURIComponent(name)
|
||||
}`;
|
||||
|
||||
// Create a temporary anchor element to trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = `${name}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
async function onClickDeleteDirectory(parentPath: string, name: string, isBulkDeleting = false) {
|
||||
if (isBulkDeleting || confirm('Are you sure you want to delete this directory?')) {
|
||||
if (!isBulkDeleting && isDeleting.value) {
|
||||
@@ -839,6 +856,7 @@ export default function MainFiles(
|
||||
onClickDeleteFile={onClickDeleteFile}
|
||||
onClickCreateShare={isFileSharingAllowed ? onClickCreateShare : undefined}
|
||||
onClickOpenManageShare={isFileSharingAllowed ? onClickOpenManageShare : undefined}
|
||||
onClickDownloadDirectory={areDirectoryDownloadsAllowed ? onClickDownloadDirectory : undefined}
|
||||
fileShareId={fileShareId}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user