This commit is contained in:
Timothy Jaeryang Baek
2026-03-24 05:34:36 -05:00
parent 24370d5c40
commit 3841e85abb
4 changed files with 112 additions and 50 deletions
+43
View File
@@ -120,6 +120,49 @@ export const downloadFileBlob = async (
return { blob, filename }; return { blob, filename };
}; };
export const downloadDirectoryArchive = async (
baseUrl: string,
apiKey: string,
path: string
): Promise<{ blob: Blob; filename: string } | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/download?path=${encodeURIComponent(path)}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
}).catch(() => null);
if (!res || !res.ok) return null;
const disposition = res.headers.get('content-disposition') ?? '';
const match = disposition.match(/filename="?([^"]+)"?/);
const filename = match?.[1] ?? `${path.split('/').filter(Boolean).pop() ?? 'download'}.zip`;
const blob = await res.blob();
return { blob, filename };
};
export const downloadMultipleAsZip = async (
baseUrl: string,
apiKey: string,
paths: string[]
): Promise<{ blob: Blob; filename: string } | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/download`;
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ paths })
}).catch(() => null);
if (!res || !res.ok) return null;
const disposition = res.headers.get('content-disposition') ?? '';
const match = disposition.match(/filename="?([^"]+)"?/);
const filename = match?.[1] ?? 'download.zip';
const blob = await res.blob();
return { blob, filename };
};
export const uploadToTerminal = async ( export const uploadToTerminal = async (
baseUrl: string, baseUrl: string,
apiKey: string, apiKey: string,
+24 -4
View File
@@ -19,6 +19,8 @@
listFiles, listFiles,
readFile, readFile,
downloadFileBlob, downloadFileBlob,
downloadDirectoryArchive,
downloadMultipleAsZip,
uploadToTerminal, uploadToTerminal,
createDirectory, createDirectory,
deleteEntry, deleteEntry,
@@ -403,7 +405,11 @@
const terminal = selectedTerminal; const terminal = selectedTerminal;
if (!terminal) return; if (!terminal) return;
const result = await downloadFileBlob(terminal.url, terminal.key, path); // Directories end with '/' — download as ZIP archive
const isDir = path.endsWith('/');
const result = isDir
? await downloadDirectoryArchive(terminal.url, terminal.key, path.replace(/\/$/, ''))
: await downloadFileBlob(terminal.url, terminal.key, path);
if (!result) return; if (!result) return;
const url = URL.createObjectURL(result.blob); const url = URL.createObjectURL(result.blob);
const a = document.createElement('a'); const a = document.createElement('a');
@@ -644,10 +650,24 @@
const terminal = selectedTerminal; const terminal = selectedTerminal;
if (!terminal) return; if (!terminal) return;
const filePaths = [...selectedEntries].filter((p) => !p.endsWith('/')); const paths = [...selectedEntries].map((p) => p.replace(/\/$/, ''));
for (const p of filePaths) { if (paths.length === 0) return;
await downloadFile(p);
// Single item — use the regular downloadFile path
if (paths.length === 1) {
await downloadFile([...selectedEntries][0]);
return;
} }
// Multiple items — bundle into a single ZIP
const result = await downloadMultipleAsZip(terminal.url, terminal.key, paths);
if (!result) return;
const url = URL.createObjectURL(result.blob);
const a = document.createElement('a');
a.href = url;
a.download = result.filename;
a.click();
URL.revokeObjectURL(url);
}; };
// Escape to clear selection // Escape to clear selection
@@ -40,29 +40,27 @@
</button> </button>
</Tooltip> </Tooltip>
{#if hasFiles} <Tooltip content={$i18n.t('Download')}>
<Tooltip content={$i18n.t('Download')}> <button
<button class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400"
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400" on:click={onDownload}
on:click={onDownload} aria-label={$i18n.t('Download')}
aria-label={$i18n.t('Download')} >
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5"
> >
<svg <path
xmlns="http://www.w3.org/2000/svg" d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
viewBox="0 0 20 20" />
fill="currentColor" <path
class="size-3.5" d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
> />
<path </svg>
d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z" </button>
/> </Tooltip>
<path
d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
/>
</svg>
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Delete')}> <Tooltip content={$i18n.t('Delete')}>
<button <button
@@ -287,31 +287,32 @@
<div <div
class="min-w-[150px] rounded-2xl p-1 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800" class="min-w-[150px] rounded-2xl p-1 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800"
> >
{#if entry.type !== 'directory'} <button
<button type="button"
type="button" class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm"
class="select-none flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2 text-sm" on:click={(e) => {
on:click={(e) => { e.stopPropagation();
e.stopPropagation(); const path = entry.type === 'directory'
onDownload(`${currentPath}${entry.name}`); ? `${currentPath}${entry.name}/`
}} : `${currentPath}${entry.name}`;
onDownload(path);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-4"
> >
<svg <path
xmlns="http://www.w3.org/2000/svg" d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z"
viewBox="0 0 20 20" />
fill="currentColor" <path
class="size-4" d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
> />
<path </svg>
d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z" <div class="flex items-center">{$i18n.t('Download')}</div>
/> </button>
<path
d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z"
/>
</svg>
<div class="flex items-center">{$i18n.t('Download')}</div>
</button>
{/if}
<button <button
type="button" type="button"