From 20e5ef0ef207de69c513838e35895f807937dd31 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Wed, 1 Jul 2026 21:28:00 +0200 Subject: [PATCH] feat(drive): personal drive are photo + music indexed by default --- docs/plan/drive.md | 87 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/docs/plan/drive.md b/docs/plan/drive.md index 1181f402..ef8fcc1f 100644 --- a/docs/plan/drive.md +++ b/docs/plan/drive.md @@ -1324,13 +1324,12 @@ mutation site updates `updated_by`. the filesystem rather than browsing a single folder: Photos, Music library, Favorites, Recent items, Search, Trash. With drives landing, each of these needs an explicit scope decision. The -table below locks the choices; the rationale is **noise risk by -file type**, not a uniform rule. +table below locks the choices. | Section | Scope | Capability flag (per-drive policy) | Why | |---|---|---|---| -| **Photos** (`/api/photos`) | Default Personal Drive only | `policies.include_in_photo_index = true` to opt a non-default drive in | Shared drives often carry images that aren't "photos" (screenshots, scans, charts-as-PNGs). Defaulting cross-drive pollutes the personal timeline. Opt-in for shared drives where the owner explicitly wants them indexed (e.g. "Family Photos" shared drive). | -| **Music** — library view (future) + playlists | Cross-drive (all accessible drives) | `policies.forbid_music_index = true` to opt a drive out | Audio files in shared drives are almost always intentional content (band collaboration, family music, podcast archive). Defaulting cross-drive matches user intent. Owner opts a drive out for the rare case it shouldn't be indexed. The Music section today is *only* playlists; a `/api/music/tracks` library view added later inherits this scope. | +| **Photos** (`/api/photos`) | Default Personal Drive only | `policies.include_in_photo_index = true` to opt a non-default drive in | Non-default drives often carry images that aren't "photos" (screenshots, scans, charts-as-PNGs). Defaulting cross-drive pollutes the personal timeline. Opt-in for non-default drives where the owner explicitly wants them indexed (e.g. "Family Photos" shared drive). | +| **Music** — library view (future) + playlists | Default Personal Drive only | `policies.include_in_music_index = true` to opt a non-default drive in | Symmetric with Photos: audio files in a work drive or a random shared folder shouldn't silently bleed into the personal music library. Owner opts a non-default drive in (e.g. "Family Music", "Band Collaboration") when the drive genuinely is a music library. The Music section today is *only* playlists; a `/api/music/tracks` library view added later inherits this scope. | | **Music playlists** (`audio.playlists`) | User-scoped, cross-drive curation | n/a | Playlists are a curation tool. `owner_id` stays on `auth.users(id)`; tracks reference files via `playlist_items.file_id` and may live in any drive the user has access to. At list time, `list_playlist_tracks` filters out tracks in drives the caller can no longer reach (see §11's defense-in-depth pattern). | | **Favorites** (`/api/favorites/resources`) | Cross-drive (all accessible drives) | n/a | Personal organisation tool. Star a PDF from the work drive AND a photo from Personal — the whole point is cross-drive curation. ReBAC visibility check at list time drops rows the user can no longer reach. | | **Recent items** (`/api/recent/*`) | Cross-drive (all accessible drives) | n/a | Personal history. Same shape as Favorites — you touched files across drives; the timeline reflects that. ReBAC visibility check at list time. | @@ -1340,33 +1339,69 @@ file type**, not a uniform rule. #### Capability flag mechanism Both `policies.include_in_photo_index` and -`policies.forbid_music_index` live under the same JSONB +`policies.include_in_music_index` live under the same JSONB `policies` column on `storage.drives` (see §8) — no new schema. -The default values reflect the table above: omitted = "off" for -photos (so non-default drives don't show photos unless the owner -opts in), omitted = "off" for music (so all accessible drives -*are* indexed unless the owner opts out). +Both flags follow the same shape: **omitted = off**. The query +predicate then reduces to a single positive rule for every +drive: -The owner-only UI in the drive settings panel toggles these -flags. The query layer reads them at request time; flipping -either flag is instant — no reindex required because the filter -applies in the query Must-clause, the index itself is unchanged. +```sql +WHERE fi.drive_id IN ( + SELECT d.id FROM storage.drives d + JOIN storage.role_grants rg + ON rg.resource_type='drive' AND rg.resource_id=d.id + WHERE rg.subject_id IN (caller's effective subjects) + AND (d.policies->>'include_in_photo_index')::boolean = true +) +``` -#### The Photos/Music asymmetry — defensible, not a smell +No `default_for_user` OR-branch, no per-kind carve-out. -Photos defaulting to "default-drive only" while Music defaults to -"cross-drive" is the one case where two similar surfaces have -different defaults. The justification is the noise-risk argument -above: image content in shared drives is heterogeneous (often -not "photos" in the gallery sense), audio content in shared -drives is usually intentional. The capability flags let owners -fix either case, but the defaults match what the typical user -will want without configuration. +**Default personal drive gets both flags set to `true` on +creation.** The `PersonalDriveLifecycleHook` (§3) that creates +the default personal drive on user provisioning populates +`policies` with `{"include_in_photo_index": true, +"include_in_music_index": true}`. Existing default personal +drives get the same two flags via a one-shot backfill migration +alongside the flag introduction. Net effect: every user's +default personal drive is in scope from moment one, no user +configuration required for the common case, but the SQL is +kind-agnostic. -If a uniform rule is ever preferred, the cheapest move is to -flip Photos to cross-drive with `forbid_photo_index` as the -opt-out (mirroring Music). That can land later without a schema -change — just a behaviour change. +**Non-default drives** (secondary personals, shared drives) are +created with the flags omitted, so they stay out of scope until +the owner explicitly opts in via the admin "Manage policies" +modal. + +Flipping either flag on any drive is instant — the query reads +`policies` at request time; the index itself is unchanged. +Toggle-off on a default personal drive is *possible* (admins +own the drive-policy mutation surface — see §8) but shows a +confirm dialog in the UI ("this will empty the user's Photos +timeline" / "…their Music library"), since it's an unusual +action. + +#### Why symmetric (both opt-in) instead of asymmetric + +An earlier version of this section had Music default to +cross-drive (`forbid_music_index` as an opt-out), on the +argument that audio in shared drives is "almost always +intentional content." That asymmetry created two problems: + +1. **Mixed-form flag naming** — one `include_in_*` and one + `forbid_*` with opposite meanings, hard to reason about in the + admin UI and the query layer. +2. **The "shared audio is always intentional" claim doesn't + hold under scrutiny** — a work drive with a few voicemail + MP3s or a project drive with a stray podcast recording + shouldn't bleed into the personal music library any more than + a work drive with screenshots should bleed into Photos. + +Symmetric opt-in (`include_in_*_index` for both) fixes both. +The "Family Music" case still works — the owner flips the flag +once on drive creation, same one-time gesture as "Family Photos" +under the pre-existing photo policy. The default-personal case +(90%+ of users) needs no configuration for either surface. #### Face indexing — per-drive clustering, scope follows Photos