3737import os
3838import xxhash
3939import zlib
40- from typing import List , Tuple
40+ from typing import List , Tuple , Union
4141
4242from hashio import config
4343from hashio .exporter import CacheExporter
4444from hashio .logger import logger
45- from hashio .utils import read_file , walk
45+ from hashio .utils import is_gzip_path , read_file , read_file_uncompressed , walk
4646
4747
4848def bytes_to_long (data : bytes ):
@@ -321,21 +321,35 @@ def checksum_data(data: bytes, encoder: Encoder, buffer_size: int = config.BUF_S
321321 return value
322322
323323
324- def checksum_file (path : str , encoder : Encoder , buffer_size : int = config .BUF_SIZE ):
324+ def checksum_file (
325+ path : str ,
326+ encoder : Encoder ,
327+ buffer_size : int = config .BUF_SIZE ,
328+ uncompress : bool = False ,
329+ with_size : bool = False ,
330+ ) -> Union [str , Tuple [str , int ]]:
325331 """Creates a checksum for a given filepath and encoder. Note: resets
326332 encoder, existing data will be lost.
327333
328334 >>> checksum_file("example.txt", MD5Encoder())
329335
330336 :param path: the path to the filepath being hashed
331337 :param encoder: instance of Encoder subclass
332- :return: hexdigest of the checksum
338+ :param buffer_size: size of each read chunk in bytes
339+ :param uncompress: if True, hash decompressed contents for supported files
340+ :param with_size: if True, also return the total bytes hashed
341+ :return: checksum hex digest, or ``(checksum, size)`` when ``with_size`` is True
333342 """
334343 encoder .reset ()
335- for data in read_file (path , buffer_size = buffer_size ):
344+ size = 0
345+ reader = read_file_uncompressed if uncompress and is_gzip_path (path ) else read_file
346+ for data in reader (path , buffer_size = buffer_size ):
347+ size += len (data )
336348 encoder .update (data )
337349 value = encoder .hexdigest ()
338350 encoder .reset ()
351+ if with_size :
352+ return value , size
339353 return value
340354
341355
@@ -372,7 +386,11 @@ def checksum_text(data: str, encoder: Encoder):
372386
373387
374388def checksum_path (
375- path : str , encoder : Encoder , filetype : str = "a" , use_cache : bool = True
389+ path : str ,
390+ encoder : Encoder ,
391+ filetype : str = "a" ,
392+ use_cache : bool = True ,
393+ uncompress : bool = False ,
376394):
377395 """Returns a checksum of for a given path, encoder and filetype.
378396
@@ -384,12 +402,12 @@ def checksum_path(
384402 :param use_cache: cache results to filesystem
385403 :return: hexdigest of the checksum
386404 """
387- if use_cache :
405+ if use_cache and not uncompress :
388406 cached_value = CacheExporter .find (path , encoder .name )
389407 if cached_value :
390408 return cached_value
391409 if os .path .isfile (path ) and filetype in ("a" , "f" ):
392- return checksum_file (path , encoder )
410+ return checksum_file (path , encoder , uncompress = uncompress )
393411 elif os .path .isdir (path ) and filetype in ("a" , "d" ):
394412 return checksum_folder (path , encoder )
395413
@@ -400,6 +418,7 @@ def checksum_gen(
400418 filetype : str = "f" ,
401419 recursive : bool = True ,
402420 use_cache : bool = True ,
421+ uncompress : bool = False ,
403422):
404423 """Checksum generator that yields tuple of (filepath, value).
405424
@@ -415,12 +434,12 @@ def checksum_gen(
415434 """
416435 if recursive :
417436 for subpath in walk (path , filetype ):
418- value = checksum_path (subpath , encoder , filetype , use_cache )
437+ value = checksum_path (subpath , encoder , filetype , use_cache , uncompress )
419438 if value :
420439 yield (subpath , value )
421440
422441 else :
423- value = checksum_path (path , encoder , filetype , use_cache )
442+ value = checksum_path (path , encoder , filetype , use_cache , uncompress )
424443 if value :
425444 yield (path , value )
426445
@@ -629,7 +648,7 @@ def dedupe_caches(target: str, source: str, algo: str = config.DEFAULT_ALGO):
629648 return [(t , s ) for t , s in dedupe_cache_gen (target , source , algo = algo )]
630649
631650
632- def verify_checksums (path : str , start : str = None ):
651+ def verify_checksums (path : str , start : str = None , uncompress : bool = False ):
633652 """Generator that yields a data tuple for hash misses in a previously
634653 generated output file. Compares mtimes in the output file with the
635654 filesystem.
@@ -653,29 +672,36 @@ def verify_checksums(path: str, start: str = None):
653672
654673 for filename , metadata in data .items ():
655674 filepath = os .path .join (root , filename )
675+ hash_path = filepath
676+
677+ if uncompress and not os .path .exists (hash_path ):
678+ gzip_path = f"{ filepath } .gz"
679+ if os .path .exists (gzip_path ):
680+ hash_path = gzip_path
656681
657682 # iterate over all the hash algos...
658683 for algo in ENCODER_MAP .keys ():
659684 if algo not in metadata .keys ():
660685 continue
661686
662687 # check if file exists and compare mtimes
663- if not os .path .exists (filepath ):
688+ if not os .path .exists (hash_path ):
664689 logger .warning ("missing: %s" , filepath )
665690 continue
666691 # if mtimes match, skip
667- elif metadata .get ("mtime" ) == os .stat (filepath ).st_mtime :
692+ elif metadata .get ("mtime" ) == os .stat (hash_path ).st_mtime :
668693 continue
669694
670695 # if mtimes don't match, re-hash the file
671- logger .debug ("mtime miss on %s" , filepath )
696+ logger .debug ("mtime miss on %s" , hash_path )
672697 old_value = metadata .get (algo )
673698 encoder = ENCODER_MAP .get (algo )()
674- new_value = checksum_path (filepath , encoder )
699+ do_uncompress = uncompress and is_gzip_path (hash_path )
700+ new_value = checksum_path (hash_path , encoder , uncompress = do_uncompress )
675701
676702 # hash values don't match, file must have changed
677703 if (new_value and old_value ) and (new_value != old_value ):
678- logger .debug ("hash miss on %s %s" , algo , filepath )
704+ logger .debug ("hash miss on %s %s" , algo , hash_path )
679705 yield (algo , new_value , filepath )
680706
681707
0 commit comments