feat: govern file lifecycle and connector writes
This commit is contained in:
@@ -295,6 +295,7 @@ export type FileConnectorProvider = {
|
||||
installed: boolean;
|
||||
browse_supported: boolean;
|
||||
import_supported: boolean;
|
||||
write_supported: boolean;
|
||||
optional_dependency?: string | null;
|
||||
permission_model: string;
|
||||
sync_strategy: string;
|
||||
@@ -343,6 +344,7 @@ export type FileConnectorSpacePayload = {
|
||||
library_id?: string | null;
|
||||
remote_path?: string;
|
||||
sync_mode?: "manual";
|
||||
read_only?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@@ -361,6 +363,10 @@ export type ManagedFile = {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at?: string | null;
|
||||
retained_until?: string | null;
|
||||
legal_hold?: boolean;
|
||||
lifecycle_revision?: number;
|
||||
lifecycle_reason?: string | null;
|
||||
audit_relevant: boolean;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
source_provenance?: FileSourceProvenance | null;
|
||||
@@ -420,6 +426,22 @@ export type FileConnectorSyncResponse = {
|
||||
previous_version_id?: string | null;
|
||||
current_version_id: string;
|
||||
};
|
||||
export type FileConnectorWritePayload = {
|
||||
file_id: string;
|
||||
remote_path: string;
|
||||
idempotency_key: string;
|
||||
expected_revision?: string | null;
|
||||
};
|
||||
export type FileConnectorWriteResponse = {
|
||||
recovery_operation_id: string;
|
||||
status: string;
|
||||
replayed: boolean;
|
||||
provider: string;
|
||||
remote_path: string;
|
||||
revision?: string | null;
|
||||
checksum_sha256: string;
|
||||
size_bytes: number;
|
||||
};
|
||||
export type FileFolder = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
@@ -434,7 +456,49 @@ export type FileFoldersResponse = {folders: FileFolder[];cursor?: string | null;
|
||||
|
||||
const DEFAULT_MANAGED_FILE_WINDOW_SIZE = 500;
|
||||
export type FolderDeleteResponse = {deleted_folders: number;deleted_files: number;};
|
||||
export type FolderRestoreResponse = {restored_folders: number;restored_files: number;};
|
||||
export type BulkDeleteResponse = {deleted_count: number;};
|
||||
export type FileRestoreResponse = {restored_count: number;};
|
||||
export type FileLifecycleUpdatePayload = {
|
||||
retained_until?: string | null;
|
||||
legal_hold: boolean;
|
||||
reason: string;
|
||||
expected_revision: number;
|
||||
};
|
||||
export type FilePurgePreviewItem = {
|
||||
file_id: string;
|
||||
filename: string;
|
||||
lifecycle_revision: number;
|
||||
deleted_at?: string | null;
|
||||
retained_until?: string | null;
|
||||
legal_hold: boolean;
|
||||
blockers: string[];
|
||||
blob_ids: string[];
|
||||
};
|
||||
export type FilePurgePreviewResponse = {
|
||||
preview_sha256: string;
|
||||
eligible: boolean;
|
||||
items: FilePurgePreviewItem[];
|
||||
};
|
||||
export type FilePurgeExecutePayload = {
|
||||
file_ids: string[];
|
||||
preview_sha256: string;
|
||||
idempotency_key: string;
|
||||
approval_reference: string;
|
||||
confirmation: "PURGE";
|
||||
};
|
||||
export type FilePurgeResponse = {
|
||||
recovery_operation_id: string;
|
||||
status: string;
|
||||
replayed: boolean;
|
||||
purged_files: number;
|
||||
released_blobs: number;
|
||||
};
|
||||
export type FileBlobGcResponse = {
|
||||
inspected_blobs: number;
|
||||
deleted_blobs: number;
|
||||
unresolved_operation_ids: string[];
|
||||
};
|
||||
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";
|
||||
@@ -748,6 +812,55 @@ export function bulkDeleteFiles(settings: ApiSettings, fileIds: string[]): Promi
|
||||
return apiFetch<BulkDeleteResponse>(settings, "/api/v1/files/bulk-delete", { method: "POST", body: JSON.stringify({ file_ids: fileIds }) });
|
||||
}
|
||||
|
||||
export function updateFileLifecycle(
|
||||
settings: ApiSettings,
|
||||
fileId: string,
|
||||
payload: FileLifecycleUpdatePayload)
|
||||
: Promise<ManagedFile> {
|
||||
return apiFetch<ManagedFile>(settings, `/api/v1/files/${encodeURIComponent(fileId)}/lifecycle`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreFile(settings: ApiSettings, fileId: string): Promise<FileRestoreResponse> {
|
||||
return apiFetch<FileRestoreResponse>(settings, `/api/v1/files/assets/${encodeURIComponent(fileId)}/restore`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function restoreFolder(
|
||||
settings: ApiSettings,
|
||||
payload: {owner_type: "user" | "group";owner_id: string;path: string;recursive?: boolean;})
|
||||
: Promise<FolderRestoreResponse> {
|
||||
return apiFetch<FolderRestoreResponse>(settings, "/api/v1/files/folders/restore", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ recursive: true, ...payload })
|
||||
});
|
||||
}
|
||||
|
||||
export function previewFilePurge(settings: ApiSettings, fileIds: string[]): Promise<FilePurgePreviewResponse> {
|
||||
return apiFetch<FilePurgePreviewResponse>(settings, "/api/v1/files/purge/preview", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ file_ids: fileIds })
|
||||
});
|
||||
}
|
||||
|
||||
export function executeFilePurge(settings: ApiSettings, payload: FilePurgeExecutePayload): Promise<FilePurgeResponse> {
|
||||
return apiFetch<FilePurgeResponse>(settings, "/api/v1/files/purge/execute", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function garbageCollectFileBlobs(
|
||||
settings: ApiSettings,
|
||||
payload: {limit?: number;approval_reference: string;})
|
||||
: Promise<FileBlobGcResponse> {
|
||||
return apiFetch<FileBlobGcResponse>(settings, "/api/v1/files/purge/blobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export type FileBulkShareResponse = {shares: FileShare[];shared_count: number;};
|
||||
export type FileSharePayload = {
|
||||
target_type: "user" | "group" | "tenant" | "campaign";
|
||||
@@ -978,6 +1091,21 @@ export function deleteFileConnectorSpace(settings: ApiSettings, spaceId: string)
|
||||
return apiFetch<FileConnectorSpace>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function restoreFileConnectorSpace(settings: ApiSettings, spaceId: string): Promise<FileConnectorSpace> {
|
||||
return apiFetch<FileConnectorSpace>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}/restore`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function writeBackFileConnectorSpace(
|
||||
settings: ApiSettings,
|
||||
spaceId: string,
|
||||
payload: FileConnectorWritePayload)
|
||||
: Promise<FileConnectorWriteResponse> {
|
||||
return apiFetch<FileConnectorWriteResponse>(settings, `/api/v1/files/connector-spaces/${encodeURIComponent(spaceId)}/write-back`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function getFileConnectorProfile(
|
||||
settings: ApiSettings,
|
||||
profileId: string,
|
||||
|
||||
@@ -76,6 +76,7 @@ type ConnectorProfileDraft = {
|
||||
canBrowse: boolean;
|
||||
canImport: boolean;
|
||||
canSync: boolean;
|
||||
canWriteRemote: boolean;
|
||||
policyMode: PolicyMode;
|
||||
allowedPaths: string;
|
||||
deniedPaths: string;
|
||||
@@ -509,7 +510,8 @@ export default function FileConnectorSettingsPanel({
|
||||
const capabilities = [
|
||||
draft.canBrowse ? "browse" : "",
|
||||
draft.canImport ? "import" : "",
|
||||
draft.canSync ? "sync" : ""].
|
||||
draft.canSync ? "sync" : "",
|
||||
draft.canWriteRemote ? "write" : ""].
|
||||
filter(Boolean);
|
||||
const sharedPayload = {
|
||||
label,
|
||||
@@ -1365,6 +1367,7 @@ export default function FileConnectorSettingsPanel({
|
||||
<ToggleSwitch label="i18n:govoplan-files.browse.2f3b5c55" checked={draft.canBrowse} disabled={saving} onChange={(canBrowse) => patchDraft({ canBrowse })} />
|
||||
<ToggleSwitch label="i18n:govoplan-files.import.d6fbc9d2" checked={draft.canImport} disabled={saving} onChange={(canImport) => patchDraft({ canImport })} />
|
||||
<ToggleSwitch label="i18n:govoplan-files.sync.905f6309" checked={draft.canSync} disabled={saving} onChange={(canSync) => patchDraft({ canSync })} />
|
||||
<ToggleSwitch label="Remote write" checked={draft.canWriteRemote} disabled={saving || draft.provider !== "s3"} onChange={(canWriteRemote) => patchDraft({ canWriteRemote })} />
|
||||
</div>
|
||||
<FormGrid columns={2} collapseAt="standard" className="">
|
||||
<FormField label="i18n:govoplan-files.policy.bb9cf141" documentation={CONNECTOR_DOCUMENTATION}>
|
||||
@@ -1528,6 +1531,7 @@ function emptyDraft(scopeType: ConnectorScope): ConnectorProfileDraft {
|
||||
canBrowse: true,
|
||||
canImport: true,
|
||||
canSync: true,
|
||||
canWriteRemote: false,
|
||||
policyMode: "allow-provider",
|
||||
allowedPaths: "",
|
||||
deniedPaths: "",
|
||||
@@ -1566,6 +1570,7 @@ function draftFromProfile(profile: FileConnectorProfile, scopeType: ConnectorSco
|
||||
canBrowse: profile.capabilities.includes("browse"),
|
||||
canImport: profile.capabilities.includes("import"),
|
||||
canSync: profile.capabilities.includes("sync"),
|
||||
canWriteRemote: profile.capabilities.includes("write"),
|
||||
policyMode: denyProviders.includes("*") || denyProviders.includes(profile.provider) ? "deny-provider" : allowPaths.length > 0 ? "allowed-paths" : "allow-provider",
|
||||
allowedPaths: allowPaths.join("\n"),
|
||||
deniedPaths: denyPaths.join("\n"),
|
||||
|
||||
@@ -167,6 +167,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const [connectorLoading, setConnectorLoading] = useState(false);
|
||||
const [connectorError, setConnectorError] = useState("");
|
||||
const [connectorSpaceLabel, setConnectorSpaceLabel] = useState("");
|
||||
const [connectorSpaceReadOnly, setConnectorSpaceReadOnly] = useState(true);
|
||||
const [connectorSpaceOwnerSpaceId, setConnectorSpaceOwnerSpaceId] = useState("");
|
||||
const [connectorSpaceItemsBySpace, setConnectorSpaceItemsBySpace] = useState<Record<string, FileConnectorBrowseItem[]>>({});
|
||||
const [connectorSpaceLibraryBySpace, setConnectorSpaceLibraryBySpace] = useState<Record<string, string | null>>({});
|
||||
@@ -584,6 +585,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
setConnectorError("");
|
||||
setConnectorSelectedItem(null);
|
||||
setConnectorSpaceLabel("");
|
||||
setConnectorSpaceReadOnly(true);
|
||||
resetArchiveUploadState();
|
||||
}
|
||||
|
||||
@@ -1103,6 +1105,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
library_id: connectorLibraryId || undefined,
|
||||
remote_path: remotePath,
|
||||
sync_mode: "manual",
|
||||
read_only: connectorSpaceReadOnly,
|
||||
metadata: {
|
||||
created_from_ui: true,
|
||||
source_label: activeConnectorProfile?.label,
|
||||
@@ -2903,6 +2906,19 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
<FormField label="i18n:govoplan-files.space_label.ec892726">
|
||||
<input value={connectorSpaceLabel} onChange={(event) => setConnectorSpaceLabel(event.target.value)} disabled={busy} placeholder={connectorSpaceSuggestedLabel()} />
|
||||
</FormField>
|
||||
<FormField label="Connector mode" help="Two-way mode permits explicit writes only. Remote deletion, rename, and move propagation remain disabled.">
|
||||
<select
|
||||
value={connectorSpaceReadOnly ? "read-only" : "two-way"}
|
||||
onChange={(event) => setConnectorSpaceReadOnly(event.target.value !== "two-way")}
|
||||
disabled={busy}>
|
||||
<option value="read-only">Read-only</option>
|
||||
<option
|
||||
value="two-way"
|
||||
disabled={activeConnectorProfile?.provider !== "s3" || !activeConnectorProfile.capabilities.includes("write")}>
|
||||
Two-way (S3, explicit writes)
|
||||
</option>
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="connector-browser">
|
||||
|
||||
Reference in New Issue
Block a user