Mounts are now browsable and downloadable over both WebDAV surfaces
(/webdav/ and NextCloud /remote.php/dav) and NextCloud clients. The work
funnels through the path-based service methods all three surfaces already use,
so both WebDAV handlers gain mount support with no handler changes.
- get_folder_by_path / get_file_by_path resolve a path that descends past a
mount root to a synthetic ext: DTO (the mount root itself stays a real row)
- list_folders_paginated_with_perms / list_files_batch_with_perms branch to the
provider, so PROPFIND Depth:1 enumerates mount directory contents in both
WebDAV handlers
- get_file_stream / get_file_range_stream branch ext: file ids to the provider,
so WebDAV GET streams mount content (Pin<Box<dyn Stream>> re-boxed)
- router injected into FileRetrievalService; new MountRouter::find_path delegates
to the registry's (drive_id, mount_path) index
- WebDAV mkdir/delete/move/rename by path work via path→ext:id→the P2 service
methods (no extra wiring)
Fixes a leading-slash normalization bug in the registry path index: materialized
folder paths arrive both as `Personal/Media` and `/Personal/Media`; keys and
lookups now normalize the leading slash (would have broken real WebDAV paths).
Known follow-up: WebDAV PUT (upload/update by path) still ingests to the CAS;
streaming a WebDAV PUT straight to the provider needs a pre-ingest branch in the
two WebDAV PUT handlers (mirrors the REST upload branch).
Integration test: get_folder_by_path/get_file_by_path resolution, PROPFIND
Depth:1 folder+file listing, and content streaming on a real mount + Postgres.
Adds full CRUD on mount contents, mirroring the P1 read pattern (handlers/
services classify; authorization stays in the service via the mount-root
folder grant; the provider does the I/O).
- mkdir / rename / delete / move-within branch inside FolderService and
FileManagementService (router injected into both)
- streaming upload via a new ExternalUploadService: the upload handler detects a
mount destination BEFORE the CAS ingest and streams the multipart body
straight to the provider (no BLAKE3/dedup). `write_stream` now takes a
lifetime-bound boxed stream so the borrowing multipart field can be passed
without buffering.
- deletes on mounts are permanent (no trash): the trash-first folder handler
routes `ext:` ids straight to the provider delete; file delete goes through the
branched delete_and_cleanup
- cross-backend move/copy (mount ↔ native, or between mounts) is forbidden
(UnsupportedOperation); the mount root itself cannot be renamed/moved/deleted
- every mutation emits a `target:"audit" event="external_mount.write"` line
- shared mount_dto builders synthesize FolderDto/FileDto from a provider MountStat
Tests: 529 unit + integration tests for mkdir/rename/delete, file rename/delete,
streaming upload, cross-boundary forbid, and stranger-denied — all against real
Postgres + a real provider (testcontainers).