270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
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<string, unknown> | 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<FileSpacesResponse> {
|
|
return apiFetch<FileSpacesResponse>(settings, "/api/v1/files/spaces");
|
|
}
|
|
|
|
|
|
export function listFolders(settings: ApiSettings, params: { owner_type: "user" | "group"; owner_id: string }): Promise<FileFoldersResponse> {
|
|
const search = new URLSearchParams();
|
|
search.set("owner_type", params.owner_type);
|
|
search.set("owner_id", params.owner_id);
|
|
return apiFetch<FileFoldersResponse>(settings, `/api/v1/files/folders?${search.toString()}`);
|
|
}
|
|
|
|
export function createFolder(
|
|
settings: ApiSettings,
|
|
payload: { owner_type: "user" | "group"; owner_id: string; path: string }
|
|
): Promise<FileFolder> {
|
|
return apiFetch<FileFolder>(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<FolderDeleteResponse> {
|
|
return apiFetch<FolderDeleteResponse>(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<FileListResponse> {
|
|
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<FileListResponse>(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<FileUploadResponse> {
|
|
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<FileUploadResponse>(settings, "/api/v1/files/upload", { method: "POST", body: form });
|
|
}
|
|
|
|
function uploadFilesWithProgress(
|
|
settings: ApiSettings,
|
|
form: FormData,
|
|
onProgress: (progress: FileUploadProgress) => void
|
|
): Promise<FileUploadResponse> {
|
|
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<BulkDeleteResponse> {
|
|
return apiFetch<BulkDeleteResponse>(settings, `/api/v1/files/${fileId}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function bulkDeleteFiles(settings: ApiSettings, fileIds: string[]): Promise<BulkDeleteResponse> {
|
|
return apiFetch<BulkDeleteResponse>(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<FileBulkShareResponse> {
|
|
return apiFetch<FileBulkShareResponse>(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<FileBulkShareResponse> {
|
|
return shareFilesWithTarget(settings, fileIds, { type: "campaign", id: campaignId, label: "campaign" });
|
|
}
|
|
|
|
export async function shareFileWithCampaign(settings: ApiSettings, fileId: string, campaignId: string): Promise<FileShare> {
|
|
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<RenameResponse> {
|
|
return apiFetch<RenameResponse>(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<PatternResolveResponse> {
|
|
return apiFetch<PatternResolveResponse>(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<TransferResponse> {
|
|
return apiFetch<TransferResponse>(settings, "/api/v1/files/transfer", { method: "POST", body: JSON.stringify(payload) });
|
|
}
|
|
|
|
export async function downloadFile(settings: ApiSettings, file: ManagedFile): Promise<void> {
|
|
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<void> {
|
|
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);
|
|
}
|