feat(share): public folder browsing API + range support + zip

Five new public endpoints under /api/s/{token}/...:
  GET /contents
  GET /contents/{folder_id}
  GET /file/{file_id}
  GET /zip
  GET /zip/{folder_id}

All honour the unlock cookie from /verify, so password-protected
folder shares work end-to-end.

Folder/file IDs are validated against the share subtree via a single
ltree containment query (O(log N) on the existing GiST index).
Out-of-scope IDs return 404.

download_shared_file refactored to a Range/304/206/416-aware
serve_share_file helper, shared with the new /file/{file_id}
endpoint. content_disposition extracted from FileHandler so RFC 5987
formatting is identical across auth and share download paths.
This commit is contained in:
abnvle
2026-05-05 22:41:55 +02:00
parent 8527765bf2
commit d15d7b8f8e
8 changed files with 725 additions and 85 deletions
+26 -5
View File
@@ -66,11 +66,32 @@ pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppStat
router = router.nest("/s", public_share_router);
// Download endpoint uses full AppState (needs FileRetrievalService)
router = router.route(
"/s/{token}/download",
get(share_handler::download_shared_file),
);
// AppState-backed share endpoints (download, contents, file, zip)
router = router
.route(
"/s/{token}/download",
get(share_handler::download_shared_file),
)
.route(
"/s/{token}/contents",
get(share_handler::list_share_contents_root),
)
.route(
"/s/{token}/contents/{folder_id}",
get(share_handler::list_share_contents_subfolder),
)
.route(
"/s/{token}/file/{file_id}",
get(share_handler::download_share_file_in_folder),
)
.route(
"/s/{token}/zip",
get(share_handler::download_share_zip_root),
)
.route(
"/s/{token}/zip/{folder_id}",
get(share_handler::download_share_zip_subfolder),
);
}
// i18n routes — no auth required (localization should be available before login)