Mount Archive Export#
The Mount Archive Export feature provides a POST /mounts/files/export endpoint that streams a ZIP archive of all files within one or more mounts. It was introduced as part of the drive UI refactor and hardened across two follow-up PRs .
Key source files:
- Router / handler:
api/oss/src/apis/fastapi/mounts/router.pyβexport_mount_files()handler - Streaming utilities:
api/oss/src/apis/fastapi/mounts/utils.pyβstream_mounts_archive(),iter_archive_members(), zip-slip helpers,_content_disposition_attachment() - Service layer:
api/oss/src/core/mounts/service.pyβbuild_archive_work_list() - DTOs:
api/oss/src/core/mounts/dtos.pyβMountArchiveSource - Object store:
api/oss/src/core/store/storage.pyβObjectStore.get_object(),list_objects_page() - Acceptance tests:
api/oss/tests/pytest/acceptance/mounts/test_mounts_basics.pyβTestMountArchiveExport - Unit tests:
api/oss/tests/pytest/unit/test_mounts_file_ops.pyβ zip-slip, mtime regression, archive work-list tests
Request Shape#
The endpoint accepts a list of MountArchiveSource objects, each specifying :
| Field | Type | Description |
|---|---|---|
mount_id | UUID | The mount to read from |
source_path | str (default "") | Folder scope within the mount; "" = entire mount |
archive_prefix | str (default "") | Prefix applied to all entries in the ZIP (enables layered drive layout) |
An optional filename query parameter controls the Content-Disposition attachment name. The route was renamed from POST /files/archive to POST /files/export to eliminate a naming collision with the mount "archive" lifecycle verb (soft-delete) that shared the same router prefix .
Eager Precomputation Before Streaming#
Previously the endpoint returned HTTP 200 and began streaming before validating mounts, causing truncated or empty ZIPs on errors. The fix separates work-list building from streaming :
-
build_archive_work_list()β called eagerly beforeStreamingResponseis constructed. It resolves eachMountArchiveSource, validates that the mount exists (raises 404 if not), builds the full list of(zip_entry_name, store_key)pairs, and pre-filters unsafe zip-slip paths. Errors raised here produce correct HTTP status codes (404/503). -
iter_archive_members(work)β receives the precomputed work list and performs bounded-concurrency object-store reads, keeping up to_ARCHIVE_READ_CONCURRENCYin-flight reads in a deque while yielding members to thestream_ziplibrary.
Object-Store-Backed Streaming#
Each file entry is fetched from the object store via ObjectStore.get_object() and yielded as a streaming chunk sequence to stream_zip. A known performance issue exists: get_object() currently opens a fresh aiohttp.ClientSession per call, creating a new TCP/TLS connection for each file. Against a remote S3 endpoint with 5,000-file mounts at 8-way concurrency, this results in ~47 seconds of pure TLS handshake overhead. Issue #5414 tracks the fix: hold one long-lived session on the ObjectStore class and reuse it across calls . The cost is negligible for the bundled local SeaweedFS instance.
A separate mtime conversion bug was found and regression-tested: StoreObject.mtime is stored in epoch milliseconds, but datetime.fromtimestamp() expects seconds. Passing raw ms values causes ValueError after the 200 headers are already sent, producing a silently broken ZIP .
Zip-Slip Prevention#
The zip-slip mitigations live in utils.py :
_zip_segments(path)β splits on both/and\(backslash is treated as a separator by Windows extractors)_has_unsafe_zip_segment(segments)β flags empty,., or..segments_safe_zip_segments(path)β returns only the safe segments
Entries with .. or backslash traversal in their store key are skipped with a warning log rather than rewritten. Rewriting (e.g., turning a/../report.txt β a/report.txt) risks silently overwriting a legitimate a/report.txt entry that may also be in the archive .
The POST /mounts/files/export endpoint also validates source_path inputs using validate_file_path(), returning HTTP 422 for paths like ../evil before archive construction begins .
Content-Disposition Hardening & Path Validation#
_content_disposition_attachment() produces a dual-format header to handle non-ASCII archive names :
- ASCII fallback (
filename=...): stripped to printable latin-1-safe characters, preventing HTTP 500 errors for CJK or emoji filenames - RFC 5987 encoding (
filename*=UTF-8''...): carries the exact percent-encoded filename for clients that support it
validate_file_path() (used for source_path inputs) uses a denylist approach: it rejects only absolute paths, empty/./.. segments, NUL bytes, and control charactersβaccepting real-world names like Next.js route groups (app/(auth)/[slug]), npm scopes (@scope/pkg), and non-ASCII characters .