Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions antarest/study/business/model/user_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ def _validate_coherence(self) -> Self:

class UserResourceDataRemoval(AntaresBaseModel):
path: str


class FolderTree(AntaresBaseModel):
name: str
files: list[str]
directories: list["FolderTree"]


class UserResourcesTree(AntaresBaseModel):
files: list[str]
directories: list[FolderTree]
56 changes: 46 additions & 10 deletions antarest/study/business/user_resources_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,69 @@
#
# This file is part of the Antares project.
from pathlib import PurePosixPath
from typing import Any

from antarest.study.business.model.user_model import ResourceType, UserResourceDataCreation, UserResourceDataRemoval
from antarest.core.exceptions import UserResourceNotFound
from antarest.study.business.model.user_model import (
ResourceType,
UserResourceDataCreation,
UserResourceDataRemoval,
UserResourcesTree,
)
from antarest.study.business.study_interface import StudyInterface
from antarest.study.storage.variantstudy.model.command.remove_user_resource import RemoveUserResource
from antarest.study.storage.variantstudy.model.command.replace_user_resource import ReplaceUserResource
from antarest.study.storage.variantstudy.model.command_context import CommandContext


def _build_tree(resources: list[UserResourceDataCreation]) -> UserResourcesTree:
root: dict[str, Any] = {"directories": [], "files": []}

for resource in resources:
parts = resource.path.parts
current = root

for part in parts[:-1]:
directory = next((d for d in current["directories"] if d["name"] == part), None)
if directory is None:
directory = {"name": part, "directories": [], "files": []}
current["directories"].append(directory)

current = directory

name = parts[-1]

if resource.resource_type == ResourceType.FILE:
current["files"].append(name)
else:
current["directories"].append({"name": name, "directories": [], "files": []})
return UserResourcesTree.model_validate(root)


class UserResourcesManager:
def __init__(self, command_context: CommandContext) -> None:
self._command_context = command_context

def get_all_user_resources_paths(self, study: StudyInterface) -> list[str]:
def get_all_user_resources(self, study: StudyInterface) -> UserResourcesTree:
user_resources = study.get_study_dao().get_all_user_resources()
sorted_resources = sorted(user_resources, key=lambda res: res.path)
return [res.path.as_posix() for res in sorted_resources]
return _build_tree(user_resources)

def get_user_resource(self, study: StudyInterface, path: PurePosixPath) -> bytes:
return study.get_study_dao().get_user_resource(path)

def delete_user_resource(self, study: StudyInterface, path: PurePosixPath) -> None:
command = RemoveUserResource(
data=UserResourceDataRemoval(path=path.as_posix()),
command_context=self._command_context,
study_version=study.version,
)
study.add_commands([command])
# First, we need to check if the resource exists
for resource in study.get_study_dao().get_all_user_resources():
if resource.path.is_relative_to(path):
# Remove the existing resource
command = RemoveUserResource(
data=UserResourceDataRemoval(path=path.as_posix()),
command_context=self._command_context,
study_version=study.version,
)
study.add_commands([command])
return
raise UserResourceNotFound(path.as_posix())

