feat(scripts/data-resources): merge tree + add parent_path filter
- Backend: GET /api/v1/data-resources accepts parent_path; LIKE
'{ws_id}/%/{escaped}/%' AND NOT LIKE '{ws_id}/%/{escaped}/%/%' on
StorageObjects.object_key (workspace-wide, escapes _ and %, mirrors
list_scripts parent_path semantics). 13 new tests in
test_resources.py (helper unit / SQL compile / SQLite behavioral).
- Frontend: listResources gains parentPath arg, propagated through
WorkspaceBoundApi + AuthContext binding. WorkspaceTreeGroup title
count and ScriptExplorer header count now include dataResources.
memberScriptGroups backfills data-only owners so users with only
data resources still render a group. loadDataResources accepts an
optional parentPath, default empty preserves prior behavior.
This commit is contained in:
@@ -28,7 +28,7 @@ from backend.dependencies import (
|
||||
database_session,
|
||||
request_context,
|
||||
)
|
||||
from backend.scripts import _escape_like_pattern
|
||||
from backend.scripts import _escape_like_pattern, normalize_user_path
|
||||
from backend.schemas import (
|
||||
CompleteResourceUploadRequest,
|
||||
CreateResourceUploadRequest,
|
||||
@@ -47,6 +47,23 @@ from backend.services.storage import (
|
||||
router = APIRouter(prefix="/api/v1/data-resources", tags=["data-resources"])
|
||||
|
||||
|
||||
def _build_list_resources_descendant_prefix(parent_path: str) -> str:
|
||||
"""Return the escaped materialized-path prefix for direct children
|
||||
of ``parent_path`` against ``StorageObjects.object_key``.
|
||||
|
||||
Data resources are workspace-wide (no per-user scoping at the API
|
||||
level). The full object_key is ``{ws_id}/{user_id}/{jupyter_path}``;
|
||||
we filter on object_key with the pattern ``{ws_id}/%/{parent_path}``
|
||||
so any owner whose jupyter_accessible_path starts with parent_path
|
||||
matches. LIKE wildcards in parent_path are escaped; the ``%`` between
|
||||
``{ws_id}/`` and the escaped parent is an intentional SQL wildcard
|
||||
matching the ``owner_user_id`` segment across all owners.
|
||||
"""
|
||||
normalized = normalize_user_path(parent_path)
|
||||
escaped = _escape_like_pattern(normalized)
|
||||
return f"{escaped}/" if escaped else ""
|
||||
|
||||
|
||||
def compute_jupyter_relative_path(script_path: str, resource_relative: str) -> str:
|
||||
"""从当前脚本所在目录算到资源文件的 Jupyter 相对路径。
|
||||
|
||||
@@ -364,6 +381,7 @@ async def bind_resource(
|
||||
# 列出当前工作区可见的数据资源,可按可见性或关键字筛选。
|
||||
@router.get("")
|
||||
async def list_resources(
|
||||
parent_path: str = Query(default="", max_length=1024),
|
||||
context: RequestContext = Depends(request_context),
|
||||
session: AsyncSession = Depends(database_session),
|
||||
visibility: str | None = Query(default=None),
|
||||
@@ -383,6 +401,23 @@ async def list_resources(
|
||||
)
|
||||
.order_by(DataResources.updated_at.desc())
|
||||
)
|
||||
if parent_path:
|
||||
# ``parent_path`` scopes to DIRECT children of that jupyter path
|
||||
# (matching /api/v1/scripts). Data resources are workspace-wide, so
|
||||
# the middle ``%`` is an intentional wildcard that matches the
|
||||
# ``owner_user_id`` segment across all owners. The parent's ``_`` /
|
||||
# ``%`` are escaped so sibling folders (e.g. ``fooXbar``) don't leak.
|
||||
descendant_prefix = _build_list_resources_descendant_prefix(parent_path)
|
||||
statement = statement.where(
|
||||
StorageObjects.object_key.like(
|
||||
f"{context.workspace.workspace_id}/%/{descendant_prefix}%",
|
||||
escape="\\",
|
||||
),
|
||||
~StorageObjects.object_key.like(
|
||||
f"{context.workspace.workspace_id}/%/{descendant_prefix}%/%",
|
||||
escape="\\",
|
||||
),
|
||||
)
|
||||
# 2026-08-11: 临时取消"用户间目录互相不可见"约束
|
||||
# 列表接口现在返回 workspace 内全部 active 资源(不再按 owner / visibility 过滤)。
|
||||
# 还原: 删除下面这段注释,恢复原来的 if not context.is_admin: ... 块。
|
||||
|
||||
Reference in New Issue
Block a user