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:
Executable
+77
@@ -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:])
|
||||
Reference in New Issue
Block a user