1 Commits

Author SHA1 Message Date
85490a3bd0 feat(deploy): add file_hashes.json manifest on device
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 19:14:51 +12:00

101
src/file_hashes.py Normal file
View File

@@ -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