from __future__ import annotations from backend.app.api.errors import AppError from backend.app.api.schemas import TaskCreateResponse from backend.app.db.task_repository import TaskRepository from backend.app.security.path_guard import PathGuard from backend.app.tasks_runner import TaskRunner class MoveTaskService: def __init__(self, path_guard: PathGuard, repository: TaskRepository, runner: TaskRunner): self._path_guard = path_guard self._repository = repository self._runner = runner def create_move_task(self, source: str, destination: str) -> TaskCreateResponse: resolved_source = self._path_guard.resolve_existing_path(source) _, _, lexical_source = self._path_guard.resolve_lexical_path(source) if lexical_source.is_symlink(): raise AppError( code="type_conflict", message="Source must be a regular file", status_code=409, details={"path": source}, ) if not resolved_source.absolute.is_file(): raise AppError( code="type_conflict", message="Source must be a file", status_code=409, details={"path": source}, ) resolved_destination = self._path_guard.resolve_path(destination) destination_parent = resolved_destination.absolute.parent parent_relative = self._path_guard.entry_relative_path(resolved_destination.alias, destination_parent) self._map_directory_validation(parent_relative) if resolved_destination.absolute.exists(): raise AppError( code="already_exists", message="Target path already exists", status_code=409, details={"path": resolved_destination.relative}, ) total_bytes = int(resolved_source.absolute.stat().st_size) task = self._repository.create_task( operation="move", source=resolved_source.relative, destination=resolved_destination.relative, ) same_root = resolved_source.alias == resolved_destination.alias self._runner.enqueue_move_file( task_id=task["id"], source=str(resolved_source.absolute), destination=str(resolved_destination.absolute), total_bytes=total_bytes, same_root=same_root, ) return TaskCreateResponse(task_id=task["id"], status=task["status"]) def _map_directory_validation(self, relative_path: str) -> None: try: self._path_guard.resolve_directory_path(relative_path) except AppError as exc: if exc.code == "path_type_conflict": raise AppError( code="type_conflict", message="Destination parent is not a directory", status_code=409, details=exc.details, ) raise