feat(drive): clarify UI routes for drive
- `/drive/<folder-id>` no change - `/config/drive/<drive-uuid>` for drive configuraton - add /magic proxy from dev vite server
This commit is contained in:
+23
-9
@@ -618,9 +618,15 @@ accommodates them without schema migration)
|
||||
|
||||
| URL | Resolves to |
|
||||
|---|---|
|
||||
| `/` | Redirect to the caller's personal drive UUID |
|
||||
| `/drive/<drive-uuid>` | Drive root view |
|
||||
| `/drive/<drive-uuid>/<folder-id>` | Folder inside the drive |
|
||||
| `/` (internal user) | Redirect to `/drive/<root-folder-id>` of the caller's default personal drive |
|
||||
| `/` (external user) | Redirect to `/sharedwithme` (no personal drive exists) |
|
||||
| `/drive/<folder-id>` | Folder view (root or descendant — drive context is recovered server-side from `folders.drive_id`) |
|
||||
| `/config/drive/<drive-uuid>` | Drive configuration surface (members, policies, quota). Page is permission-aware: owner sees member management; editor/viewer see a read-only "Drive info" view |
|
||||
| `/config/user/<user-uuid>` | (Future) User configuration — same shape so the `/config/<resource-type>/<uuid>` pattern is consistent across resources |
|
||||
|
||||
**Why folder-id, not drive-uuid + folder-id**: every `storage.folders` row carries `drive_id` after D0, so a single folder UUID recovers the drive context in one cheap lookup. Stable across cross-drive moves (D6): bookmarks keep working when a folder hops drives, because the folder UUID doesn't change.
|
||||
|
||||
**Why `/config/` is a separate top-level segment**, not `/drive/<uuid>/settings`: the URL prefix encodes intent ("we are configuring something"), not just resource location. Future configuration surfaces (`/config/user/<id>`, `/config/group/<id>`, `/config/share/<id>`) compose cleanly under the same prefix. It also avoids the singular-vs-plural ambiguity (`/drive/<X>` vs `/drives/<X>/settings`) that's easy to typo and hard to grep for.
|
||||
|
||||
#### Native WebDAV (`/webdav/...`)
|
||||
|
||||
@@ -1371,7 +1377,7 @@ us a real rollback window while the new model bakes in production.
|
||||
|---|---|---|
|
||||
| **D-Prep — role_grants refactor** | `access_grants → role_grants` schema migration with role-bundle semantics. `Manage` Permission added to the enum + role bundle. Engine reads role_grants only; `access_grants` removed (after one dual-write release if compat is needed). API gains `role` parameter on grant endpoints; audit log emits one `role_grant.*` event per role assignment instead of N permission events. **No Drive concept yet.** Sets the foundation that all subsequent PRs build on. **Data shape confirmed**: empirical audit shows >99% of existing `access_grants` rows already cluster into the standard bundles (viewer/editor/owner) — the migration is mechanical for the vast majority of data; the <1% edge cases get absorbed by shipping `commenter` and `contributor` roles on day one or get an explicit per-row migration decision logged. | **Medium** — touches the load-bearing authorisation table, but the data shape removes the main migration risk |
|
||||
| **D0 — foundation** | `storage.drives` schema (no `drive_members` — uses `role_grants` from D-Prep); `Drive` domain entity; migration creating personal drives + backfilling `drive_id` on every resource; read-only `GET /api/drives` listing the caller's drives (single query: `SELECT … FROM role_grants WHERE subject_id=$caller AND resource_type='drive'`). Dual-write `user_id` alongside `drive_id` for safety. **No new UI.** **Every upload path stamps `drive_id` at insert**: classic multipart (`file_handler::upload`), chunked NC (`uploads_handler`), streaming CDC (`upload_ingest`), delta upload (`delta_upload_service`), instant upload by hash. Tantivy reindex (see §11) is part of this PR. **Provenance columns added** (see §14): `created_by` and `updated_by` on both `storage.folders` and `storage.files`, FK to `auth.users` with `ON DELETE SET NULL`; backfilled from `user_id` so pre-Drive content has provenance from day one; every mutation path that touches `updated_at` also sets `updated_by`. | **High** — every storage query touches, all upload paths touched |
|
||||
| **D1 — UI switcher + URL routing** | Sidebar drive picker, `/drive/<uuid>/<folder-id>` frontend routes, default-drive redirect from `/`. WebDAV path dispatcher recognising `drives/<uuid>` as the drive-explicit prefix on both `/webdav/` and `/remote.php/dav/`. | Medium |
|
||||
| **D1 — UI switcher + URL routing** | Sidebar drive picker, `/drive/<folder-id>` frontend route (drive context recovered server-side from `folders.drive_id`), `/config/drive/<drive-uuid>` for drive admin. `/` redirects to `/drive/<root-folder-id>` of the caller's default personal drive (internal users) or `/sharedwithme` (external users with no personal drive). WebDAV path dispatcher recognising `drives/<uuid>` as the drive-explicit prefix on `/webdav/` (NC keeps the credential-side scheme — see §9). | Medium |
|
||||
| **D2 — drive membership API + per-drive trash auth** | `POST /api/drives/{id}/members`, `DELETE`, `PUT` for role changes — thin handlers that translate to `role_grants` INSERT/DELETE/UPDATE with `resource_type='drive'`. `Resource::Drive(Uuid)` (added in D-Prep at the enum level) gets its specialised handler surface here. Shared-drive last-owner protection. Group-as-subject support reuses the existing `subject_groups` machinery. **Personal-drive guards** (`add_member`, `remove_member`, `delete_drive` refuse on `kind='personal'` — see §2). **Per-drive trash authorisation** (§12): trash listing filters by drive(s) the caller can read; trash mutations (send/restore/permanent-delete) require `role='owner'` on the drive; `storage.trash_items` VIEW updated to surface `drive_id`; orphan/aborted-upload sweep becomes per-drive. | Medium |
|
||||
| **D3 — group-owned shared drives** | "Create shared drive" flow — admin or group owner triggers, drive created with `kind='shared'`, initial owner row is the group. Group-deletion guard refuses if the group is the last owner of any drive. Drive-rename, drive-delete. | Low |
|
||||
| **D4 — per-drive quota** | Move storage accounting off `auth.users.storage_used_bytes` onto `storage.drives.used_bytes`. **Re-point the existing per-user incremental CTE** (introduced in v0.7.0 — see `b5b80549`, `d6987329`) at drive rows; don't reinvent the counting logic. Upload paths check `drive.quota_bytes` instead of (or in addition to) the user's quota for the dual-write window. **Per-chunk incremental quota check on the NC chunked path** (see §13): MKCOL refuses when the drive is already over quota; each PUT chunk runs an O(1) `used + session_so_far + chunk_size > quota` test and refuses with 507 within one chunk of wasted upload. Closes a pre-existing wart where NC clients could upload GB before learning they were over quota. Reconciliation job runs once per day to fix drift. | Medium |
|
||||
@@ -1652,13 +1658,21 @@ test`), **(c)** `cargo fmt && cargo clippy --all-features
|
||||
personal drive without reconfiguration. The chroot POC's `~`
|
||||
username (or app-password binding) lands a sync into the chosen
|
||||
drive transparently.
|
||||
- **Manual smoke**: open `/`, get redirected to
|
||||
`/drive/<default-uuid>`. Click sidebar drive switcher → URL
|
||||
updates, listing reloads. Drive picker shows all of the caller's
|
||||
drives (default first), each with its quota usage.
|
||||
- **Manual smoke (internal user)**: open `/`, get redirected to
|
||||
`/drive/<default-personal-drive-root-folder-id>`. Click sidebar
|
||||
drive switcher → URL updates to `/drive/<picked-drive-root-folder-id>`,
|
||||
listing reloads. Drive picker shows all of the caller's drives
|
||||
(default first), each with its quota usage. Open
|
||||
`/config/drive/<personal-drive-uuid>` → owner sees member list +
|
||||
policies. Open `/config/drive/<shared-drive-uuid>` as a viewer →
|
||||
read-only "Drive info" surface.
|
||||
- **Manual smoke (external user)**: open `/`, get redirected to
|
||||
`/sharedwithme` (no `/drive/...` for an account without a
|
||||
personal drive).
|
||||
- **Playwright**: a new `tests/e2e/drive-switching.spec.ts` exercises
|
||||
sidebar → URL → listing → cross-drive isolation (folders in
|
||||
drive A don't appear in drive B's listing).
|
||||
drive A don't appear in drive B's listing), plus the
|
||||
internal-vs-external root redirect split.
|
||||
|
||||
### D2
|
||||
- **Membership API**: `POST /api/drives/{id}/members` with user, with
|
||||
|
||||
@@ -15,7 +15,8 @@ const proxy = {
|
||||
'/webdav': { target: BACKEND, changeOrigin: true },
|
||||
'/caldav': { target: BACKEND, changeOrigin: true },
|
||||
'/carddav': { target: BACKEND, changeOrigin: true },
|
||||
'/wopi': { target: BACKEND, changeOrigin: true }
|
||||
'/wopi': { target: BACKEND, changeOrigin: true },
|
||||
'/magic': { target: BACKEND, changeOrigin: true }
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
Reference in New Issue
Block a user