# 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

- `CloudConnection` already has a UUID primary key and `display_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.py` keys listings as `{user_id}:{provider}:{folder_id}`, so same-provider accounts would collide.
- `CloudStorageView.vue` correctly iterates connection IDs, but navigates with `conn.provider` and 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 result
- `capabilities()` 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`, UUID `user_id`, UUID `connection_id`
- provider-native `provider_item_id`, optional canonical path snapshot
- optional `parent_item_id` plus provider parent reference/path
- `name`, `item_type` (`file`/`folder`), optional `content_type`, `size_bytes`
- optional `etag`, `version`, provider `modified_at`
- `last_seen_at`, optional `deleted_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.capabilities` includes item-level booleans such as `canRename`, `canTrash`, and `canMoveItemWithinDrive`.
- Microsoft Graph `driveItem` supplies 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`, or `temporarily_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, never `HTTPException`.
- `backend/api/cloud/connections.py` and `backend/api/cloud/browse.py` or an equivalent sub-router split using the project’s no-prefix sub-router rule.
- `backend/api/schemas.py` or `backend/api/cloud/schemas.py`: explicit whitelisted response models; never include `credentials_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

1. Authenticate regular user and resolve `connection_id` with `(connection.id, connection.user_id)`.
2. Read durable child metadata for `(user_id, connection_id, parent_ref)`.
3. Return cached rows promptly with freshness fields and normalized capabilities.
4. Refresh provider metadata in a bounded request/background path.
5. Upsert observed resources by `(connection_id, provider_item_id)`; update parent/name/type/size/mtime/etag/version and `last_seen_at`.
6. 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.
7. 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_only` now; 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`/`folder` facets, `size`, `lastModifiedDateTime`, `eTag`, and `cTag` when 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`, and `resourcetype` where 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

1. **Schema overreach into Phase 14:** Persist metadata and analysis/index placeholders, not byte-cache implementation or analysis jobs.
2. **Provider-specific branches leaking into UI:** Normalize capabilities and items in backend/provider adapters.
3. **Same-provider account ambiguity:** Connection UUID is mandatory in route, cache, and uniqueness boundaries.
4. **Silent OAuth mismatch:** Surface insufficient scope; leave consent/reconnect implementation to Phase 13.
5. **False deletion from pagination/failure:** Track complete successful listing before marking missing children absent.
6. **SQLite-only confidence:** Run PostgreSQL integration tests for UUID/FK/index/uniqueness and ownership-sensitive queries.
7. **Tooltip inaccessible on touch:** Use focusable/tappable `aria-disabled` controls, not an inert `disabled` button.

## Recommended Plan Shape

1. **Cloud resource contract and schema:** normalized capability/item types, migrations, owner-scoped cloud item service, tests.
2. **Provider normalization and durable browsing API:** connection-ID routes, pagination/completeness, metadata reconciliation, security tests.
3. **Shared browser integration:** connection roots, breadcrumbs/freshness, capability-aware actions and accessible explanations, frontend tests.
4. **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

## RESEARCH COMPLETE
