mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 13:33:24 -05:00
fix: write artifacts whole or not at all
This commit is contained in:
1 parent
53bd42b2f4
commit
f47088fe37
4 files changed
+105
-10
No files matched your search
+30
-6
@@ -43,11 +43,18 @@ def write_if_changed(path: str, content: str, normalize=None) -> bool:
|
||||
)
|
||||
if _strip_timestamps(before) == _strip_timestamps(after):
|
||||
return False
|
||||
# Truncate-then-write leaves a half-written artifact behind an interrupt,
|
||||
# and every generator in the repo funnels through here: a partial
|
||||
# database.json or README.md is committed-looking and silently wrong.
|
||||
# The scratch file sits beside the target so the rename stays on one
|
||||
# filesystem, which is what makes it atomic.
|
||||
write_text_atomic(path, content)
|
||||
return True
|
||||
|
||||
|
||||
def write_text_atomic(path: str, content: str) -> None:
|
||||
"""Write a whole file or nothing.
|
||||
|
||||
Truncate-then-write leaves a half-written artifact behind an interrupt:
|
||||
a partial database.json, manifest or README.md is committed-looking and
|
||||
silently wrong. The scratch file sits beside the target so the rename
|
||||
stays on one filesystem, which is what makes it atomic.
|
||||
"""
|
||||
directory = os.path.dirname(os.path.abspath(path))
|
||||
handle, scratch = tempfile.mkstemp(
|
||||
dir=directory, prefix=f".{os.path.basename(path)}.", suffix=".tmp"
|
||||
@@ -60,7 +67,24 @@ def write_if_changed(path: str, content: str, normalize=None) -> bool:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(scratch)
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def copy_file_atomic(source: str, path: str) -> None:
|
||||
"""Copy a file into place whole or not at all, metadata included."""
|
||||
import shutil
|
||||
|
||||
directory = os.path.dirname(os.path.abspath(path))
|
||||
handle, scratch = tempfile.mkstemp(
|
||||
dir=directory, prefix=f".{os.path.basename(path)}.", suffix=".tmp"
|
||||
)
|
||||
os.close(handle)
|
||||
try:
|
||||
shutil.copy2(source, scratch)
|
||||
os.replace(scratch, path)
|
||||
except BaseException:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(scratch)
|
||||
raise
|
||||
|
||||
def _strip_timestamps(text: str) -> str:
|
||||
"""Remove known timestamp patterns for content comparison."""
|
||||
|
||||
@@ -24,6 +24,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import split_pack
|
||||
from common import write_text_atomic
|
||||
|
||||
RECORD = "release.json"
|
||||
|
||||
@@ -124,7 +125,7 @@ def main() -> int:
|
||||
print(f"Error: {line}", file=sys.stderr)
|
||||
if mismatches:
|
||||
return 1
|
||||
args.output.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
||||
write_text_atomic(str(args.output), json.dumps(record, indent=2) + "\n")
|
||||
print(f"Wrote {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -19,11 +19,10 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import load_database
|
||||
from common import copy_file_atomic, load_database
|
||||
|
||||
|
||||
def gitignored_paths(gitignore: str) -> set[str]:
|
||||
@@ -72,7 +71,9 @@ def restore(
|
||||
unsatisfied.append(path)
|
||||
continue
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
shutil.copy2(source, path)
|
||||
# Whole or absent: a scan running beside a plain copy hashed the
|
||||
# truncated file and published that in the database.
|
||||
copy_file_atomic(source, path)
|
||||
print(f"Restored: {path}")
|
||||
restored += 1
|
||||
print(f"Total: {restored} files restored")
|
||||
|
||||
Reference in new issue
Block a user