- Add estimateAnalysis, enqueueAnalysis, getAnalysisJobStatus, listAnalysisJobs, cancelAnalysisJob, cancelAnalysisItem, skipAnalysisItem, retryAnalysisItem, getCacheSettings, updateCacheSettings to api/cloud.js - Extend cloudConnections store with analysis state: activeAnalysisJob, analysisQueue (computed), failureBehavior, detailedAnalysisProgress, cacheLimit - Add store actions: requestEstimate, enqueueAnalysis, fetchJobStatus, cancelJob, retryItem, skipItem, cancelItem, setFailureBehavior, setDetailedAnalysisProgress, updateCacheSettings - Add translateAnalysisStatus (D-06: single translation source for components) - Status labels: simplified (waiting/working/done/skipped/failed) and detailed modes - Credentials never stored in queue state (T-14-02)
373 lines
13 KiB
JavaScript
373 lines
13 KiB
JavaScript
/**
|
|
* 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: "<authorization_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: '<authorized-docuvault-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: '<docuvault-relative-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 settings.
|
|
*
|
|
* Returns { cloud_cache_limit_bytes, ... }
|
|
*/
|
|
export function getCacheSettings() {
|
|
return request('/api/users/me/settings')
|
|
}
|
|
|
|
/**
|
|
* Update the current user cache settings.
|
|
*
|
|
* @param {{ cloud_cache_limit_bytes?: number }} params
|
|
*/
|
|
export function updateCacheSettings(params) {
|
|
return jsonRequest('/api/users/me/settings', 'PATCH', params)
|
|
}
|