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 sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependencies import (
@@ -591,6 +590,7 @@ async def create_workspace_directory(
child_path = f"{parent}/{name}" if parent else name
relative_path = user_relative_path(context, child_path)
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
# relative_path is exactly the parent directory (the directory row itself)
# 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,
"parent directory not found",
)
# S3 has no real directory objects — the prefix is implicitly
# created when a file is uploaded. Conflict detection is best-effort.
# Conflict check uses the unique index on (workspace_id, storage_backend, path_hash).
# A soft-deleted row at the same path can be revived; an available row is a conflict.
existing = await session.scalar(
select(StorageObjects.storage_object_id).where(
select(StorageObjects)
.where(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.relative_path == relative_path,
StorageObjects.object_status == "available",
StorageObjects.storage_backend == "rustfs",
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(
status.HTTP_409_CONFLICT,
"a file or directory with the same path already exists",
)
directory = StorageObjects(
storage_object_id=new_ulid(),
workspace_id=context.workspace.workspace_id,
object_type="directory",
usage_type="working_copy",
storage_backend="rustfs",
storage_uri=f"inline://directory/{relative_path}",
relative_path=relative_path,
path_hash=hashlib.sha256(relative_path.encode("utf-8")).digest(),
object_status="available",
size_bytes=0,
visibility="private",
created_by=context.user.user_id,
)
session.add(directory)
try:
await session.flush()
except IntegrityError as exc:
raise HTTPException(
status.HTTP_409_CONFLICT,
"directory already exists",
) from exc
if existing is None:
directory = StorageObjects(
storage_object_id=new_ulid(),
workspace_id=context.workspace.workspace_id,
storage_backend="rustfs",
object_type="directory",
usage_type="working_copy",
storage_uri=f"inline://directory/{relative_path}",
relative_path=relative_path,
path_hash=path_hash,
object_status="available",
size_bytes=0,
visibility="private",
created_by=context.user.user_id,
)
session.add(directory)
else:
# Revive the soft-deleted row, keeping its original storage_object_id.
directory = existing
directory.object_status = "available"
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 {
"request_id": context.request_id,
"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 { WorkspaceTreeGroup } from "~/features/platform/WorkspaceTree";
import type { ScriptItem, WorkspaceDirectory } from "~/services/api";
import type { AuthUser } from "~/context/AuthContext";
import type { ScriptItem, WorkspaceDirectory, WorkspaceMember } from "~/services/api";
import { useApi, useAuth, type AuthUser } from "~/context/AuthContext";
type ScriptExplorerProps = {
scripts: ScriptItem[];
@@ -50,7 +50,38 @@ export function ScriptExplorer({
uploadInputRef,
onHandleUpload,
}: 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 =
user?.is_system_admin === true
? filteredScripts
@@ -74,13 +105,17 @@ export function ScriptExplorer({
directories: WorkspaceDirectory[];
}[] = [];
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 =
ownerUserId === user?.user_id
? user
: ({
user_id: ownerUserId,
username: ownerUserId,
display_name: ownerUserId,
display_name: displayName,
email: null,
status: "unknown",
role_code: null,
@@ -100,7 +135,7 @@ export function ScriptExplorer({
});
return groups;
})();
}, [filteredScripts, user, displayNameByUserId]);
return (
<aside className="explorer">
@@ -166,7 +201,7 @@ export function ScriptExplorer({
{memberScriptGroups.map((group) => (
<WorkspaceTreeGroup
key={group.user?.user_id ?? "anon"}
title={`${group.user?.display_name}的文件`}
title={`${group.user?.display_name}`}
scripts={group.scripts}
directories={group.directories}
selectedId={selectedId}