import { ApiError, apiFetch, apiUrl, authHeaders, csrfToken, type ApiSettings, type DeltaDeletedItem, type FilesManagedFileLinkTarget } from "@govoplan/core-webui"; export type FileSpace = { id: string; label: string; owner_type: "user" | "group"; owner_id: string; description?: string | null; space_type?: "managed" | "connector"; connector_space_id?: string | null; connector_profile_id?: string | null; provider?: string | null; library_id?: string | null; remote_path?: string | null; sync_mode?: string | null; read_only?: boolean; }; export type FileShare = { id: string; target_type: string; target_id: string; permission: string; created_at: string; revoked_at?: string | null; }; export type FileSourceProvenance = { source_type?: string | null; connector_id?: string | null; provider?: string | null; external_id?: string | null; external_path?: string | null; external_url?: string | null; revision?: string | null; revision_label?: string | null; observed_at?: string | null; imported_at?: string | null; metadata?: Record; }; export type FileConnectorPolicySource = { scope_type: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; label?: string | null; policy: Record; }; export type FileConnectorPolicyDecision = { allowed: boolean; reason?: string | null; source_path: unknown[]; requirements: string[]; details: Record; }; export type FileConnectorPolicyStep = { scope_type: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; path: string; label: string; applied_fields: string[]; policy: Record; }; export type FileConnectorPolicyResponse = { scope_type: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; policy: Record; effective_policy?: Record | null; parent_policy?: Record | null; effective_policy_sources?: FileConnectorPolicyStep[]; parent_policy_sources?: FileConnectorPolicyStep[]; }; export type FileConnectorProfile = { id: string; label: string; provider: string; endpoint_url?: string | null; base_path?: string | null; enabled: boolean; scope_type: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; source_path: string; credential_profile_id?: string | null; credential_profile_label?: string | null; credential_mode: string; credential_source?: string | null; credentials_configured: boolean; username?: string | null; capabilities: string[]; policy_sources: FileConnectorPolicySource[]; metadata: Record; source_kind?: string; }; export type FileConnectorCredential = { id: string; label: string; provider?: string | null; enabled: boolean; scope_type: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; source_path: string; credential_mode: string; credential_secret_source?: string | null; credentials_configured: boolean; username?: string | null; policy_sources: FileConnectorPolicySource[]; metadata: Record; source_kind?: string; }; export type FileConnectorCredentialsPayload = { username?: string | null; password?: string | null; token?: string | null; password_env?: string | null; token_env?: string | null; secret_ref?: string | null; }; export type FileConnectorDiscoveryCandidate = { endpoint_url: string; status: "failed" | "usable" | "found" | "credentials_rejected"; message: string; }; export type FileConnectorDiscoveryPayload = { provider: FileConnectorProfilePayload["provider"]; endpoint_url: string; base_path?: string | null; credential_mode?: FileConnectorProfilePayload["credential_mode"]; credentials?: FileConnectorCredentialsPayload; metadata?: Record; require_valid_credentials?: boolean; }; export type FileConnectorDiscoveryResponse = { provider: string; endpoint_url?: string | null; base_path?: string | null; status: "usable" | "found" | "credentials_rejected" | "not_found" | "unsupported"; message: string; candidates: FileConnectorDiscoveryCandidate[]; metadata: Record; }; export type FileConnectorProfilePayload = { id: string; label: string; provider: "seafile" | "nextcloud" | "webdav" | "smb" | "nfs" | "dms" | "generic"; endpoint_url?: string | null; base_path?: string | null; enabled?: boolean; scope_type?: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; credential_profile_id?: string | null; credential_mode?: "none" | "anonymous" | "basic" | "token" | "secret_ref"; credentials?: FileConnectorCredentialsPayload; capabilities?: string[]; policy?: Record; metadata?: Record; }; export type FileConnectorProfileUpdatePayload = Partial> & { clear_password?: boolean; clear_token?: boolean; }; export type FileConnectorCredentialPayload = { id: string; label: string; provider?: "seafile" | "nextcloud" | "webdav" | "smb" | "nfs" | "dms" | "generic" | null; enabled?: boolean; scope_type?: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; credential_mode?: "none" | "anonymous" | "basic" | "token" | "secret_ref"; credentials?: FileConnectorCredentialsPayload; policy?: Record; metadata?: Record; }; export type FileConnectorCredentialUpdatePayload = Partial> & { clear_password?: boolean; clear_token?: boolean; }; export type FileConnectorBrowseItem = { kind: "library" | "folder" | "file"; name: string; path: string; external_id?: string | null; external_url?: string | null; size_bytes?: number | null; content_type?: string | null; modified_at?: string | null; etag?: string | null; metadata: Record; }; export type FileConnectorBrowseResponse = { profile_id: string; provider: string; path: string; library_id?: string | null; read_only: boolean; decision: FileConnectorPolicyDecision; items: FileConnectorBrowseItem[]; }; export type FileConnectorProvider = { provider: string; label: string; protocol: string; implemented: boolean; installed: boolean; browse_supported: boolean; import_supported: boolean; optional_dependency?: string | null; permission_model: string; sync_strategy: string; conflict_strategy: string; preview_strategy: string; audit_events: string[]; notes?: string | null; }; export type FileConnectorSpace = { id: string; tenant_id: string; owner_type: "user" | "group"; owner_id: string; label: string; connector_profile_id: string; provider: string; library_id?: string | null; remote_path: string; sync_mode: string; read_only: boolean; is_active: boolean; created_at: string; updated_at: string; deleted_at?: string | null; metadata: Record; }; export type FileConnectorSettingsDeltaResponse = { profiles: FileConnectorProfile[]; credentials: FileConnectorCredential[]; spaces: FileConnectorSpace[]; policy?: FileConnectorPolicyResponse | null; changed_sections?: string[]; deleted: DeltaDeletedItem[]; watermark?: string | null; has_more: boolean; full: boolean; }; export type FileConnectorSpacePayload = { owner_type?: "user" | "group"; owner_id?: string | null; label: string; connector_profile_id: string; library_id?: string | null; remote_path?: string; sync_mode?: "manual"; metadata?: Record; }; 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; source_provenance?: FileSourceProvenance | null; source_revision?: string | null; shares?: FileShare[]; }; export type FileListResponse = {files: ManagedFile[];cursor?: string | null;next_cursor?: string | null;watermark?: string | null;}; export type FileDeltaDeletedItem = {id: string;resource_type?: string | null;revision?: string | null;deleted_at?: string | null;}; export type FileDeltaResponse = { files: ManagedFile[]; folders: FileFolder[]; deleted: FileDeltaDeletedItem[]; watermark?: string | null; has_more: boolean; full: boolean; }; export type ManagedFileSnapshotResponse = {files: ManagedFile[];folders: FileFolder[];watermark?: string | null;}; export type FileSpacesResponse = {spaces: FileSpace[];}; export type FileUploadResponse = {files: ManagedFile[];}; export type FileUploadProgress = {loaded: number;total?: number;percentage: number | null;}; export type FileConnectorFilePayload = { library_id: string; path: string; owner_type: "user" | "group"; owner_id?: string | null; target_folder?: string | null; target_path?: string | null; campaign_id?: string | null; conflict_strategy?: ConflictStrategy; source_revision?: string | null; metadata?: Record; }; export type FileConnectorSyncResponse = { file: ManagedFile; action: "created" | "updated" | "unchanged"; previous_version_id?: string | null; current_version_id: string; }; 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[];cursor?: string | null;next_cursor?: string | null;watermark?: string | null;}; const DEFAULT_MANAGED_FILE_WINDOW_SIZE = 500; 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;page_size?: number;cursor?: string | null;}): Promise { const search = new URLSearchParams(); search.set("owner_type", params.owner_type); search.set("owner_id", params.owner_id); if (params.page_size) search.set("page_size", String(params.page_size)); if (params.cursor) search.set("cursor", params.cursor); 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;page_size?: number;cursor?: string | null;} = {}): Promise { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value) search.set(key, String(value)); } const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files${suffix}`); } export function listFilesDelta( settings: ApiSettings, params: {owner_type?: string;owner_id?: string;campaign_id?: string;path_prefix?: string;since?: string;limit?: number;} = {}) : Promise { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== "") search.set(key, String(value)); } const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files/delta${suffix}`); } function applyDeltaToSnapshot( files: ManagedFile[], folders: FileFolder[], response: FileDeltaResponse) : {files: ManagedFile[];folders: FileFolder[];} { if (response.full) return { files: response.files, folders: response.folders }; const deletedFileIds = new Set(response.deleted.filter((item) => !item.resource_type || item.resource_type === "file").map((item) => item.id)); const deletedFolderIds = new Set(response.deleted.filter((item) => item.resource_type === "folder").map((item) => item.id)); const filesById = new Map(files.map((file) => [file.id, file])); const foldersById = new Map(folders.map((folder) => [folder.id, folder])); deletedFileIds.forEach((id) => filesById.delete(id)); deletedFolderIds.forEach((id) => foldersById.delete(id)); response.files.forEach((file) => filesById.set(file.id, file)); response.folders.forEach((folder) => foldersById.set(folder.id, folder)); return { files: Array.from(filesById.values()), folders: Array.from(foldersById.values()) }; } export async function listManagedFileSnapshot( settings: ApiSettings, params: {owner_type: "user" | "group";owner_id: string;path_prefix?: string;page_size?: number;}) : Promise { const pageSize = params.page_size ?? DEFAULT_MANAGED_FILE_WINDOW_SIZE; let watermark: string | null | undefined = null; let folderCursor: string | null | undefined = null; let folders: FileFolder[] = []; do { const response = await listFolders(settings, { owner_type: params.owner_type, owner_id: params.owner_id, page_size: pageSize, cursor: folderCursor }); watermark = watermark || response.watermark; folders = folders.concat(response.folders); folderCursor = response.next_cursor; } while (folderCursor); let fileCursor: string | null | undefined = null; let files: ManagedFile[] = []; do { const response = await listFiles(settings, { owner_type: params.owner_type, owner_id: params.owner_id, path_prefix: params.path_prefix, page_size: pageSize, cursor: fileCursor }); watermark = watermark || response.watermark; files = files.concat(response.files); fileCursor = response.next_cursor; } while (fileCursor); if (watermark) { let since = watermark; let response: FileDeltaResponse | null = null; do { response = await listFilesDelta(settings, { owner_type: params.owner_type, owner_id: params.owner_id, path_prefix: params.path_prefix, since, limit: pageSize }); const snapshot = applyDeltaToSnapshot(files, folders, response); files = snapshot.files; folders = snapshot.folders; since = response.watermark || ""; } while (response.has_more && response.watermark); watermark = since || watermark; } return { files, folders, watermark }; } 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[]; source_provenance?: FileSourceProvenance; source_revision?: string; connector_policy_sources?: FileConnectorPolicySource[]; 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.source_provenance) form.append("source_provenance_json", JSON.stringify(options.source_provenance)); if (options.source_revision) form.append("source_revision", options.source_revision); if (options.connector_policy_sources?.length) form.append("connector_policy_json", JSON.stringify({ sources: options.connector_policy_sources })); 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("i18n:govoplan-files.upload_failed_because_the_network_request_could_.360a5ab3")); xhr.onabort = () => reject(new Error("i18n:govoplan-files.upload_was_cancelled.5bab0f9b")); 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 type AccessExplanationUser = { id: string; account_id?: string | null; email?: string | null; display_name?: string | null; }; export type AccessDecisionProvenanceItem = { kind: string; id?: string | null; label?: string | null; tenant_id?: string | null; source?: string | null; details?: Record; }; export type ResourceAccessExplanationResponse = { user: AccessExplanationUser; resource_type: string; resource_id: string; action: string; provenance: AccessDecisionProvenanceItem[]; }; export function fetchResourceAccessExplanation( settings: ApiSettings, options: {userId: string;resourceType: string;resourceId: string;action: string;tenantId?: string | null;}) : Promise { const params = new URLSearchParams({ user_id: options.userId, resource_type: options.resourceType, resource_id: options.resourceId, action: options.action }); if (options.tenantId) params.set("tenant_id", options.tenantId); return apiFetch(settings, `/api/v1/admin/access/resource-explanation?${params.toString()}`); } export function virtualFolderResourceId(options: {tenantId: string;ownerType: "user" | "group";ownerId: string;path: string;}): string { return `virtual-folder:v1:${options.tenantId}:${options.ownerType}:${options.ownerId}:${base64Url(options.path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""))}`; } function base64Url(value: string): string { const bytes = new TextEncoder().encode(value); let binary = ""; bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } 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 evaluateFileConnectorPolicy( settings: ApiSettings, payload: {source_provenance: FileSourceProvenance;operation?: string;policy_sources?: FileConnectorPolicySource[];}) : Promise<{decision: FileConnectorPolicyDecision;}> { return apiFetch<{decision: FileConnectorPolicyDecision;}>(settings, "/api/v1/files/connector-policy/evaluate", { method: "POST", body: JSON.stringify({ operation: "access", policy_sources: [], ...payload }) }); } export function getFileConnectorPolicy( settings: ApiSettings, scopeType: "system" | "tenant" | "user" | "group" | "campaign", scopeId?: string | null) : Promise { const search = new URLSearchParams(); if (scopeId) search.set("scope_id", scopeId); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files/connectors/policies/${encodeURIComponent(scopeType)}${suffix}`); } export function fetchFileConnectorSettingsDelta( settings: ApiSettings, params: { scope_type?: "system" | "tenant" | "user" | "group" | "campaign"; scope_id?: string | null; provider?: string; campaign_id?: string | null; include_disabled?: boolean; include_inactive?: boolean; owner_type?: "user" | "group"; owner_id?: string; since?: string | null; limit?: number; } = {}) : Promise { const search = new URLSearchParams(); if (params.scope_type) search.set("scope_type", params.scope_type); if (params.scope_id) search.set("scope_id", params.scope_id); if (params.provider) search.set("provider", params.provider); if (params.campaign_id) search.set("campaign_id", params.campaign_id); if (params.include_disabled) search.set("include_disabled", "true"); if (params.include_inactive) search.set("include_inactive", "true"); if (params.owner_type) search.set("owner_type", params.owner_type); if (params.owner_id) search.set("owner_id", params.owner_id); if (params.since) search.set("since", params.since); if (params.limit) search.set("limit", String(params.limit)); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files/connectors/settings/delta${suffix}`); } export function updateFileConnectorPolicy( settings: ApiSettings, scopeType: "system" | "tenant" | "user" | "group" | "campaign", policy: Record, scopeId?: string | null) : Promise { const search = new URLSearchParams(); if (scopeId) search.set("scope_id", scopeId); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files/connectors/policies/${encodeURIComponent(scopeType)}${suffix}`, { method: "PUT", body: JSON.stringify({ policy }) }); } export function listFileConnectorProfiles( settings: ApiSettings, params: {provider?: string;campaign_id?: string;include_disabled?: boolean;} = {}) : Promise<{profiles: FileConnectorProfile[];}> { const search = new URLSearchParams(); if (params.provider) search.set("provider", params.provider); if (params.campaign_id) search.set("campaign_id", params.campaign_id); if (params.include_disabled) search.set("include_disabled", "true"); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch<{profiles: FileConnectorProfile[];}>(settings, `/api/v1/files/connectors/profiles${suffix}`); } export function listFileConnectorProviders(settings: ApiSettings): Promise<{providers: FileConnectorProvider[];}> { return apiFetch<{providers: FileConnectorProvider[];}>(settings, "/api/v1/files/connectors/providers"); } export function discoverFileConnectorEndpoint(settings: ApiSettings, payload: FileConnectorDiscoveryPayload): Promise { return apiFetch(settings, "/api/v1/files/connectors/discover", { method: "POST", body: JSON.stringify(payload) }); } export function listFileConnectorCredentials( settings: ApiSettings, params: {provider?: string;include_disabled?: boolean;} = {}) : Promise<{credentials: FileConnectorCredential[];}> { const search = new URLSearchParams(); if (params.provider) search.set("provider", params.provider); if (params.include_disabled) search.set("include_disabled", "true"); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch<{credentials: FileConnectorCredential[];}>(settings, `/api/v1/files/connectors/credentials${suffix}`); } export function createFileConnectorCredential(settings: ApiSettings, payload: FileConnectorCredentialPayload): Promise { return apiFetch(settings, "/api/v1/files/connectors/credentials", { method: "POST", body: JSON.stringify(payload) }); } export function updateFileConnectorCredential( settings: ApiSettings, credentialId: string, payload: FileConnectorCredentialUpdatePayload) : Promise { return apiFetch(settings, `/api/v1/files/connectors/credentials/${encodeURIComponent(credentialId)}`, { method: "PATCH", body: JSON.stringify(payload) }); } export function deactivateFileConnectorCredential(settings: ApiSettings, credentialId: string): Promise { return apiFetch(settings, `/api/v1/files/connectors/credentials/${encodeURIComponent(credentialId)}`, { method: "DELETE" }); } export function listFileConnectorSpaces( settings: ApiSettings, params: {owner_type?: "user" | "group";owner_id?: string;include_inactive?: boolean;} = {}) : Promise<{spaces: FileConnectorSpace[];}> { const search = new URLSearchParams(); if (params.owner_type) search.set("owner_type", params.owner_type); if (params.owner_id) search.set("owner_id", params.owner_id); if (params.include_inactive) search.set("include_inactive", "true"); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch<{spaces: FileConnectorSpace[];}>(settings, `/api/v1/files/connector-spaces${suffix}`); } export function createFileConnectorSpace(settings: ApiSettings, payload: FileConnectorSpacePayload): Promise { return apiFetch(settings, "/api/v1/files/connector-spaces", { method: "POST", body: JSON.stringify({ owner_type: "user", remote_path: "", sync_mode: "manual", metadata: {}, ...payload }) }); } export function updateFileConnectorSpace( settings: ApiSettings, spaceId: string, payload: Partial> & {is_active?: boolean;}) : Promise { return apiFetch(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}`, { method: "PATCH", body: JSON.stringify(payload) }); } export function deleteFileConnectorSpace(settings: ApiSettings, spaceId: string): Promise { return apiFetch(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}`, { method: "DELETE" }); } export function getFileConnectorProfile( settings: ApiSettings, profileId: string, params: {campaign_id?: string;include_disabled?: boolean;} = {}) : Promise { const search = new URLSearchParams(); if (params.campaign_id) search.set("campaign_id", params.campaign_id); if (params.include_disabled) search.set("include_disabled", "true"); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}${suffix}`); } export function createFileConnectorProfile(settings: ApiSettings, payload: FileConnectorProfilePayload): Promise { return apiFetch(settings, "/api/v1/files/connectors/profiles", { method: "POST", body: JSON.stringify(payload) }); } export function updateFileConnectorProfile( settings: ApiSettings, profileId: string, payload: FileConnectorProfileUpdatePayload) : Promise { return apiFetch(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}`, { method: "PATCH", body: JSON.stringify(payload) }); } export function deactivateFileConnectorProfile(settings: ApiSettings, profileId: string): Promise { return apiFetch(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}`, { method: "DELETE" }); } export function browseFileConnectorProfile( settings: ApiSettings, profileId: string, params: {path?: string;library_id?: string;campaign_id?: string;} = {}) : Promise { const search = new URLSearchParams(); if (params.path) search.set("path", params.path); if (params.library_id) search.set("library_id", params.library_id); if (params.campaign_id) search.set("campaign_id", params.campaign_id); const suffix = search.toString() ? `?${search.toString()}` : ""; return apiFetch(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/browse${suffix}`); } export function importFileConnectorFile( settings: ApiSettings, profileId: string, payload: FileConnectorFilePayload) : Promise { return apiFetch(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/import`, { method: "POST", body: JSON.stringify({ conflict_strategy: "reject", metadata: {}, ...payload }) }); } export function syncFileConnectorFile( settings: ApiSettings, profileId: string, payload: FileConnectorFilePayload) : Promise { return apiFetch(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/sync`, { method: "POST", body: JSON.stringify({ conflict_strategy: "rename", metadata: {}, ...payload }) }); } 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("i18n:govoplan-files.file_share_was_not_created.033ac476"); 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); }