feat: feedback verbetering - 06
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,8 @@ async def delete(
|
||||
request: DeleteRequest,
|
||||
service: DeleteTaskService = Depends(get_delete_task_service),
|
||||
) -> TaskCreateResponse:
|
||||
if request.paths is not None:
|
||||
return service.create_batch_delete_task(paths=request.paths, recursive_paths=request.recursive_paths or [])
|
||||
return service.create_delete_task(path=request.path, recursive=request.recursive)
|
||||
|
||||
|
||||
|
||||
@@ -51,8 +51,10 @@ class RenameResponse(BaseModel):
|
||||
|
||||
|
||||
class DeleteRequest(BaseModel):
|
||||
path: str
|
||||
path: str | None = None
|
||||
recursive: bool = False
|
||||
paths: list[str] | None = None
|
||||
recursive_paths: list[str] | None = None
|
||||
|
||||
|
||||
class DeleteResponse(BaseModel):
|
||||
|
||||
Binary file not shown.
@@ -25,7 +25,13 @@ class DeleteTaskService:
|
||||
self._runner = runner
|
||||
self._history_repository = history_repository
|
||||
|
||||
def create_delete_task(self, path: str, recursive: bool = False) -> TaskCreateResponse:
|
||||
def create_delete_task(self, path: str | None, recursive: bool = False) -> TaskCreateResponse:
|
||||
if not path:
|
||||
raise AppError(
|
||||
code="invalid_request",
|
||||
message="Query parameter 'path' is required",
|
||||
status_code=400,
|
||||
)
|
||||
try:
|
||||
item = self._build_delete_item(path=path, recursive=recursive)
|
||||
|
||||
@@ -71,11 +77,82 @@ class DeleteTaskService:
|
||||
)
|
||||
raise error
|
||||
|
||||
def _build_delete_item(self, path: str, recursive: bool) -> dict:
|
||||
def create_batch_delete_task(self, paths: list[str] | None, recursive_paths: list[str] | None = None) -> TaskCreateResponse:
|
||||
if not paths or len(paths) < 2:
|
||||
raise AppError(
|
||||
code="invalid_request",
|
||||
message="Batch delete requires at least 2 paths",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
recursive_paths_set = set(recursive_paths or [])
|
||||
invalid_recursive = sorted(path for path in recursive_paths_set if path not in paths)
|
||||
if invalid_recursive:
|
||||
raise AppError(
|
||||
code="invalid_request",
|
||||
message="Recursive delete paths must be included in the batch selection",
|
||||
status_code=400,
|
||||
details={"path": invalid_recursive[0]},
|
||||
)
|
||||
|
||||
try:
|
||||
items = [
|
||||
self._build_delete_item(
|
||||
path=path,
|
||||
recursive=path in recursive_paths_set,
|
||||
include_root_prefix=True,
|
||||
)
|
||||
for path in paths
|
||||
]
|
||||
|
||||
task_id = str(uuid.uuid4())
|
||||
task = self._repository.create_task(
|
||||
operation="delete",
|
||||
source=f"{len(items)} items",
|
||||
destination="",
|
||||
task_id=task_id,
|
||||
)
|
||||
self._record_history(
|
||||
entry_id=task_id,
|
||||
operation="delete",
|
||||
status="queued",
|
||||
path=f"{len(items)} items",
|
||||
)
|
||||
self._runner.enqueue_delete_batch(task_id=task["id"], items=items)
|
||||
return TaskCreateResponse(task_id=task["id"], status=task["status"])
|
||||
except AppError as exc:
|
||||
self._record_history(
|
||||
operation="delete",
|
||||
status="failed",
|
||||
path=f"{len(paths or [])} items",
|
||||
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=f"{len(paths or [])} items",
|
||||
error_code=error.code,
|
||||
error_message=error.message,
|
||||
finished_at=self._now_iso(),
|
||||
)
|
||||
raise error
|
||||
|
||||
def _build_delete_item(self, path: str, recursive: bool, include_root_prefix: bool = False) -> dict:
|
||||
resolved_target = self._path_guard.resolve_existing_path(path)
|
||||
|
||||
if resolved_target.absolute.is_file():
|
||||
files = [{"path": str(resolved_target.absolute), "label": resolved_target.absolute.name}]
|
||||
label = resolved_target.absolute.name
|
||||
files = [{"path": str(resolved_target.absolute), "label": label}]
|
||||
directories: list[str] = []
|
||||
kind = "file"
|
||||
elif resolved_target.absolute.is_dir():
|
||||
@@ -88,7 +165,10 @@ class DeleteTaskService:
|
||||
details={"path": resolved_target.relative},
|
||||
)
|
||||
if recursive:
|
||||
files, directories = self._build_recursive_delete_plan(resolved_target.absolute)
|
||||
files, directories = self._build_recursive_delete_plan(
|
||||
resolved_target.absolute,
|
||||
include_root_prefix=include_root_prefix,
|
||||
)
|
||||
else:
|
||||
files = []
|
||||
directories = [str(resolved_target.absolute)]
|
||||
@@ -111,9 +191,10 @@ class DeleteTaskService:
|
||||
"progress_label": files[0]["label"] if files else None,
|
||||
}
|
||||
|
||||
def _build_recursive_delete_plan(self, root: Path) -> tuple[list[dict[str, str]], list[str]]:
|
||||
def _build_recursive_delete_plan(self, root: Path, include_root_prefix: bool = False) -> tuple[list[dict[str, str]], list[str]]:
|
||||
files: list[dict[str, str]] = []
|
||||
directories: list[str] = []
|
||||
start_prefix = Path(root.name) if include_root_prefix else Path()
|
||||
|
||||
def walk(path: Path, relative_prefix: Path) -> None:
|
||||
for entry in sorted(path.iterdir(), key=lambda child: child.name.lower()):
|
||||
@@ -127,7 +208,7 @@ class DeleteTaskService:
|
||||
continue
|
||||
files.append({"path": str(entry), "label": relative_path.as_posix()})
|
||||
|
||||
walk(root, Path())
|
||||
walk(root, start_prefix)
|
||||
directories.append(str(root))
|
||||
return files, directories
|
||||
|
||||
|
||||
@@ -89,6 +89,14 @@ class TaskRunner:
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def enqueue_delete_batch(self, task_id: str, items: list[dict[str, object]]) -> None:
|
||||
thread = threading.Thread(
|
||||
target=self._run_delete_batch,
|
||||
args=(task_id, items),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def enqueue_archive_prepare(self, worker) -> None:
|
||||
thread = threading.Thread(
|
||||
target=worker,
|
||||
@@ -490,6 +498,66 @@ class TaskRunner:
|
||||
)
|
||||
self._update_history_failed(task_id, str(exc))
|
||||
|
||||
def _run_delete_batch(self, task_id: str, items: list[dict[str, object]]) -> None:
|
||||
total_items = self._total_delete_work_count(items)
|
||||
current_item = self._first_delete_item_label(items)
|
||||
if not self._repository.mark_running(
|
||||
task_id=task_id,
|
||||
done_items=0,
|
||||
total_items=total_items,
|
||||
current_item=current_item,
|
||||
):
|
||||
self._finalize_if_already_cancelled(task_id, done_items=0, total_items=total_items)
|
||||
return
|
||||
|
||||
completed_items = 0
|
||||
current_target = str(items[0]["target"]) if items else ""
|
||||
try:
|
||||
for item in items:
|
||||
if self._is_cancel_requested(task_id):
|
||||
self._finalize_cancelled(task_id, done_items=completed_items, total_items=total_items)
|
||||
return
|
||||
target = str(item["target"])
|
||||
current_target = target
|
||||
kind = str(item["kind"])
|
||||
recursive = bool(item["recursive"])
|
||||
files = list(item.get("files", [])) # type: ignore[arg-type]
|
||||
directories = list(item.get("directories", [])) # type: ignore[arg-type]
|
||||
if kind == "file":
|
||||
completed_items = self._delete_planned_file(task_id, files[0], completed_items, total_items)
|
||||
elif recursive:
|
||||
for file_entry in files:
|
||||
if self._is_cancel_requested(task_id):
|
||||
self._finalize_cancelled(task_id, done_items=completed_items, total_items=total_items)
|
||||
return
|
||||
completed_items = self._delete_planned_file(task_id, file_entry, completed_items, total_items)
|
||||
if self._is_cancel_requested(task_id):
|
||||
self._finalize_cancelled(task_id, done_items=completed_items, total_items=total_items)
|
||||
return
|
||||
for directory in directories:
|
||||
self._filesystem.delete_empty_directory(Path(directory))
|
||||
else:
|
||||
self._filesystem.delete_empty_directory(Path(target))
|
||||
self._complete_or_cancel_item_task(
|
||||
task_id=task_id,
|
||||
done_items=completed_items,
|
||||
total_items=total_items,
|
||||
)
|
||||
except OSError as exc:
|
||||
task = self._repository.get_task(task_id)
|
||||
failed_item = (task.get("current_item") if task else None) or current_target
|
||||
self._repository.mark_failed(
|
||||
task_id=task_id,
|
||||
error_code="io_error",
|
||||
error_message=str(exc),
|
||||
failed_item=failed_item,
|
||||
done_bytes=None,
|
||||
total_bytes=None,
|
||||
done_items=completed_items,
|
||||
total_items=total_items,
|
||||
)
|
||||
self._update_history_failed(task_id, str(exc))
|
||||
|
||||
def _cleanup_partial_duplicate(self, path: Path) -> None:
|
||||
if not path.exists():
|
||||
return
|
||||
@@ -556,6 +624,26 @@ class TaskRunner:
|
||||
return files[0]["label"]
|
||||
return None
|
||||
|
||||
def _total_delete_work_count(self, items: list[dict[str, object]]) -> int:
|
||||
total = 0
|
||||
for item in items:
|
||||
progress_total_items = item.get("progress_total_items")
|
||||
if progress_total_items is not None:
|
||||
total += int(progress_total_items)
|
||||
continue
|
||||
total += len(self._file_entries(item))
|
||||
return total
|
||||
|
||||
def _first_delete_item_label(self, items: list[dict[str, object]]) -> str | None:
|
||||
for item in items:
|
||||
progress_label = item.get("progress_label")
|
||||
if isinstance(progress_label, str) and progress_label:
|
||||
return progress_label
|
||||
files = self._file_entries(item)
|
||||
if files:
|
||||
return files[0]["label"]
|
||||
return None
|
||||
|
||||
def _copy_single_planned_file(
|
||||
self,
|
||||
task_id: str,
|
||||
|
||||
Reference in New Issue
Block a user