You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
6.2 KiB
153 lines
6.2 KiB
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Literal
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.exceptions import DirectoryConflictError, DirectoryNotFoundError, ValidationError
|
|
from app.core.ids import new_id
|
|
from app.models.entities import Directory, FileEntry
|
|
from app.repositories.directory_repository import DirectoryRepository
|
|
from app.repositories.file_repository import FileRepository
|
|
|
|
ROOT_DIRECTORY_ID = "dir_root"
|
|
ROOT_DIRECTORY_NAME = "/"
|
|
ROOT_PATH_KEY = "/"
|
|
|
|
|
|
@dataclass
|
|
class NamespaceItem:
|
|
kind: Literal["directory", "file"]
|
|
item: Directory | FileEntry
|
|
|
|
|
|
@dataclass
|
|
class DirectoryChildrenResult:
|
|
directory: Directory
|
|
children: list[NamespaceItem]
|
|
|
|
|
|
class DirectoryService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repository = DirectoryRepository(session)
|
|
self.file_repository = FileRepository(session)
|
|
|
|
async def ensure_root_directory(self) -> Directory:
|
|
root = await self.repository.get_root()
|
|
if root is not None:
|
|
return root
|
|
|
|
root = Directory(
|
|
id=ROOT_DIRECTORY_ID,
|
|
parent_id=None,
|
|
name=ROOT_DIRECTORY_NAME,
|
|
path_key=ROOT_PATH_KEY,
|
|
)
|
|
await self.repository.create(root)
|
|
await self.session.commit()
|
|
return root
|
|
|
|
async def list_children(self, directory_id: str) -> DirectoryChildrenResult:
|
|
directory = await self._get_directory(directory_id)
|
|
child_directories = await self.repository.list_children(directory.id)
|
|
child_files = await self.file_repository.list_by_directory(directory.id)
|
|
|
|
items = [NamespaceItem(kind="directory", item=child) for child in child_directories]
|
|
items.extend(NamespaceItem(kind="file", item=file_entry) for file_entry in child_files)
|
|
items.sort(key=lambda item: (item.kind != "directory", item.item.name.lower()))
|
|
return DirectoryChildrenResult(directory=directory, children=items)
|
|
|
|
async def create_directory(self, parent_id: str, name: str) -> Directory:
|
|
cleaned_name = self._validate_directory_name(name)
|
|
|
|
parent = await self._get_directory(parent_id)
|
|
await self._ensure_name_available(parent.id, cleaned_name)
|
|
|
|
directory = Directory(
|
|
id=new_id("dir"),
|
|
parent_id=parent.id,
|
|
name=cleaned_name,
|
|
path_key=self._build_path_key(parent.path_key, cleaned_name),
|
|
)
|
|
await self.repository.create(directory)
|
|
await self.session.commit()
|
|
return directory
|
|
|
|
async def rename_directory(self, directory_id: str, name: str) -> Directory:
|
|
directory = await self._get_directory(directory_id)
|
|
if directory.id == ROOT_DIRECTORY_ID:
|
|
raise ValidationError("Root directory cannot be renamed.")
|
|
|
|
cleaned_name = self._validate_directory_name(name)
|
|
parent = await self._get_directory(directory.parent_id or ROOT_DIRECTORY_ID)
|
|
if directory.name == cleaned_name:
|
|
return directory
|
|
|
|
await self._ensure_name_available(parent.id, cleaned_name)
|
|
|
|
old_path_key = directory.path_key
|
|
directory.name = cleaned_name
|
|
directory.path_key = self._build_path_key(parent.path_key, cleaned_name)
|
|
await self._rewrite_descendant_paths(old_path_key, directory.path_key)
|
|
saved = await self.repository.save(directory)
|
|
await self.session.commit()
|
|
return saved
|
|
|
|
async def move_directory(self, directory_id: str, target_parent_id: str) -> Directory:
|
|
directory = await self._get_directory(directory_id)
|
|
if directory.id == ROOT_DIRECTORY_ID:
|
|
raise ValidationError("Root directory cannot be moved.")
|
|
|
|
target_parent = await self._get_directory(target_parent_id)
|
|
if directory.parent_id == target_parent.id:
|
|
return directory
|
|
if target_parent.path_key == directory.path_key or target_parent.path_key.startswith(f"{directory.path_key}/"):
|
|
raise ValidationError("A directory cannot be moved into itself or its descendants.")
|
|
|
|
await self._ensure_name_available(target_parent.id, directory.name)
|
|
|
|
old_path_key = directory.path_key
|
|
directory.parent_id = target_parent.id
|
|
directory.path_key = self._build_path_key(target_parent.path_key, directory.name)
|
|
await self._rewrite_descendant_paths(old_path_key, directory.path_key)
|
|
saved = await self.repository.save(directory)
|
|
await self.session.commit()
|
|
return saved
|
|
|
|
async def _get_directory(self, directory_id: str) -> Directory:
|
|
directory = await self.repository.get_by_id(directory_id)
|
|
if directory is None or directory.deleted_at is not None:
|
|
raise DirectoryNotFoundError()
|
|
return directory
|
|
|
|
async def _ensure_name_available(self, parent_id: str, name: str) -> None:
|
|
conflicting_file = await self.file_repository.get_by_directory_and_name(parent_id, name)
|
|
if conflicting_file is not None:
|
|
raise DirectoryConflictError("A file with this name already exists in the target location.")
|
|
|
|
existing = await self.repository.get_by_parent_and_name(parent_id, name)
|
|
if existing is not None:
|
|
raise DirectoryConflictError("A directory with this name already exists in the target location.")
|
|
|
|
async def _rewrite_descendant_paths(self, old_prefix: str, new_prefix: str) -> None:
|
|
descendants = await self.repository.list_descendants_by_path_prefix(old_prefix)
|
|
for descendant in descendants:
|
|
suffix = descendant.path_key.removeprefix(old_prefix)
|
|
descendant.path_key = f"{new_prefix}{suffix}"
|
|
|
|
@staticmethod
|
|
def _validate_directory_name(name: str) -> str:
|
|
cleaned_name = name.strip()
|
|
if not cleaned_name:
|
|
raise ValidationError("Directory name must not be empty.")
|
|
if "/" in cleaned_name:
|
|
raise ValidationError("Directory name must not contain '/'.")
|
|
return cleaned_name
|
|
|
|
@staticmethod
|
|
def _build_path_key(parent_path_key: str, name: str) -> str:
|
|
if parent_path_key == ROOT_PATH_KEY:
|
|
return f"/{name}"
|
|
return f"{parent_path_key}/{name}"
|
|
|