Write an update archive with only the files changed since the last publish

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 01:07:47 +00:00
co-authored by Claude Sonnet 5.5
parent c6eb1355a8
commit ae37e0be53
2 changed files with 104 additions and 7 deletions
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Archives the files of a publish folder that differ from the previous archive.
usage: publish-update.py <previous archive> <folder> <output archive>
The previous archive is the full one the last publish left behind (.zip or
.tar.gz). A file goes into the output when it is new or its content changed;
files the new publish no longer has are listed but cannot be removed by
unpacking over the old install. The archive keeps the folder name as its top
directory, like the full one. Prints nothing and writes nothing when no file
changed.
"""
import hashlib
import os
import sys
import tarfile
import zipfile
def digest(data):
return hashlib.sha256(data).hexdigest()
def read_previous(path):
"""Maps each file's path inside the archive to a hash of its content."""
hashes = {}
if path.endswith(".zip"):
with zipfile.ZipFile(path) as archive:
for info in archive.infolist():
if not info.is_dir():
hashes[info.filename] = digest(archive.read(info))
else:
with tarfile.open(path) as archive:
for member in archive.getmembers():
if member.isfile():
hashes[member.name] = digest(archive.extractfile(member).read())
return hashes
def main(previous, folder, output):
root = os.path.basename(os.path.normpath(folder))
old = read_previous(previous)
changed = []
current = set()
for directory, _, names in os.walk(folder):
for name in sorted(names):
full = os.path.join(directory, name)
inside = root + "/" + os.path.relpath(full, folder).replace(os.sep, "/")
current.add(inside)
with open(full, "rb") as file:
if old.get(inside) != digest(file.read()):
changed.append((full, inside))
removed = sorted(set(old) - current)
if os.path.exists(output):
os.remove(output)
if not changed:
print("no file changed since the previous archive")
return
changed.sort(key=lambda pair: pair[1])
if output.endswith(".zip"):
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
for full, inside in changed:
archive.write(full, inside)
else:
with tarfile.open(output, "w:gz") as archive:
for full, inside in changed:
archive.add(full, inside, recursive=False)
print(f"{output}: {len(changed)} changed of {len(current)} files")
for gone in removed:
print(f" no longer in the publish: {gone}")
if __name__ == "__main__":
if len(sys.argv) != 4:
sys.exit(__doc__)
main(*sys.argv[1:])