Skip to content

Commit e989d9b

Browse files
committed
feat: add --store-hash-all to write all Table F.1 hash types (parallelized)
1 parent 8b60ef0 commit e989d9b

3 files changed

Lines changed: 83 additions & 12 deletions

File tree

man/ltfs_ordered_copy.1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Compute a content hash of each copied file and store it in the \fBltfs.hash.\fR\
4646
.TP
4747
\fB--hash-algo\fR \fIHASHTYPE\fR
4848
LTFS hash type to use with \fB--store-hash\fR (LTFS Format Spec 2.4, Table F.1). One of \fBcrc32sum\fR, \fBmd5sum\fR, \fBsha1sum\fR, \fBsha256sum\fR, \fBsha512sum\fR; the bare names \fBcrc32\fR, \fBmd5\fR, \fBsha1\fR, \fBsha256\fR, \fBsha512\fR are accepted as aliases. Defaults to \fBsha256sum\fR.
49+
.TP
50+
\fB--store-hash-all\fR
51+
Like \fB--store-hash\fR, but compute and store every standardized LTFS hash type (\fBcrc32sum\fR, \fBmd5sum\fR, \fBsha1sum\fR, \fBsha256sum\fR, \fBsha512sum\fR) for each file. Each file is read once and the hash types are computed in parallel, one worker thread per type. Overrides \fB--hash-algo\fR.
4952
.SH "COMMAND EXAMPLES"
5053
.PP
5154
This section shows various command examples.

man/sgml/ltfs_ordered_copy.sgml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@
116116
<para>LTFS hash type to use with <option>--store-hash</option> (LTFS Format Spec 2.4, Table F.1). One of <literal>crc32sum</literal>, <literal>md5sum</literal>, <literal>sha1sum</literal>, <literal>sha256sum</literal>, <literal>sha512sum</literal>; the bare names crc32, md5, sha1, sha256, sha512 are accepted as aliases. Defaults to <literal>sha256sum</literal>.</para>
117117
</listitem>
118118
</varlistentry>
119+
<varlistentry>
120+
<term><option>--store-hash-all</option></term>
121+
<listitem>
122+
<para>Like <option>--store-hash</option>, but compute and store every standardized LTFS hash type (<literal>crc32sum</literal>, <literal>md5sum</literal>, <literal>sha1sum</literal>, <literal>sha256sum</literal>, <literal>sha512sum</literal>) for each file. Each file is read once and the hash types are computed in parallel, one worker thread per type. Overrides <option>--hash-algo</option>.</para>
123+
</listitem>
124+
</varlistentry>
119125
</variablelist>
120126
</refsect1>
121127

src/utils/ltfs_ordered_copy

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,57 @@ def compute_file_hash(path, hashtype):
7979
h.update(chunk)
8080
return h.hexdigest()
8181

