-
Notifications
You must be signed in to change notification settings - Fork 34
/
hashname
executable file
·103 lines (88 loc) · 3.98 KB
/
hashname
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python3
import pathlib as pl, itertools as it, functools as ft, hashlib as hl, shutil as su
import os, sys, base64, re
# Crockford's base32 is used to be simple, and yet visually distinctive
_b32_abcs = dict(zip(
# Python base32 - "Table 3: The Base 32 Alphabet" from RFC3548
'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
# Crockford's base32 - http://www.crockford.com/wrmg/base32.html
'0123456789ABCDEFGHJKMNPQRSTVWXYZ' ))
_b32_abcs['='] = ''
def b32encode( v, chunk=4, simple=False,
_trans=str.maketrans(_b32_abcs),
_check=''.join(_b32_abcs.values()) + '*~$=U' ):
chksum = 0
for c in bytearray(v): chksum = chksum << 8 | c
res = base64.b32encode(v).decode().strip().translate(_trans)
if simple: return res.lower()
res = '-'.join(''.join(s) for s in it.batched(res, chunk))
return '{}-{}'.format(res, _check[chksum % 37].lower())
hash_person = b'hashname.1'
hash_size = 8
hash_chunk = 1 * 2**20 # 1 MiB
tag_len, tag_fmt = 4, r'={}'
def get_hash(p, enc_func=ft.partial(b32encode, simple=True)):
p_hash = hl.blake2b(digest_size=hash_size, person=hash_person)
with p.open('rb') as src:
for chunk in iter(ft.partial(src.read, hash_chunk), b''): p_hash.update(chunk)
return enc_func(p_hash.digest())
def main(args=None):
global tag_len
import argparse, textwrap
dd = lambda text: re.sub( r' \t+', ' ',
textwrap.dedent(text).strip('\n') + '\n' ).replace('\t', ' ')
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter, description=dd(f'''
Give file(s) distinctive names using hash of their content.
Default naming scheme is "{{name}}.{{hash}}.{{ext}}",
using {hash_size}B crockford-base32-encoded {{hash}} output in there.'''))
parser.add_argument('files', nargs='+', help='File(s) to rename.')
parser.add_argument('-p', '--dry-run',
action='store_true', help='Print renames but not actually do it.')
parser.add_argument('-d', '--move-dir', metavar='dir',
help='Move renamed files to specified dir.')
parser.add_argument('-r', '--replace', action='store_true',
help='Replace name with hash, i.e. use "{name}.{ext}" template.')
parser.add_argument('-t', '--tag', action='store_true', help=dd('''
Use "{name}.{hash-tag}.{ext}" naming scheme, with shorter
and distinctive tag instead of hash, replacing it if detected in filename.
This is useful to re-run the tool on changed images, to replace
hash part of the name instead of appending extra hash each time.'''))
parser.add_argument('-T', '--tag-len', type=int, metavar='nchars', help=dd(f'''
Number of (crocks-base32) characters that hash part of tag should be using.
Default is {tag_len} characters. If this option is specified, -t/--tag is implied too.'''))
opts = parser.parse_args(sys.argv[1:] if args is None else args)
if (p_mv := opts.move_dir and pl.Path(opts.move_dir)) and not p_mv.is_dir():
parser.error(f'-d/--move-dir path is not a directory: {p_mv}')
if opts.tag_len:
tag_len, opts.tag = opts.tag_len, True
if opts.tag:
b32_abcs = ''.join(_b32_abcs.values())
tag_rx = tag_fmt.format(f'[{b32_abcs}]{{{tag_len}}}')
exit_code = 0
def p_err(*args):
nonlocal exit_code
print('ERROR:', *args, file=sys.stderr)
exit_code = 1
for p in opts.files:
try: p_hash = get_hash(p := pl.Path(p))
except OSError as err:
p_err(f'failed to process path [{p}]: {err}')
continue
name, ext = name if len( name :=
(name_old := p.name).rsplit('.', 1) ) == 2 else (name[0], '')
if opts.tag:
if m := re.search(fr'(?i)\.{tag_rx}$', name): name = name[:m.start()]
p_hash = tag_fmt.format(p_hash[:tag_len])
name_new = [name, p_hash, ext] if not opts.replace else [p_hash, ext]
name_new = '.'.join(filter(None, name_new))
if name_old == name_new: continue # same tag
print(p.name, '->', name_new)
if not opts.dry_run:
try:
if not opts.move_dir and name_old != name_new: p.rename(p.parent / name_new)
else: su.move(p, p_mv / name_new)
except OSError as err:
p_err(f'failed to rename/move path [{p}]: {err}')
return exit_code
if __name__ == '__main__': sys.exit(main())