diff --git a/src/file_hashes.py b/src/file_hashes.py new file mode 100644 index 0000000..e7fca81 --- /dev/null +++ b/src/file_hashes.py @@ -0,0 +1,101 @@ +""" +Deploy hash manifest at flash root (file_hashes.json). + +Updated by led-cli after directory uploads; used to skip unchanged files on +the next deploy. Format: {"version": 1, "algorithm": "sha256", "files": {...}} +""" + +import json +import os + +MANIFEST_VERSION = 1 +MANIFEST_FILENAME = "file_hashes.json" +HASH_ALGO = "sha256" + +_SKIP_NAMES = frozenset({MANIFEST_FILENAME, "__pycache__"}) +_SKIP_SUFFIXES = (".pyc", ".pyo") + + +def _normalize_path(path): + return path.replace("\\", "/").lstrip("/") + + +def load(): + """Return path -> sha256 hex map, or {} if missing or invalid.""" + try: + with open(MANIFEST_FILENAME, "r") as f: + doc = json.load(f) + except OSError: + return {} + if not isinstance(doc, dict): + return {} + files = doc.get("files") + return files if isinstance(files, dict) else {} + + +def save(files): + """Write manifest (path keys use forward slashes, no leading slash).""" + if not isinstance(files, dict): + files = {} + doc = { + "version": MANIFEST_VERSION, + "algorithm": HASH_ALGO, + "files": files, + } + with open(MANIFEST_FILENAME, "w") as f: + json.dump(doc, f) + + +def _hash_file(path): + import hashlib + + h = hashlib.sha256() + with open(path, "rb") as f: + while True: + chunk = f.read(256) + if not chunk: + break + h.update(chunk) + return h.hexdigest() + + +def _walk_dir(base, prefix, out): + try: + names = os.listdir(base) + except OSError: + return + for name in names: + if name in _SKIP_NAMES or name.endswith(_SKIP_SUFFIXES): + continue + full = base + "/" + name if base else name + key = _normalize_path((prefix + "/" + name) if prefix else name) + try: + mode = os.stat(full)[0] + except OSError: + continue + if mode & 0x4000: + _walk_dir(full, key, out) + else: + out[key] = _hash_file(full) + + +def rebuild(): + """Rebuild manifest from root .py files plus patterns/ and lib/ trees.""" + files = {} + try: + for name in os.listdir("."): + if name in _SKIP_NAMES or name.endswith(_SKIP_SUFFIXES): + continue + try: + mode = os.stat(name)[0] + except OSError: + continue + if mode & 0x4000: + if name in ("patterns", "lib"): + _walk_dir(name, name, files) + else: + files[_normalize_path(name)] = _hash_file(name) + except OSError: + pass + save(files) + return files