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