]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/cli.py
bin/tahoe: clean up global-vs-subcommand arguments like --node-directory
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / cli.py
1 import os.path, re, fnmatch
2 from twisted.python import usage
3 from allmydata.scripts.common import get_aliases, get_default_nodedir, \
4      DEFAULT_ALIAS, BaseOptions
5 from allmydata.util.encodingutil import argv_to_unicode, argv_to_abspath, quote_output
6
7 NODEURL_RE=re.compile("http(s?)://([^:]*)(:([1-9][0-9]*))?")
8
9 _default_nodedir = get_default_nodedir()
10
11 class VDriveOptions(BaseOptions):
12     optParameters = [
13         ["node-url", "u", None,
14          "Specify the URL of the Tahoe gateway node, such as "
15          "'http://127.0.0.1:3456'. "
16          "This overrides the URL found in the --node-directory ."],
17         ["dir-cap", None, None,
18          "Specify which dirnode URI should be used as the 'tahoe' alias."]
19         ]
20
21     def postOptions(self):
22         self["quiet"] = self.parent["quiet"]
23         if self.parent['node-directory']:
24             self['node-directory'] = argv_to_abspath(self.parent['node-directory'])
25         else:
26             self['node-directory'] = _default_nodedir
27
28         # compute a node-url from the existing options, put in self['node-url']
29         if self['node-url']:
30             if (not isinstance(self['node-url'], basestring)
31                 or not NODEURL_RE.match(self['node-url'])):
32                 msg = ("--node-url is required to be a string and look like "
33                        "\"http://HOSTNAMEORADDR:PORT\", not: %r" %
34                        (self['node-url'],))
35                 raise usage.UsageError(msg)
36         else:
37             node_url_file = os.path.join(self['node-directory'], "node.url")
38             self['node-url'] = open(node_url_file, "r").read().strip()
39         if self['node-url'][-1] != "/":
40             self['node-url'] += "/"
41
42         aliases = get_aliases(self['node-directory'])
43         if self['dir-cap']:
44             aliases[DEFAULT_ALIAS] = self['dir-cap']
45         self.aliases = aliases # maps alias name to dircap
46
47
48 class MakeDirectoryOptions(VDriveOptions):
49     optParameters = [
50         ("format", None, None, "Create a directory with the given format: SDMF or MDMF (case-insensitive)"),
51         ]
52
53     def parseArgs(self, where=""):
54         self.where = argv_to_unicode(where)
55
56         if self['format']:
57             if self['format'].upper() not in ("SDMF", "MDMF"):
58                 raise usage.UsageError("%s is an invalid format" % self['format'])
59
60     def getSynopsis(self):
61         return "Usage:  %s mkdir [options] [REMOTE_DIR]" % (self.command_name,)
62
63     longdesc = """Create a new directory, either unlinked or as a subdirectory."""
64
65 class AddAliasOptions(VDriveOptions):
66     def parseArgs(self, alias, cap):
67         self.alias = argv_to_unicode(alias)
68         if self.alias.endswith(u':'):
69             self.alias = self.alias[:-1]
70         self.cap = cap
71
72     def getSynopsis(self):
73         return "Usage:  %s add-alias [options] ALIAS[:] DIRCAP" % (self.command_name,)
74
75     longdesc = """Add a new alias for an existing directory."""
76
77 class CreateAliasOptions(VDriveOptions):
78     def parseArgs(self, alias):
79         self.alias = argv_to_unicode(alias)
80         if self.alias.endswith(u':'):
81             self.alias = self.alias[:-1]
82
83     def getSynopsis(self):
84         return "Usage:  %s create-alias [options] ALIAS[:]" % (self.command_name,)
85
86     longdesc = """Create a new directory and add an alias for it."""
87
88 class ListAliasesOptions(VDriveOptions):
89     def getSynopsis(self):
90         return "Usage:  %s list-aliases [options]" % (self.command_name,)
91
92     longdesc = """Display a table of all configured aliases."""
93
94 class ListOptions(VDriveOptions):
95     optFlags = [
96         ("long", "l", "Use long format: show file sizes, and timestamps."),
97         ("uri", "u", "Show file/directory URIs."),
98         ("readonly-uri", None, "Show read-only file/directory URIs."),
99         ("classify", "F", "Append '/' to directory names, and '*' to mutable."),
100         ("json", None, "Show the raw JSON output."),
101         ]
102     def parseArgs(self, where=""):
103         self.where = argv_to_unicode(where)
104
105     def getSynopsis(self):
106         return "Usage:  %s ls [options] [PATH]" % (self.command_name,)
107
108     longdesc = """
109     List the contents of some portion of the grid.
110
111     If PATH is omitted, "tahoe:" is assumed.
112
113     When the -l or --long option is used, each line is shown in the
114     following format:
115
116     drwx <size> <date/time> <name in this directory>
117
118     where each of the letters on the left may be replaced by '-'.
119     If 'd' is present, it indicates that the object is a directory.
120     If the 'd' is replaced by a '?', the object type is unknown.
121     'rwx' is a Unix-like permissions mask: if the mask includes 'w',
122     then the object is writeable through its link in this directory
123     (note that the link might be replaceable even if the object is
124     not writeable through the current link).
125     The 'x' is a legacy of Unix filesystems. In Tahoe it is used
126     only to indicate that the contents of a directory can be listed.
127
128     Directories have no size, so their size field is shown as '-'.
129     Otherwise the size of the file, when known, is given in bytes.
130     The size of mutable files or unknown objects is shown as '?'.
131
132     The date/time shows when this link in the Tahoe filesystem was
133     last modified.
134     """
135
136 class GetOptions(VDriveOptions):
137     def parseArgs(self, arg1, arg2=None):
138         # tahoe get FOO |less            # write to stdout
139         # tahoe get tahoe:FOO |less      # same
140         # tahoe get FOO bar              # write to local file
141         # tahoe get tahoe:FOO bar        # same
142
143         self.from_file = argv_to_unicode(arg1)
144
145         if arg2:
146             self.to_file = argv_to_unicode(arg2)
147         else:
148             self.to_file = None
149
150         if self.to_file == "-":
151             self.to_file = None
152
153     def getSynopsis(self):
154         return "Usage:  %s get [options] REMOTE_FILE LOCAL_FILE" % (self.command_name,)
155
156     longdesc = """
157     Retrieve a file from the grid and write it to the local filesystem. If
158     LOCAL_FILE is omitted or '-', the contents of the file will be written to
159     stdout."""
160
161     def getUsage(self, width=None):
162         t = VDriveOptions.getUsage(self, width)
163         t += """
164 Examples:
165  % tahoe get FOO |less            # write to stdout
166  % tahoe get tahoe:FOO |less      # same
167  % tahoe get FOO bar              # write to local file
168  % tahoe get tahoe:FOO bar        # same
169 """
170         return t
171
172 class PutOptions(VDriveOptions):
173     optFlags = [
174         ("mutable", "m", "Create a mutable file instead of an immutable one (like --format=SDMF)"),
175         ]
176     optParameters = [
177         ("format", None, None, "Create a file with the given format: SDMF and MDMF for mutable, CHK (default) for immutable. (case-insensitive)"),
178         ]
179
180     def parseArgs(self, arg1=None, arg2=None):
181         # see Examples below
182
183         if arg1 is not None and arg2 is not None:
184             self.from_file = argv_to_unicode(arg1)
185             self.to_file =  argv_to_unicode(arg2)
186         elif arg1 is not None and arg2 is None:
187             self.from_file = argv_to_unicode(arg1) # might be "-"
188             self.to_file = None
189         else:
190             self.from_file = None
191             self.to_file = None
192         if self.from_file == u"-":
193             self.from_file = None
194
195         if self['format']:
196             if self['format'].upper() not in ("SDMF", "MDMF", "CHK"):
197                 raise usage.UsageError("%s is an invalid format" % self['format'])
198
199     def getSynopsis(self):
200         return "Usage:  %s put [options] LOCAL_FILE REMOTE_FILE" % (self.command_name,)
201
202     longdesc = """
203     Put a file into the grid, copying its contents from the local filesystem.
204     If REMOTE_FILE is missing, upload the file but do not link it into a
205     directory; also print the new filecap to stdout. If LOCAL_FILE is missing
206     or '-', data will be copied from stdin. REMOTE_FILE is assumed to start
207     with tahoe: unless otherwise specified.
208
209     If the destination file already exists and is mutable, it will be modified
210     in-place, whether or not --mutable is specified. (--mutable only affects
211     creation of new files.)"""
212
213     def getUsage(self, width=None):
214         t = VDriveOptions.getUsage(self, width)
215         t += """
216 Examples:
217  % cat FILE | tahoe put                # create unlinked file from stdin
218  % cat FILE | tahoe put -              # same
219  % tahoe put bar                       # create unlinked file from local 'bar'
220  % cat FILE | tahoe put - FOO          # create tahoe:FOO from stdin
221  % tahoe put bar FOO                   # copy local 'bar' to tahoe:FOO
222  % tahoe put bar tahoe:FOO             # same
223  % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
224 """
225         return t
226
227 class CpOptions(VDriveOptions):
228     optFlags = [
229         ("recursive", "r", "Copy source directory recursively."),
230         ("verbose", "v", "Be noisy about what is happening."),
231         ("caps-only", None,
232          "When copying to local files, write out filecaps instead of actual "
233          "data (only useful for debugging and tree-comparison purposes)."),
234         ]
235
236     def parseArgs(self, *args):
237         if len(args) < 2:
238             raise usage.UsageError("cp requires at least two arguments")
239         self.sources = map(argv_to_unicode, args[:-1])
240         self.destination = argv_to_unicode(args[-1])
241
242     def getSynopsis(self):
243         return "Usage: %s cp [options] FROM.. TO" % (self.command_name,)
244
245     longdesc = """
246     Use 'tahoe cp' to copy files between a local filesystem and a Tahoe grid.
247     Any FROM/TO arguments that begin with an alias indicate Tahoe-side
248     files or non-file arguments. Directories will be copied recursively.
249     New Tahoe-side directories will be created when necessary. Assuming that
250     you have previously set up an alias 'home' with 'tahoe create-alias home',
251     here are some examples:
252
253     tahoe cp ~/foo.txt home:  # creates tahoe-side home:foo.txt
254
255     tahoe cp ~/foo.txt /tmp/bar.txt home:  # copies two files to home:
256
257     tahoe cp ~/Pictures home:stuff/my-pictures  # copies directory recursively
258
259     You can also use a dircap as either FROM or TO target:
260
261     tahoe cp URI:DIR2-RO:ixqhc4kdbjxc7o65xjnveoewym:5x6lwoxghrd5rxhwunzavft2qygfkt27oj3fbxlq4c6p45z5uneq/blog.html ./   # copy Zooko's wiki page to a local file
262
263     This command still has some limitations: symlinks and special files
264     (device nodes, named pipes) are not handled very well. Arguments should
265     probably not have trailing slashes. 'tahoe cp' does not behave as much
266     like /bin/cp as you would wish, especially with respect to trailing
267     slashes.
268     """
269
270 class UnlinkOptions(VDriveOptions):
271     def parseArgs(self, where):
272         self.where = argv_to_unicode(where)
273
274     def getSynopsis(self):
275         return "Usage:  %s unlink [options] REMOTE_FILE" % (self.command_name,)
276
277 class RmOptions(UnlinkOptions):
278     def getSynopsis(self):
279         return "Usage:  %s rm [options] REMOTE_FILE" % (self.command_name,)
280
281 class MvOptions(VDriveOptions):
282     def parseArgs(self, frompath, topath):
283         self.from_file = argv_to_unicode(frompath)
284         self.to_file = argv_to_unicode(topath)
285
286     def getSynopsis(self):
287         return "Usage:  %s mv [options] FROM TO" % (self.command_name,)
288
289     longdesc = """
290     Use 'tahoe mv' to move files that are already on the grid elsewhere on
291     the grid, e.g., 'tahoe mv alias:some_file alias:new_file'.
292
293     If moving a remote file into a remote directory, you'll need to append a
294     '/' to the name of the remote directory, e.g., 'tahoe mv tahoe:file1
295     tahoe:dir/', not 'tahoe mv tahoe:file1 tahoe:dir'.
296
297     Note that it is not possible to use this command to move local files to
298     the grid -- use 'tahoe cp' for that.
299     """
300
301 class LnOptions(VDriveOptions):
302     def parseArgs(self, frompath, topath):
303         self.from_file = argv_to_unicode(frompath)
304         self.to_file = argv_to_unicode(topath)
305
306     def getSynopsis(self):
307         return "Usage:  %s ln [options] FROM_LINK TO_LINK" % (self.command_name,)
308
309     longdesc = """
310     Use 'tahoe ln' to duplicate a link (directory entry) already on the grid
311     to elsewhere on the grid. For example 'tahoe ln alias:some_file
312     alias:new_file'. causes 'alias:new_file' to point to the same object that
313     'alias:some_file' points to.
314
315     (The argument order is the same as Unix ln. To remember the order, you
316     can think of this command as copying a link, rather than copying a file
317     as 'tahoe cp' does. Then the argument order is consistent with that of
318     'tahoe cp'.)
319
320     When linking a remote file into a remote directory, you'll need to append
321     a '/' to the name of the remote directory, e.g. 'tahoe ln tahoe:file1
322     tahoe:dir/' (which is shorthand for 'tahoe ln tahoe:file1
323     tahoe:dir/file1'). If you forget the '/', e.g. 'tahoe ln tahoe:file1
324     tahoe:dir', the 'ln' command will refuse to overwrite the 'tahoe:dir'
325     directory, and will exit with an error.
326
327     Note that it is not possible to use this command to create links between
328     local and remote files.
329     """
330
331 class BackupConfigurationError(Exception):
332     pass
333
334 class BackupOptions(VDriveOptions):
335     optFlags = [
336         ("verbose", "v", "Be noisy about what is happening."),
337         ("ignore-timestamps", None, "Do not use backupdb timestamps to decide whether a local file is unchanged."),
338         ]
339
340     vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore',
341                     '.svn', '.arch-ids','{arch}', '=RELEASE-ID',
342                     '=meta-update', '=update', '.bzr', '.bzrignore',
343                     '.bzrtags', '.hg', '.hgignore', '_darcs')
344
345     def __init__(self):
346         super(BackupOptions, self).__init__()
347         self['exclude'] = set()
348
349     def parseArgs(self, localdir, topath):
350         self.from_dir = argv_to_unicode(localdir)
351         self.to_dir = argv_to_unicode(topath)
352
353     def getSynopsis(self):
354         return "Usage:  %s backup [options] FROM ALIAS:TO" % (self.command_name,)
355
356     def opt_exclude(self, pattern):
357         """Ignore files matching a glob pattern. You may give multiple
358         '--exclude' options."""
359         g = argv_to_unicode(pattern).strip()
360         if g:
361             exclude = self['exclude']
362             exclude.add(g)
363
364     def opt_exclude_from(self, filepath):
365         """Ignore file matching glob patterns listed in file, one per
366         line. The file is assumed to be in the argv encoding."""
367         abs_filepath = argv_to_abspath(filepath)
368         try:
369             exclude_file = file(abs_filepath)
370         except:
371             raise BackupConfigurationError('Error opening exclude file %s.' % quote_output(abs_filepath))
372         try:
373             for line in exclude_file:
374                 self.opt_exclude(line)
375         finally:
376             exclude_file.close()
377
378     def opt_exclude_vcs(self):
379         """Exclude files and directories used by following version control
380         systems: CVS, RCS, SCCS, Git, SVN, Arch, Bazaar(bzr), Mercurial,
381         Darcs."""
382         for pattern in self.vcs_patterns:
383             self.opt_exclude(pattern)
384
385     def filter_listdir(self, listdir):
386         """Yields non-excluded childpaths in path."""
387         exclude = self['exclude']
388         exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
389         for filename in listdir:
390             for regexp in exclude_regexps:
391                 if regexp.match(filename):
392                     break
393             else:
394                 yield filename
395
396     longdesc = """
397     Add a versioned backup of the local FROM directory to a timestamped
398     subdirectory of the TO/Archives directory on the grid, sharing as many
399     files and directories as possible with earlier backups. Create TO/Latest
400     as a reference to the latest backup. Behaves somewhat like 'rsync -a
401     --link-dest=TO/Archives/(previous) FROM TO/Archives/(new); ln -sf
402     TO/Archives/(new) TO/Latest'."""
403
404 class WebopenOptions(VDriveOptions):
405     optFlags = [
406         ("info", "i", "Open the t=info page for the file"),
407         ]
408     def parseArgs(self, where=''):
409         self.where = argv_to_unicode(where)
410
411     def getSynopsis(self):
412         return "Usage:  %s webopen [options] [ALIAS:PATH]" % (self.command_name,)
413
414     longdesc = """Open a web browser to the contents of some file or
415     directory on the grid. When run without arguments, open the Welcome
416     page."""
417
418 class ManifestOptions(VDriveOptions):
419     optFlags = [
420         ("storage-index", "s", "Only print storage index strings, not pathname+cap."),
421         ("verify-cap", None, "Only print verifycap, not pathname+cap."),
422         ("repair-cap", None, "Only print repaircap, not pathname+cap."),
423         ("raw", "r", "Display raw JSON data instead of parsed."),
424         ]
425     def parseArgs(self, where=''):
426         self.where = argv_to_unicode(where)
427
428     def getSynopsis(self):
429         return "Usage:  %s manifest [options] [ALIAS:PATH]" % (self.command_name,)
430
431     longdesc = """Print a list of all files and directories reachable from
432     the given starting point."""
433
434 class StatsOptions(VDriveOptions):
435     optFlags = [
436         ("raw", "r", "Display raw JSON data instead of parsed"),
437         ]
438     def parseArgs(self, where=''):
439         self.where = argv_to_unicode(where)
440
441     def getSynopsis(self):
442         return "Usage:  %s stats [options] [ALIAS:PATH]" % (self.command_name,)
443
444     longdesc = """Print statistics about of all files and directories
445     reachable from the given starting point."""
446
447 class CheckOptions(VDriveOptions):
448     optFlags = [
449         ("raw", None, "Display raw JSON data instead of parsed."),
450         ("verify", None, "Verify all hashes, instead of merely querying share presence."),
451         ("repair", None, "Automatically repair any problems found."),
452         ("add-lease", None, "Add/renew lease on all shares."),
453         ]
454     def parseArgs(self, where=''):
455         self.where = argv_to_unicode(where)
456
457     def getSynopsis(self):
458         return "Usage:  %s check [options] [ALIAS:PATH]" % (self.command_name,)
459
460     longdesc = """
461     Check a single file or directory: count how many shares are available and
462     verify their hashes. Optionally repair the file if any problems were
463     found."""
464
465 class DeepCheckOptions(VDriveOptions):
466     optFlags = [
467         ("raw", None, "Display raw JSON data instead of parsed."),
468         ("verify", None, "Verify all hashes, instead of merely querying share presence."),
469         ("repair", None, "Automatically repair any problems found."),
470         ("add-lease", None, "Add/renew lease on all shares."),
471         ("verbose", "v", "Be noisy about what is happening."),
472         ]
473     def parseArgs(self, where=''):
474         self.where = argv_to_unicode(where)
475
476     def getSynopsis(self):
477         return "Usage:  %s deep-check [options] [ALIAS:PATH]" % (self.command_name,)
478
479     longdesc = """
480     Check all files and directories reachable from the given starting point
481     (which must be a directory), like 'tahoe check' but for multiple files.
482     Optionally repair any problems found."""
483
484 subCommands = [
485     ["mkdir", None, MakeDirectoryOptions, "Create a new directory."],
486     ["add-alias", None, AddAliasOptions, "Add a new alias cap."],
487     ["create-alias", None, CreateAliasOptions, "Create a new alias cap."],
488     ["list-aliases", None, ListAliasesOptions, "List all alias caps."],
489     ["ls", None, ListOptions, "List a directory."],
490     ["get", None, GetOptions, "Retrieve a file from the grid."],
491     ["put", None, PutOptions, "Upload a file into the grid."],
492     ["cp", None, CpOptions, "Copy one or more files or directories."],
493     ["unlink", None, UnlinkOptions, "Unlink a file or directory on the grid."],
494     ["rm", None, RmOptions, "Unlink a file or directory on the grid (same as unlink)."],
495     ["mv", None, MvOptions, "Move a file within the grid."],
496     ["ln", None, LnOptions, "Make an additional link to an existing file or directory."],
497     ["backup", None, BackupOptions, "Make target dir look like local dir."],
498     ["webopen", None, WebopenOptions, "Open a web browser to a grid file or directory."],
499     ["manifest", None, ManifestOptions, "List all files/directories in a subtree."],
500     ["stats", None, StatsOptions, "Print statistics about all files/directories in a subtree."],
501     ["check", None, CheckOptions, "Check a single file or directory."],
502     ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point."],
503     ]
504
505 def mkdir(options):
506     from allmydata.scripts import tahoe_mkdir
507     rc = tahoe_mkdir.mkdir(options)
508     return rc
509
510 def add_alias(options):
511     from allmydata.scripts import tahoe_add_alias
512     rc = tahoe_add_alias.add_alias(options)
513     return rc
514
515 def create_alias(options):
516     from allmydata.scripts import tahoe_add_alias
517     rc = tahoe_add_alias.create_alias(options)
518     return rc
519
520 def list_aliases(options):
521     from allmydata.scripts import tahoe_add_alias
522     rc = tahoe_add_alias.list_aliases(options)
523     return rc
524
525 def list(options):
526     from allmydata.scripts import tahoe_ls
527     rc = tahoe_ls.list(options)
528     return rc
529
530 def get(options):
531     from allmydata.scripts import tahoe_get
532     rc = tahoe_get.get(options)
533     if rc == 0:
534         if options.to_file is None:
535             # be quiet, since the file being written to stdout should be
536             # proof enough that it worked, unless the user is unlucky
537             # enough to have picked an empty file
538             pass
539         else:
540             print >>options.stderr, "%s retrieved and written to %s" % \
541                   (options.from_file, options.to_file)
542     return rc
543
544 def put(options):
545     from allmydata.scripts import tahoe_put
546     rc = tahoe_put.put(options)
547     return rc
548
549 def cp(options):
550     from allmydata.scripts import tahoe_cp
551     rc = tahoe_cp.copy(options)
552     return rc
553
554 def unlink(options, command="unlink"):
555     from allmydata.scripts import tahoe_unlink
556     rc = tahoe_unlink.unlink(options, command=command)
557     return rc
558
559 def rm(options):
560     return unlink(options, command="rm")
561
562 def mv(options):
563     from allmydata.scripts import tahoe_mv
564     rc = tahoe_mv.mv(options, mode="move")
565     return rc
566
567 def ln(options):
568     from allmydata.scripts import tahoe_mv
569     rc = tahoe_mv.mv(options, mode="link")
570     return rc
571
572 def backup(options):
573     from allmydata.scripts import tahoe_backup
574     rc = tahoe_backup.backup(options)
575     return rc
576
577 def webopen(options, opener=None):
578     from allmydata.scripts import tahoe_webopen
579     rc = tahoe_webopen.webopen(options, opener=opener)
580     return rc
581
582 def manifest(options):
583     from allmydata.scripts import tahoe_manifest
584     rc = tahoe_manifest.manifest(options)
585     return rc
586
587 def stats(options):
588     from allmydata.scripts import tahoe_manifest
589     rc = tahoe_manifest.stats(options)
590     return rc
591
592 def check(options):
593     from allmydata.scripts import tahoe_check
594     rc = tahoe_check.check(options)
595     return rc
596
597 def deepcheck(options):
598     from allmydata.scripts import tahoe_check
599     rc = tahoe_check.deepcheck(options)
600     return rc
601
602 dispatch = {
603     "mkdir": mkdir,
604     "add-alias": add_alias,
605     "create-alias": create_alias,
606     "list-aliases": list_aliases,
607     "ls": list,
608     "get": get,
609     "put": put,
610     "cp": cp,
611     "unlink": unlink,
612     "rm": rm,
613     "mv": mv,
614     "ln": ln,
615     "backup": backup,
616     "webopen": webopen,
617     "manifest": manifest,
618     "stats": stats,
619     "check": check,
620     "deep-check": deepcheck,
621     }