feature: duplicate 01

This commit is contained in:
kodi
2026-03-14 17:20:36 +01:00
parent 14600dd5b6
commit 7f7665880f
27 changed files with 583 additions and 3 deletions
+101
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import os
import shutil
import threading
from pathlib import Path
@@ -69,6 +71,14 @@ class TaskRunner:
)
thread.start()
def enqueue_duplicate_batch(self, task_id: str, items: list[dict[str, str]]) -> None:
thread = threading.Thread(
target=self._run_duplicate_batch,
args=(task_id, items),
daemon=True,
)
thread.start()
def enqueue_archive_prepare(self, worker) -> None:
thread = threading.Thread(
target=worker,
@@ -322,6 +332,97 @@ class TaskRunner:
)
self._update_history_completed(task_id)
def _run_duplicate_batch(self, task_id: str, items: list[dict[str, str]]) -> None:
total_items = len(items)
current_item = items[0]["source"] if items else None
self._repository.mark_running(
task_id=task_id,
done_items=0,
total_items=total_items,
current_item=current_item,
)
completed_items = 0
for index, item in enumerate(items):
source = item["source"]
destination = item["destination"]
try:
if item["kind"] == "directory":
self._duplicate_directory(source=Path(source), destination=Path(destination))
else:
self._filesystem.copy_file(source=source, destination=destination)
completed_items = index + 1
next_item = items[index + 1]["source"] if index + 1 < total_items else source
self._repository.update_progress(
task_id=task_id,
done_items=completed_items,
total_items=total_items,
current_item=next_item,
)
except OSError as exc:
self._cleanup_partial_duplicate(Path(destination))
self._repository.mark_failed(
task_id=task_id,
error_code="io_error",
error_message=str(exc),
failed_item=source,
done_bytes=None,
total_bytes=None,
done_items=completed_items,
total_items=total_items,
)
self._update_history_failed(task_id, str(exc))
return
self._repository.mark_completed(
task_id=task_id,
done_items=total_items,
total_items=total_items,
)
self._update_history_completed(task_id)
def _duplicate_directory(self, source: Path, destination: Path) -> None:
destination.mkdir()
copied_directories: list[tuple[Path, Path]] = [(source, destination)]
try:
for root, dirnames, filenames in os.walk(source, topdown=True, followlinks=False):
root_path = Path(root)
target_root = destination / root_path.relative_to(source)
dirnames[:] = [name for name in dirnames if not name.startswith("._")]
for name in dirnames:
source_dir = root_path / name
if source_dir.is_symlink():
raise OSError("Source directory must not contain symlinks")
target_dir = target_root / name
target_dir.mkdir()
copied_directories.append((source_dir, target_dir))
for name in filenames:
if name.startswith("._"):
continue
source_file = root_path / name
if source_file.is_symlink():
raise OSError("Source directory must not contain symlinks")
self._filesystem.copy_file(
source=str(source_file),
destination=str(target_root / name),
)
for source_dir, target_dir in reversed(copied_directories):
shutil.copystat(source_dir, target_dir, follow_symlinks=False)
except Exception:
self._cleanup_partial_duplicate(destination)
raise
def _cleanup_partial_duplicate(self, path: Path) -> None:
if not path.exists():
return
if path.is_dir():
shutil.rmtree(path)
return
path.unlink()
def _update_history_completed(self, task_id: str) -> None:
if self._history_repository:
self._history_repository.update_entry(entry_id=task_id, status="completed")