mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-19 15:14:34 +00:00
Make NC shadow tree replacement rollback-safe
This commit is contained in:
@@ -56,12 +56,7 @@ def load_manifest(path: Path) -> list[dict[str, str]]:
|
|||||||
share_id, item_type, display_name, source_path = fields
|
share_id, item_type, display_name, source_path = fields
|
||||||
if item_type not in {"folder", "file"}:
|
if item_type not in {"folder", "file"}:
|
||||||
raise ValueError(f"{path}:{line_number}: unsupported item_type {item_type!r}")
|
raise ValueError(f"{path}:{line_number}: unsupported item_type {item_type!r}")
|
||||||
entries.append({
|
entries.append({"share_id": share_id, "item_type": item_type, "display_name": display_name, "source_path": source_path})
|
||||||
"share_id": share_id,
|
|
||||||
"item_type": item_type,
|
|
||||||
"display_name": display_name,
|
|
||||||
"source_path": source_path,
|
|
||||||
})
|
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
@@ -87,8 +82,7 @@ def preferred_target(source_key: str, raw_name: str, parent_target: Path, old_ma
|
|||||||
def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None:
|
def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None:
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
used: set[str] = set()
|
used: set[str] = set()
|
||||||
children = sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name))
|
for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
|
||||||
for child in children:
|
|
||||||
if child.is_symlink():
|
if child.is_symlink():
|
||||||
continue
|
continue
|
||||||
child_source_key = f"{source_key}/{child.name}"
|
child_source_key = f"{source_key}/{child.name}"
|
||||||
@@ -103,17 +97,28 @@ def mirror_directory(source: Path, target: Path, source_key: str, target_key: Pa
|
|||||||
child_target.symlink_to(child.resolve())
|
child_target.symlink_to(child.resolve())
|
||||||
|
|
||||||
|
|
||||||
|
def remove_tree(path: Path) -> None:
|
||||||
|
if path.is_symlink() or path.is_file():
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
elif path.exists():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
|
||||||
def build(manifest: Path, destination: Path, state_file: Path) -> None:
|
def build(manifest: Path, destination: Path, state_file: Path) -> None:
|
||||||
entries = load_manifest(manifest)
|
entries = load_manifest(manifest)
|
||||||
old_map = load_map(state_file)
|
old_map = load_map(state_file)
|
||||||
new_map: dict[str, str] = {}
|
new_map: dict[str, str] = {}
|
||||||
staging = destination.with_name(f".{destination.name}.next")
|
staging = destination.with_name(f".{destination.name}.next")
|
||||||
previous = destination.with_name(f".{destination.name}.previous")
|
previous = destination.with_name(f".{destination.name}.previous")
|
||||||
|
state_staging = state_file.with_suffix(state_file.suffix + ".next")
|
||||||
|
|
||||||
if staging.exists():
|
remove_tree(staging)
|
||||||
shutil.rmtree(staging)
|
remove_tree(previous)
|
||||||
|
if state_staging.exists():
|
||||||
|
state_staging.unlink()
|
||||||
staging.mkdir(parents=True)
|
staging.mkdir(parents=True)
|
||||||
|
|
||||||
|
try:
|
||||||
used_roots: set[str] = set()
|
used_roots: set[str] = set()
|
||||||
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
|
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
|
||||||
source = Path(entry["source_path"])
|
source = Path(entry["source_path"])
|
||||||
@@ -123,31 +128,37 @@ def build(manifest: Path, destination: Path, state_file: Path) -> None:
|
|||||||
root_name = unique_name(preferred, used_roots, is_file_share)
|
root_name = unique_name(preferred, used_roots, is_file_share)
|
||||||
root_key = Path(root_name)
|
root_key = Path(root_name)
|
||||||
new_map[source_key] = root_key.as_posix()
|
new_map[source_key] = root_key.as_posix()
|
||||||
|
|
||||||
if is_file_share:
|
if is_file_share:
|
||||||
if not source.is_file():
|
if not source.is_file():
|
||||||
raise FileNotFoundError(f"source is not a readable file: {source}")
|
raise FileNotFoundError(f"source is not a readable file: {source}")
|
||||||
(staging / root_name).symlink_to(source.resolve())
|
(staging / root_name).symlink_to(source.resolve())
|
||||||
continue
|
else:
|
||||||
|
|
||||||
if not source.is_dir():
|
if not source.is_dir():
|
||||||
raise FileNotFoundError(f"source is not a readable directory: {source}")
|
raise FileNotFoundError(f"source is not a readable directory: {source}")
|
||||||
mirror_directory(source, staging / root_name, source_key, root_key, old_map, new_map)
|
mirror_directory(source, staging / root_name, source_key, root_key, old_map, new_map)
|
||||||
|
|
||||||
state_staging = state_file.with_suffix(state_file.suffix + ".next")
|
|
||||||
state_staging.parent.mkdir(parents=True, exist_ok=True)
|
state_staging.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with state_staging.open("w", encoding="utf-8") as handle:
|
with state_staging.open("w", encoding="utf-8") as handle:
|
||||||
json.dump(new_map, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
json.dump(new_map, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
handle.write("\n")
|
handle.write("\n")
|
||||||
|
|
||||||
if previous.exists():
|
had_destination = destination.exists()
|
||||||
shutil.rmtree(previous)
|
if had_destination:
|
||||||
if destination.exists():
|
|
||||||
destination.rename(previous)
|
destination.rename(previous)
|
||||||
|
try:
|
||||||
staging.rename(destination)
|
staging.rename(destination)
|
||||||
state_staging.replace(state_file)
|
state_staging.replace(state_file)
|
||||||
if previous.exists():
|
except Exception:
|
||||||
shutil.rmtree(previous)
|
remove_tree(destination)
|
||||||
|
if had_destination and previous.exists():
|
||||||
|
previous.rename(destination)
|
||||||
|
raise
|
||||||
|
remove_tree(previous)
|
||||||
|
except Exception:
|
||||||
|
remove_tree(staging)
|
||||||
|
if state_staging.exists():
|
||||||
|
state_staging.unlink()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user