import { ApiError, apiFetch, apiUrl, authHeaders, csrfToken, type ApiSettings, type FilesManagedFileLinkTarget } from "@govoplan/core-webui"; export type FileSpace = { id: string; label: string; owner_type: "user" | "group"; owner_id: string; description?: string | null; }; export type FileShare = { id: string; target_type: string; target_id: string; permission: string; created_at: string; revoked_at?: string | null; }; export type ManagedFile = { id: string; tenant_id: string; owner_type: "user" | "group"; owner_id: string; display_path: string; filename: string; description?: string | null; size_bytes: number; content_type?: string | null; checksum_sha256: string; version_id: string; created_at: string; updated_at: string; deleted_at?: string | null; audit_relevant: boolean; metadata?: Record | null; shares?: FileShare[]; }; export type FileListResponse = { files: ManagedFile[] }; export type FileSpacesResponse = { spaces: FileSpace[] }; export type FileUploadResponse = { files: ManagedFile[] }; export type FileUploadProgress = { loaded: number; total?: number; percentage: number | null }; export type FileFolder = { id: string; tenant_id: string; owner_type: "user" | "group"; owner_id: string; path: string; created_at: string; updated_at: string; deleted_at?: string | null; }; export type FileFoldersResponse = { folders: FileFolder[] }; export type FolderDeleteResponse = { deleted_folders: number; deleted_files: number }; export type BulkDeleteResponse = { deleted_count: number }; export type RenameResponse = { dry_run: boolean; items: { kind: "file" | "folder"; id: string; file_id?: string | null; folder_path?: string | null; old_path: string; new_path: string }[] }; export type TransferResponse = { operation: "move" | "copy"; files: number; folders: number }; export type ConflictAction = "overwrite" | "rename" | "skip"; export type ConflictStrategy = "reject" | "overwrite" | "rename"; export type ConflictResolution = { target_path: string; action: ConflictAction; new_path?: string }; export type PatternResolveResponse = { patterns: { pattern: string; matches: ManagedFile[] }[]; unmatched: ManagedFile[]; }; export function listFileSpaces(settings: ApiSettings): Promise { return apiFetch(settings, "/api/v1/files/spaces"); } export function listFolders(settings: ApiSettings, params: { owner_type: "user" | "group"; owner_id: string }): Promise { const search = new URLSearchParams(); search.set("owner_type", params.owner_type); search.set("owner_id", params.owner_id); return apiFetch(settings, `/api/v1/files/folders?${search.toString()}`); } export function createFolder( settings: ApiSettings, payload: { owner_type: "user" | "group"; owner_id: string; path: string } ): Promise { return apiFetch(settings, "/api/v1/files/folders", { method: "POST", body: JSON.stringify(payload) }); } export function deleteFolder( settings: ApiSettings, payload: { owner_type: "user" | "group"; owner_id: string; path: string; recursive?: boolean } ): Promise { return apiFetch(settings, "/api/v1/files/folders/delete", { method: "POST", body: JSON.stringify({ recursive: true, ...payload }) }); } export function listFiles(settings: ApiSettings, params: { owner_type?: string; owner_id?: string; campaign_id?: string; path_prefix?: string } = {}): Promise { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value) search.set(key, value); } const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files${suffix}`); } export async function uploadFiles( settings: ApiSettings, files: File[], options: { owner_type: "user" | "group"; owner_id: string; path?: string; campaign_id?: string; unpack_zip?: boolean; conflict_strategy?: ConflictStrategy; conflict_resolutions?: ConflictResolution[]; onProgress?: (progress: FileUploadProgress) => void; } ): Promise { const form = new FormData(); files.forEach((file) => form.append("files", file)); form.append("owner_type", options.owner_type); form.append("owner_id", options.owner_id); form.append("path", options.path ?? ""); if (options.campaign_id) form.append("campaign_id", options.campaign_id); if (options.unpack_zip) form.append("unpack_zip", "true"); if (options.conflict_strategy) form.append("conflict_strategy", options.conflict_strategy); if (options.conflict_resolutions?.length) form.append("conflict_resolutions_json", JSON.stringify(options.conflict_resolutions)); if (options.onProgress) return uploadFilesWithProgress(settings, form, options.onProgress); return apiFetch(settings, "/api/v1/files/upload", { method: "POST", body: form }); } function uploadFilesWithProgress( settings: ApiSettings, form: FormData, onProgress: (progress: FileUploadProgress) => void ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", apiUrl(settings, "/api/v1/files/upload")); xhr.withCredentials = true; for (const [key, value] of authHeaders(settings)) xhr.setRequestHeader(key, value); const csrf = csrfToken(); if (csrf) xhr.setRequestHeader("X-CSRF-Token", csrf); xhr.upload.onprogress = (event) => { const total = event.lengthComputable ? event.total : undefined; onProgress({ loaded: event.loaded, total, percentage: total && total > 0 ? Math.round((event.loaded / total) * 100) : null }); }; xhr.onerror = () => reject(new Error("Upload failed because the network request could not be completed.")); xhr.onabort = () => reject(new Error("Upload was cancelled.")); xhr.onload = () => { const responseText = xhr.responseText || ""; if (xhr.status < 200 || xhr.status >= 300) { reject(new ApiError(xhr.status, xhr.statusText, responseText)); return; } onProgress({ loaded: 1, total: 1, percentage: 100 }); if (xhr.status === 204 || !responseText) { resolve({ files: [] }); return; } const contentType = xhr.getResponseHeader("content-type") || ""; resolve(contentType.includes("application/json") ? JSON.parse(responseText) as FileUploadResponse : { files: [] }); }; onProgress({ loaded: 0, total: undefined, percentage: 0 }); xhr.send(form); }); } export function deleteFile(settings: ApiSettings, fileId: string): Promise { return apiFetch(settings, `/api/v1/files/${fileId}`, { method: "DELETE" }); } export function bulkDeleteFiles(settings: ApiSettings, fileIds: string[]): Promise { return apiFetch(settings, "/api/v1/files/bulk-delete", { method: "POST", body: JSON.stringify({ file_ids: fileIds }) }); } export type FileBulkShareResponse = { shares: FileShare[]; shared_count: number }; export function shareFilesWithTarget(settings: ApiSettings, fileIds: string[], target: FilesManagedFileLinkTarget): Promise { return apiFetch(settings, "/api/v1/files/bulk-shares", { method: "POST", body: JSON.stringify({ file_ids: fileIds, target_type: target.type, target_id: target.id, permission: target.permission ?? "read" }) }); } export function shareFilesWithCampaign(settings: ApiSettings, fileIds: string[], campaignId: string): Promise { return shareFilesWithTarget(settings, fileIds, { type: "campaign", id: campaignId, label: "campaign" }); } export async function shareFileWithCampaign(settings: ApiSettings, fileId: string, campaignId: string): Promise { const response = await shareFilesWithCampaign(settings, [fileId], campaignId); const share = response.shares[0]; if (!share) throw new Error("File share was not created."); return share; } export function bulkRenameFiles( settings: ApiSettings, payload: { file_ids: string[]; folder_paths?: string[]; owner_type?: "user" | "group"; owner_id?: string; mode: "direct" | "prefix" | "suffix" | "replace"; new_name?: string; find?: string; replacement?: string; prefix?: string; suffix?: string; recursive?: boolean; dry_run?: boolean } ): Promise { return apiFetch(settings, "/api/v1/files/bulk-rename", { method: "POST", body: JSON.stringify({ replacement: "", prefix: "", suffix: "", dry_run: true, ...payload }) }); } export function resolveFilePatterns( settings: ApiSettings, payload: { patterns: string[]; owner_type?: "user" | "group"; owner_id?: string; campaign_id?: string; path_prefix?: string; include_unmatched?: boolean; case_sensitive?: boolean } ): Promise { return apiFetch(settings, "/api/v1/files/resolve-patterns", { method: "POST", body: JSON.stringify(payload) }); } export function transferFiles( settings: ApiSettings, payload: { operation: "move" | "copy"; file_ids: string[]; folder_paths: string[]; source_owner_type: "user" | "group"; source_owner_id: string; target_owner_type: "user" | "group"; target_owner_id: string; target_folder: string; conflict_strategy?: ConflictStrategy; conflict_resolutions?: ConflictResolution[]; } ): Promise { return apiFetch(settings, "/api/v1/files/transfer", { method: "POST", body: JSON.stringify(payload) }); } export async function downloadFile(settings: ApiSettings, file: ManagedFile): Promise { const response = await fetch(apiUrl(settings, `/api/v1/files/${file.id}/download`), { headers: authHeaders(settings), credentials: "include" }); if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`); const blob = await response.blob(); triggerDownload(blob, file.filename); } export async function downloadFilesAsZip(settings: ApiSettings, fileIds: string[], filename = "files.zip"): Promise { const headers = authHeaders(settings); headers.set("Content-Type", "application/json"); const csrf = csrfToken(); if (csrf) headers.set("X-CSRF-Token", csrf); const response = await fetch(apiUrl(settings, "/api/v1/files/archive.zip"), { method: "POST", headers, credentials: "include", body: JSON.stringify({ file_ids: fileIds, filename }) }); if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`); const blob = await response.blob(); triggerDownload(blob, filename); } function triggerDownload(blob: Blob, filename: string): void { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); }