0 | 0 |
# encoding: UTF-8
|
1 | 1 |
|
2 | 2 |
from argparse import ArgumentParser
|
3 | |
import errno
|
4 | 3 |
import os
|
5 | 4 |
import sys
|
6 | |
from subprocess import Popen, STDOUT, PIPE
|
|
5 |
|
|
6 |
from tagfarm.utils import (
|
|
7 |
mkdir_p, find_media_root, tag_file, perform_repair
|
|
8 |
)
|
7 | 9 |
|
8 | 10 |
|
9 | |
# - - - -
|
10 | |
|
11 | |
|
12 | |
def mkdir_p(path):
|
13 | |
try:
|
14 | |
os.makedirs(path)
|
15 | |
except OSError as exc:
|
16 | |
if exc.errno == errno.EEXIST and os.path.isdir(path):
|
17 | |
pass
|
18 | |
else:
|
19 | |
raise
|
20 | |
|
21 | |
|
22 | |
def find_media_root(path):
|
23 | |
while path != '/':
|
24 | |
if os.path.isdir(os.path.join(path, 'by-tag')):
|
25 | |
return path
|
26 | |
path = os.path.abspath(os.path.join(path, os.pardir))
|
27 | |
raise ValueError("could not locate `by-tag` directory in curent directory or any parent directory thereof")
|
28 | |
|
29 | |
|
30 | |
def index_files(media_root):
|
31 | |
index = {}
|
32 | |
for root, dirs, files in os.walk(media_root):
|
33 | |
if os.path.normpath(root) == os.path.normpath(os.path.join(media_root, 'by-tag')):
|
34 | |
dirs[:] = []
|
35 | |
continue
|
36 | |
|
37 | |
for basename in files:
|
38 | |
filename = os.path.normpath(os.path.join(root, basename))
|
39 | |
if os.path.islink(filename):
|
40 | |
continue
|
41 | |
|
42 | |
index.setdefault(basename, set()).add(filename)
|
43 | |
return index
|
44 | |
|
45 | |
|
46 | |
def is_broken_link(path):
|
47 | |
return os.path.lexists(path) and not os.path.exists(path)
|
48 | |
|
49 | |
|
50 | |
def is_absolute_link(path):
|
51 | |
return os.path.lexists(path) and os.readlink(path).startswith('/')
|
52 | |
|
53 | |
|
54 | |
def tag_file(media_root, filename, tag):
|
55 | |
mkdir_p(os.path.join(media_root, 'by-tag', tag))
|
56 | |
linkname = os.path.join(media_root, 'by-tag', tag, os.path.basename(filename))
|
57 | |
if not os.path.lexists(linkname):
|
58 | |
srcname = os.path.join('..', '..', os.path.relpath(filename, media_root))
|
59 | |
os.symlink(srcname, linkname)
|
60 | |
|
61 | |
|
62 | |
def perform_repair(media_root, verbose=False, force_relink=False, prune=False):
|
63 | |
index = index_files(media_root)
|
64 | |
|
65 | |
by_tags_dir = os.path.join(media_root, 'by-tag')
|
66 | |
for tag in sorted(os.listdir(by_tags_dir)):
|
67 | |
tagdir = os.path.join(by_tags_dir, tag)
|
68 | |
if not os.path.isdir(tagdir):
|
69 | |
continue
|
70 | |
print('*** {}'.format(tag))
|
71 | |
for basename in sorted(os.listdir(tagdir)):
|
72 | |
|
73 | |
linkname = os.path.join(tagdir, basename)
|
74 | |
|
75 | |
if basename.startswith('Link to '):
|
76 | |
new_basename = basename[8:]
|
77 | |
new_linkname = os.path.join(tagdir, new_basename)
|
78 | |
if os.path.lexists(new_linkname):
|
79 | |
print("WARNING: not renaming '{}' because '{}' already exists".format(linkname, new_linkname))
|
80 | |
else:
|
81 | |
print("RENAMING {} -> {}".format(linkname, new_linkname))
|
82 | |
os.rename(linkname, new_linkname)
|
83 | |
linkname = new_linkname
|
84 | |
|
85 | |
if not force_relink:
|
86 | |
if not os.path.islink(linkname):
|
87 | |
print("WARNING: skipping {} (is regular file, but --force-relink not given)".format(linkname))
|
88 | |
continue
|
89 | |
if not (is_broken_link(linkname) or is_absolute_link(linkname)):
|
90 | |
if verbose:
|
91 | |
print('kept {} -> {}'.format(linkname, os.readlink(linkname)))
|
92 | |
continue
|
93 | |
|
94 | |
candidates = index.get(basename, set())
|
95 | |
if len(candidates) == 0:
|
96 | |
if prune:
|
97 | |
print("NOTICE: no candidates for {}, DELETING".format(basename))
|
98 | |
os.remove(linkname)
|
99 | |
else:
|
100 | |
print("WARNING: no candidates for {}".format(basename))
|
101 | |
elif len(candidates) > 1:
|
102 | |
print("WARNING: multiple candidates for {}: {}".format(basename, candidates))
|
103 | |
else:
|
104 | |
os.remove(linkname)
|
105 | |
filename = list(candidates)[0]
|
106 | |
srcname = os.path.join('..', '..', os.path.relpath(filename, media_root))
|
107 | |
os.symlink(srcname, linkname)
|
108 | |
print('FIXED {} -> {}'.format(linkname, srcname))
|
109 | |
|
110 | |
|
111 | |
# - - - - commands - - - -
|
112 | |
|
113 | |
|
114 | |
def tag(media_root, args, verbose=False):
|
115 | |
argparser = ArgumentParser()
|
116 | |
argparser.add_argument('tag', metavar='TAG', type=str,
|
117 | |
help='Name of tag to apply'
|
118 | |
)
|
119 | |
argparser.add_argument('filenames', metavar='FILENAME', type=str, nargs='+',
|
120 | |
help='Names of files to tag'
|
121 | |
)
|
122 | |
options = argparser.parse_args(args)
|
123 | |
|
|
11 |
def tag(media_root, options):
|
124 | 12 |
for filename in options.filenames:
|
125 | 13 |
tag_file(media_root, filename, options.tag)
|
126 | 14 |
|
127 | 15 |
|
128 | |
def untag(media_root, args, verbose=False):
|
129 | |
argparser = ArgumentParser()
|
130 | |
argparser.add_argument('tag', metavar='TAG', type=str,
|
131 | |
help='Name of tag to remove'
|
132 | |
)
|
133 | |
argparser.add_argument('filenames', metavar='FILENAME', type=str, nargs='+',
|
134 | |
help='Names of files to untag'
|
135 | |
)
|
136 | |
options = argparser.parse_args(args)
|
137 | |
|
|
16 |
def untag(media_root, options):
|
138 | 17 |
for filename in options.filenames:
|
139 | 18 |
linkname = os.path.join(media_root, 'by-tag', options.tag, os.path.basename(filename))
|
140 | 19 |
if os.path.lexists(linkname):
|
141 | 20 |
os.remove(linkname)
|
142 | 21 |
|
143 | 22 |
|
144 | |
def showtags(media_root, args, verbose=False):
|
145 | |
argparser = ArgumentParser()
|
146 | |
argparser.add_argument('filenames', metavar='FILENAME', type=str, nargs='+',
|
147 | |
help='Names of files to show tags of',
|
148 | |
)
|
149 | |
argparser.add_argument('--show-only-fewer-than', type=int, default=None,
|
150 | |
help='If given, report only those files that have fewer than this number of tags',
|
151 | |
)
|
152 | |
options = argparser.parse_args(args)
|
153 | |
|
|
23 |
def showtags(media_root, options):
|
154 | 24 |
by_tags_dir = os.path.join(media_root, 'by-tag')
|
155 | |
|
156 | 25 |
for filename in options.filenames:
|
157 | 26 |
tags = []
|
158 | 27 |
basename = os.path.basename(os.path.normpath(filename))
|
|
164 | 33 |
print('{}: {}'.format(filename, ', '.join(tags)))
|
165 | 34 |
|
166 | 35 |
|
167 | |
def repair(media_root, args, verbose=False):
|
168 | |
argparser = ArgumentParser()
|
169 | |
argparser.add_argument('--force-relink', action='store_true',
|
170 | |
help='Replace files found in taglinks directory even when they are not symlinks or not broken'
|
171 | |
)
|
172 | |
argparser.add_argument('--prune', action='store_true',
|
173 | |
help='Remove broken symlinks for which no candidate files can be found'
|
174 | |
)
|
175 | |
options = argparser.parse_args(args)
|
176 | |
|
177 | |
perform_repair(media_root, verbose=verbose, force_relink=options.force_relink, prune=options.prune)
|
|
36 |
def repair(media_root, options):
|
|
37 |
perform_repair(media_root, verbose=options.verbose, force_relink=options.force_relink, prune=options.prune)
|
178 | 38 |
|
179 | 39 |
|
180 | |
def rename(media_root, args, verbose=False):
|
181 | |
argparser = ArgumentParser()
|
182 | |
argparser.add_argument('src', metavar='FILENAME', type=str,
|
183 | |
help='Current name of file that is to be renamed'
|
184 | |
)
|
185 | |
argparser.add_argument('dest', metavar='FILENAME', type=str,
|
186 | |
help='New name of file'
|
187 | |
)
|
188 | |
options = argparser.parse_args(args)
|
189 | |
|
|
40 |
def rename(media_root, options):
|
190 | 41 |
src = os.path.normpath(options.src)
|
191 | 42 |
dest = os.path.normpath(options.dest)
|
192 | 43 |
|
|
206 | 57 |
print('UPDATED {} -> {}'.format(new_linkname, target))
|
207 | 58 |
|
208 | 59 |
|
209 | |
def collect(media_root, args, verbose=False):
|
210 | |
argparser = ArgumentParser()
|
211 | |
argparser.add_argument('tag', metavar='TAG', type=str,
|
212 | |
help='Tag to select files by'
|
213 | |
)
|
214 | |
argparser.add_argument('dest', metavar='DIRNAME', type=str,
|
215 | |
help='Directory to move files into'
|
216 | |
)
|
217 | |
options = argparser.parse_args(args)
|
218 | |
|
|
60 |
def collect(media_root, options):
|
219 | 61 |
tagdir = os.path.join(media_root, 'by-tag', options.tag)
|
220 | 62 |
if not os.path.isdir(tagdir):
|
221 | 63 |
print("WARNING: no files tagged '{}'".format(options.tag))
|
|
241 | 83 |
perform_repair(media_root, verbose=verbose)
|
242 | 84 |
|
243 | 85 |
|
244 | |
COMMANDS = {
|
245 | |
'tag': tag,
|
246 | |
'untag': untag,
|
247 | |
'showtags': showtags,
|
248 | |
'repair': repair,
|
249 | |
'rename': rename,
|
250 | |
'collect': collect,
|
251 | |
}
|
252 | |
|
253 | 86 |
# - - - - driver - - - -
|
254 | 87 |
|
255 | 88 |
|
256 | 89 |
def main(args):
|
257 | |
argparser = ArgumentParser()
|
|
90 |
parser = ArgumentParser()
|
258 | 91 |
|
259 | |
argparser.add_argument('command', metavar='COMMAND', type=str,
|
260 | |
help='The action to take. One of: {}'.format(', '.join(COMMANDS.keys()))
|
261 | |
)
|
262 | |
argparser.add_argument('--verbose', action='store_true',
|
|
92 |
parser.add_argument('--verbose', action='store_true',
|
263 | 93 |
help='Produce more reporting output'
|
264 | 94 |
)
|
265 | 95 |
|
266 | |
options, remaining_args = argparser.parse_known_args(args)
|
|
96 |
subparsers = parser.add_subparsers()
|
267 | 97 |
|
|
98 |
# - - - - tag - - - -
|
|
99 |
parser_tag = subparsers.add_parser('tag', help='Add a given tag to one or more files')
|
|
100 |
parser_tag.add_argument('tag', metavar='TAG', type=str,
|
|
101 |
help='Name of tag to apply'
|
|
102 |
)
|
|
103 |
parser_tag.add_argument('filenames', metavar='FILENAME', type=str, nargs='+',
|
|
104 |
help='Names of files to tag'
|
|
105 |
)
|
|
106 |
parser_tag.set_defaults(func=tag)
|
|
107 |
|
|
108 |
# - - - - untag - - - -
|
|
109 |
parser_untag = subparsers.add_parser('untag', help='Remove a given tag from one or more files')
|
|
110 |
parser_untag.add_argument('tag', metavar='TAG', type=str,
|
|
111 |
help='Name of tag to remove'
|
|
112 |
)
|
|
113 |
parser_untag.add_argument('filenames', metavar='FILENAME', type=str, nargs='+',
|
|
114 |
help='Names of files to untag'
|
|
115 |
)
|
|
116 |
parser_untag.set_defaults(func=untag)
|
|
117 |
|
|
118 |
# - - - - showtags - - - -
|
|
119 |
parser_showtags = subparsers.add_parser('showtags', help='Report the tags currently on one or more files')
|
|
120 |
parser_showtags.add_argument('filenames', metavar='FILENAME', type=str, nargs='+',
|
|
121 |
help='Names of files to show tags of',
|
|
122 |
)
|
|
123 |
parser_showtags.add_argument('--show-only-fewer-than', type=int, default=None,
|
|
124 |
help='If given, report only those files that have fewer than this number of tags',
|
|
125 |
)
|
|
126 |
parser_showtags.set_defaults(func=showtags)
|
|
127 |
|
|
128 |
# - - - - repair - - - -
|
|
129 |
parser_repair = subparsers.add_parser('repair', help='Re-assign broken tag links to relocated files')
|
|
130 |
parser_repair.add_argument('--force-relink', action='store_true',
|
|
131 |
help='Replace files found in taglinks directory even when they are not symlinks or not broken'
|
|
132 |
)
|
|
133 |
parser_repair.add_argument('--prune', action='store_true',
|
|
134 |
help='Remove broken symlinks for which no candidate files can be found'
|
|
135 |
)
|
|
136 |
parser_repair.set_defaults(func=repair)
|
|
137 |
|
|
138 |
# - - - - rename - - - -
|
|
139 |
parser_rename = subparsers.add_parser('rename', help='Rename file whille updating all its tag links')
|
|
140 |
parser_rename.add_argument('src', metavar='FILENAME', type=str,
|
|
141 |
help='Current name of file that is to be renamed'
|
|
142 |
)
|
|
143 |
parser_rename.add_argument('dest', metavar='FILENAME', type=str,
|
|
144 |
help='New name of file'
|
|
145 |
)
|
|
146 |
parser_rename.set_defaults(func=rename)
|
|
147 |
|
|
148 |
# - - - - collect - - - -
|
|
149 |
parser_collect = subparsers.add_parser('collect', help='Move all files with a given tag into a given directory')
|
|
150 |
parser_collect.add_argument('tag', metavar='TAG', type=str,
|
|
151 |
help='Tag to select files by'
|
|
152 |
)
|
|
153 |
parser_collect.add_argument('dest', metavar='DIRNAME', type=str,
|
|
154 |
help='Directory to move files into'
|
|
155 |
)
|
|
156 |
parser_collect.set_defaults(func=collect)
|
|
157 |
|
|
158 |
options = parser.parse_args(args)
|
268 | 159 |
media_root = find_media_root(os.path.realpath('.'))
|
269 | |
|
270 | |
command = COMMANDS.get(options.command, None)
|
271 | |
if command:
|
272 | |
command(media_root, remaining_args, verbose=options.verbose)
|
273 | |
else:
|
274 | |
argparser.print_help()
|
275 | |
sys.exit(1)
|
|
160 |
options.func(media_root, options)
|