comparison cutils/treesum.py @ 157:27d1aaf5fe39

Implement "--mode" flag for "treesum.py" to consider file portable mode bits
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 06 Jan 2025 21:03:10 +0100
parents 481cc9b26861
children d8cdd1985d43
comparison
equal deleted inserted replaced
156:481cc9b26861 157:27d1aaf5fe39
16 import argparse 16 import argparse
17 import base64 17 import base64
18 import binascii 18 import binascii
19 import datetime 19 import datetime
20 import os 20 import os
21 import stat
21 import sys 22 import sys
22 import time 23 import time
23 24
24 from . import (__version__, __revision__) 25 from . import (__version__, __revision__)
25 from . import util 26 from . import util
75 "empty use it as the leading \"ROOT (<TAG>)\" output.") 76 "empty use it as the leading \"ROOT (<TAG>)\" output.")
76 gp.add_argument( 77 gp.add_argument(
77 "--mmap", action="store_true", dest="mmap", default=None, 78 "--mmap", action="store_true", dest="mmap", default=None,
78 help="Use mmap if available. Default is to determine " 79 help="Use mmap if available. Default is to determine "
79 "automatically from the filesize.") 80 "automatically from the filesize.")
81 gp.add_argument(
82 "--mode", action="store_true", dest="metadata_mode",
83 help="Consider the permission bits of stat using S_IMODE (i.e. "
84 "all bits without the filetype bits) when "
85 "computing directory digests. Note that symbolic links "
86 "are not considered.")
80 gp.add_argument( 87 gp.add_argument(
81 "--mtime", action="store_true", dest="metadata_mtime", 88 "--mtime", action="store_true", dest="metadata_mtime",
82 help="Consider the mtime of files (non-directories) when " 89 help="Consider the mtime of files (non-directories) when "
83 "generating digests for directories. Digests for files are " 90 "generating digests for directories. Digests for files are "
84 "not affected.") 91 "not affected.")
187 base64=False, 194 base64=False,
188 comment=[], 195 comment=[],
189 follow_directory_symlinks=False, 196 follow_directory_symlinks=False,
190 logical=None, 197 logical=None,
191 minimal=None, 198 minimal=None,
199 mode=False,
192 mmap=None, 200 mmap=None,
193 mtime=False, 201 mtime=False,
194 output=None): 202 output=None):
195 opts = argparse.Namespace( 203 opts = argparse.Namespace(
196 directories=directories, 204 directories=directories,
201 comment=comment, 209 comment=comment,
202 follow_directory_symlinks=follow_directory_symlinks, 210 follow_directory_symlinks=follow_directory_symlinks,
203 logical=logical, 211 logical=logical,
204 minimal=minimal, 212 minimal=minimal,
205 mmap=mmap, 213 mmap=mmap,
214 metadata_mode=mode,
206 metadata_mtime=mtime, 215 metadata_mtime=mtime,
207 output=output) 216 output=output)
208 return opts 217 return opts
209 218
210 219
238 with out_cm as outfp: 247 with out_cm as outfp:
239 for d in opts.directories: 248 for d in opts.directories:
240 generate_treesum_for_directory( 249 generate_treesum_for_directory(
241 outfp, d, opts.algorithm, opts.mmap, opts.base64, opts.logical, 250 outfp, d, opts.algorithm, opts.mmap, opts.base64, opts.logical,
242 opts.follow_directory_symlinks, 251 opts.follow_directory_symlinks,
252 opts.metadata_mode,
243 opts.metadata_mtime, 253 opts.metadata_mtime,
244 minimal=opts.minimal, 254 minimal=opts.minimal,
245 comment=opts.comment) 255 comment=opts.comment)
246 256
247 257
248 def generate_treesum_for_directory( 258 def generate_treesum_for_directory(
249 outfp, root, algorithm, use_mmap, use_base64, handle_root_logical, 259 outfp, root, algorithm, use_mmap, use_base64, handle_root_logical,
250 follow_directory_symlinks, with_metadata_mtime, 260 follow_directory_symlinks, with_metadata_mode, with_metadata_mtime,
251 minimal=None, comment=None): 261 minimal=None, comment=None):
252 """ 262 """
253 263
254 :param outfp: a *binary* file with a "write()" and a "flush()" method 264 :param outfp: a *binary* file with a "write()" and a "flush()" method
255 265
319 linktgt = util.fsencode(os.readlink(fso.path)) 329 linktgt = util.fsencode(os.readlink(fso.path))
320 linkdgst = algorithm[0]() 330 linkdgst = algorithm[0]()
321 linkdgst.update(b"%d:%s," % (len(linktgt), linktgt)) 331 linkdgst.update(b"%d:%s," % (len(linktgt), linktgt))
322 dir_dgst.update(b"1:S,%d:%s," 332 dir_dgst.update(b"1:S,%d:%s,"
323 % (len(fso.fsname), fso.fsname)) 333 % (len(fso.fsname), fso.fsname))
334 # no mtime and no mode for symlinks
324 dir_dgst.update(linkdgst.digest()) 335 dir_dgst.update(linkdgst.digest())
325 opath = "/".join(top) + "/" + fso.name if top else fso.name 336 opath = "/".join(top) + "/" + fso.name if top else fso.name
326 outfp.write( 337 outfp.write(
327 format_bsd_line( 338 format_bsd_line(
328 algorithm[1], 339 algorithm[1],
333 continue 344 continue
334 # fetch from dir_digests 345 # fetch from dir_digests
335 dgst = dir_digests[top + (fso.name,)] 346 dgst = dir_digests[top + (fso.name,)]
336 dir_dgst.update(b"1:d,%d:%s," % (len(fso.fsname), fso.fsname)) 347 dir_dgst.update(b"1:d,%d:%s," % (len(fso.fsname), fso.fsname))
337 dir_dgst.update(dgst) 348 dir_dgst.update(dgst)
349 if with_metadata_mode:
350 modestr = normalized_mode_str(fso.stat.st_mode)
351 if not isinstance(modestr, bytes):
352 modestr = modestr.encode("ascii")
353 dir_dgst.update(b"4:mode,%d:%s," % (len(modestr), modestr))
338 else: 354 else:
339 dir_dgst.update(b"1:f,%d:%s," % (len(fso.fsname), fso.fsname)) 355 dir_dgst.update(b"1:f,%d:%s," % (len(fso.fsname), fso.fsname))
340 if with_metadata_mtime: 356 if with_metadata_mtime:
341 mtime = datetime.datetime.utcfromtimestamp( 357 mtime = datetime.datetime.utcfromtimestamp(
342 int(fso.stat.st_mtime)) 358 int(fso.stat.st_mtime))
343 mtime = mtime.isoformat("T") + "Z" 359 mtime = mtime.isoformat("T") + "Z"
344 if not isinstance(mtime, bytes): 360 if not isinstance(mtime, bytes):
345 mtime = mtime.encode("ascii") 361 mtime = mtime.encode("ascii")
346 dir_dgst.update(b"5:mtime,%d:%s," % (len(mtime), mtime)) 362 dir_dgst.update(b"5:mtime,%d:%s," % (len(mtime), mtime))
363 if with_metadata_mode:
364 modestr = normalized_mode_str(fso.stat.st_mode)
365 if not isinstance(modestr, bytes):
366 modestr = modestr.encode("ascii")
367 dir_dgst.update(b"4:mode,%d:%s," % (len(modestr), modestr))
347 dgst = digest.compute_digest_file( 368 dgst = digest.compute_digest_file(
348 algorithm[0], fso.path, use_mmap=use_mmap) 369 algorithm[0], fso.path, use_mmap=use_mmap)
349 dir_dgst.update(dgst) 370 dir_dgst.update(dgst)
350 opath = "/".join(top) + "/" + fso.name if top else fso.name 371 opath = "/".join(top) + "/" + fso.name if top else fso.name
351 outfp.write( 372 outfp.write(
355 opath = "/".join(top) + "/" if top else "" 376 opath = "/".join(top) + "/" if top else ""
356 outfp.write(format_bsd_line( 377 outfp.write(format_bsd_line(
357 algorithm[1], dir_dgst.digest(), opath, use_base64)) 378 algorithm[1], dir_dgst.digest(), opath, use_base64))
358 outfp.flush() 379 outfp.flush()
359 dir_digests[top] = dir_dgst.digest() 380 dir_digests[top] = dir_dgst.digest()
381
382
383 def normalized_mode_str(mode):
384 # XXX FIXME: Windows and "executable"
385 modebits = stat.S_IMODE(mode)
386 modestr = "%o" % (modebits,)
387 if not modestr.startswith("0"):
388 modestr = "0" + modestr
389 return modestr
360 390
361 391
362 def format_bsd_line(digestname, value, filename, use_base64): 392 def format_bsd_line(digestname, value, filename, use_base64):
363 ls = os.linesep if isinstance(os.linesep, bytes) \ 393 ls = os.linesep if isinstance(os.linesep, bytes) \
364 else os.linesep.encode("utf-8") 394 else os.linesep.encode("utf-8")