feat: voortgang delete in headerbar

This commit is contained in:
kodi
2026-03-15 11:52:39 +01:00
parent 73b09d2802
commit 7d910479f9
23 changed files with 311 additions and 43 deletions
+7 -6
View File
@@ -4,9 +4,10 @@ from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
from fastapi.responses import StreamingResponse
from starlette.background import BackgroundTask
from backend.app.api.schemas import ArchivePrepareRequest, DeleteRequest, DeleteResponse, FileInfoResponse, MkdirRequest, MkdirResponse, RenameRequest, RenameResponse, SaveRequest, SaveResponse, TaskCreateResponse, TaskDetailResponse, UploadResponse, ViewResponse
from backend.app.dependencies import get_archive_download_task_service, get_file_ops_service
from backend.app.api.schemas import ArchivePrepareRequest, DeleteRequest, FileInfoResponse, MkdirRequest, MkdirResponse, RenameRequest, RenameResponse, SaveRequest, SaveResponse, TaskCreateResponse, TaskDetailResponse, UploadResponse, ViewResponse
from backend.app.dependencies import get_archive_download_task_service, get_delete_task_service, get_file_ops_service
from backend.app.services.archive_download_task_service import ArchiveDownloadTaskService
from backend.app.services.delete_task_service import DeleteTaskService
from backend.app.services.file_ops_service import FileOpsService
router = APIRouter(prefix="/files")
@@ -28,12 +29,12 @@ async def rename(
return service.rename(path=request.path, new_name=request.new_name)
@router.post("/delete", response_model=DeleteResponse)
@router.post("/delete", response_model=TaskCreateResponse, status_code=202)
async def delete(
request: DeleteRequest,
service: FileOpsService = Depends(get_file_ops_service),
) -> DeleteResponse:
return service.delete(path=request.path, recursive=request.recursive)
service: DeleteTaskService = Depends(get_delete_task_service),
) -> TaskCreateResponse:
return service.create_delete_task(path=request.path, recursive=request.recursive)
@router.post("/upload", response_model=UploadResponse)
+1 -1
View File
@@ -7,7 +7,7 @@ from datetime import datetime, timezone
from pathlib import Path
VALID_STATUSES = {"queued", "running", "completed", "failed", "requested", "preparing", "ready", "cancelled"}
VALID_OPERATIONS = {"copy", "move", "download", "duplicate"}
VALID_OPERATIONS = {"copy", "move", "download", "duplicate", "delete"}
NON_TERMINAL_STATUSES = ("queued", "running", "requested", "preparing")
TASK_MIGRATION_COLUMNS: dict[str, str] = {
"operation": "TEXT NOT NULL DEFAULT 'copy'",
+10
View File
@@ -14,6 +14,7 @@ from backend.app.services.bookmark_service import BookmarkService
from backend.app.services.browse_service import BrowseService
from backend.app.services.copy_task_service import CopyTaskService
from backend.app.services.archive_download_task_service import ArchiveDownloadTaskService
from backend.app.services.delete_task_service import DeleteTaskService
from backend.app.services.duplicate_task_service import DuplicateTaskService
from backend.app.services.file_ops_service import FileOpsService
from backend.app.services.history_service import HistoryService
@@ -113,6 +114,15 @@ async def get_copy_task_service() -> CopyTaskService:
)
async def get_delete_task_service() -> DeleteTaskService:
return DeleteTaskService(
path_guard=get_path_guard(),
repository=get_task_repository(),
runner=get_task_runner(),
history_repository=get_history_repository(),
)
async def get_duplicate_task_service() -> DuplicateTaskService:
return DuplicateTaskService(
path_guard=get_path_guard(),
@@ -0,0 +1,103 @@
from __future__ import annotations
import uuid
from datetime import datetime, timezone
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 DeleteTaskService:
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_delete_task(self, path: str, recursive: bool = False) -> TaskCreateResponse:
try:
resolved_target = self._path_guard.resolve_existing_path(path)
if resolved_target.absolute.is_file():
kind = "file"
elif resolved_target.absolute.is_dir():
kind = "directory"
if not recursive and any(resolved_target.absolute.iterdir()):
raise AppError(
code="directory_not_empty",
message="Directory is not empty",
status_code=409,
details={"path": resolved_target.relative},
)
else:
raise AppError(
code="type_conflict",
message="Unsupported path type for delete",
status_code=409,
details={"path": resolved_target.relative},
)
task_id = str(uuid.uuid4())
task = self._repository.create_task(
operation="delete",
source=resolved_target.relative,
destination="",
task_id=task_id,
)
self._record_history(
entry_id=task_id,
operation="delete",
status="queued",
path=resolved_target.relative,
)
self._runner.enqueue_delete_path(
task_id=task["id"],
target=str(resolved_target.absolute),
kind=kind,
recursive=recursive,
)
return TaskCreateResponse(task_id=task["id"], status=task["status"])
except AppError as exc:
self._record_history(
operation="delete",
status="failed",
path=path,
error_code=exc.code,
error_message=exc.message,
finished_at=self._now_iso(),
)
raise
except OSError as exc:
error = AppError(
code="io_error",
message="Filesystem operation failed",
status_code=500,
details={"reason": str(exc)},
)
self._record_history(
operation="delete",
status="failed",
path=path,
error_code=error.code,
error_message=error.message,
finished_at=self._now_iso(),
)
raise error
def _record_history(self, **kwargs) -> None:
if self._history_repository:
self._history_repository.create_entry(**kwargs)
@staticmethod
def _now_iso() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
+43
View File
@@ -79,6 +79,14 @@ class TaskRunner:
)
thread.start()
def enqueue_delete_path(self, task_id: str, target: str, kind: str, recursive: bool) -> None:
thread = threading.Thread(
target=self._run_delete_path,
args=(task_id, target, kind, recursive),
daemon=True,
)
thread.start()
def enqueue_archive_prepare(self, worker) -> None:
thread = threading.Thread(
target=worker,
@@ -381,6 +389,41 @@ class TaskRunner:
)
self._update_history_completed(task_id)
def _run_delete_path(self, task_id: str, target: str, kind: str, recursive: bool) -> None:
self._repository.mark_running(
task_id=task_id,
done_items=0,
total_items=1,
current_item=target,
)
try:
path = Path(target)
if kind == "file":
self._filesystem.delete_file(path)
elif recursive:
self._filesystem.delete_directory_recursive(path)
else:
self._filesystem.delete_empty_directory(path)
self._repository.mark_completed(
task_id=task_id,
done_items=1,
total_items=1,
)
self._update_history_completed(task_id)
except OSError as exc:
self._repository.mark_failed(
task_id=task_id,
error_code="io_error",
error_message=str(exc),
failed_item=target,
done_bytes=None,
total_bytes=None,
done_items=0,
total_items=1,
)
self._update_history_failed(task_id, str(exc))
def _duplicate_directory(self, source: Path, destination: Path) -> None:
destination.mkdir()
copied_directories: list[tuple[Path, Path]] = [(source, destination)]