File browser: folder upload + copy/move (0.13.0)

Folder upload: the Files page gained an "Upload folder" picker
(webkitdirectory); each file is sent with its webkitRelativePath and the
backend recreates the directory tree. upload_target now accepts an optional
rel_path, creating intermediate dirs (mkdir -p) inside the sandbox with each
component validated against traversal.

Copy/move: new file_service.copy/move + POST /api/files/{copy,move}
(admin, audit-logged). The UI adds per-row copy/cut actions, a clipboard bar
to paste into the current directory, and an overwrite prompt on conflict.
Both refuse to move/copy a folder into itself or its own subtree and are
sandbox-checked on source and destination.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-08 10:53:57 +00:00
co-authored by Claude Opus 4.8
parent e3313fb4ac
commit 25bba1cf2c
8 changed files with 290 additions and 16 deletions
+12 -1
View File
@@ -44,15 +44,26 @@ export const filesApi = {
remove: (path: string, recursive = false) =>
api.delete("/api/files", { params: { path, recursive } }).then((r) => r.data),
copy: (src: string, destDir: string, overwrite = false) =>
api
.post<{ path: string }>("/api/files/copy", { src, dest_dir: destDir, overwrite })
.then((r) => r.data),
move: (src: string, destDir: string, overwrite = false) =>
api
.post<{ path: string }>("/api/files/move", { src, dest_dir: destDir, overwrite })
.then((r) => r.data),
download: async (path: string, filename: string) => {
const res = await api.get("/api/files/download", { params: { path }, responseType: "blob" });
triggerDownload(res.data as Blob, filename);
},
upload: async (path: string, file: File, overwrite = false) => {
upload: async (path: string, file: File, overwrite = false, relPath = "") => {
const form = new FormData();
form.append("path", path);
form.append("overwrite", String(overwrite));
if (relPath) form.append("rel_path", relPath);
form.append("file", file);
const res = await api.post<{ ok: boolean; name: string }>("/api/files/upload", form);
return res.data;