0.37.0: download whole folders (recursive) as a .zip from the file browser

The file browser/editor could only download individual files. Add a
recursive directory download that streams the folder as a zip archive,
on the local host and on every remote agent.

- file_service.archive_dir(): zip a directory recursively into a temp
  file, preserving the folder name as the archive root and empty
  subdirectories; symlinks are skipped (no sandbox escape / loops).
- /api/files/download and /agent/files/download branch on directories
  and return application/zip, cleaning up the temp file afterwards.
- Files page: show the download button for folders too (as <name>.zip).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-21 19:54:50 +00:00
co-authored by Claude Opus 4.8
parent 5bcec06bbd
commit f1782eca0e
6 changed files with 67 additions and 12 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "stackpilot-frontend",
"private": true,
"version": "0.36.1",
"version": "0.37.0",
"type": "module",
"scripts": {
"dev": "vite",
+8 -10
View File
@@ -202,7 +202,7 @@ export function Files() {
const download = (e: HostPathEntry) =>
filesApi
.download(join(path, e.name), e.name, host)
.download(join(path, e.name), e.type === "dir" ? `${e.name}.zip` : e.name, host)
.catch((err) => toast.error(apiErrorMessage(err)));
return (
@@ -407,15 +407,13 @@ export function Files() {
</td>
<td className="px-4 py-2">
<div className="flex items-center justify-end gap-1">
{e.type === "file" && (
<button
title="Download"
onClick={() => download(e)}
className="rounded p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700"
>
<Download className="h-4 w-4 text-slate-500" />
</button>
)}
<button
title={e.type === "dir" ? "Download as .zip" : "Download"}
onClick={() => download(e)}
className="rounded p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700"
>
<Download className="h-4 w-4 text-slate-500" />
</button>
{isAdmin && (
<>
<button