Files

289 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
apiRequest,
ApiRequestError,
createUuid,
type ApiErrorEnvelope,
} from "./_shared";
export type WorkspaceDirectory = {
path: string;
name: string;
parent_path: string;
owner_user_id: string;
has_children?: boolean;
};
export type ResourceItem = {
resource_id: string;
workspace_id: string;
storage_object_id: string;
owner_user_id: string;
owner_display_name?: string | null;
resource_name: string;
description: string | null;
visibility: "private" | "workspace" | "public";
status: string;
created_at: string;
updated_at: string;
file: {
file_name: string;
file_extension: string | null;
mime_type: string | null;
size_bytes: number;
content_hash: string | null;
object_status: string;
};
jupyter_accessible_path: string;
absolute_path: string;
};
export async function listResources(
workspaceId: string,
parentPath: string = "",
opts?: { visibility?: string; keyword?: string; ownerUserId?: string },
): Promise<ResourceItem[]> {
// Default (no ownerUserId) scopes to the requester's own object_key
// subtree; passing ownerUserId scopes to that owner's subtree so the tree
// can lazily fetch another member's data resources on group expand.
const parameters = new URLSearchParams();
if (parentPath) parameters.set("parent_path", parentPath);
if (opts?.visibility) parameters.set("visibility", opts.visibility);
if (opts?.keyword) parameters.set("keyword", opts.keyword);
if (opts?.ownerUserId) parameters.set("owner_user_id", opts.ownerUserId);
const query = parameters.toString();
// apiRequest<T> already unwraps the envelope's `data` field, so we
// request `ResourceItem[]` directly here (matching listScripts).
return apiRequest<ResourceItem[]>(
`/api/v1/data-resources${query ? `?${query}` : ""}`,
{},
workspaceId,
);
}
export async function deleteResource(
workspaceId: string,
resourceId: string,
): Promise<{ resource_id: string; status: string }> {
return apiRequest<{ resource_id: string; status: string }>(
`/api/v1/data-resources/${encodeURIComponent(resourceId)}`,
{ method: "DELETE" },
workspaceId,
);
}
/** 同源流式下载数据资源字节,供 Excel 等预览器使用。 */
export async function fetchResourceContentFile(
workspaceId: string,
resourceId: string,
fileName: string,
signal?: AbortSignal,
): Promise<File> {
const response = await fetch(
`/api/v1/data-resources/${encodeURIComponent(resourceId)}/content?workspace_id=${encodeURIComponent(workspaceId)}`,
{
credentials: "same-origin",
signal,
headers: {
"X-Request-ID": createUuid().replaceAll("-", ""),
},
},
);
if (response.status === 401 && typeof window !== "undefined") {
const here = window.location.pathname;
if (here !== "/login") {
window.location.assign("/login");
}
throw new ApiRequestError("未登录或登录已过期", 401);
}
if (!response.ok) {
let message = `请求失败(HTTP ${response.status}`;
try {
const payload = (await response.json()) as ApiErrorEnvelope;
const detailMessage =
typeof payload.detail === "string"
? payload.detail
: payload.detail?.message;
if (detailMessage) message = detailMessage;
} catch {
/* ignore non-JSON error bodies */
}
throw new ApiRequestError(message, response.status);
}
const blob = await response.blob();
return new File([blob], fileName, {
type: blob.type || "application/octet-stream",
});
}
export type ResourcePreviewPayload = {
kind: "table";
columns: string[];
rows: string[][];
row_count: number;
truncated: boolean;
delimiter: string;
};
/** 表格类数据资源抽样预览(csv / tsv)。 */
export async function fetchResourcePreview(
workspaceId: string,
resourceId: string,
input: { limit?: number } = {},
signal?: AbortSignal,
): Promise<ResourcePreviewPayload> {
const parameters = new URLSearchParams();
parameters.set("workspace_id", workspaceId);
if (input.limit != null) parameters.set("limit", String(input.limit));
const response = await fetch(
`/api/v1/data-resources/${encodeURIComponent(resourceId)}/preview?${parameters.toString()}`,
{
credentials: "same-origin",
signal,
headers: {
"X-Request-ID": createUuid().replaceAll("-", ""),
},
},
);
if (response.status === 401 && typeof window !== "undefined") {
const here = window.location.pathname;
if (here !== "/login") {
window.location.assign("/login");
}
throw new ApiRequestError("未登录或登录已过期", 401);
}
const payload = await response.json();
if (!response.ok) {
const error = payload as ApiErrorEnvelope;
const detailMessage =
typeof error.detail === "string" ? error.detail : error.detail?.message;
throw new ApiRequestError(
detailMessage ?? `请求失败(HTTP ${response.status}`,
response.status,
);
}
const data = (payload as { data: ResourcePreviewPayload }).data;
if (!data || data.kind !== "table" || !Array.isArray(data.columns)) {
throw new ApiRequestError("响应数据格式错误", response.status);
}
return data;
}
export async function createResourceUpload(
workspaceId: string,
body: {
file_name: string;
content_type: string;
expected_size_bytes: number;
expected_hash: string | null;
target_path?: string;
},
): Promise<{ upload_id: string; upload_path: string }> {
return apiRequest<{ upload_id: string; upload_path: string }>(
"/api/v1/data-resources/uploads",
{
method: "POST",
body: JSON.stringify(body),
headers: { "Idempotency-Key": createUuid().replaceAll("-", "") },
},
workspaceId,
);
}
export async function uploadResourceBytes(
workspaceId: string,
uploadId: string,
fileBytes: ArrayBuffer | Blob,
contentType: string,
): Promise<{ storage_object_id: string }> {
return apiRequest<{ storage_object_id: string }>(
`/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}`,
{
method: "PUT",
headers: { "Content-Type": contentType },
body: fileBytes,
},
workspaceId,
);
}
export async function bindResourceUpload(
workspaceId: string,
uploadId: string,
body: {
resource_name: string;
description: string;
visibility: "private" | "workspace" | "public";
},
): Promise<ResourceItem> {
return apiRequest<ResourceItem>(
`/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}/bind`,
{
method: "POST",
body: JSON.stringify(body),
},
workspaceId,
);
}
export async function listWorkspaceDirectories(
workspaceId: string,
parentPath: string = "",
ownerUserId?: string,
): Promise<WorkspaceDirectory[]> {
// Default (no ownerUserId) scopes to the requester's own subtree; passing
// ownerUserId scopes to that owner so the tree can lazily render their
// directory structure on expand. Directories are structural rows; file
// visibility is still enforced by the scripts/data-resources endpoints.
const parameters = new URLSearchParams();
if (parentPath) parameters.set("parent_path", parentPath);
if (ownerUserId) parameters.set("owner_user_id", ownerUserId);
const query = parameters.toString();
const data = await apiRequest<{ directories: WorkspaceDirectory[] }>(
`/api/v1/workspace-directories${query ? `?${query}` : ""}`,
{},
workspaceId,
);
return data.directories;
}
export async function createWorkspaceDirectory(
workspaceId: string,
directoryName: string,
parentPath = "",
): Promise<WorkspaceDirectory> {
return apiRequest<WorkspaceDirectory>(
"/api/v1/workspace-directories",
{
method: "POST",
body: JSON.stringify({
directory_name: directoryName,
parent_path: parentPath,
}),
},
workspaceId,
);
}
export async function deleteWorkspaceDirectory(
workspaceId: string,
path: string,
): Promise<{
path: string;
status: string;
deleted_scripts: number;
versions_preserved: boolean;
}> {
const parameters = new URLSearchParams({ path });
return apiRequest(
`/api/v1/workspace-directories?${parameters.toString()}`,
{ method: "DELETE" },
workspaceId,
);
}