]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/cli.py
change docs and --help to use "grid" instead of "virtual drive": closes #892.
[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 = """Create a new directory and add an alias for it."""
73
74 class ListAliasOptions(VDriveOptions):
75     longdesc = """Display 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 grid."""
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 REMOTE_FILE LOCAL_FILE" % (os.path.basename(sys.argv[0]),)
104
105     longdesc = """
106     Retrieve a file from the grid and write it to the local filesystem. If
107     LOCAL_FILE is omitted or '-', the contents of the file will be written to
108     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         # see Examples below
128
129         if arg1 is not None and arg2 is not None:
130             self.from_file = arg1
131             self.to_file = arg2
132         elif arg1 is not None and arg2 is None:
133             self.from_file = arg1 # might be "-"
134             self.to_file = None
135         else:
136             self.from_file = None
137             self.to_file = None
138         if self.from_file == "-":
139             self.from_file = None
140
141     def getSynopsis(self):
142         return "%s put LOCAL_FILE REMOTE_FILE" % (os.path.basename(sys.argv[0]),)
143
144     longdesc = """
145     Put a file into the grid, copying its contents from the local filesystem.
146     If REMOTE_FILE is missing, upload the file but do not link it into a
147     directory; also print the new filecap to stdout. If LOCAL_FILE is missing
148     or '-', data will be copied from stdin. REMOTE_FILE is assumed to start
149     with tahoe: unless otherwise specified."""
150
151     def getUsage(self, width=None):
152         t = VDriveOptions.getUsage(self, width)
153         t += """
154 Examples:
155  % cat FILE | tahoe put                # create unlinked file from stdin
156  % cat FILE | tahoe -                  # same
157  % tahoe put bar                       # create unlinked file from local 'bar'
158  % cat FILE | tahoe put - FOO          # create tahoe:FOO from stdin
159  % tahoe put bar FOO                   # copy local 'bar' to tahoe:FOO
160  % tahoe put bar tahoe:FOO             # same
161  % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
162 """
163         return t
164
165 class CpOptions(VDriveOptions):
166     optFlags = [
167         ("recursive", "r", "Copy source directory recursively."),
168         ("verbose", "v", "Be noisy about what is happening."),
169         ("caps-only", None,
170          "When copying to local files, write out filecaps instead of actual "
171          "data (only useful for debugging and tree-comparison purposes)."),
172         ]
173     def parseArgs(self, *args):
174         if len(args) < 2:
175             raise usage.UsageError("cp requires at least two arguments")
176         self.sources = args[:-1]
177         self.destination = args[-1]
178     def getSynopsis(self):
179         return "Usage: tahoe [options] cp FROM.. TO"
180     longdesc = """
181     Use 'tahoe cp' to copy files between a local filesystem and a Tahoe grid.
182     Any FROM/TO arguments that begin with an alias indicate Tahoe-side
183     files or non-file arguments. Directories will be copied recursively.
184     New Tahoe-side directories will be created when necessary. Assuming that
185     you have previously set up an alias 'home' with 'tahoe create-alias home',
186     here are some examples:
187
188     tahoe cp ~/foo.txt home:  # creates tahoe-side home:foo.txt
189
190     tahoe cp ~/foo.txt /tmp/bar.txt home:  # copies two files to home:
191
192     tahoe cp ~/Pictures home:stuff/my-pictures  # copies directory recursively
193
194     You can also use a dircap as either FROM or TO target:
195
196     tahoe cp URI:DIR2-RO:j74uhg25nwdpjpacl6rkat2yhm:kav7ijeft5h7r7rxdp5bgtlt3viv32yabqajkrdykozia5544jqa/wiki.html ./   # copy Zooko's wiki page to a local file
197
198     This command still has some limitations: symlinks, special files (device
199     nodes, named pipes), and non-ASCII filenames are not handled very well.
200     Arguments should probably not have trailing slashes. 'tahoe cp' does not
201     behave as much like /bin/cp as you would wish, especially with respect to
202     trailing slashes.
203     """
204
205 class RmOptions(VDriveOptions):
206     def parseArgs(self, where):
207         self.where = where
208
209     def getSynopsis(self):
210         return "%s rm REMOTE_FILE" % (os.path.basename(sys.argv[0]),)
211
212 class MvOptions(VDriveOptions):
213     def parseArgs(self, frompath, topath):
214         self.from_file = frompath
215         self.to_file = topath
216
217     def getSynopsis(self):
218         return "%s mv FROM TO" % (os.path.basename(sys.argv[0]),)
219     longdesc = """
220     Use 'tahoe mv' to move files that are already on the grid elsewhere on
221     the grid, e.g., 'tahoe mv alias:some_file alias:new_file'.
222
223     If moving a remote file into a remote directory, you'll need to append a
224     '/' to the name of the remote directory, e.g., 'tahoe mv tahoe:file1
225     tahoe:dir/', not 'tahoe mv tahoe:file1 tahoe:dir'.
226
227     Note that it is not possible to use this command to move local files to
228     the grid -- use 'tahoe cp' for that.
229     """
230
231 class LnOptions(VDriveOptions):
232     def parseArgs(self, frompath, topath):
233         self.from_file = frompath
234         self.to_file = topath
235
236     def getSynopsis(self):
237         return "%s ln FROM TO" % (os.path.basename(sys.argv[0]),)
238
239 class BackupConfigurationError(Exception):
240     pass
241
242 class BackupOptions(VDriveOptions):
243     optFlags = [
244         ("verbose", "v", "Be noisy about what is happening."),
245         ("ignore-timestamps", None, "Do not use backupdb timestamps to decide whether a local file is unchanged."),
246         ]
247
248     vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore',
249                     '.svn', '.arch-ids','{arch}', '=RELEASE-ID',
250                     '=meta-update', '=update', '.bzr', '.bzrignore',
251                     '.bzrtags', '.hg', '.hgignore', '_darcs')
252
253     def __init__(self):
254         super(BackupOptions, self).__init__()
255         self['exclude'] = set()
256
257     def parseArgs(self, localdir, topath):
258         self.from_dir = localdir
259         self.to_dir = topath
260
261     def getSynopsis(Self):
262         return "%s backup FROM ALIAS:TO" % os.path.basename(sys.argv[0])
263
264     def opt_exclude(self, pattern):
265         """Ignore files matching a glob pattern. You may give multiple
266         '--exclude' options."""
267         g = pattern.strip()
268         if g:
269             exclude = self['exclude']
270             exclude.add(g)
271
272     def opt_exclude_from(self, filepath):
273         """Ignore file matching glob patterns listed in file, one per
274         line."""
275         try:
276             exclude_file = file(filepath)
277         except:
278             raise BackupConfigurationError('Error opening exclude file %r.' % filepath)
279         try:
280             for line in exclude_file:
281                 self.opt_exclude(line)
282         finally:
283             exclude_file.close()
284
285     def opt_exclude_vcs(self):
286         """Exclude files and directories used by following version
287         control systems: 'CVS', 'RCS', 'SCCS', 'SVN', 'Arch',
288         'Bazaar', 'Mercurial', and 'Darcs'."""
289         for pattern in self.vcs_patterns:
290             self.opt_exclude(pattern)
291
292     def filter_listdir(self, listdir):
293         """Yields non-excluded childpaths in path."""
294         exclude = self['exclude']
295         exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
296         for filename in listdir:
297             for regexp in exclude_regexps:
298                 if regexp.match(filename):
299                     break
300             else:
301                 yield filename
302
303     longdesc = """
304     Add a versioned backup of the local FROM directory to a timestamped
305     subdirectory of the TO/Archives directory on the grid, sharing as many
306     files and directories as possible with earlier backups. Create TO/Latest
307     as a reference to the latest backup. Behaves somewhat like 'rsync -a
308     --link-dest=TO/Archives/(previous) FROM TO/Archives/(new); ln -sf
309     TO/Archives/(new) TO/Latest'."""
310
311 class WebopenOptions(VDriveOptions):
312     def parseArgs(self, where=''):
313         self.where = where
314
315     def getSynopsis(self):
316         return "%s webopen [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
317
318     longdesc = """Open a web browser to the contents of some file or
319     directory on the grid."""
320
321 class ManifestOptions(VDriveOptions):
322     optFlags = [
323         ("storage-index", "s", "Only print storage index strings, not pathname+cap"),
324         ("verify-cap", None, "Only print verifycap, not pathname+cap"),
325         ("repair-cap", None, "Only print repaircap, not pathname+cap"),
326         ("raw", "r", "Display raw JSON data instead of parsed"),
327         ]
328     def parseArgs(self, where=''):
329         self.where = where
330
331     def getSynopsis(self):
332         return "%s manifest [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
333
334     longdesc = """Print a list of all files and directories reachable from
335     the given starting point."""
336
337 class StatsOptions(VDriveOptions):
338     optFlags = [
339         ("raw", "r", "Display raw JSON data instead of parsed"),
340         ]
341     def parseArgs(self, where=''):
342         self.where = where
343
344     def getSynopsis(self):
345         return "%s stats [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
346
347     longdesc = """Print statistics about of all files and directories
348     reachable from the given starting point."""
349
350 class CheckOptions(VDriveOptions):
351     optFlags = [
352         ("raw", None, "Display raw JSON data instead of parsed"),
353         ("verify", None, "Verify all hashes, instead of merely querying share presence"),
354         ("repair", None, "Automatically repair any problems found"),
355         ("add-lease", None, "Add/renew lease on all shares"),
356         ]
357     def parseArgs(self, where=''):
358         self.where = where
359
360     def getSynopsis(self):
361         return "%s check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
362
363     longdesc = """
364     Check a single file or directory: count how many shares are available and
365     verify their hashes. Optionally repair the file if any problems were
366     found."""
367
368 class DeepCheckOptions(VDriveOptions):
369     optFlags = [
370         ("raw", None, "Display raw JSON data instead of parsed"),
371         ("verify", None, "Verify all hashes, instead of merely querying share presence"),
372         ("repair", None, "Automatically repair any problems found"),
373         ("add-lease", None, "Add/renew lease on all shares"),
374         ("verbose", "v", "Be noisy about what is happening."),
375         ]
376     def parseArgs(self, where=''):
377         self.where = where
378
379     def getSynopsis(self):
380         return "%s deep-check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
381
382     longdesc = """
383     Check all files and directories reachable from the given starting point
384     (which must be a directory), like 'tahoe check' but for multiple files.
385     Optionally repair any problems found."""
386
387 subCommands = [
388     ["mkdir", None, MakeDirectoryOptions, "Create a new directory"],
389     ["add-alias", None, AddAliasOptions, "Add a new alias cap"],
390     ["create-alias", None, CreateAliasOptions, "Create a new alias cap"],
391     ["list-aliases", None, ListAliasOptions, "List all alias caps"],
392     ["ls", None, ListOptions, "List a directory"],
393     ["get", None, GetOptions, "Retrieve a file from the grid."],
394     ["put", None, PutOptions, "Upload a file into the grid."],
395     ["cp", None, CpOptions, "Copy one or more files."],
396     ["rm", None, RmOptions, "Unlink a file or directory on the grid."],
397     ["mv", None, MvOptions, "Move a file within the grid."],
398     ["ln", None, LnOptions, "Make an additional link to an existing file."],
399     ["backup", None, BackupOptions, "Make target dir look like local dir."],
400     ["webopen", None, WebopenOptions, "Open a web browser to a grid file or directory."],
401     ["manifest", None, ManifestOptions, "List all files/directories in a subtree"],
402     ["stats", None, StatsOptions, "Print statistics about all files/directories in a subtree"],
403     ["check", None, CheckOptions, "Check a single file or directory"],
404     ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point"],
405     ]
406
407 def mkdir(options):
408     from allmydata.scripts import tahoe_mkdir
409     rc = tahoe_mkdir.mkdir(options)
410     return rc
411
412 def add_alias(options):
413     from allmydata.scripts import tahoe_add_alias
414     rc = tahoe_add_alias.add_alias(options)
415     return rc
416
417 def create_alias(options):
418     from allmydata.scripts import tahoe_add_alias
419     rc = tahoe_add_alias.create_alias(options)
420     return rc
421
422 def list_aliases(options):
423     from allmydata.scripts import tahoe_add_alias
424     rc = tahoe_add_alias.list_aliases(options)
425     return rc
426
427 def list(options):
428     from allmydata.scripts import tahoe_ls
429     rc = tahoe_ls.list(options)
430     return rc
431
432 def get(options):
433     from allmydata.scripts import tahoe_get
434     rc = tahoe_get.get(options)
435     if rc == 0:
436         if options.to_file is None:
437             # be quiet, since the file being written to stdout should be
438             # proof enough that it worked, unless the user is unlucky
439             # enough to have picked an empty file
440             pass
441         else:
442             print >>options.stderr, "%s retrieved and written to %s" % \
443                   (options.from_file, options.to_file)
444     return rc
445
446 def put(options):
447     from allmydata.scripts import tahoe_put
448     rc = tahoe_put.put(options)
449     return rc
450
451 def cp(options):
452     from allmydata.scripts import tahoe_cp
453     rc = tahoe_cp.copy(options)
454     return rc
455
456 def rm(options):
457     from allmydata.scripts import tahoe_rm
458     rc = tahoe_rm.rm(options)
459     return rc
460
461 def mv(options):
462     from allmydata.scripts import tahoe_mv
463     rc = tahoe_mv.mv(options, mode="move")
464     return rc
465
466 def ln(options):
467     from allmydata.scripts import tahoe_mv
468     rc = tahoe_mv.mv(options, mode="link")
469     return rc
470
471 def backup(options):
472     from allmydata.scripts import tahoe_backup
473     rc = tahoe_backup.backup(options)
474     return rc
475
476 def webopen(options, opener=None):
477     from allmydata.scripts import tahoe_webopen
478     rc = tahoe_webopen.webopen(options, opener=opener)
479     return rc
480
481 def manifest(options):
482     from allmydata.scripts import tahoe_manifest
483     rc = tahoe_manifest.manifest(options)
484     return rc
485
486 def stats(options):
487     from allmydata.scripts import tahoe_manifest
488     rc = tahoe_manifest.stats(options)
489     return rc
490
491 def check(options):
492     from allmydata.scripts import tahoe_check
493     rc = tahoe_check.check(options)
494     return rc
495
496 def deepcheck(options):
497     from allmydata.scripts import tahoe_check
498     rc = tahoe_check.deepcheck(options)
499     return rc
500
501 dispatch = {
502     "mkdir": mkdir,
503     "add-alias": add_alias,
504     "create-alias": create_alias,
505     "list-aliases": list_aliases,
506     "ls": list,
507     "get": get,
508     "put": put,
509     "cp": cp,
510     "rm": rm,
511     "mv": mv,
512     "ln": ln,
513     "backup": backup,
514     "webopen": webopen,
515     "manifest": manifest,
516     "stats": stats,
517     "check": check,
518     "deep-check": deepcheck,
519     }