88from __future__ import annotations
99
1010import json
11- import os
12- from dataclasses import asdict , dataclass
11+ from dataclasses import dataclass
1312from json import JSONDecodeError
1413from pathlib import Path
1514from pathlib import PurePosixPath
2120SUPPORTED_IDENTITY_MODES = frozenset ({"blake3" , "meta" })
2221_BLAKE3_HEX_LENGTH = 64
2322_HEX_DIGITS = frozenset ("0123456789abcdef" )
23+ _BLOB_BACKED_MANIFEST_TAG = "b"
24+ _META_ONLY_MANIFEST_TAG = "m"
2425
2526
2627def _is_hex_digest (value : str ) -> bool :
@@ -144,6 +145,86 @@ def from_dict(data: dict[str, object]) -> "ManifestEntry":
144145 )
145146
146147
148+ def serialize_manifest_entry (entry : ManifestEntry ) -> str :
149+ if entry .identity_mode == "blake3" :
150+ payload : list [object ] = [
151+ _BLOB_BACKED_MANIFEST_TAG ,
152+ entry .path ,
153+ entry .hash ,
154+ entry .size ,
155+ entry .mtime_ns ,
156+ ]
157+ elif entry .identity_mode == "meta" :
158+ payload = [
159+ _META_ONLY_MANIFEST_TAG ,
160+ entry .path ,
161+ entry .hash ,
162+ entry .size ,
163+ entry .mtime_ns ,
164+ entry .source_uri ,
165+ ]
166+ else :
167+ supported_modes = ", " .join (sorted (SUPPORTED_IDENTITY_MODES ))
168+ raise ValueError (
169+ f"Manifest entry identity_mode must be one of: { supported_modes } "
170+ )
171+ return json .dumps (payload , separators = ("," , ":" ))
172+
173+
174+ def deserialize_manifest_entry (payload_text : str ) -> ManifestEntry :
175+ try :
176+ payload = _load_manifest_payload (payload_text )
177+ except JSONDecodeError as error :
178+ raise ValueError ("Corrupt manifest entry payload" ) from error
179+
180+ return _manifest_entry_from_payload (payload )
181+
182+
183+ def manifest_entry_path (payload_text : str ) -> str :
184+ try :
185+ payload = _load_manifest_payload (payload_text )
186+ except JSONDecodeError as error :
187+ raise ValueError ("Corrupt manifest entry payload" ) from error
188+ if not isinstance (payload , list ) or len (payload ) < 2 :
189+ raise ValueError ("Manifest entry payload must be a JSON array" )
190+ return str (payload [1 ])
191+
192+
193+ def _load_manifest_payload (payload_text : str ) -> object :
194+ return json .loads (payload_text )
195+
196+
197+ def _manifest_entry_from_payload (payload : object ) -> ManifestEntry :
198+
199+ if not isinstance (payload , list ):
200+ raise ValueError ("Manifest entry payload must be a JSON array" )
201+ if len (payload ) == 5 and payload [0 ] == _BLOB_BACKED_MANIFEST_TAG :
202+ _ , path , hash_value , size , mtime_ns = payload
203+ return ManifestEntry (
204+ path = str (path ),
205+ hash = str (hash_value ),
206+ size = int (size ),
207+ mtime_ns = int (mtime_ns ),
208+ identity_mode = "blake3" ,
209+ identity_value = str (hash_value ),
210+ blob_hash = str (hash_value ),
211+ source_uri = None ,
212+ )
213+ if len (payload ) == 6 and payload [0 ] == _META_ONLY_MANIFEST_TAG :
214+ _ , path , hash_value , size , mtime_ns , source_uri = payload
215+ return ManifestEntry (
216+ path = str (path ),
217+ hash = str (hash_value ),
218+ size = int (size ),
219+ mtime_ns = int (mtime_ns ),
220+ identity_mode = "meta" ,
221+ identity_value = str (hash_value ),
222+ blob_hash = None ,
223+ source_uri = str (source_uri ),
224+ )
225+ raise ValueError ("Manifest entry payload has an unsupported shape" )
226+
227+
147228class ManifestWriter :
148229 def __init__ (self , manifest_path : str | Path ) -> None :
149230 self .manifest_path = Path (manifest_path )
@@ -153,7 +234,7 @@ def write_entries(self, entries: Iterable[ManifestEntry]) -> int:
153234 written = 0
154235 with self .manifest_path .open ("w" , encoding = "utf-8" ) as handle :
155236 for entry in entries :
156- handle .write (json . dumps ( asdict ( entry ), separators = ( "," , ":" ) ))
237+ handle .write (serialize_manifest_entry ( entry ))
157238 handle .write ("\n " )
158239 written += 1
159240 return written
@@ -184,13 +265,13 @@ def iter_entries(self) -> Iterator[ManifestEntry]:
184265 if not line :
185266 continue
186267 try :
187- payload = json . loads (line )
268+ payload = _load_manifest_payload (line )
188269 except JSONDecodeError as error :
189270 raise ValueError (
190271 f"Corrupt manifest JSON at line { line_number } in { self .manifest_path } "
191272 ) from error
192273 try :
193- yield ManifestEntry . from_dict (payload )
274+ yield _manifest_entry_from_payload (payload )
194275 except ValueError as error :
195276 raise ValueError (
196277 f"Invalid manifest entry at line { line_number } in { self .manifest_path } : { error } "
@@ -230,7 +311,15 @@ def build_manifest_entries(
230311
231312def walk_files (root : str | Path ) -> Iterator [Path ]:
232313 root_path = Path (root ).resolve ()
233- for dirpath , dirnames , filenames in os .walk (root_path ):
234- dirnames [:] = [name for name in dirnames if name != ".fluxel" ]
235- for filename in filenames :
236- yield Path (dirpath ) / filename
314+
315+ def iter_dir (path : Path ) -> Iterator [Path ]:
316+ for child in sorted (path .iterdir (), key = lambda item : item .name ):
317+ if child .name == ".fluxel" :
318+ continue
319+ if child .is_dir ():
320+ yield from iter_dir (child )
321+ continue
322+ if child .is_file ():
323+ yield child
324+
325+ yield from iter_dir (root_path )
0 commit comments