#!/usr/bin/env python3 """Archives the files of a publish folder that differ from the previous archive. usage: publish-update.py 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:])