feat(deploy): add file_hashes.json manifest on device
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
101
src/file_hashes.py
Normal file
101
src/file_hashes.py
Normal 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
|
||||||
Reference in New Issue
Block a user