/** * Cloud Storage API — connections, OAuth initiation, folder browsing, config. * * Consumers: stores/cloudConnections.js, SettingsCloudTab.vue, * CloudCredentialModal.vue, CloudProviderTreeItem.vue, * CloudFolderTreeItem.vue */ import { request, jsonRequest } from './utils.js' export function listCloudConnections() { return request('/api/cloud/connections') } export function disconnectCloud(id) { return request(`/api/cloud/connections/${id}`, { method: 'DELETE' }) } export function connectWebDav(provider, serverUrl, username, password) { return jsonRequest('/api/cloud/connections/webdav', 'POST', { provider, server_url: serverUrl, username, password, }) } export function updateDefaultStorage(backend) { return jsonRequest('/api/users/me/default-storage', 'PATCH', { backend }) } /** * Browse a cloud folder by connection UUID and optional parent_ref. * connectionId is always a UUID; parentRef is a provider path string (default ''). */ export function getCloudFoldersByConnectionId(connectionId, parentRef = '') { const qs = parentRef ? `?parent_ref=${encodeURIComponent(parentRef)}` : '' return request(`/api/cloud/connections/${connectionId}/items${qs}`) } /** @deprecated Use getCloudFoldersByConnectionId instead. */ export function getCloudFolders(provider, folderId) { return request(`/api/cloud/folders/${provider}/${folderId}`) } /** * Rename a cloud connection's display name. */ export function renameCloudConnection(id, displayName) { return jsonRequest(`/api/cloud/connections/${id}`, 'PATCH', { display_name: displayName }) } /** * Initiate OAuth flow for Google Drive or OneDrive. * * Returns a JSON object {url: ""} from the backend. * The caller is responsible for navigating: window.location.href = data.url * * Using request() (not bare window.location.href) ensures the Bearer header * is injected and the 401→refresh retry path fires if the token has expired. * See plan 05-10 trust boundary: frontend→/api/cloud/oauth/initiate/{provider}. */ export function initiateOAuth(provider) { return request(`/api/cloud/oauth/initiate/${provider}`) } /** * Fetch non-secret configuration for a WebDAV/Nextcloud connection (edit flow). * * Returns {id, provider, server_url, connection_username} — never the password. * Used to pre-populate the Edit modal when re-editing an existing connection. */ export function getConnectionConfig(connectionId) { return request(`/api/cloud/connections/${connectionId}/config`) } /** * Update credentials for an existing WebDAV/Nextcloud connection (owner-scoped). * * Only fields included are changed. Omitting password preserves the existing secret. * Backend re-validates the URL and runs a health-check before committing. */ export function updateWebDavCredentials(connectionId, { serverUrl, username, password }) { const body = {} if (serverUrl !== undefined) body.server_url = serverUrl if (username !== undefined) body.username = username if (password !== undefined && password !== '') body.password = password return jsonRequest(`/api/cloud/connections/${connectionId}/credentials`, 'PUT', body) } // ── Phase 13: Connection health, reconnect, and test ───────────────────────── /** * Get the health status of a cloud connection without probing the provider. * * D-12: Health is available explicitly (not inferred from browse). * Returns {status, connection_id, provider, display_name}. * status: 'healthy' | 'degraded' | 'auth_failed' | 'offline' */ export function getConnectionHealth(connectionId) { return request(`/api/cloud/connections/${connectionId}/health`) } /** * Reconnect an AUTH_FAILED connection in-place. * * CONN-01: Patches the existing row — no new connection row created. * CONN-02: Re-encrypts refreshed credentials in place. * CONN-03: Response is credential-free. * D-14: Cached metadata preserved as stale; caches invalidated server-side. */ export function reconnectCloudConnection(connectionId) { return jsonRequest(`/api/cloud/connections/${connectionId}/reconnect`, 'POST', {}) } /** * Explicitly test a cloud connection by probing the provider. * * D-13: Explicit Test action — not triggered on every folder navigation. * Updates the connection status row and returns the new health status. */ export function testCloudConnection(connectionId) { return jsonRequest(`/api/cloud/connections/${connectionId}/test`, 'POST', {}) } // ── Phase 13: Authorized cloud content access ───────────────────────────────── /** * Open a cloud file via a DocuVault-authorized URL. * * D-02: Provider credentials and raw provider URLs are never exposed. * Returns {kind: 'open', url: ''}. * The caller must navigate to the returned URL — never window.open to a provider URL. * * @param {string} connectionId - Connection UUID * @param {string} itemId - Provider item ID (opaque reference) * @param {object} [fileContext] - Optional file metadata context (name, contentType) for display */ export function openCloudFile(connectionId, itemId, fileContext) { return request(`/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/open`) } /** * Get an in-app preview of a cloud file (binary formats only). * * D-18: Preview is binary-only. Unsupported formats return * {kind: 'unsupported_preview', reason: 'unsupported_format'} and the caller * should fall back to openCloudFile() for authorized download. * D-02: No raw provider URL or credentials in the response. */ export function previewCloudFile(connectionId, itemId) { return request(`/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/preview`) } // ── Phase 13: Cloud mutations ───────────────────────────────────────────────── /** * Create a folder in cloud storage. * * D-05: Collision auto-naming: 'Projects (1)', 'Projects (2)', etc. * Returns {kind: 'folder', provider_item_id, name} on success. * Returns {kind: 'unsupported_operation', reason: 'provider_unsupported'} when * the provider does not support folder creation. */ export function createCloudFolder(connectionId, parentRef, name) { return jsonRequest(`/api/cloud/connections/${connectionId}/folders`, 'POST', { parent_ref: parentRef, name, }) } /** * Rename a cloud item. * * D-05: Counter-suffix collision policy applies. * D-07: etag guards against operating on stale metadata. * Returns {kind: 'stale', reason: 'item_changed'} when externally changed. * Returns {kind: 'renamed', name} on success. */ export function renameCloudItem(connectionId, itemId, newName, etag) { return jsonRequest( `/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/rename`, 'PATCH', { new_name: newName, etag }, ) } /** * Move a cloud item within the same connection. * * D-08: Moves are restricted to the same cloud connection. * D-09: Self and descendant destinations are rejected. * Returns {kind: 'moved', parent_ref} on success. * Returns {kind: 'invalid_destination', reason: '...'} for invalid moves. */ export function moveCloudItem(connectionId, itemId, destinationParentRef, etag) { return jsonRequest( `/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/move`, 'POST', { destination_parent_ref: destinationParentRef, etag }, ) } /** * Delete a cloud item (trash preferred, permanent when unavailable). * * D-11: Response discloses whether trash or permanent delete was performed via * {kind: 'deleted', reason: 'trashed' | 'permanent'}. * D-10: Callers must confirm before calling — the API deletes immediately. */ export function deleteCloudItem(connectionId, itemId) { return request( `/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}`, { method: 'DELETE' }, ) } /** * Upload a file to a cloud folder. * * D-03: Same-name upload returns {kind: 'conflict', reason: 'name_collision'} — * never overwrites silently. * D-04: Caller manages the sequential upload queue; this function uploads one file. * * @param {string} connectionId - Connection UUID * @param {string} parentRef - Parent folder provider_item_id * @param {string} filename - Target filename * @param {File|Blob} file - File bytes * @param {'keep_both'|'replace'|undefined} conflictAction - Optional conflict resolution action */ export function uploadCloudFile(connectionId, parentRef, filename, file, conflictAction) { const formData = new FormData() formData.append('file', file, filename) formData.append('parent_ref', parentRef ?? '') formData.append('filename', filename) if (conflictAction) formData.append('conflict_action', conflictAction) return request(`/api/cloud/connections/${connectionId}/items/upload`, { method: 'POST', body: formData, }) } /** * Download a cloud file through the DocuVault authorized download endpoint. * * D-02: No raw provider URL or credential is exposed. * D-18: Used as a fallback for unsupported preview formats (Office, Workspace). * The authorized endpoint serves bytes through DocuVault's own proxy. * * Returns {kind: 'download', url: ''}. */ export function downloadCloudFile(connectionId, itemId) { return request( `/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/download`, { method: 'GET' }, ) } // ── Phase 14: Analysis API ──────────────────────────────────────────────────── /** * Estimate the scope of an analysis job for a cloud connection. * * T-14-04: No provider bytes are downloaded during estimate — metadata only. * Returns AnalysisEstimateOut: {supported_count, unsupported_count, total_provider_bytes, * recursive, is_partial, scope_kind} * * @param {string} connectionId - Connection UUID * @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean }} params */ export function estimateAnalysis(connectionId, params) { return jsonRequest(`/api/cloud/analysis/connections/${connectionId}/estimate`, 'POST', params) } /** * Enqueue an analysis job for a cloud connection scope. * * Returns AnalysisEnqueueOut: {job_id, status, total_count, queued_count, * already_current_count, unsupported_count} * * @param {string} connectionId - Connection UUID * @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean, * failure_behavior?: string }} params */ export function enqueueAnalysis(connectionId, params) { return jsonRequest(`/api/cloud/analysis/connections/${connectionId}/jobs`, 'POST', params) } /** * Get the status of an analysis job. * * T-14-02: Response never includes credentials or object_key. * * @param {string} jobId - Job UUID * @param {{ detail?: boolean }} [opts] */ export function getAnalysisJobStatus(jobId, opts = {}) { const qs = opts.detail ? '?detail=true' : '' return request(`/api/cloud/analysis/jobs/${jobId}${qs}`) } /** * List analysis jobs, optionally filtered by connection or status. * * @param {{ connection_id?: string, status?: string }} [filters] */ export function listAnalysisJobs(filters = {}) { const params = new URLSearchParams() if (filters.connection_id) params.set('connection_id', filters.connection_id) if (filters.status) params.set('status', filters.status) const qs = params.toString() ? `?${params}` : '' return request(`/api/cloud/analysis/jobs${qs}`) } /** * Cancel an entire analysis job batch. * * Returns AnalysisControlOut: {kind, reason, status} * * @param {string} jobId - Job UUID */ export function cancelAnalysisJob(jobId) { return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/cancel`, 'POST', {}) } /** * Cancel a single analysis job item. * * @param {string} jobId - Job UUID * @param {string} itemId - Item UUID */ export function cancelAnalysisItem(jobId, itemId) { return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/cancel`, 'POST', {}) } /** * Skip a queued or failed analysis job item. * * @param {string} jobId - Job UUID * @param {string} itemId - Item UUID */ export function skipAnalysisItem(jobId, itemId) { return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/skip`, 'POST', {}) } /** * Retry a failed analysis job item. * * @param {string} jobId - Job UUID * @param {string} itemId - Item UUID */ export function retryAnalysisItem(jobId, itemId) { return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/retry`, 'POST', {}) } /** * Get the current user cache status and analysis settings. * * Returns CacheStatusOut: { entry_count, total_bytes, cache_limit_bytes, * tier_cap_bytes, analysis_progress_detail, analysis_failure_behavior } * * Never includes object_key, credentials_enc, or raw provider URLs (T-14-02/T-14-08). */ export function getCacheSettings() { return request('/api/cloud/analysis/cache') } /** * Update the current user analysis settings and/or cache limit. * * PATCH /api/cloud/analysis/cache/settings * Body fields (all optional): cloud_cache_limit_bytes, analysis_progress_detail, * analysis_failure_behavior. * Returns fresh CacheStatusOut after update. * * @param {{ cloud_cache_limit_bytes?: number, analysis_progress_detail?: boolean, * analysis_failure_behavior?: string }} params */ export function updateCacheSettings(params) { return jsonRequest('/api/cloud/analysis/cache/settings', 'PATCH', params) }