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