fix: bugs
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user