82+
def compute_file_hashes(path, hashtypes):
83+
"""Compute one or more LTFS Table F.1 hashtypes for a file and return
84+
{hashtype: hexdigest}. The file is read only once. With several hashtypes each is
85+
updated in its own worker thread fed by a single reader; hashlib and zlib release
86+
the GIL during their update calls, so the per-type work runs across CPU cores in
87+
parallel."""
88+
if len(hashtypes) == 1:
89+
ht = hashtypes[0]
90+
return {ht: compute_file_hash(path, ht)}
91+
92+
try:
93+
from queue import Queue # Python 3
94+
except ImportError:
95+
from Queue import Queue # Python 2
96+
97+
queues = dict((ht, Queue(maxsize=8)) for ht in hashtypes)
98+
results = {}
99+
100+
def worker(ht):
101+
q = queues[ht]
102+
algo = HASH_TYPES[ht]
103+
if algo == 'crc32':
104+
crc = 0
105+
chunk = q.get()
106+
while chunk is not None:
107+
crc = zlib.crc32(chunk, crc)
108+
chunk = q.get()
109+
results[ht] = '%08x' % (crc & 0xffffffff)
110+
else:
111+
h = hashlib.new(algo)
112+
chunk = q.get()
113+
while chunk is not None:
114+
h.update(chunk)
115+
chunk = q.get()
116+
results[ht] = h.hexdigest()
117+
118+
threads = [threading.Thread(target=worker, args=(ht,)) for ht in hashtypes]
119+
for t in threads:
120+
t.start()
121+
try:
122+
with open(path, 'rb') as f:
123+
for chunk in iter(lambda: f.read(1024 * 1024), b''):
124+
for q in queues.values():
125+
q.put(chunk)
126+
finally:
127+
for q in queues.values():
128+
q.put(None) # signal end-of-stream to every worker
129+
for t in threads:
130+
t.join()
131+
return results
132+
82133
def ensure_ltfs_hash_supported(probe_path, logger):
83134
"""When --store-hash targets an LTFS volume, require LTFS Format Spec >= 2.4 (the
84135
version that introduced the stored ltfs.hash.* VEA), aborting on an older LTFS
@@ -167,17 +218,19 @@ class CopyItem:
167218
return False
168219

169220
if self.store_hash:
170-
# Store the content hash in the ltfs.hash.<hashtype> VEA on the destination.
171-
# The hash is computed from the (on-disk) source, whose bytes are identical
172-
# to what was just copied; on LTFS this is persisted into the index.
221+
# Store one ltfs.hash.<hashtype> VEA per requested hashtype on the
222+
# destination. The hash(es) are computed from the (on-disk) source, whose
223+
# bytes are identical to what was just copied; on LTFS this is persisted
224+
# into the index.
173225
try:
174226
target = self.dst
175227
if os.path.isdir(target):
176228
target = os.path.join(target, os.path.basename(self.src))
177-
digest = compute_file_hash(self.src, self.store_hash)
178-
xattr.set(target, self.vea_pre + 'ltfs.hash.' + self.store_hash, digest.encode('ascii'))
229+
digests = compute_file_hashes(self.src, self.store_hash)
230+
for ht in self.store_hash:
231+
xattr.set(target, self.vea_pre + 'ltfs.hash.' + ht, digests[ht].encode('ascii'))
179232
except Exception as e:
180-
self.logger.error('Copied "{0}" to "{1}" but failed to store {2} hash: {3}'.format(self.src, self.dst, self.store_hash, str(e)))
233+
self.logger.error('Copied "{0}" to "{1}" but failed to store hash(es): {2}'.format(self.src, self.dst, str(e)))
181234
return False
182235

183236
return True
@@ -371,6 +424,11 @@ parser.add_argument('--hash-algo', default='sha256sum', metavar='HASHTYPE',
371424
+ ', '.join(sorted(HASH_TYPES))
372425
+ ' (the bare names ' + ', '.join(sorted(HASH_ALIASES))
373426
+ ' are accepted as aliases). Default sha256sum.')
427+
parser.add_argument('--store-hash-all', action='store_true',
428+
help='Like --store-hash, but compute and store every standardized LTFS '
429+
'hash type (' + ', '.join(sorted(HASH_TYPES)) + ') for each file. '
430+
'The file is read once and the hash types are computed in parallel '
431+
'(one worker thread each). Overrides --hash-algo.')
374432

375433
args=parser.parse_args()
376434

@@ -409,16 +467,19 @@ else:
409467

410468
logger.info('Tape order aware copy for LTFS')
411469

412-
# Resolve --store-hash / --hash-algo into a single value: the LTFS spec hashtype when
413-
# hashing is enabled, otherwise None. Downstream code treats it as "hashtype or falsy".
414-
if args.store_hash:
470+
# Resolve --store-hash / --store-hash-all / --hash-algo into args.store_hash: a list of
471+
# LTFS spec hashtypes to compute when hashing is enabled, otherwise None. Downstream code
472+
# treats it as "list of hashtypes or falsy".
473+
if args.store_hash_all:
474+
args.store_hash = sorted(HASH_TYPES) # every standardized hashtype
475+
elif args.store_hash:
415476
ht = args.hash_algo.lower()
416477
ht = HASH_ALIASES.get(ht, ht) # accept a bare algorithm name as an alias
417478
if ht not in HASH_TYPES:
418479
logger.error("Unsupported hash type '{0}'. LTFS Format Spec 2.4 (Table F.1) defines: {1}.".format(
419480
args.hash_algo, ', '.join(sorted(HASH_TYPES))))
420481
exit(2)
421-
args.store_hash = ht
482+
args.store_hash = [ht]
422483
else:
423484
args.store_hash = None
424485

@@ -464,8 +525,9 @@ if args.recursive == False and len(args.SOURCE) == 1:
464525
target = args.DEST
465526
if os.path.isdir(target):
466527
target = os.path.join(target, os.path.basename(args.SOURCE[0]))
467-
digest = compute_file_hash(args.SOURCE[0], args.store_hash)
468-
xattr.set(target, VEA_PREFIX + 'ltfs.hash.' + args.store_hash, digest.encode('ascii'))
528+
digests = compute_file_hashes(args.SOURCE[0], args.store_hash)
529+
for ht in args.store_hash:
530+
xattr.set(target, VEA_PREFIX + 'ltfs.hash.' + ht, digests[ht].encode('ascii'))
469531
except Exception as e:
470532
logger.error(str(e))
471533
exit(1)

0 commit comments

Comments
 (0)