Plan — #247 下載檔案/資料夾 (KB + workspace)¶
Download files and folders from BOTH the KB document IDE and the workspace
(investigation/app-item) file IDE, surfaced through the shared FileTree.
Locked decisions (grill-me)¶
- Scope — one "Download" action in the shared
FileTreecovering all four combos: KB file, KB folder, workspace file, workspace folder. File = direct download; folder = ZIP. - Single file — pure FE:
<a download={basename(path)} href={fileDownloadUrl(path)}>.fileDownloadUrlresolves per service (KB →/source-doc/{id}/blobs/{file_id}, workspace →/a/{slug}/items/{id}/files/{path}). Always setdownloadto the basename so KB does not save as the xxh3 hash. No backend change for single file. - Folder / whole ZIP — reuse the #101 two-step
prepare → streampattern (downloads_dir temp file +sweep_stale_downloadsreaper + FE anchor click). New endpoints for KB-folder and workspace-folder; share the zip-temp / stream / reaper plumbing with the existing collection export. - ZIP contents — raw original bytes only, no manifest (export-to-use, not backup; re-importable backup stays the #101 whole-collection export).
- ZIP paths / name — entries relative to the selected folder; zip filename =
{folder basename}.zip(root → collection name / item title). - Tree root — root also goes through the new raw-file ZIP (KB root = whole
collection raw files, coexists with #101 manifest export). Entry point = a
"Download" button in the Files toolbar header (new
caps.download). - Confirm modal — only the toolbar "Download all" (root) shows a confirm modal (with file count). Per-node folder ZIP and single files download directly.
- Filtering — folder/root ZIP skips
.readonly/(workspace agent diff snapshots) and.gitkeep(KB folder placeholders).
Defaults (vetoable)¶
- KB folder/root ZIP includes every SourceDoc under the prefix regardless of index
status (original bytes are stored at
store()time, before indexing). - Workspace reads go through the existing
WorkspaceFilesfacade (live sandbox if warm, snapshot if cold) — download matches what the IDE shows. - Access control reuses the existing file-read route rules; no new gating.
- No download size cap (reading already-stored files; the 8 MB cap is upload-only).
- New UI strings via
useT(en source + zh-TW).
Out of scope for v1¶
- Multi-select zip (would need a path-list endpoint, not a prefix).
- Manifest-bearing subset export.
Phases (flat)¶
- P1 (BE) — extract #101 prepare/stream/reaper into a reusable
prepare_zip_download(build_fn) → DownloadPrepared+stream_zip_download(id)helper; collection export behaviour unchanged (regression test). - P2 (BE) — KB folder/root raw-zip endpoints
POST /kb/collections/{id}/folder-download/prepare?prefix=+GET /kb/collections/{id}/folder-download/{download_id}.prefix=""= whole collection; skip.gitkeep; entries relative to prefix. - P3 (BE) — workspace folder/root raw-zip endpoints
POST /a/{slug}/items/{id}/files/download/prepare?prefix=+GET /a/{slug}/items/{id}/files/download/{download_id}. Read via facade; skip.readonly/;prefix=""= whole workspace. - P4 (FE) —
FileServiceseam: addcaps.download,fileDownloadUrl(path),prepareDirDownload(prefix),dirDownloadUrl(downloadId)to bothinvestigationFileServiceandkbFileService. - P5 (FE) —
FileTreewiring: context-menu "Download" (file → anchor, folder → prepare+stream), toolbar "Download" button (root + confirm modal showing file count), gated bycaps.download; i18n strings. - P6 — full gate: BE
coverage … --fail-under=100+ ruff + ty; FE vitest + tsc + build; live smoke (download a file / folder / whole tree on both surfaces).