Skip to content

feat(share): public folder browsing with descendant check, range, and zip#347

Open
abnvle wants to merge 2 commits intoAtalayaLabs:mainfrom
abnvle:feat/share-folder-browsing
Open

feat(share): public folder browsing with descendant check, range, and zip#347
abnvle wants to merge 2 commits intoAtalayaLabs:mainfrom
abnvle:feat/share-folder-browsing

Conversation

@abnvle
Copy link
Copy Markdown
Contributor

@abnvle abnvle commented May 5, 2026

Depends on #346. Branch is built on top of fix/share-password-download; once # merges, this PR will rebase to show only its own commits.

What

Replaces the "Folder browsing is not yet available" placeholder in share.html with a public folder gallery, backed by five new endpoints under /api/s/{token}/...:

Endpoint Purpose
GET /contents list shared folder root
GET /contents/{folder_id} list a subfolder
GET /file/{file_id} stream a file inside the share (Range / 304)
GET /zip ZIP the shared folder
GET /zip/{folder_id} ZIP a subfolder

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

The existing /api/s/{token}/download is also refactored to use the new range-aware streaming helper, gaining 206 Partial Content (video seeking, resumable downloads).

Security

Every folder/file ID accepted by the new endpoints is validated against the share's subtree via a single ltree <@ containment query (O(log N) on the existing GiST index). Out-of-scope IDs return 404, not 403, so a folder-share token cannot be used to enumerate IDs across the instance. Trashed folders/files are excluded at SQL. Listings always run owner-scoped under the share creator.

Implementation

Backend

  • FolderRepository::is_folder_in_subtree and is_file_in_subtree: new trait methods with safe Ok(false) defaults; pg impl uses one ltree query each.
  • ShareBrowseService: composes ShareService (token validation + unlock), FolderService (listing), FileRetrievalService, FolderDbRepository (descendant check). Wired into AppState.
  • download_shared_file refactored to call serve_share_file with full Range / 206 / 416 / 304 / Accept-Ranges / ETag support; /file/{file_id} reuses the same helper.
  • FileHandler::content_disposition extracted into pub(super) build_content_disposition so RFC 5987 disposition formatting is identical across auth and share download paths.

Frontend

share.html keeps its structure; only two changes:

  • <script type="module" src="/js/core/icons.js"> so <i class="fa-…"> is auto-replaced with inline SVGs by the existing icon system
  • #share-folder is now an empty container the JS fills

publicShare.js rewritten with:

  • Grid / list view toggle (persisted in localStorage)
  • Image thumbnails via /api/s/{token}/file/{id} (cover-cropped 4:3)
  • Video posters via lazy <video preload="metadata"> (IntersectionObserver, 300 px lookahead) with play-marker overlay
  • Click-to-open lightbox: image or video, with Esc / arrow-key navigation, Download button, and image error retry
  • "Download ZIP" button calling /api/s/{token}/zip[/{folder_id}]
  • Subfolder navigation via URL hash, History API for browser back
  • File-type icons reusing existing FA classes (no new asset bundling)

share-public.css extended with gallery, list view, and lightbox styles using existing theme tokens (light and dark modes).

Test plan

  • Folder share: gallery renders with image thumbs, video posters, file-type icons
  • Click subfolder: URL hash updates, browser Back returns to root
  • Click image / video: lightbox opens, Esc closes, arrows navigate
  • ZIP root and subfolder: archives download
  • Range request on /file/{id}: returns 206 with Content-Range
  • File ID outside share scope: 404
  • Password-protected folder share: password form → unlock cookie → gallery
  • docker compose build succeeds

@abnvle abnvle requested a review from DioCrafts as a code owner May 5, 2026 20:17
@abnvle abnvle force-pushed the feat/share-folder-browsing branch 3 times, most recently from 2c11d5c to 7a80285 Compare May 5, 2026 20:36
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.
@abnvle abnvle force-pushed the feat/share-folder-browsing branch from 7a80285 to b4ca223 Compare May 5, 2026 20:42
@EdouardVanbelle
Copy link
Copy Markdown
Contributor

Nice !

I see lot of duplicate code (in CSS & JS)
will be interesting to reuse the existing functions (for example fileIconClass is a duplicate of uiFileType.getIconClass() )

The reuse the current builder to display list & grid is more complicated: this part need a refactor (currently too deeply included in ui.js) I will check to extract this part in the future

@EdouardVanbelle
Copy link
Copy Markdown
Contributor

EdouardVanbelle commented May 5, 2026

I tested your branch, this is great

Just seen that .zip date seems to wrong date (see below)

sharing this:
Capture d’écran 2026-05-05 à 23 37 39

the shared view:
Capture d’écran 2026-05-05 à 23 38 15

otherwise everything is functionnal !

small issue: .zip files are at 30 nov 1979:

% cd test\ share
% ls -la
total 606464
drwxrwxr-x@   14 ed  staff        448 30 nov  1979 .
drwx------@ 1533 ed  staff      49056  5 mai 23:34 ..
-rw-rw-r--@    1 ed  staff  173713927 30 nov  1979 276047.mp4
-rw-rw-r--@    1 ed  staff    7591474 30 nov  1979 35862-408654167.mp4
-rw-rw-r--@    1 ed  staff     169764 30 nov  1979 API Stage.png
-rw-rw-r--@    1 ed  staff  121283919 30 nov  1979 BigBuckBunny.m4v
-rw-rw-r--@    1 ed  staff    5174784 30 nov  1979 premiummusicodyssey-sunday-lights-446231.mp3
-rw-rw-r--@    1 ed  staff    1971244 30 nov  1979 projet four pizza.pdf
-rw-rw-r--@    1 ed  staff          5 30 nov  1979 README.md
-rw-rw-r--@    1 ed  staff     407710 30 nov  1979 ReBAC example.png
drwxrwxr-x@    2 ed  staff         64 30 nov  1979 test subfolder
-rw-rw-r--@    1 ed  staff          0 30 nov  1979 test.doc
-rw-rw-r--@    1 ed  staff          0 30 nov  1979 test.ppt
-rw-rw-r--@    1 ed  staff     176068 30 nov  1979 this is a very long name, i repeat, this is a very long name.jpg

Replaces the placeholder with a gallery rendered by publicShare.js
against the new public endpoints.

- Grid / list view toggle (localStorage)
- Image thumbs and lazy-loaded video posters
- Click-to-open lightbox with Esc / arrow nav
- ZIP download for the current folder
- Subfolder navigation via URL hash, History API for back

Adds five overlay-related tokens to base/variables.css for the
lightbox surface (on-overlay text, translucent button, drop shadow).

share.html itself only adds an icons.js module import and empties
#share-folder for JS to populate.
@abnvle abnvle force-pushed the feat/share-folder-browsing branch from b4ca223 to cb454d8 Compare May 5, 2026 22:01
@abnvle
Copy link
Copy Markdown
Contributor Author

abnvle commented May 5, 2026

Thanks a lot :)
I just removed the duplicate icons. I changed publicShare.js to import a module, now it uses uiFileTypes.getIconClass(file.name). Now, only rare extensions (like those I have locally) will have a problem, as they will be displayed with a generic icon.

I saw a problem with .zip - I think it's easy to fix. The zip builds without the .last_modification_date(...)

@EdouardVanbelle
Copy link
Copy Markdown
Contributor

feel free to update uiFileTypes mapping if needed ;)

@abnvle
Copy link
Copy Markdown
Contributor Author

abnvle commented May 5, 2026

Okay, so I'll try to do that. For now, I'm doing one more thing related to the PRs I made today. You should have received a notification because I've taken the liberty of adding you to my private repo :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants