upload volledige repo
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Service layer."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from backend.app.api.errors import AppError
|
||||
from backend.app.api.schemas import BookmarkDeleteResponse, BookmarkItem, BookmarkListResponse
|
||||
from backend.app.db.bookmark_repository import BookmarkRepository
|
||||
from backend.app.security.path_guard import PathGuard
|
||||
|
||||
|
||||
class BookmarkService:
|
||||
def __init__(self, path_guard: PathGuard, repository: BookmarkRepository):
|
||||
self._path_guard = path_guard
|
||||
self._repository = repository
|
||||
|
||||
def create_bookmark(self, path: str, label: str) -> BookmarkItem:
|
||||
normalized_label = (label or "").strip()
|
||||
if not normalized_label:
|
||||
raise AppError(
|
||||
code="invalid_request",
|
||||
message="Label is required",
|
||||
status_code=400,
|
||||
details={"label": label},
|
||||
)
|
||||
|
||||
resolved = self._path_guard.resolve_path(path)
|
||||
|
||||
try:
|
||||
bookmark = self._repository.create_bookmark(path=resolved.relative, label=normalized_label)
|
||||
except sqlite3.IntegrityError:
|
||||
raise AppError(
|
||||
code="already_exists",
|
||||
message="Bookmark already exists for path",
|
||||
status_code=409,
|
||||
details={"path": resolved.relative},
|
||||
)
|
||||
|
||||
return BookmarkItem(**bookmark)
|
||||
|
||||
def list_bookmarks(self) -> BookmarkListResponse:
|
||||
items = [BookmarkItem(**row) for row in self._repository.list_bookmarks()]
|
||||
return BookmarkListResponse(items=items)
|
||||
|
||||
def delete_bookmark(self, bookmark_id: int) -> BookmarkDeleteResponse:
|
||||
deleted = self._repository.delete_bookmark(bookmark_id)
|
||||
if not deleted:
|
||||
raise AppError(
|
||||
code="path_not_found",
|
||||
message="Bookmark was not found",
|
||||
status_code=404,
|
||||
details={"bookmark_id": str(bookmark_id)},
|
||||
)
|
||||
return BookmarkDeleteResponse(id=bookmark_id)
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.app.api.schemas import BrowseResponse, DirectoryEntry, FileEntry
|
||||
from backend.app.fs.filesystem_adapter import FilesystemAdapter
|
||||
from backend.app.security.path_guard import PathGuard
|
||||
|
||||
|
||||
class BrowseService:
|
||||
def __init__(self, path_guard: PathGuard, filesystem: FilesystemAdapter):
|
||||
self._path_guard = path_guard
|
||||
self._filesystem = filesystem
|
||||
|
||||
def browse(self, path: str, show_hidden: bool) -> BrowseResponse:
|
||||
resolved = self._path_guard.resolve_directory_path(path)
|
||||
directories_raw, files_raw = self._filesystem.list_directory(resolved.absolute, show_hidden=show_hidden)
|
||||
|
||||
directories = [
|
||||
DirectoryEntry(
|
||||
name=item["name"],
|
||||
path=self._path_guard.entry_relative_path(resolved.alias, item["absolute"]),
|
||||
modified=item["modified"],
|
||||
)
|
||||
for item in directories_raw
|
||||
]
|
||||
|
||||
files = [
|
||||
FileEntry(
|
||||
name=item["name"],
|
||||
path=self._path_guard.entry_relative_path(resolved.alias, item["absolute"]),
|
||||
size=item["size"],
|
||||
modified=item["modified"],
|
||||
)
|
||||
for item in files_raw
|
||||
]
|
||||
|
||||
return BrowseResponse(path=resolved.relative, directories=directories, files=files)
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
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 CopyTaskService:
|
||||
def __init__(self, path_guard: PathGuard, repository: TaskRepository, runner: TaskRunner):
|
||||
self._path_guard = path_guard
|
||||
self._repository = repository
|
||||
self._runner = runner
|
||||
|
||||
def create_copy_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="copy",
|
||||
source=resolved_source.relative,
|
||||
destination=resolved_destination.relative,
|
||||
)
|
||||
|
||||
self._runner.enqueue_copy_file(
|
||||
task_id=task["id"],
|
||||
source=str(resolved_source.absolute),
|
||||
destination=str(resolved_destination.absolute),
|
||||
total_bytes=total_bytes,
|
||||
)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from backend.app.api.errors import AppError
|
||||
from backend.app.api.schemas import DeleteResponse, MkdirResponse, RenameResponse
|
||||
from backend.app.fs.filesystem_adapter import FilesystemAdapter
|
||||
from backend.app.security.path_guard import PathGuard
|
||||
|
||||
|
||||
class FileOpsService:
|
||||
def __init__(self, path_guard: PathGuard, filesystem: FilesystemAdapter):
|
||||
self._path_guard = path_guard
|
||||
self._filesystem = filesystem
|
||||
|
||||
def mkdir(self, parent_path: str, name: str) -> MkdirResponse:
|
||||
resolved_parent = self._path_guard.resolve_directory_path(parent_path)
|
||||
safe_name = self._path_guard.validate_name(name, field="name")
|
||||
target_relative = self._join_relative(resolved_parent.relative, safe_name)
|
||||
resolved_target = self._path_guard.resolve_path(target_relative)
|
||||
|
||||
if resolved_target.absolute.exists():
|
||||
raise AppError(
|
||||
code="already_exists",
|
||||
message="Target path already exists",
|
||||
status_code=409,
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
|
||||
try:
|
||||
self._filesystem.make_directory(resolved_target.absolute)
|
||||
except FileExistsError:
|
||||
raise AppError(
|
||||
code="already_exists",
|
||||
message="Target path already exists",
|
||||
status_code=409,
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
except OSError as exc:
|
||||
raise AppError(
|
||||
code="io_error",
|
||||
message="Filesystem operation failed",
|
||||
status_code=500,
|
||||
details={"reason": str(exc)},
|
||||
)
|
||||
|
||||
return MkdirResponse(path=resolved_target.relative)
|
||||
|
||||
def rename(self, path: str, new_name: str) -> RenameResponse:
|
||||
resolved_source = self._path_guard.resolve_existing_path(path)
|
||||
safe_name = self._path_guard.validate_name(new_name, field="new_name")
|
||||
|
||||
parent_relative = self._path_guard.entry_relative_path(resolved_source.alias, resolved_source.absolute.parent)
|
||||
target_relative = self._join_relative(parent_relative, safe_name)
|
||||
resolved_target = self._path_guard.resolve_path(target_relative)
|
||||
|
||||
if resolved_target.absolute.exists():
|
||||
raise AppError(
|
||||
code="already_exists",
|
||||
message="Target path already exists",
|
||||
status_code=409,
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
|
||||
try:
|
||||
self._filesystem.rename_path(resolved_source.absolute, resolved_target.absolute)
|
||||
except FileNotFoundError:
|
||||
raise AppError(
|
||||
code="path_not_found",
|
||||
message="Requested path was not found",
|
||||
status_code=404,
|
||||
details={"path": path},
|
||||
)
|
||||
except FileExistsError:
|
||||
raise AppError(
|
||||
code="already_exists",
|
||||
message="Target path already exists",
|
||||
status_code=409,
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
except OSError as exc:
|
||||
raise AppError(
|
||||
code="io_error",
|
||||
message="Filesystem operation failed",
|
||||
status_code=500,
|
||||
details={"reason": str(exc)},
|
||||
)
|
||||
|
||||
return RenameResponse(path=resolved_target.relative)
|
||||
|
||||
def delete(self, path: str) -> DeleteResponse:
|
||||
resolved_target = self._path_guard.resolve_existing_path(path)
|
||||
|
||||
try:
|
||||
if resolved_target.absolute.is_file():
|
||||
self._filesystem.delete_file(resolved_target.absolute)
|
||||
elif resolved_target.absolute.is_dir():
|
||||
if not self._filesystem.is_directory_empty(resolved_target.absolute):
|
||||
raise AppError(
|
||||
code="directory_not_empty",
|
||||
message="Directory is not empty",
|
||||
status_code=409,
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
self._filesystem.delete_empty_directory(resolved_target.absolute)
|
||||
else:
|
||||
raise AppError(
|
||||
code="type_conflict",
|
||||
message="Unsupported path type for delete",
|
||||
status_code=409,
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
except AppError:
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
raise AppError(
|
||||
code="path_not_found",
|
||||
message="Requested path was not found",
|
||||
status_code=404,
|
||||
details={"path": path},
|
||||
)
|
||||
except OSError as exc:
|
||||
raise AppError(
|
||||
code="io_error",
|
||||
message="Filesystem operation failed",
|
||||
status_code=500,
|
||||
details={"reason": str(exc)},
|
||||
)
|
||||
|
||||
return DeleteResponse(path=resolved_target.relative)
|
||||
|
||||
@staticmethod
|
||||
def _join_relative(base: str, name: str) -> str:
|
||||
return f"{base}/{name}" if base else name
|
||||
@@ -0,0 +1,77 @@
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.app.api.errors import AppError
|
||||
from backend.app.api.schemas import TaskDetailResponse, TaskListItem, TaskListResponse
|
||||
from backend.app.db.task_repository import TaskRepository
|
||||
|
||||
|
||||
class TaskService:
|
||||
def __init__(self, repository: TaskRepository):
|
||||
self._repository = repository
|
||||
|
||||
def create_task(self, operation: str, source: str, destination: str) -> TaskDetailResponse:
|
||||
task = self._repository.create_task(operation=operation, source=source, destination=destination)
|
||||
return TaskDetailResponse(**task)
|
||||
|
||||
def get_task(self, task_id: str) -> TaskDetailResponse:
|
||||
task = self._repository.get_task(task_id)
|
||||
if not task:
|
||||
raise AppError(
|
||||
code="task_not_found",
|
||||
message="Task was not found",
|
||||
status_code=404,
|
||||
details={"task_id": task_id},
|
||||
)
|
||||
return TaskDetailResponse(**task)
|
||||
|
||||
def list_tasks(self) -> TaskListResponse:
|
||||
tasks = self._repository.list_tasks()
|
||||
return TaskListResponse(
|
||||
items=[
|
||||
TaskListItem(
|
||||
id=task["id"],
|
||||
operation=task["operation"],
|
||||
status=task["status"],
|
||||
source=task["source"],
|
||||
destination=task["destination"],
|
||||
created_at=task["created_at"],
|
||||
finished_at=task["finished_at"],
|
||||
)
|
||||
for task in tasks
|
||||
]
|
||||
)
|
||||
Reference in New Issue
Block a user