18 KiB
Phase 12: Cloud Resource Foundation - Research
Researched: 2026-06-18 Status: Complete
Research Question
What must be understood to plan a provider-neutral cloud capability contract, durable owner-scoped cloud item index, and capability-aware shared browser without copying provider-owned files into DocuVault storage?
Executive Summary
Phase 12 should introduce a cloud-resource layer beside the existing object-storage StorageBackend, not expand that byte-oriented contract until it becomes a file manager. The durable identity is CloudConnection.id; provider type is only an adapter selector. This is required for multiple accounts from one provider and means current provider-keyed routes, cache keys, and _get_active_connection() queries must change.
Browsing should use stale-while-revalidate semantics backed by PostgreSQL: return normalized metadata immediately, refresh from the provider, then upsert owner/connection-scoped rows. Provider-owned bytes remain absent. Capability responses need both operation support and a reason state so StorageBrowser.vue can distinguish unsupported actions from temporarily blocked actions without provider-specific branches.
Two cross-phase constraints must be made explicit. First, Google currently requests the drive.file scope, which is limited to files created by or opened with the app and cannot satisfy whole-existing-drive discovery; Phase 13 reconnect/credential work must request and communicate appropriate broader access. Second, retained byte-cache quota accounting belongs to Phase 14, but Phase 12 schemas should not conflate metadata size with stored byte usage.
Current-State Findings
Connection Identity Is Provider-Keyed
CloudConnectionalready has a UUID primary key anddisplay_name, but_upsert_cloud_connection()and_get_active_connection()select by(user_id, provider).- Browse routes are
/api/cloud/folders/{provider}/{folder_id}and frontend routes are/cloud/:provider/:folderId; both make a second account of the same provider ambiguous. cloud_cache.pykeys listings as{user_id}:{provider}:{folder_id}, so same-provider accounts would collide.CloudStorageView.vuecorrectly iterates connection IDs, but navigates withconn.providerand discards the connection identity.
Planning consequence: migrate browse identity to connection UUID throughout API, route, cache, store, and view boundaries. Do not expose encrypted credentials or infer ownership from a client-supplied provider.
Existing Storage Contract Is Byte-Oriented
StorageBackend defines put/get/delete, presigned URLs, health, and stat. It does not model folders, normalized resource metadata, rename/move, provider capabilities, or change cursors. Nextcloud adds list_folder() outside the abstract contract, while Google and OneDrive listing functions live directly in api/cloud.py.
Planning consequence: add a separate cloud resource adapter contract. Keep StorageBackend for DocuVault-owned/local object storage and existing document byte operations. A cloud adapter can later support:
list_folder(parent_ref)returning normalized resources and a listing resultcapabilities()for connection-level support- item-level capability overrides from provider metadata
- future
open,preview,upload,create_folder,rename,move,delete, and change-cursor operations
Phase 12 implements browse and capability reporting; Phase 13 implements mutations.
Listings Are Too Sparse and Ephemeral
Current Google/OneDrive/WebDAV listing dictionaries expose only id, name, is_dir, and size. The in-process TTL cache lasts 60 seconds and disappears on restart or another application instance. There is no persisted parent, MIME type, modified time, version/etag, last-seen time, or analysis/index placeholder.
Planning consequence: a cloud_items table should persist normalized metadata with a uniqueness boundary at (connection_id, provider_item_id) and explicit user_id for defense-in-depth ownership queries. Recommended fields:
- UUID
id, UUIDuser_id, UUIDconnection_id - provider-native
provider_item_id, optional canonical path snapshot - optional
parent_item_idplus provider parent reference/path name,item_type(file/folder), optionalcontent_type,size_bytes- optional
etag,version, providermodified_at last_seen_at, optionaldeleted_at, timestamps- analysis/index placeholders that do not imply byte ownership (
analysis_status, optional extracted-text/index relation or future-compatible status fields)
Use foreign keys with cascade from user/connection, indexes for owner+connection+parent browsing, and a unique connection+provider-item constraint. Never make path the stable primary identity: rename and move change paths, while provider IDs are generally more stable.
Capability Data Has Two Levels
Provider APIs expose capability information differently:
- Google Drive
files.capabilitiesincludes item-level booleans such ascanRename,canTrash, andcanMoveItemWithinDrive. - Microsoft Graph
driveItemsupplies stable IDs, parent references, eTags/cTags, facets, and permissions; some effective capability decisions require granted scopes and item context rather than one universal provider matrix. - WebDAV capability discovery can use standards-visible methods/properties and server responses, but arbitrary servers vary. Safe discovery must use
OPTIONS,PROPFIND, configured permission information, and observed non-mutating responses, never test mutations.
Model each action as structured data rather than booleans alone:
- state:
supported,unsupported, ortemporarily_unavailable - reason code: stable machine identifier such as
provider_unsupported,insufficient_scope,read_only,reauth_required,offline,item_restricted - message: short user-facing explanation
- optional remedy code for frontend action routing in later phases
Connection-level defaults should be merged with item-level provider capabilities. Keep the action vocabulary fixed across providers: browse, open, preview, upload, create-folder, rename, move, delete, and change-tracking.
Google OAuth Scope Is a Functional Blocker for the Milestone Vision
The current OAuth flow requests https://www.googleapis.com/auth/drive.file. Google documents this as access to files the app created or that users explicitly opened with the app. It is insufficient for discovering and analyzing all documents already organized in a user’s Drive.
Planning consequence: Phase 12 should expose an insufficient_scope capability/health reason when credentials cannot browse the intended corpus. Phase 13 owns reconnect and credential lifecycle, including a deliberate broader-scope consent flow. Do not silently widen permissions in Phase 12.
Metadata and Byte Quota Must Stay Separate
The context locks these semantics:
- provider bytes are authoritative;
- metadata/search/index records do not consume document-storage quota;
- actual file bytes retained in DocuVault storage consume quota while present;
- transient streaming that is immediately discarded does not consume storage quota.
Planning consequence: Phase 12 must not create MinIO objects or increment quota during browse. A future byte-cache table/service should reference cloud items and account for actual retained bytes atomically in Phase 14. cloud_items.size_bytes is provider metadata, not DocuVault quota usage.
Recommended Architecture
Backend Modules
Keep routers thin and split the growing cloud monolith along current project conventions:
backend/storage/cloud_base.py: normalized enums/dataclasses or Pydantic-neutral domain types and the abstract cloud resource adapter.backend/storage/cloud_backend_factory.py: return resource-capable provider adapters by connection/provider.- Provider adapters: normalize metadata and capabilities close to provider-specific SDK/API responses.
backend/services/cloud_items.py: owner-scoped upsert/list/reconciliation service; raises domain errors, neverHTTPException.backend/api/cloud/connections.pyandbackend/api/cloud/browse.pyor an equivalent sub-router split using the project’s no-prefix sub-router rule.backend/api/schemas.pyorbackend/api/cloud/schemas.py: explicit whitelisted response models; never includecredentials_enc.
Avoid a generic repository abstraction unless it removes repeated owner-scoped queries. The important abstraction is the provider resource contract and normalized response.
Browse/Reconciliation Flow
- Authenticate regular user and resolve
connection_idwith(connection.id, connection.user_id). - Read durable child metadata for
(user_id, connection_id, parent_ref). - Return cached rows promptly with freshness fields and normalized capabilities.
- Refresh provider metadata in a bounded request/background path.
- Upsert observed resources by
(connection_id, provider_item_id); update parent/name/type/size/mtime/etag/version andlast_seen_at. - Reconcile disappeared children conservatively. Phase 12 may mark listing membership stale/deleted only after a successful complete listing; never delete rows after a failed or partial provider response.
- Preserve stable DocuVault cloud item UUIDs so the frontend can reconcile silently without losing selection/scroll.
The API should distinguish fresh, refreshing, and warning folder states and include last_refreshed_at/safe error metadata. Exact asynchronous transport (request-triggered task, Celery refresh, or immediate refresh after cached response) is planner discretion, but it must work across multiple backend instances and not rely solely on process memory.
Frontend Contract
StorageBrowser.vue stays the only grid. Prefer data-driven props:
- normalized rows with stable DocuVault IDs plus provider IDs hidden from presentation
- connection root/breadcrumb data
- action capability map with state/message/remedy metadata
- folder freshness state and last-successful timestamp
- byte-availability state (
cloud_onlynow; cache states become active in Phase 14)
Emit generic actions. CloudStorageView and CloudFolderView remain thin route/store data providers. Replace mode === 'local' action hiding with capability rendering; local mode can supply a local capability set so one rendering path serves both sources.
Disabled native HTML buttons do not receive click events, so the touch requirement needs an accessible wrapper/trigger or aria-disabled control that blocks execution while still accepting focus/tap for explanation. The plan must test keyboard focus and touch/click explanation behavior.
Security Threat Model Inputs
| Threat | Phase 12 mitigation to plan |
|---|---|
| IDOR across cloud connections | Resolve every connection by both UUID and current user; return 404 for foreign IDs |
| IDOR across cloud items | Query item by user and connection; never trust provider ID alone |
| Credential disclosure | Explicit response schemas; negative tests for credentials_enc, access tokens, refresh tokens, passwords |
| SSRF regression | Preserve validate_cloud_url() before all WebDAV/Nextcloud outbound requests and redirects |
| Cache/data collision | Include user and connection UUID in every durable/in-memory key |
| Destructive capability probing | Only provider declarations and non-mutating calls; tests assert no mutation methods invoked |
| Partial-list data loss | Reconcile removals only after a successful complete listing/page traversal |
| Admin content access | Continue get_regular_user; admin-negative tests for browse and metadata endpoints |
| Quota inflation/escape | Browse writes metadata only; assert quota and MinIO remain unchanged |
| Stored XSS through filenames/messages | Vue text rendering only; no v-html; provider errors mapped to controlled messages |
Provider-Specific Planning Notes
Google Drive
- Request fields explicitly: IDs, parents, name, mime type, size, modified time, version/md5 where applicable, capabilities, and pagination token.
- Handle all pages and shared-drive flags where intended; a partial first page must not trigger removals.
- Native Google Docs may have no byte size and require export later; metadata normalization must allow nullable/zero size without treating the item as a folder.
- Scope remediation belongs to Phase 13.
OneDrive
- Normalize
id,parentReference,file/folderfacets,size,lastModifiedDateTime,eTag, andcTagwhen present. - Follow
@odata.nextLink; incomplete pagination cannot be considered a complete reconciliation. - Preserve drive identity where IDs are only meaningful with a drive/account context.
Nextcloud/WebDAV
- Replace N+1
list()+info()where practical with a Depth-1 PROPFIND that requests needed properties in one response. - Normalize
getetag,getlastmodified,getcontentlength,getcontenttype, andresourcetypewhere available. - Paths can change on rename/move and require careful percent encoding; retain provider path as a reference/snapshot, not global identity.
- Treat missing properties as unknown, not proof that an action is unsupported.
Validation Architecture
Test Layers
| Layer | What to prove | Suggested location |
|---|---|---|
| Domain/unit | Capability merge/state/reason behavior; provider normalization; no destructive probes | backend/tests/test_cloud_capabilities.py |
| Service/unit | Owner-scoped upsert, stable identity, parent move/rename metadata update, complete-vs-partial reconciliation | backend/tests/test_cloud_items.py |
| DB integration | Constraints/indexes/cascades and same provider item IDs isolated across connections/users using PostgreSQL | integration-marked cloud item tests |
| API integration | Connection-ID routes, multiple same-provider accounts, foreign-owner/admin 404/403, credential exclusion, cached-first response schema | backend/tests/test_cloud.py or split cloud API tests |
| Provider contract | Google/OneDrive/WebDAV fixtures normalize to the same resource and capability schema; pagination is complete | backend/tests/test_cloud_backends.py plus focused adapter tests |
| Frontend unit | Connection-ID navigation, shared browser capabilities, disabled tooltip on focus/hover/tap, warning states, breadcrumb freshness | component/store/view Vitest files |
| Regression | Existing local browser actions and cloud connect/disconnect remain intact | current backend/frontend suites |
Required Commands
- Fast backend loop:
cd backend && pytest -q tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_capabilities.py tests/test_cloud_items.py - Full backend gate:
cd backend && pytest -v - Frontend focused loop:
cd frontend && npm test -- --run StorageBrowser CloudFolderView CloudStorageView cloudConnections - Full frontend gate:
cd frontend && npm test - Production build:
cd frontend && npm run build - PostgreSQL integration gate for schema/ownership constraints using the project integration environment.
Requirement-to-Evidence Map
| Requirement | Primary evidence |
|---|---|
| CONN-04 | Provider contract tests plus shared-browser capability state tests |
| CLOUD-01 | Connection-ID browse API and StorageBrowser integration tests |
| CLOUD-08 | Unsupported/warning explanation tests across pointer, keyboard, and touch behavior |
| CACHE-01 | Browse tests asserting no byte fetch, MinIO object, or quota mutation |
| CACHE-02 | Cloud item schema/service tests proving metadata/index placeholders persist independently |
| SYNC-01 | Provider normalization and durable metadata persistence tests for IDs, parent, size, mtime, etag/version |
Nyquist Rule
Every implementation task that creates a contract, service, endpoint, or component behavior should create or update its focused tests in the same plan. Do not defer all tests to a final plan. The final integration plan may add cross-layer and negative-security tests, but it must not be the first coverage for earlier functionality.
Planning Risks and Mitigations
- Schema overreach into Phase 14: Persist metadata and analysis/index placeholders, not byte-cache implementation or analysis jobs.
- Provider-specific branches leaking into UI: Normalize capabilities and items in backend/provider adapters.
- Same-provider account ambiguity: Connection UUID is mandatory in route, cache, and uniqueness boundaries.
- Silent OAuth mismatch: Surface insufficient scope; leave consent/reconnect implementation to Phase 13.
- False deletion from pagination/failure: Track complete successful listing before marking missing children absent.
- SQLite-only confidence: Run PostgreSQL integration tests for UUID/FK/index/uniqueness and ownership-sensitive queries.
- Tooltip inaccessible on touch: Use focusable/tappable
aria-disabledcontrols, not an inertdisabledbutton.
Recommended Plan Shape
- Cloud resource contract and schema: normalized capability/item types, migrations, owner-scoped cloud item service, tests.
- Provider normalization and durable browsing API: connection-ID routes, pagination/completeness, metadata reconciliation, security tests.
- Shared browser integration: connection roots, breadcrumbs/freshness, capability-aware actions and accessible explanations, frontend tests.
- Cross-layer verification and documentation: PostgreSQL integration, admin/owner negatives, no-byte/no-quota assertions, version/docs updates required by project protocol.
Plans 1 and frontend scaffolding can begin in parallel only if their shared response schema is locked first. Provider/API work depends on the domain schema. Final integration depends on all prior plans.
Primary References
- Microsoft Graph driveItem: https://learn.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0
- Microsoft Graph OneDrive resource model: https://learn.microsoft.com/en-us/graph/api/resources/onedrive?view=graph-rest-1.0
- Google Drive files resource and item capabilities: https://developers.google.com/workspace/drive/api/reference/rest/v3/files
- Google Drive OAuth scopes: https://developers.google.com/workspace/drive/api/guides/api-specific-auth
- Nextcloud WebDAV API: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html
- RFC 4918 WebDAV: https://www.rfc-editor.org/rfc/rfc4918
- Celery retry behavior: https://docs.celeryq.dev/en/stable/userguide/tasks.html#retrying