Add batch duplicate review workflow
This commit is contained in:
+40
-10
@@ -83,8 +83,28 @@ def apply_duplicate_repair(
|
||||
backup_limit: Optional[int] = 10,
|
||||
) -> RepairReceipt:
|
||||
"""Create a complete rollback snapshot, then replace extras with symlinks."""
|
||||
return apply_duplicate_repair_batch((plan,), serato_root, backup_limit)
|
||||
|
||||
|
||||
def apply_duplicate_repair_batch(
|
||||
plans: Iterable[DuplicateRepairPlan],
|
||||
serato_root: Path,
|
||||
backup_limit: Optional[int] = 10,
|
||||
) -> RepairReceipt:
|
||||
"""Apply several approved duplicate choices as one atomic backup."""
|
||||
plans = tuple(plans)
|
||||
if not plans:
|
||||
raise ValueError("Choose at least one duplicate group")
|
||||
if backup_limit is not None and backup_limit < 1:
|
||||
raise ValueError("Backup limit must be at least 1, or unlimited")
|
||||
replaced_paths = tuple(
|
||||
path for plan in plans for path in plan.replaced
|
||||
)
|
||||
if len(set(replaced_paths)) != len(replaced_paths):
|
||||
raise ValueError("The same file appears in more than one repair choice")
|
||||
keepers = {plan.keeper for plan in plans}
|
||||
if keepers.intersection(replaced_paths):
|
||||
raise ValueError("A selected keeper cannot be removed by another choice")
|
||||
backup_root = serato_root.expanduser().resolve() / BACKUP_FOLDER
|
||||
backup = backup_root / _backup_name()
|
||||
files_root = backup / "files"
|
||||
@@ -93,35 +113,45 @@ def apply_duplicate_repair(
|
||||
|
||||
entries = []
|
||||
try:
|
||||
for path in plan.replaced:
|
||||
destination = files_root / _safe_backup_path(path)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, destination)
|
||||
entries.append({"original": str(path), "backup": str(destination)})
|
||||
for path in plan.metadata_files:
|
||||
for plan in plans:
|
||||
for path in plan.replaced:
|
||||
destination = files_root / _safe_backup_path(path)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, destination)
|
||||
entries.append(
|
||||
{
|
||||
"original": str(path),
|
||||
"backup": str(destination),
|
||||
"keeper": str(plan.keeper),
|
||||
}
|
||||
)
|
||||
for path in plans[0].metadata_files:
|
||||
relative = path.relative_to(serato_root.expanduser().resolve())
|
||||
destination = metadata_root / relative
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, destination)
|
||||
manifest = {
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"keeper": str(plan.keeper),
|
||||
"keeper": str(plans[0].keeper),
|
||||
"keepers": [str(plan.keeper) for plan in plans],
|
||||
"choice_count": len(plans),
|
||||
"replaced": entries,
|
||||
"strategy": "symlink",
|
||||
}
|
||||
(backup / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2), encoding="utf-8"
|
||||
)
|
||||
for path in plan.replaced:
|
||||
for entry in entries:
|
||||
path = Path(entry["original"])
|
||||
path.unlink()
|
||||
path.symlink_to(plan.keeper)
|
||||
path.symlink_to(Path(entry["keeper"]))
|
||||
except Exception:
|
||||
_rollback_entries(entries)
|
||||
shutil.rmtree(backup, ignore_errors=True)
|
||||
raise
|
||||
|
||||
rotate_backups(backup_root, backup_limit)
|
||||
return RepairReceipt(backup, plan.keeper, plan.replaced)
|
||||
return RepairReceipt(backup, plans[0].keeper, replaced_paths)
|
||||
|
||||
|
||||
def restore_backup(backup: Path) -> Tuple[Path, ...]:
|
||||
|
||||
Reference in New Issue
Block a user