5353from .patterns import PathPrefixPattern , FnmatchPattern , IECommand
5454from .item import Item , ArchiveItem , ItemDiff
5555from .platform import acl_get , acl_set , set_flags , get_flags , set_times , swidth
56- from .repository import Repository , NoManifestError
56+ from .hashindex import ChunkIndex , ChunkIndexEntry
57+ from .repository import Repository , NoManifestError , PackReader
5758from .repoobj import RepoObj , object_validator
5859
5960# macOS: SF_DATALESS marks dataless placeholder files (e.g. cloud files not materialized locally).
@@ -2213,9 +2214,31 @@ class ArchiveChecker:
22132214 def __init__ (self ):
22142215 self .error_found = False
22152216 self .key = None
2216- # True once repair drops a defect chunk or writes a new one, i.e. once the chunks index no
2217- # longer matches the packs.
2217+ # True once repair wrote a pack: it stored a chunk or deleted a defect chunk.
22182218 self .chunks_modified = False
2219+ # True if check() built the chunks index from the packs with a validator, so every indexed object
2220+ # passed it. A validator (repoobj.object_validator) checks an object header against the object's
2221+ # tagged metadata.
2222+ self .rebuild_validated = False
2223+ # ids of the existing packs repair stored (put(), flush()) or wrote by rewriting a pack (delete()).
2224+ self .written_packs = set ()
2225+
2226+ def record_stored (self , results ):
2227+ """Add the pack ids in results to written_packs.
2228+
2229+ results: the (chunk_id, pack_id, obj_offset, obj_size) tuples Repository.put() or .flush() returns
2230+ for the packs it stored, or None if it stored no pack.
2231+ """
2232+ if results :
2233+ self .written_packs .update (pack_id for _ , pack_id , _ , _ in results )
2234+
2235+ def create_archive_entry (self , name , id , ts ):
2236+ """Store the pack writer buffer, add the stored packs to written_packs, create the archives entry.
2237+
2238+ Archives.create() also stores the pack writer buffer, but does not return the stored packs.
2239+ """
2240+ self .record_stored (self .repository .flush ())
2241+ self .manifest .archives .create (name , id , ts )
22192242
22202243 def note_dropped_objects (self ):
22212244 # The chunk index rebuild skipped repository content to get past a corrupt object header.
@@ -2305,6 +2328,7 @@ def check(
23052328 drop_corrupt_tail = repair ,
23062329 write_immediately = False ,
23072330 )
2331+ self .rebuild_validated = repair and validate is not None
23082332 # a rebuild from the packs sets F_NEW (entry not stored in the index/ fragments yet) on every
23092333 # entry. Clear it, so Repository.close() does not store this index as a new fragment: only
23102334 # finish() stores the index, and only with --repair.
@@ -2439,20 +2463,23 @@ def verify_data(self):
24392463 assert_id_place = "verify_data" ,
24402464 )
24412465 except IntegrityErrorBase :
2442- # failed twice -> remove this defect chunk. delete rewrites its pack without it,
2443- # keeping the other chunks. update_index=False: finish() rebuilds the index from
2444- # the rewritten packs anyway, so a per-chunk full index write would be wasted.
2445- # delete() also removes the chunk from self.chunks, so rebuild_archives reports
2446- # the file it belongs to.
2466+ # failed twice -> remove this defect chunk. delete() removes it from self.chunks,
2467+ # so rebuild_archives reports the file it belongs to. update_index=False: finish()
2468+ # writes the index.
24472469 if not index_marked_invalid :
24482470 # the index/ fragments point the other chunks of a rewritten pack at the
24492471 # deleted pack until finish() stores the new index. Mark them invalid before
24502472 # the first delete, so if the check stops before finish(), the next use
24512473 # rebuilds the index from the packs.
24522474 write_chunkindex_invalid (self .repository )
24532475 index_marked_invalid = True
2454- self .repository .delete (defect_chunk , update_index = False , validate = validate )
2476+ old_pack_id = self .chunks [defect_chunk ].pack_id
2477+ new_pack_id , _ = self .repository .delete (defect_chunk , update_index = False , validate = validate )
24552478 self .chunks_modified = True
2479+ # delete() replaced old_pack_id by new_pack_id, None if no object was left.
2480+ self .written_packs .discard (old_pack_id )
2481+ if new_pack_id is not None :
2482+ self .written_packs .add (new_pack_id )
24562483 else :
24572484 logger .warning ("chunk %s not deleted, did not consistently fail." , bin_to_hex (defect_chunk ))
24582485 else :
@@ -2546,7 +2573,7 @@ def valid_archive(obj):
25462573 self .error_found = True
25472574 if self .repair :
25482575 logger .warning (f"Creating archives directory entry for { name } { archive_id_hex } ." )
2549- self .manifest . archives . create (name , archive_id , archive .time )
2576+ self .create_archive_entry (name , archive_id , archive .time )
25502577 else :
25512578 logger .warning (f"Would create archives directory entry for { name } { archive_id_hex } ." )
25522579
@@ -2602,7 +2629,7 @@ def add_reference(id_, size, cdata):
26022629 # --repair: store a chunk the repository does not have. put() adds it to self.chunks.
26032630 if self .repair and id_ not in self .chunks :
26042631 assert cdata is not None
2605- self .repository .put (id_ , cdata )
2632+ self .record_stored ( self . repository .put (id_ , cdata ) )
26062633 self .chunks_modified = True
26072634
26082635 def verify_file_chunks (archive_name , item ):
@@ -2812,23 +2839,113 @@ def valid_item(obj):
28122839 logger .debug (f"archive id new: { bin_to_hex (new_archive_id )} " )
28132840 cdata = self .repo_objs .format (new_archive_id , {}, data , ro_type = ROBJ_ARCHIVE_META )
28142841 add_reference (new_archive_id , len (data ), cdata )
2815- self .manifest . archives . create (info .name , new_archive_id , info .ts )
2842+ self .create_archive_entry (info .name , new_archive_id , info .ts )
28162843 if archive_id != new_archive_id :
28172844 self .manifest .archives .delete_by_id (archive_id )
28182845 finally :
28192846 pi .finish ()
28202847 report_missing_chunks ()
28212848
2849+ def verify_written_packs (self ):
2850+ """Read the object headers of the packs in written_packs and make the chunks index match them.
2851+
2852+ put() and delete() compute the index entries of the packs they write without reading the packs.
2853+ This compares the (chunk_id, obj_offset, obj_size) of each object header in a written pack, read
2854+ with a validator, with the index entries that name the pack. Each difference is a check finding,
2855+ logged and fixed in the index:
2856+
2857+ - an index entry names an object the pack does not hold: the entry is removed.
2858+ - the pack holds an object whose chunk id is not indexed: the object is indexed.
2859+ - the pack does not exist: its index entries are removed.
2860+
2861+ An object whose chunk id is indexed at another location is a superseded duplicate, not a finding.
2862+ A pack delete() wrote can hold one: compact_pack copies the byte ranges no index entry covers into
2863+ the new pack, except the superseded duplicates it finds there, and its search in such a range stops
2864+ at a corrupt object header.
2865+ """
2866+ pack_ids = sorted (self .written_packs )
2867+ if not pack_ids :
2868+ return
2869+ logger .info (f"Re-reading the packs written by the repair: { len (pack_ids )} ." )
2870+ # (chunk_id, obj_offset, obj_size) of the index entries, per written pack.
2871+ indexed = {pack_id : set () for pack_id in pack_ids }
2872+ for chunk_id , entry in self .chunks .iteritems ():
2873+ entries = indexed .get (entry .pack_id )
2874+ if entries is not None :
2875+ entries .add ((chunk_id , entry .obj_offset , entry .obj_size ))
2876+ validate = object_validator (self .repo_objs )
2877+ for pack_id in pack_ids :
2878+ # PackReader reads from the store, which does not refresh the repository lock.
2879+ self .repository ._lock_refresh ()
2880+ pack_hex = bin_to_hex (pack_id )
2881+ expected = indexed .pop (pack_id )
2882+ reader = PackReader (self .repository .store , pack_id )
2883+ # iter_headers() yields nothing for a missing pack: the store reports size 0 for it.
2884+ if not self .repository .store .info (reader .key ).exists :
2885+ self .error_found = True
2886+ logger .error (f"pack { pack_hex } : written by the repair, but it is missing. Removing its index entries." )
2887+ for chunk_id , _ , _ in expected :
2888+ del self .chunks [chunk_id ]
2889+ continue
2890+ found = list (reader .iter_headers (validate = validate , on_drop = self .note_dropped_objects ))
2891+ not_found = sorted (expected .difference (found ))
2892+ for chunk_id , _ , _ in not_found :
2893+ del self .chunks [chunk_id ]
2894+ # the loop indexes each unindexed object, so of several unindexed copies of a chunk, the first
2895+ # is indexed and the others are superseded duplicates.
2896+ unindexed = []
2897+ for obj in found :
2898+ chunk_id , obj_offset , obj_size = obj
2899+ if obj in expected :
2900+ continue
2901+ if chunk_id in self .chunks :
2902+ logger .debug (
2903+ f"pack { pack_hex } : { bin_to_hex (chunk_id )} at offset { obj_offset } , { obj_size } bytes: "
2904+ "superseded duplicate"
2905+ )
2906+ continue
2907+ unindexed .append (obj )
2908+ # size=0: the object header does not hold the plaintext size.
2909+ self .chunks [chunk_id ] = ChunkIndexEntry (
2910+ flags = ChunkIndex .F_USED , size = 0 , pack_id = pack_id , obj_offset = obj_offset , obj_size = obj_size
2911+ )
2912+ if not (not_found or unindexed ):
2913+ continue
2914+ self .error_found = True
2915+ logger .error (
2916+ f"pack { pack_hex } : the chunks index does not match the pack. Indexed objects not in the pack: "
2917+ f"{ len (not_found )} , objects in the pack with an unindexed chunk id: { len (unindexed )} . "
2918+ "Fixed the index."
2919+ )
2920+ for chunk_id , obj_offset , obj_size in not_found :
2921+ logger .debug (
2922+ f"pack { pack_hex } : { bin_to_hex (chunk_id )} at offset { obj_offset } , { obj_size } bytes: not in pack"
2923+ )
2924+ for chunk_id , obj_offset , obj_size in unindexed :
2925+ logger .debug (
2926+ f"pack { pack_hex } : { bin_to_hex (chunk_id )} at offset { obj_offset } , { obj_size } bytes: not indexed"
2927+ )
2928+
28222929 def finish (self ):
28232930 if self .repair :
2824- # flush chunks re-added during repair so their packs are on the store and out of the pack
2825- # writer buffer (close() requires an empty buffer, #10055) before we (re)build the index.
2826- self .repository .flush ()
2827- if self .chunks_modified :
2828- # the packs changed: rebuild the index from them, validating every object header, and
2829- # store it. Free the current index first, so only one is in memory.
2830- self .repository .invalidate_chunk_index ()
2831- self .chunks = None
2931+ # store the pack writer buffer before the index is written (close() requires an empty buffer, #10055).
2932+ self .record_stored (self .repository .flush ())
2933+ # without a validator (the key was not readable), check() indexed objects no validator
2934+ # checked, and skipped the rest of a pack after a corrupt object header. If repair wrote
2935+ # packs, rebuild the index from all packs with a validator.
2936+ full_rebuild = self .chunks_modified and not self .rebuild_validated
2937+ if not full_rebuild :
2938+ if self .chunks_modified :
2939+ # the other packs did not change since check() indexed them with a validator.
2940+ self .verify_written_packs ()
2941+ logger .info ("Writing the rebuilt repository chunks index." )
2942+ write_chunkindex_to_repo (
2943+ self .repository , self .chunks , incremental = False , clear = False , force_write = True , delete_other = True
2944+ )
2945+ # free the in-memory index: close() must not store it, and the full rebuild builds its own.
2946+ self .repository .invalidate_chunk_index ()
2947+ self .chunks = None
2948+ if full_rebuild :
28322949 logger .info ("Rebuilding and writing the repository chunks index." )
28332950 build_chunkindex_from_repo (
28342951 self .repository ,
@@ -2837,17 +2954,9 @@ def finish(self):
28372954 on_drop = self .note_dropped_objects ,
28382955 write_immediately = True ,
28392956 )
2840- else :
2841- # the packs are unchanged, so the index still matches them: persist it as is.
2842- logger .info ("Writing the rebuilt repository chunks index." )
2843- write_chunkindex_to_repo (
2844- self .repository , self .chunks , incremental = False , clear = False , force_write = True , delete_other = True
2845- )
28462957 # the index just written matches the packs: clear the invalid marker, set by verify_data() or left
28472958 # by an interrupted operation.
28482959 delete_chunkindex_invalid (self .repository )
2849- # drop the in-memory index so close() does not persist it over the index just written.
2850- self .repository .invalidate_chunk_index ()
28512960 self .manifest .write ()
28522961
28532962
0 commit comments