def replace_user_resource(
self, study: StudyInterface, resource_type: ResourceType, path: PurePosixPath, content: bytes | None
Expand Down
8 changes: 4 additions & 4 deletions antarest/study/web/study_data_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
ThermalClusterCreation,
ThermalClusterUpdate,
)
from antarest.study.business.model.user_model import ResourceType
from antarest.study.business.model.user_model import ResourceType, UserResourcesTree
from antarest.study.business.table_mode_management import TableDataDTO, TableModeType
from antarest.study.model import CommentsDto
from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id
Expand Down Expand Up @@ -2366,11 +2366,11 @@ def get_study_data(
"""
return study_service.get_study_data(study_id)

@bp.get("/studies/{uuid}/user-resources", summary="Fetches paths of all user resources for a given study")
def get_all_user_resources(study_service: StudyServiceDep, uuid: UuidStr) -> list[str]:
@bp.get("/studies/{uuid}/user-resources", summary="Fetches tree structure of all user resources for a given study")
def get_all_user_resources(study_service: StudyServiceDep, uuid: UuidStr) -> UserResourcesTree:
study = study_service.check_study_access(uuid, StudyPermissionType.READ)
study_interface = study_service.get_study_interface(study)
return study_service.user_resources_manager.get_all_user_resources_paths(study_interface)
return study_service.user_resources_manager.get_all_user_resources(study_interface)

@bp.get(
"/studies/{uuid}/user-resources/content",
Expand Down
45 changes: 38 additions & 7 deletions tests/integration/study_data_blueprint/test_user_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_nominal_case(client: TestClient, user_access_token: str, storage_mode:
# Fetches all user resources. Should be empty
res = client.get(f"/v1/studies/{study_id}/user-resources")
assert res.status_code == 200
assert res.json() == []
assert res.json() == {"directories": [], "files": []}

# Create a folder
params = {"path": "my/folder", "resource_type": "folder"}
Expand All @@ -38,8 +38,23 @@ def test_nominal_case(client: TestClient, user_access_token: str, storage_mode:
# Fetch all resources. Should contain the folder
res = client.get(f"/v1/studies/{study_id}/user-resources")
assert res.status_code == 200
assert res.json() == ["my/folder"]

expected_structure = {
"directories": [
{
"name": "my",
"files": [],
"directories": [
{
"name": "folder",
"files": [],
"directories": [],
}
],
}
],
"files": [],
}
assert res.json() == expected_structure
# Create a file with a specific content
content = b"specific content"
res = client.put(
Expand All @@ -52,7 +67,22 @@ def test_nominal_case(client: TestClient, user_access_token: str, storage_mode:
# Fetch all resources. Should contain the file
res = client.get(f"/v1/studies/{study_id}/user-resources")
assert res.status_code == 200
assert res.json() == ["my/file", "my/folder"]
assert res.json() == {
"directories": [
{
"name": "my",
"files": ["file"],
"directories": [
{
"name": "folder",
"files": [],
"directories": [],
}
],
}
],
"files": [],
}

# Fetch the content of the created file
res = client.get(f"/v1/studies/{study_id}/user-resources/content?path=my/file")
Expand All @@ -66,14 +96,14 @@ def test_nominal_case(client: TestClient, user_access_token: str, storage_mode:
# Fetch all resources. Should contain the folder only
res = client.get(f"/v1/studies/{study_id}/user-resources")
assert res.status_code == 200
assert res.json() == ["my/folder"]
assert res.json() == expected_structure

# Create a folder "my". Should be a no-op as it already exists.
res = client.put(f"/v1/studies/{study_id}/user-resources", params={"path": "my", "resource_type": "folder"})
assert res.status_code == 200
res = client.get(f"/v1/studies/{study_id}/user-resources")
assert res.status_code == 200
assert res.json() == ["my/folder"]
assert res.json() == expected_structure

# Delete the folder
res = client.delete(f"/v1/studies/{study_id}/user-resources?path=my/folder")
Expand All @@ -82,7 +112,7 @@ def test_nominal_case(client: TestClient, user_access_token: str, storage_mode:
# Fetch all resources. Should still contain the parent folder "my"
res = client.get(f"/v1/studies/{study_id}/user-resources")
assert res.status_code == 200
assert res.json() == ["my"]
assert res.json() == {"directories": [{"name": "my", "files": [], "directories": []}], "files": []}

##########################
# Error cases
Expand All @@ -96,6 +126,7 @@ def test_nominal_case(client: TestClient, user_access_token: str, storage_mode:

# Deletes a fake user resource. Should fail
res = client.delete(f"/v1/studies/{study_id}/user-resources?path=fake/path/to/file")
assert res.status_code == 404
description = res.json()["description"]
assert (
"User resources not found: 'fake/path/to/file'" in description
Expand Down
Loading