922 lines
35 KiB
TypeScript
922 lines
35 KiB
TypeScript
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<string, unknown>;
|
|
};
|
|
|
|
export type FileConnectorPolicySource = {
|
|
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
|
|
scope_id?: string | null;
|
|
label?: string | null;
|
|
policy: Record<string, unknown>;
|
|
};
|
|
|
|
export type FileConnectorPolicyDecision = {
|
|
allowed: boolean;
|
|
reason?: string | null;
|
|
source_path: unknown[];
|
|
requirements: string[];
|
|
details: Record<string, unknown>;
|
|
};
|
|
|
|
export type FileConnectorPolicyStep = {
|
|
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
|
|
scope_id?: string | null;
|
|
path: string;
|
|
label: string;
|
|
applied_fields: string[];
|
|
policy: Record<string, unknown>;
|
|
};
|
|
|
|
export type FileConnectorPolicyResponse = {
|
|
scope_type: "system" | "tenant" | "user" | "group" | "campaign";
|
|
scope_id?: string | null;
|
|
policy: Record<string, unknown>;
|
|
effective_policy?: Record<string, unknown> | null;
|
|
parent_policy?: Record<string, unknown> | 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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
};
|
|
|
|
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<string, unknown>;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type FileConnectorProfileUpdatePayload = Partial<Omit<FileConnectorProfilePayload, "id" | "scope_type" | "scope_id">> & {
|
|
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<string, unknown>;
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export type FileConnectorCredentialUpdatePayload = Partial<Omit<FileConnectorCredentialPayload, "id" | "scope_type" | "scope_id">> & {
|
|
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<string, unknown>;
|
|
};
|
|
|
|
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<string, unknown>;
|
|
};
|
|
|
|
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<string, unknown>;
|
|
};
|
|
|
|
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;
|
|
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<string, unknown>;
|
|
};
|
|
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<FileSpacesResponse> {
|
|
return apiFetch<FileSpacesResponse>(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<FileFoldersResponse> {
|
|
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<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;page_size?: number;cursor?: string | null;} = {}): Promise<FileListResponse> {
|
|
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<FileListResponse>(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<FileDeltaResponse> {
|
|
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<FileDeltaResponse>(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<ManagedFileSnapshotResponse> {
|
|
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<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.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<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("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<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 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<string, unknown>;
|
|
};
|
|
|
|
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<ResourceAccessExplanationResponse> {
|
|
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<ResourceAccessExplanationResponse>(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<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 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<FileConnectorPolicyResponse> {
|
|
const search = new URLSearchParams();
|
|
if (scopeId) search.set("scope_id", scopeId);
|
|
const suffix = search.toString() ? `?${search.toString()}` : "";
|
|
return apiFetch<FileConnectorPolicyResponse>(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<FileConnectorSettingsDeltaResponse> {
|
|
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<FileConnectorSettingsDeltaResponse>(settings, `/api/v1/files/connectors/settings/delta${suffix}`);
|
|
}
|
|
|
|
export function updateFileConnectorPolicy(
|
|
settings: ApiSettings,
|
|
scopeType: "system" | "tenant" | "user" | "group" | "campaign",
|
|
policy: Record<string, unknown>,
|
|
scopeId?: string | null)
|
|
: Promise<FileConnectorPolicyResponse> {
|
|
const search = new URLSearchParams();
|
|
if (scopeId) search.set("scope_id", scopeId);
|
|
const suffix = search.toString() ? `?${search.toString()}` : "";
|
|
return apiFetch<FileConnectorPolicyResponse>(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<FileConnectorDiscoveryResponse> {
|
|
return apiFetch<FileConnectorDiscoveryResponse>(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<FileConnectorCredential> {
|
|
return apiFetch<FileConnectorCredential>(settings, "/api/v1/files/connectors/credentials", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function updateFileConnectorCredential(
|
|
settings: ApiSettings,
|
|
credentialId: string,
|
|
payload: FileConnectorCredentialUpdatePayload)
|
|
: Promise<FileConnectorCredential> {
|
|
return apiFetch<FileConnectorCredential>(settings, `/api/v1/files/connectors/credentials/${encodeURIComponent(credentialId)}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function deactivateFileConnectorCredential(settings: ApiSettings, credentialId: string): Promise<FileConnectorCredential> {
|
|
return apiFetch<FileConnectorCredential>(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<FileConnectorSpace> {
|
|
return apiFetch<FileConnectorSpace>(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<Omit<FileConnectorSpacePayload, "connector_profile_id" | "owner_type" | "owner_id">> & {is_active?: boolean;})
|
|
: Promise<FileConnectorSpace> {
|
|
return apiFetch<FileConnectorSpace>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function deleteFileConnectorSpace(settings: ApiSettings, spaceId: string): Promise<FileConnectorSpace> {
|
|
return apiFetch<FileConnectorSpace>(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<FileConnectorProfile> {
|
|
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<FileConnectorProfile>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}${suffix}`);
|
|
}
|
|
|
|
export function createFileConnectorProfile(settings: ApiSettings, payload: FileConnectorProfilePayload): Promise<FileConnectorProfile> {
|
|
return apiFetch<FileConnectorProfile>(settings, "/api/v1/files/connectors/profiles", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function updateFileConnectorProfile(
|
|
settings: ApiSettings,
|
|
profileId: string,
|
|
payload: FileConnectorProfileUpdatePayload)
|
|
: Promise<FileConnectorProfile> {
|
|
return apiFetch<FileConnectorProfile>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function deactivateFileConnectorProfile(settings: ApiSettings, profileId: string): Promise<FileConnectorProfile> {
|
|
return apiFetch<FileConnectorProfile>(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<FileConnectorBrowseResponse> {
|
|
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<FileConnectorBrowseResponse>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/browse${suffix}`);
|
|
}
|
|
|
|
export function importFileConnectorFile(
|
|
settings: ApiSettings,
|
|
profileId: string,
|
|
payload: FileConnectorFilePayload)
|
|
: Promise<FileUploadResponse> {
|
|
return apiFetch<FileUploadResponse>(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<FileConnectorSyncResponse> {
|
|
return apiFetch<FileConnectorSyncResponse>(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<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("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<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);
|
|
}
|