feat: logging toegevoegd

This commit is contained in:
kodi
2026-03-12 07:32:44 +01:00
parent ea6eac9536
commit 9901c77919
30 changed files with 1069 additions and 124 deletions
+80 -46
View File
@@ -2,70 +2,94 @@ from __future__ import annotations
from pathlib import Path
import uuid
from backend.app.api.errors import AppError
from backend.app.api.schemas import TaskCreateResponse
from backend.app.db.history_repository import HistoryRepository
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):
def __init__(self, path_guard: PathGuard, repository: TaskRepository, runner: TaskRunner, history_repository: HistoryRepository | None = None):
self._path_guard = path_guard
self._repository = repository
self._runner = runner
self._history_repository = history_repository
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},
try:
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,
display_style=resolved_destination.display_style,
)
if not resolved_source.absolute.is_file():
raise AppError(
code="type_conflict",
message="Source must be a file",
status_code=409,
details={"path": source},
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_id = str(uuid.uuid4())
task = self._repository.create_task(
operation="copy",
source=resolved_source.relative,
destination=resolved_destination.relative,
task_id=task_id,
)
self._record_history(
entry_id=task_id,
operation="copy",
status="queued",
source=resolved_source.relative,
destination=resolved_destination.relative,
)
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,
display_style=resolved_destination.display_style,
)
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},
self._runner.enqueue_copy_file(
task_id=task["id"],
source=str(resolved_source.absolute),
destination=str(resolved_destination.absolute),
total_bytes=total_bytes,
)
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"])
return TaskCreateResponse(task_id=task["id"], status=task["status"])
except AppError as exc:
self._record_history(
operation="copy",
status="failed",
source=source,
destination=destination,
error_code=exc.code,
error_message=exc.message,
finished_at=self._now_iso(),
)
raise
def _map_directory_validation(self, relative_path: str) -> None:
try:
@@ -79,3 +103,13 @@ class CopyTaskService:
details=exc.details,
)
raise
def _record_history(self, **kwargs) -> None:
if self._history_repository:
self._history_repository.create_entry(**kwargs)
@staticmethod
def _now_iso() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
+132 -51
View File
@@ -4,6 +4,7 @@ from pathlib import Path
from backend.app.api.errors import AppError
from backend.app.api.schemas import DeleteResponse, MkdirResponse, RenameResponse, SaveResponse, ViewResponse
from backend.app.db.history_repository import HistoryRepository
from backend.app.fs.filesystem_adapter import FilesystemAdapter
from backend.app.security.path_guard import PathGuard
@@ -27,93 +28,117 @@ SPECIAL_TEXT_FILENAMES = {
class FileOpsService:
def __init__(self, path_guard: PathGuard, filesystem: FilesystemAdapter):
def __init__(self, path_guard: PathGuard, filesystem: FilesystemAdapter, history_repository: HistoryRepository | None = None):
self._path_guard = path_guard
self._filesystem = filesystem
self._history_repository = history_repository
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:
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},
)
self._filesystem.make_directory(resolved_target.absolute)
self._record_history(operation="mkdir", status="completed", path=resolved_target.relative, finished_at=self._now_iso())
return MkdirResponse(path=resolved_target.relative)
except FileExistsError:
raise AppError(
error = AppError(
code="already_exists",
message="Target path already exists",
status_code=409,
details={"path": resolved_target.relative},
details={"path": self._join_relative(parent_path, name)},
)
self._record_history_error(operation="mkdir", path=self._join_relative(parent_path, name), error=error)
raise error
except AppError as exc:
self._record_history_error(operation="mkdir", path=self._join_relative(parent_path, name), error=exc)
raise
except OSError as exc:
raise AppError(
error = AppError(
code="io_error",
message="Filesystem operation failed",
status_code=500,
details={"reason": str(exc)},
)
return MkdirResponse(path=resolved_target.relative)
self._record_history_error(operation="mkdir", path=self._join_relative(parent_path, name), error=error)
raise error
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,
display_style=resolved_source.display_style,
)
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:
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,
display_style=resolved_source.display_style,
)
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},
)
self._filesystem.rename_path(resolved_source.absolute, resolved_target.absolute)
self._record_history(
operation="rename",
status="completed",
source=path,
destination=resolved_target.relative,
path=resolved_target.relative,
finished_at=self._now_iso(),
)
return RenameResponse(path=resolved_target.relative)
except FileNotFoundError:
raise AppError(
error = AppError(
code="path_not_found",
message="Requested path was not found",
status_code=404,
details={"path": path},
)
self._record_history_error(operation="rename", source=path, destination=new_name, path=path, error=error)
raise error
except FileExistsError:
raise AppError(
error = AppError(
code="already_exists",
message="Target path already exists",
status_code=409,
details={"path": resolved_target.relative},
details={"path": new_name},
)
self._record_history_error(operation="rename", source=path, destination=new_name, path=path, error=error)
raise error
except AppError as exc:
self._record_history_error(operation="rename", source=path, destination=new_name, path=path, error=exc)
raise
except OSError as exc:
raise AppError(
error = AppError(
code="io_error",
message="Filesystem operation failed",
status_code=500,
details={"reason": str(exc)},
)
return RenameResponse(path=resolved_target.relative)
self._record_history_error(operation="rename", source=path, destination=new_name, path=path, error=error)
raise error
def delete(self, path: str) -> DeleteResponse:
resolved_target = self._path_guard.resolve_existing_path(path)
try:
resolved_target = self._path_guard.resolve_existing_path(path)
if resolved_target.absolute.is_file():
self._filesystem.delete_file(resolved_target.absolute)
elif resolved_target.absolute.is_dir():
@@ -132,24 +157,29 @@ class FileOpsService:
status_code=409,
details={"path": resolved_target.relative},
)
except AppError:
self._record_history(operation="delete", status="completed", path=resolved_target.relative, finished_at=self._now_iso())
return DeleteResponse(path=resolved_target.relative)
except AppError as exc:
self._record_history_error(operation="delete", path=path, error=exc)
raise
except FileNotFoundError:
raise AppError(
error = AppError(
code="path_not_found",
message="Requested path was not found",
status_code=404,
details={"path": path},
)
self._record_history_error(operation="delete", path=path, error=error)
raise error
except OSError as exc:
raise AppError(
error = AppError(
code="io_error",
message="Filesystem operation failed",
status_code=500,
details={"reason": str(exc)},
)
return DeleteResponse(path=resolved_target.relative)
self._record_history_error(operation="delete", path=path, error=error)
raise error
def view(self, path: str, for_edit: bool = False) -> ViewResponse:
resolved_target = self._path_guard.resolve_existing_path(path)
@@ -282,3 +312,54 @@ class FileOpsService:
if special_name:
return special_name
return TEXT_CONTENT_TYPES.get(path.suffix.lower())
def _record_history(
self,
*,
operation: str,
status: str,
source: str | None = None,
destination: str | None = None,
path: str | None = None,
error_code: str | None = None,
error_message: str | None = None,
finished_at: str | None = None,
) -> None:
if not self._history_repository:
return
self._history_repository.create_entry(
operation=operation,
status=status,
source=source,
destination=destination,
path=path,
error_code=error_code,
error_message=error_message,
finished_at=finished_at,
)
def _record_history_error(
self,
*,
operation: str,
error: AppError,
source: str | None = None,
destination: str | None = None,
path: str | None = None,
) -> None:
self._record_history(
operation=operation,
status="failed",
source=source,
destination=destination,
path=path,
error_code=error.code,
error_message=error.message,
finished_at=self._now_iso(),
)
@staticmethod
def _now_iso() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -0,0 +1,12 @@
from __future__ import annotations
from backend.app.api.schemas import HistoryListResponse
from backend.app.db.history_repository import HistoryRepository
class HistoryService:
def __init__(self, repository: HistoryRepository):
self._repository = repository
def list_history(self) -> HistoryListResponse:
return HistoryListResponse(items=self._repository.list_history(limit=100))
+65 -22
View File
@@ -2,18 +2,22 @@ from __future__ import annotations
from pathlib import Path
import uuid
from backend.app.api.errors import AppError
from backend.app.api.schemas import TaskCreateResponse
from backend.app.db.history_repository import HistoryRepository
from backend.app.db.task_repository import TaskRepository
from backend.app.security.path_guard import PathGuard, ResolvedPath
from backend.app.tasks_runner import TaskRunner
class MoveTaskService:
def __init__(self, path_guard: PathGuard, repository: TaskRepository, runner: TaskRunner):
def __init__(self, path_guard: PathGuard, repository: TaskRepository, runner: TaskRunner, history_repository: HistoryRepository | None = None):
self._path_guard = path_guard
self._repository = repository
self._runner = runner
self._history_repository = history_repository
def create_move_task(self, source: str | None, destination: str | None) -> TaskCreateResponse:
if not source or not destination:
@@ -23,30 +27,50 @@ class MoveTaskService:
status_code=400,
)
item = self._build_move_item(source=source, destination=destination)
task = self._repository.create_task(
operation="move",
source=item["source_relative"],
destination=item["destination_relative"],
)
if item["kind"] == "directory":
self._runner.enqueue_move_directory(
task_id=task["id"],
source=item["source_absolute"],
destination=item["destination_absolute"],
try:
item = self._build_move_item(source=source, destination=destination)
task_id = str(uuid.uuid4())
task = self._repository.create_task(
operation="move",
source=item["source_relative"],
destination=item["destination_relative"],
task_id=task_id,
)
else:
self._runner.enqueue_move_file(
task_id=task["id"],
source=item["source_absolute"],
destination=item["destination_absolute"],
total_bytes=item["total_bytes"],
same_root=item["same_root"],
self._record_history(
entry_id=task_id,
operation="move",
status="queued",
source=item["source_relative"],
destination=item["destination_relative"],
)
return TaskCreateResponse(task_id=task["id"], status=task["status"])
if item["kind"] == "directory":
self._runner.enqueue_move_directory(
task_id=task["id"],
source=item["source_absolute"],
destination=item["destination_absolute"],
)
else:
self._runner.enqueue_move_file(
task_id=task["id"],
source=item["source_absolute"],
destination=item["destination_absolute"],
total_bytes=item["total_bytes"],
same_root=item["same_root"],
)
return TaskCreateResponse(task_id=task["id"], status=task["status"])
except AppError as exc:
self._record_history(
operation="move",
status="failed",
source=source,
destination=destination,
error_code=exc.code,
error_message=exc.message,
finished_at=self._now_iso(),
)
raise
def create_batch_move_task(self, sources: list[str] | None, destination_base: str | None) -> TaskCreateResponse:
if not sources or len(sources) < 2:
@@ -92,10 +116,19 @@ class MoveTaskService:
)
items.append(item)
task_id = str(uuid.uuid4())
task = self._repository.create_task(
operation="move",
source=f"{len(items)} items",
destination=resolved_destination_base.relative,
task_id=task_id,
)
self._record_history(
entry_id=task_id,
operation="move",
status="queued",
source=f"{len(items)} items",
destination=resolved_destination_base.relative,
)
self._runner.enqueue_move_batch(
task_id=task["id"],
@@ -225,3 +258,13 @@ class MoveTaskService:
return True
except ValueError:
return False
def _record_history(self, **kwargs) -> None:
if self._history_repository:
self._history_repository.create_entry(**kwargs)
@staticmethod
def _now_iso() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")