Commit d9c3161
feat(drive): support Google Shared Drive destinations end-to-end (#142)
Backlog item #7 (epic #34): thread Google Shared Drive destinations
through every Drive request path, the folder picker, and per-source
persistence, so a source can back up into a Shared Drive as well as My
Drive.
## Request-path checklist (every path + the param it gained)
`supportsAllDrives=true` is now sent UNCONDITIONALLY on every `files.*`
request (harmless for My Drive; const `SUPPORTS_ALL_DRIVES` in
`google/mod.rs`). The LIST/search paths additionally scope the corpus by
`DriveContext`.
| Trait method / path | Endpoint | Param(s) gained |
|---|---|---|
| `ensure_folder` search | `GET /files` (via `list_query`) |
`supportsAllDrives`; Shared Drive:
`corpora=drive`+`driveId`+`includeItemsFromAllDrives` |
| `ensure_folder` create | `POST /files` | `supportsAllDrives` |
| `list_folder` | `GET /files` (paginated) | `supportsAllDrives`; Shared
Drive: `corpora=drive`+`driveId`+`includeItemsFromAllDrives` |
| `find_by_op_uuid` | `GET /files` (appProperties query) | same as list
|
| `create` (small) | `POST /upload/files?uploadType=multipart` |
`supportsAllDrives` |
| `update` (small) | `PATCH /upload/files/{id}?uploadType=multipart` |
`supportsAllDrives` |
| `resumable_session` init | `POST/PATCH
/upload/files[/{id}]?uploadType=resumable` | `supportsAllDrives` |
| `resume_chunk` PUT | `PUT <session-url>` | none (session URL already
encodes the drive) |
| `trash` | `PATCH /files/{id}` `{trashed:true}` | `supportsAllDrives` |
| `delete_permanent` | `DELETE /files/{id}` | `supportsAllDrives` |
| `metadata` | `GET /files/{id}` | `supportsAllDrives` |
| `download` | `GET /files/{id}?alt=media` | `supportsAllDrives` |
| appProperties patch | `PATCH /files/{id}` | `supportsAllDrives` |
| `about` | `GET /about` | n/a (not a `files.*` call) |
| `list_shared_drives` (new) | `GET /drives` | picker enumeration of
Shared Drive roots |
The My-Drive-vs-Shared-Drive list params are built by the pure fn
`pagination::list_query_params(q, page_token, &DriveContext)` and the
resumable init params by `resumable::open_session_query_params()` - both
unit-tested for both contexts (this is the "assert the actual query
params built" coverage).
## Drive-context persistence design
The `GoogleDriveStore` is built PER ACCOUNT and shared across all of
that account's sources, and an account can hold a My-Drive source AND a
Shared-Drive source at once. So drive context is threaded PER CALL (a
new `DriveContext { MyDrive | SharedDrive{drive_id} }` argument on the
three list methods), exactly the way `parent_id`/`drive_folder_id` is
already threaded - NOT held as a store field.
- New nullable column `drive_id` on `backup_sources` (migration
`0009_shared_drive_id.sql`; additive, NULL / "my-drive" decode to My
Drive).
- `SourceRow.drive_id: Option<String>` + `SourceRow.drive_context()`;
executor list call sites pass `&source.drive_context()`.
- `add_source` normalises + persists the picker's `driveId` (validated:
bounded, no control/whitespace).
- `.sqlx` offline cache regenerated.
## Picker UX
`pick_drive_folder` gained a `driveId` param and a `drives.list` call.
At the My Drive root it lists the account's Shared Drive roots (badged
"Shared drive") above the My Drive folders; descending into one carries
its `driveId` back in so the listing switches to `corpora=drive` scope,
and selecting a folder persists the correct `drive_id`.
`DriveFolderEntry`/`DriveFolderListing` carry the `driveId` through
descent. UI: `DriveFolderPicker.vue` gains a `drive-id` v-model wired
through both the first-run `SetupWizard` and the `AddSourceWizard`;
localized (`drivePicker.sharedDriveBadge`); vitest mount coverage in
`drive-folder-picker.test.ts` (badge render + Shared Drive descent
scoping + driveId propagation).
## Trash semantics (item 4)
The existing `trash` path (`files.update trashed=true`) now carries
`supportsAllDrives=true`, so trashing an object inside a Shared Drive
works with no new behavior. Caveat (Drive-side, not code): trashing in a
Shared Drive requires the account to have at least the Content Manager
role on that drive; a Viewer/Commenter/Contributor cannot trash and
Drive returns 403 (surfaced as the existing classified permission
error). No new permission handling was added.
## Live e2e activation
`google_e2e.rs` runs the portable contract scenarios against a real
Shared Drive when, IN ADDITION to the existing
`DRIVEN_E2E_REFRESH_TOKEN` + `DRIVEN_E2E_DEST_FOLDER_ID` +
`DRIVEN_OAUTH_CLIENT_SECRET` gate, `DRIVEN_E2E_SHARED_DRIVE_ID` is set
to a Shared Drive id the token's account can manage. Each test creates a
UUID-named child directly under the Shared Drive root (its id doubles as
the root folder id) and trashes it on success and failure. Unset -> the
`google_shared_drive_*` tests print a skip line and pass (honest env
gate, not `#[ignore]`). Run once creds exist:
```
DRIVEN_E2E_REFRESH_TOKEN=... DRIVEN_E2E_DEST_FOLDER_ID=... \
DRIVEN_OAUTH_CLIENT_SECRET=... DRIVEN_E2E_SHARED_DRIVE_ID=0A... \
cargo test -p driven-drive --test google_e2e
```
## Tests
- Pure query-param builders unit-tested for My Drive vs Shared Drive
(`pagination.rs`, `resumable.rs`).
- Fake contract: drive-context plumb-through (`drive_contexts_seen`),
My-Drive-vs-Shared distinctness, `list_shared_drives` config
(`fake_contract.rs`); portable scenarios parameterized on
`DriveContext`.
- Real e2e: `google_shared_drive_*` scenarios gated on
`DRIVEN_E2E_SHARED_DRIVE_ID`.
- UI: `drive-folder-picker.test.ts` mount coverage.
Refs #34
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 929e93d commit d9c3161
51 files changed
Lines changed: 1502 additions & 288 deletions
File tree
- .sqlx
- crates
- driven-chaos/src/scenarios
- driven-cli
- src
- tests
- driven-core
- src
- migrations
- state
- tests
- driven-drive
- src
- fake
- google
- tests
- common
- src-tauri/src
- commands
- ui/src
- __tests__
- components
- ipc
- locales
- stores
- views
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
| 138 | + | |
138 | 139 | | |
139 | 140 | | |
140 | 141 | | |
| |||
184 | 185 | | |
185 | 186 | | |
186 | 187 | | |
187 | | - | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
188 | 192 | | |
189 | 193 | | |
190 | 194 | | |
| |||
355 | 359 | | |
356 | 360 | | |
357 | 361 | | |
358 | | - | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
359 | 365 | | |
360 | 366 | | |
361 | 367 | | |
| |||
511 | 517 | | |
512 | 518 | | |
513 | 519 | | |
514 | | - | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
515 | 523 | | |
516 | 524 | | |
517 | 525 | | |
| |||
718 | 726 | | |
719 | 727 | | |
720 | 728 | | |
721 | | - | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
722 | 732 | | |
723 | 733 | | |
724 | 734 | | |
| |||
0 commit comments