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