fix: bugs

This commit is contained in:
tao.chen
2026-08-11 21:53:52 +08:00
parent dd6f176ca8
commit bff7b7f9a6
2 changed files with 82 additions and 36 deletions
+40 -29
View File
@@ -27,7 +27,6 @@ from fastapi import (
) )
from loguru import logger from loguru import logger
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependencies import ( from backend.dependencies import (
@@ -591,6 +590,7 @@ async def create_workspace_directory(
child_path = f"{parent}/{name}" if parent else name child_path = f"{parent}/{name}" if parent else name
relative_path = user_relative_path(context, child_path) relative_path = user_relative_path(context, child_path)
scoped_prefix = user_relative_path(context) scoped_prefix = user_relative_path(context)
path_hash = hashlib.sha256(relative_path.encode("utf-8")).digest()
# Validate parent exists: there must be at least one StorageObject whose # Validate parent exists: there must be at least one StorageObject whose
# relative_path is exactly the parent directory (the directory row itself) # relative_path is exactly the parent directory (the directory row itself)
# OR lives somewhere below the parent (any file/dir nested under it). # OR lives somewhere below the parent (any file/dir nested under it).
@@ -609,42 +609,53 @@ async def create_workspace_directory(
status.HTTP_404_NOT_FOUND, status.HTTP_404_NOT_FOUND,
"parent directory not found", "parent directory not found",
) )
# S3 has no real directory objects — the prefix is implicitly # Conflict check uses the unique index on (workspace_id, storage_backend, path_hash).
# created when a file is uploaded. Conflict detection is best-effort. # A soft-deleted row at the same path can be revived; an available row is a conflict.
existing = await session.scalar( existing = await session.scalar(
select(StorageObjects.storage_object_id).where( select(StorageObjects)
.where(
StorageObjects.workspace_id == context.workspace.workspace_id, StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.relative_path == relative_path, StorageObjects.storage_backend == "rustfs",
StorageObjects.object_status == "available", StorageObjects.path_hash == path_hash,
) )
.with_for_update()
) )
if existing is not None: if existing is not None and existing.object_status == "available":
raise HTTPException( raise HTTPException(
status.HTTP_409_CONFLICT, status.HTTP_409_CONFLICT,
"a file or directory with the same path already exists", "a file or directory with the same path already exists",
) )
directory = StorageObjects(
storage_object_id=new_ulid(), if existing is None:
workspace_id=context.workspace.workspace_id, directory = StorageObjects(
object_type="directory", storage_object_id=new_ulid(),
usage_type="working_copy", workspace_id=context.workspace.workspace_id,
storage_backend="rustfs", storage_backend="rustfs",
storage_uri=f"inline://directory/{relative_path}", object_type="directory",
relative_path=relative_path, usage_type="working_copy",
path_hash=hashlib.sha256(relative_path.encode("utf-8")).digest(), storage_uri=f"inline://directory/{relative_path}",
object_status="available", relative_path=relative_path,
size_bytes=0, path_hash=path_hash,
visibility="private", object_status="available",
created_by=context.user.user_id, size_bytes=0,
) visibility="private",
session.add(directory) created_by=context.user.user_id,
try: )
await session.flush() session.add(directory)
except IntegrityError as exc: else:
raise HTTPException( # Revive the soft-deleted row, keeping its original storage_object_id.
status.HTTP_409_CONFLICT, directory = existing
"directory already exists", directory.object_status = "available"
) from exc directory.is_deleted = 0
directory.deleted_at = None
directory.object_type = "directory"
directory.usage_type = "working_copy"
directory.storage_uri = f"inline://directory/{relative_path}"
directory.size_bytes = 0
directory.visibility = "private"
directory.created_by = context.user.user_id
await session.flush()
return { return {
"request_id": context.request_id, "request_id": context.request_id,
"data": { "data": {
@@ -1,8 +1,8 @@
import { type ChangeEvent, type MouseEvent as ReactMouseEvent, type RefObject } from "react"; import { type ChangeEvent, type MouseEvent as ReactMouseEvent, type RefObject, useEffect, useMemo, useState } from "react";
import Icon from "../common/Icon"; import Icon from "../common/Icon";
import { WorkspaceTreeGroup } from "~/features/platform/WorkspaceTree"; import { WorkspaceTreeGroup } from "~/features/platform/WorkspaceTree";
import type { ScriptItem, WorkspaceDirectory } from "~/services/api"; import type { ScriptItem, WorkspaceDirectory, WorkspaceMember } from "~/services/api";
import type { AuthUser } from "~/context/AuthContext"; import { useApi, useAuth, type AuthUser } from "~/context/AuthContext";
type ScriptExplorerProps = { type ScriptExplorerProps = {
scripts: ScriptItem[]; scripts: ScriptItem[];
@@ -50,7 +50,38 @@ export function ScriptExplorer({
uploadInputRef, uploadInputRef,
onHandleUpload, onHandleUpload,
}: ScriptExplorerProps) { }: ScriptExplorerProps) {
const memberScriptGroups = (() => { const api = useApi();
const { currentWorkspace } = useAuth();
const [members, setMembers] = useState<WorkspaceMember[]>([]);
useEffect(() => {
if (!currentWorkspace) {
setMembers([]);
return;
}
let cancelled = false;
api
.listWorkspaceMembers(currentWorkspace.workspace_id)
.then((list) => {
if (!cancelled) setMembers(list);
})
.catch(() => {
if (!cancelled) setMembers([]);
});
return () => {
cancelled = true;
};
}, [api, currentWorkspace]);
const displayNameByUserId = useMemo(() => {
const map = new Map<string, string>();
for (const m of members) {
map.set(m.user_id, m.display_name || m.username || m.user_id);
}
return map;
}, [members]);
const memberScriptGroups = useMemo(() => {
const visibleScripts = const visibleScripts =
user?.is_system_admin === true user?.is_system_admin === true
? filteredScripts ? filteredScripts
@@ -74,13 +105,17 @@ export function ScriptExplorer({
directories: WorkspaceDirectory[]; directories: WorkspaceDirectory[];
}[] = []; }[] = [];
for (const [ownerUserId, groupScripts] of byOwner.entries()) { for (const [ownerUserId, groupScripts] of byOwner.entries()) {
const displayName =
displayNameByUserId.get(ownerUserId) ??
(ownerUserId === user?.user_id ? user?.display_name : null) ??
`${ownerUserId.slice(-6)}`;
const groupUser = const groupUser =
ownerUserId === user?.user_id ownerUserId === user?.user_id
? user ? user
: ({ : ({
user_id: ownerUserId, user_id: ownerUserId,
username: ownerUserId, username: ownerUserId,
display_name: ownerUserId, display_name: displayName,
email: null, email: null,
status: "unknown", status: "unknown",
role_code: null, role_code: null,
@@ -100,7 +135,7 @@ export function ScriptExplorer({
}); });
return groups; return groups;
})(); }, [filteredScripts, user, displayNameByUserId]);
return ( return (
<aside className="explorer"> <aside className="explorer">
@@ -166,7 +201,7 @@ export function ScriptExplorer({
{memberScriptGroups.map((group) => ( {memberScriptGroups.map((group) => (
<WorkspaceTreeGroup <WorkspaceTreeGroup
key={group.user?.user_id ?? "anon"} key={group.user?.user_id ?? "anon"}
title={`${group.user?.display_name}的文件`} title={`${group.user?.display_name}`}
scripts={group.scripts} scripts={group.scripts}
directories={group.directories} directories={group.directories}
selectedId={selectedId} selectedId={selectedId}