List, preview, download, and delete files stored in Backblaze B2.
- UI:
/filespage, file browser component - API:
GET /files,GET /files-by-key/metadata?key=...,GET /files-by-key/download?key=...,GET /files-by-key/preview?key=...,DELETE /files-by-key?key=... - Legacy API:
GET /files/{key},GET /files/{key}/download,GET /files/{key}/preview,DELETE /files/{key}
apps/web/src/components/files/file-browser.tsx— tree view container with loading, empty, error, refresh, preview, download, and delete flowsapps/web/src/components/files/file-tree-row.tsx— recursive folder/file rows with keyboard-friendly actions and long-name handlingapps/web/src/components/files/file-preview.tsx— dialog modal for file previewapps/web/src/components/files/file-metadata-panel.tsx— structured metadata displayapps/web/src/lib/file-tree.ts—buildFileTree()converts flat S3 keys to folder/file hierarchyapps/web/src/lib/api-client.ts—getFiles(),getFile(),getDownloadUrl(),getPreviewUrl(),deleteFile(); sends object keys as query parameters so slashes and reserved route names cannot be decoded into path segmentsservices/api/app/runtime/files.py— HTTP handlers for list, get, download, deleteservices/api/app/service/files.py— business logic, key validationservices/api/app/repo/b2_client.py—list_files(),get_file_metadata(),get_presigned_url(),delete_file()
- File route handlers:
services/api/app/runtime/files.py - File tree builder:
apps/web/src/lib/file-tree.ts - B2 data access pattern:
services/api/app/repo/b2_client.py
- prefix: string (optional filter for file listing)
- limit: int (max files to return, 1-1000, default 100)
- key: string (file key for get/download/delete — sent as a query parameter by the web client; no path traversal)
GET /files→FileMetadata[](sorted most recent first)GET /files-by-key/metadata?key=...→FileMetadataGET /files-by-key/download?key=...→{ url: string }(presigned URL, attachment disposition, 10-min expiry). Increments thetotal_downloadscounter exposed on/files/stats. The counter is persisted toservices/api/data/download_count.json(override viaDOWNLOAD_COUNT_FILEenv var) so it survives API restarts.GET /files-by-key/preview?key=...→{ url: string }(presigned URL for inline rendering, 10-min expiry). Does not increment the download counter — used by the preview modal for images / PDFs.DELETE /files-by-key?key=...→{ deleted: true, key: string }- Legacy
/files/{key}routes remain available for compatibility. The web client uses them only as a rolling-deploy fallback when/files-by-keyis unavailable and the key is safe to place in a legacy path. - Side effects: DELETE removes file from B2;
/downloadincrements the in-memory download counter
- Page loads → fetches file list from
GET /files(sorted most recent first) - Files organized into tree view — folders expand/collapse, files shown with type-specific icons
- Top-level folders auto-expand on load
- User hovers or focuses a file row → action menu appears (preview / download / delete); touch-sized menu button remains visible on small screens
- Preview: opens dialog, fetches a preview-only presigned URL via
/files-by-key/preview?key=...(does not count as a download) and renders image/PDF inline - Download: fetches presigned URL via
/files-by-key/download?key=...(attachment disposition, 10-min expiry), opens in new tab, bumps the download counter, triggers a stats refresh - Delete: calls
DELETE /files-by-key?key=..., removes row from tree, shows toast - All key-based API calls send the key in the query string and validate it against path-traversal patterns in the API service layer
- During frontend/API version skew, the web client falls back to legacy path routes only for keys that cannot collide with reserved file routes such as stats, download, or preview.
- File not found (deleted externally) → API returns 404
- Invalid file key (traversal attempt, empty key) → API returns 400
- File key contains
/, spaces,#,?,%, reserved route names, or suffixes like/downloadand/preview→ web client sends the key as a query parameter before calling get/download/preview/delete routes - B2 unreachable → persistent error state with retry
- Empty bucket → upload prompt with direct Upload action
- Delete failure → API returns 500, toast error
- Empty: centered message with upload prompt and Upload action
- Loading: skeleton rows
- Error: inline error state with Retry
- Loaded: tree view with expand/collapse folders and focus/hover action menus
- Preview: responsive dialog with wrapped file names, fallback copy for preview URL failures, and metadata that tolerates long keys
- Test files:
services/api/tests/test_file_key_routes.py,apps/web/src/lib/api-client.test.ts - Required cases: list files, empty list, file not found, presigned URL generation, delete success, delete failure
- Quick verify command:
pnpm test:api - Client route-construction tests:
pnpm --filter @vibe-coding-starter-kit/web test - Full verify command:
pnpm lint && pnpm --filter @vibe-coding-starter-kit/web test && pnpm build && pnpm lint:api && pnpm test:api && pnpm check:structure - Pass criteria: all pytest tests green, no ruff violations