]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/cli.py
Better implementation of filtering algorithm.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / cli.py
1
2 import os.path, re, sys, fnmatch
3 from twisted.python import usage
4 from allmydata.scripts.common import BaseOptions, get_aliases
5
6 NODEURL_RE=re.compile("http://([^:]*)(:([1-9][0-9]*))?")
7
8 class VDriveOptions(BaseOptions, usage.Options):
9     optParameters = [
10         ["node-directory", "d", "~/.tahoe",
11          "Look here to find out which Tahoe node should be used for all "
12          "operations. The directory should either contain a full Tahoe node, "
13          "or a file named node.url which points to some other Tahoe node. "
14          "It should also contain a file named private/aliases which contains "
15          "the mapping from alias name to root dirnode URI."
16          ],
17         ["node-url", "u", None,
18          "URL of the tahoe node to use, a URL like \"http://127.0.0.1:3456\". "
19          "This overrides the URL found in the --node-directory ."],
20         ["dir-cap", None, None,
21          "Which dirnode URI should be used as the 'tahoe' alias."]
22         ]
23
24     def postOptions(self):
25         # compute a node-url from the existing options, put in self['node-url']
26         if self['node-directory']:
27             if sys.platform == 'win32' and self['node-directory'] == '~/.tahoe':
28                 from allmydata.windows import registry
29                 self['node-directory'] = registry.get_base_dir_path()
30             else:
31                 self['node-directory'] = os.path.expanduser(self['node-directory'])
32         if self['node-url']:
33             if (not isinstance(self['node-url'], basestring)
34                 or not NODEURL_RE.match(self['node-url'])):
35                 msg = ("--node-url is required to be a string and look like "
36                        "\"http://HOSTNAMEORADDR:PORT\", not: %r" %
37                        (self['node-url'],))
38                 raise usage.UsageError(msg)
39         else:
40             node_url_file = os.path.join(self['node-directory'], "node.url")
41             self['node-url'] = open(node_url_file, "r").read().strip()
42         if self['node-url'][-1] != "/":
43             self['node-url'] += "/"
44
45         aliases = get_aliases(self['node-directory'])
46         if self['dir-cap']:
47             aliases["tahoe"] = self['dir-cap']
48         self.aliases = aliases # maps alias name to dircap
49
50
51 class MakeDirectoryOptions(VDriveOptions):
52     def parseArgs(self, where=""):
53         self.where = where
54     longdesc = """Create a new directory, either unlinked or as a subdirectory."""
55
56 class AddAliasOptions(VDriveOptions):
57     def parseArgs(self, alias, cap):
58         self.alias = alias
59         self.cap = cap
60
61     def getSynopsis(self):
62         return "%s add-alias ALIAS DIRCAP" % (os.path.basename(sys.argv[0]),)
63
64     longdesc = """Add a new alias for an existing directory."""
65
66 class CreateAliasOptions(VDriveOptions):
67     def parseArgs(self, alias):
68         self.alias = alias
69
70     def getSynopsis(self):
71         return "%s create-alias ALIAS" % (os.path.basename(sys.argv[0]),)
72
73     longdesc = """Creates a new directory and adds an alias for it."""
74
75 class ListAliasOptions(VDriveOptions):
76     longdesc = """Displays a table of all configured aliases."""
77
78 class ListOptions(VDriveOptions):
79     optFlags = [
80         ("long", "l", "Use long format: show file sizes, and timestamps"),
81         ("uri", "u", "Show file/directory URIs"),
82         ("readonly-uri", None, "Show readonly file/directory URIs"),
83         ("classify", "F", "Append '/' to directory names, and '*' to mutable"),
84         ("json", None, "Show the raw JSON output"),
85         ]
86     def parseArgs(self, where=""):
87         self.where = where
88
89     longdesc = """List the contents of some portion of the virtual drive."""
90
91 class GetOptions(VDriveOptions):
92     def parseArgs(self, arg1, arg2=None):
93         # tahoe get FOO |less            # write to stdout
94         # tahoe get tahoe:FOO |less      # same
95         # tahoe get FOO bar              # write to local file
96         # tahoe get tahoe:FOO bar        # same
97
98         self.from_file = arg1
99         self.to_file = arg2
100         if self.to_file == "-":
101             self.to_file = None
102
103     def getSynopsis(self):
104         return "%s get VDRIVE_FILE LOCAL_FILE" % (os.path.basename(sys.argv[0]),)
105
106     longdesc = """Retrieve a file from the virtual drive and write it to the
107     local filesystem. If LOCAL_FILE is omitted or '-', the contents of the file
108     will be written to stdout."""
109
110     def getUsage(self, width=None):
111         t = VDriveOptions.getUsage(self, width)
112         t += """
113 Examples:
114  % tahoe get FOO |less            # write to stdout
115  % tahoe get tahoe:FOO |less      # same
116  % tahoe get FOO bar              # write to local file
117  % tahoe get tahoe:FOO bar        # same
118 """
119         return t
120
121 class PutOptions(VDriveOptions):
122     optFlags = [
123         ("mutable", "m", "Create a mutable file instead of an immutable one."),
124         ]
125
126     def parseArgs(self, arg1=None, arg2=None):
127         # cat FILE | tahoe put           # create unlinked file from stdin
128         # cat FILE | tahoe put -         # same
129         # tahoe put bar                  # create unlinked file from local 'bar'
130         # cat FILE | tahoe put - FOO     # create tahoe:FOO from stdin
131         # tahoe put bar FOO              # copy local 'bar' to tahoe:FOO
132         # tahoe put bar tahoe:FOO        # same
133
134         if arg1 is not None and arg2 is not None:
135             self.from_file = arg1
136             self.to_file = arg2
137         elif arg1 is not None and arg2 is None:
138             self.from_file = arg1 # might be "-"
139             self.to_file = None
140         else:
141             self.from_file = None
142             self.to_file = None
143         if self.from_file == "-":
144             self.from_file = None
145
146     def getSynopsis(self):
147         return "%s put LOCAL_FILE VDRIVE_FILE" % (os.path.basename(sys.argv[0]),)
148
149     longdesc = """Put a file into the virtual drive (copying the file's
150     contents from the local filesystem). If VDRIVE_FILE is missing, upload
151     the file but do not link it into a directory: prints the new filecap to
152     stdout. If LOCAL_FILE is missing or '-', data will be copied from stdin.
153     VDRIVE_FILE is assumed to start with tahoe: unless otherwise specified."""
154
155     def getUsage(self, width=None):
156         t = VDriveOptions.getUsage(self, width)
157         t += """
158 Examples:
159  % cat FILE | tahoe put                # create unlinked file from stdin
160  % cat FILE | tahoe -                  # same
161  % tahoe put bar                       # create unlinked file from local 'bar'
162  % cat FILE | tahoe put - FOO          # create tahoe:FOO from stdin
163  % tahoe put bar FOO                   # copy local 'bar' to tahoe:FOO
164  % tahoe put bar tahoe:FOO             # same
165  % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
166 """
167         return t
168
169 class CpOptions(VDriveOptions):
170     optFlags = [
171         ("recursive", "r", "Copy source directory recursively."),
172         ("verbose", "v", "Be noisy about what is happening."),
173         ]
174     def parseArgs(self, *args):
175         if len(args) < 2:
176             raise usage.UsageError("cp requires at least two arguments")
177         self.sources = args[:-1]
178         self.destination = args[-1]
179
180 class RmOptions(VDriveOptions):
181     def parseArgs(self, where):
182         self.where = where
183
184     def getSynopsis(self):
185         return "%s rm VDRIVE_FILE" % (os.path.basename(sys.argv[0]),)
186
187 class MvOptions(VDriveOptions):
188     def parseArgs(self, frompath, topath):
189         self.from_file = frompath
190         self.to_file = topath
191
192     def getSynopsis(self):
193         return "%s mv FROM TO" % (os.path.basename(sys.argv[0]),)
194
195 class LnOptions(VDriveOptions):
196     def parseArgs(self, frompath, topath):
197         self.from_file = frompath
198         self.to_file = topath
199
200     def getSynopsis(self):
201         return "%s ln FROM TO" % (os.path.basename(sys.argv[0]),)
202
203 class BackupConfigurationError(Exception):
204     pass
205
206 class BackupOptions(VDriveOptions):
207     optFlags = [
208         ("verbose", "v", "Be noisy about what is happening."),
209         ("no-backupdb", None, "Do not use the SQLite-based backup-database (always upload all files)."),
210         ("ignore-timestamps", None, "Do not use backupdb timestamps to decide if a local file is unchanged."),
211         ]
212
213     vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore', '.svn',
214                    '.arch-ids','{arch}', '=RELEASE-ID', '=meta-update', '=update',
215                    '.bzr', '.bzrignore', '.bzrtags', '.hg', '.hgignore', '_darcs')
216
217     def __init__(self):
218         super(BackupOptions, self).__init__()
219         self['exclude'] = set()
220
221     def parseArgs(self, localdir, topath):
222         self.from_dir = localdir
223         self.to_dir = topath
224
225     def getSynopsis(Self):
226         return "%s backup FROM ALIAS:TO" % os.path.basename(sys.argv[0])
227
228     def opt_exclude(self, pattern):
229         """Ignore files matching a glob pattern. You may give multiple
230         '--exclude' options."""
231         g = pattern.strip()
232         if g:
233             exclude = self['exclude']
234             exclude.add(g)
235
236     def opt_exclude_from(self, filepath):
237         """Ignore file matching glob patterns listed in file, one per
238         line."""
239         try:
240             exclude_file = file(filepath)
241         except:
242             raise BackupConfigurationError('Error opening exclude file %r.' % filepath)
243         try:
244             for line in exclude_file:
245                 self.opt_exclude(line)
246         finally:
247             exclude_file.close()
248
249     def opt_exclude_vcs(self):
250         """Exclude files and directories used by following version
251         control systems: 'CVS', 'RCS', 'SCCS', 'SVN', 'Arch',
252         'Bazaar', 'Mercurial', and 'Darcs'."""
253         for pattern in self.vcs_patterns:
254             self.opt_exclude(pattern)
255
256     def filter_listdir(self, listdir):
257         """Yields non-excluded childpaths in path."""
258         exclude = self['exclude']
259         exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
260         for filename in listdir:
261             for regexp in exclude_regexps:
262                 if regexp.match(filename):
263                     break
264             else:
265                 yield filename
266
267     longdesc = """Add a versioned backup of the local FROM directory to a timestamped subdir of the (tahoe) TO/Archives directory, sharing as many files and directories as possible with the previous backup. Creates TO/Latest as a reference to the latest backup. Behaves somewhat like 'rsync -a --link-dest=TO/Archives/(previous) FROM TO/Archives/(new); ln -sf TO/Archives/(new) TO/Latest'."""
268
269 class WebopenOptions(VDriveOptions):
270     def parseArgs(self, where=''):
271         self.where = where
272
273     def getSynopsis(self):
274         return "%s webopen [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
275
276     longdesc = """Opens a webbrowser to the contents of some portion of the virtual drive."""
277
278 class ManifestOptions(VDriveOptions):
279     optFlags = [
280         ("storage-index", "s", "Only print storage index strings, not pathname+cap"),
281         ("verify-cap", None, "Only print verifycap, not pathname+cap"),
282         ("repair-cap", None, "Only print repaircap, not pathname+cap"),
283         ("raw", "r", "Display raw JSON data instead of parsed"),
284         ]
285     def parseArgs(self, where=''):
286         self.where = where
287
288     def getSynopsis(self):
289         return "%s manifest [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
290
291     longdesc = """Print a list of all files/directories reachable from the given starting point."""
292
293 class StatsOptions(VDriveOptions):
294     optFlags = [
295         ("raw", "r", "Display raw JSON data instead of parsed"),
296         ]
297     def parseArgs(self, where=''):
298         self.where = where
299
300     def getSynopsis(self):
301         return "%s stats [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
302
303     longdesc = """Print statistics about of all files/directories reachable from the given starting point."""
304
305 class CheckOptions(VDriveOptions):
306     optFlags = [
307         ("raw", None, "Display raw JSON data instead of parsed"),
308         ("verify", None, "Verify all hashes, instead of merely querying share presence"),
309         ("repair", None, "Automatically repair any problems found"),
310         ("add-lease", None, "Add/renew lease on all shares"),
311         ]
312     def parseArgs(self, where=''):
313         self.where = where
314
315     def getSynopsis(self):
316         return "%s check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
317
318     longdesc = """Check a single file or directory: count how many shares are available, verify their hashes. Optionally repair the file if any problems were found."""
319
320 class DeepCheckOptions(VDriveOptions):
321     optFlags = [
322         ("raw", None, "Display raw JSON data instead of parsed"),
323         ("verify", None, "Verify all hashes, instead of merely querying share presence"),
324         ("repair", None, "Automatically repair any problems found"),
325         ("add-lease", None, "Add/renew lease on all shares"),
326         ("verbose", "v", "Be noisy about what is happening."),
327         ]
328     def parseArgs(self, where=''):
329         self.where = where
330
331     def getSynopsis(self):
332         return "%s deep-check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
333
334     longdesc = """Check all files/directories reachable from the given starting point (which must be a directory), like 'tahoe check' but for multiple files. Optionally repair any problems found."""
335
336 subCommands = [
337     ["mkdir", None, MakeDirectoryOptions, "Create a new directory"],
338     ["add-alias", None, AddAliasOptions, "Add a new alias cap"],
339     ["create-alias", None, CreateAliasOptions, "Create a new alias cap"],
340     ["list-aliases", None, ListAliasOptions, "List all alias caps"],
341     ["ls", None, ListOptions, "List a directory"],
342     ["get", None, GetOptions, "Retrieve a file from the virtual drive."],
343     ["put", None, PutOptions, "Upload a file into the virtual drive."],
344     ["cp", None, CpOptions, "Copy one or more files."],
345     ["rm", None, RmOptions, "Unlink a file or directory in the virtual drive."],
346     ["mv", None, MvOptions, "Move a file within the virtual drive."],
347     ["ln", None, LnOptions, "Make an additional link to an existing file."],
348     ["backup", None, BackupOptions, "Make target dir look like local dir."],
349     ["webopen", None, WebopenOptions, "Open a webbrowser to the root_dir"],
350     ["manifest", None, ManifestOptions, "List all files/dirs in a subtree"],
351     ["stats", None, StatsOptions, "Print statistics about all files/dirs in a subtree"],
352     ["check", None, CheckOptions, "Check a single file or directory"],
353     ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point"],
354     ]
355
356 def mkdir(options):
357     from allmydata.scripts import tahoe_mkdir
358     rc = tahoe_mkdir.mkdir(options)
359     return rc
360
361 def add_alias(options):
362     from allmydata.scripts import tahoe_add_alias
363     rc = tahoe_add_alias.add_alias(options)
364     return rc
365
366 def create_alias(options):
367     from allmydata.scripts import tahoe_add_alias
368     rc = tahoe_add_alias.create_alias(options)
369     return rc
370
371 def list_aliases(options):
372     from allmydata.scripts import tahoe_add_alias
373     rc = tahoe_add_alias.list_aliases(options)
374     return rc
375
376 def list(options):
377     from allmydata.scripts import tahoe_ls
378     rc = tahoe_ls.list(options)
379     return rc
380
381 def get(options):
382     from allmydata.scripts import tahoe_get
383     rc = tahoe_get.get(options)
384     if rc == 0:
385         if options.to_file is None:
386             # be quiet, since the file being written to stdout should be
387             # proof enough that it worked, unless the user is unlucky
388             # enough to have picked an empty file
389             pass
390         else:
391             print >>options.stderr, "%s retrieved and written to %s" % \
392                   (options.from_file, options.to_file)
393     return rc
394
395 def put(options):
396     from allmydata.scripts import tahoe_put
397     rc = tahoe_put.put(options)
398     return rc
399
400 def cp(options):
401     from allmydata.scripts import tahoe_cp
402     rc = tahoe_cp.copy(options)
403     return rc
404
405 def rm(options):
406     from allmydata.scripts import tahoe_rm
407     rc = tahoe_rm.rm(options)
408     return rc
409
410 def mv(options):
411     from allmydata.scripts import tahoe_mv
412     rc = tahoe_mv.mv(options, mode="move")
413     return rc
414
415 def ln(options):
416     from allmydata.scripts import tahoe_mv
417     rc = tahoe_mv.mv(options, mode="link")
418     return rc
419
420 def backup(options):
421     from allmydata.scripts import tahoe_backup
422     rc = tahoe_backup.backup(options)
423     return rc
424
425 def webopen(options, opener=None):
426     from allmydata.scripts import tahoe_webopen
427     rc = tahoe_webopen.webopen(options, opener=opener)
428     return rc
429
430 def manifest(options):
431     from allmydata.scripts import tahoe_manifest
432     rc = tahoe_manifest.manifest(options)
433     return rc
434
435 def stats(options):
436     from allmydata.scripts import tahoe_manifest
437     rc = tahoe_manifest.stats(options)
438     return rc
439
440 def check(options):
441     from allmydata.scripts import tahoe_check
442     rc = tahoe_check.check(options)
443     return rc
444
445 def deepcheck(options):
446     from allmydata.scripts import tahoe_check
447     rc = tahoe_check.deepcheck(options)
448     return rc
449
450 dispatch = {
451     "mkdir": mkdir,
452     "add-alias": add_alias,
453     "create-alias": create_alias,
454     "list-aliases": list_aliases,
455     "ls": list,
456     "get": get,
457     "put": put,
458     "cp": cp,
459     "rm": rm,
460     "mv": mv,
461     "ln": ln,
462     "backup": backup,
463     "webopen": webopen,
464     "manifest": manifest,
465     "stats": stats,
466     "check": check,
467     "deep-check": deepcheck,
468     }
469