]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/cli.py
one last tweak
[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 directory recursively
196
197     You can also use a dircap as either FROM or TO target:
198
199     tahoe cp URI:DIR2-RO:j74uhg25nwdpjpacl6rkat2yhm:kav7ijeft5h7r7rxdp5bgtlt3viv32yabqajkrdykozia5544jqa/wiki.html ./   # copy Zooko's wiki page to a local file
200
201     This command still has some limitations: symlinks, special files (device
202     nodes, named pipes), and non-ASCII filenames are not handled very well.
203     Arguments should probably not have trailing slashes. 'tahoe cp' does not
204     behave as much like /bin/cp as you would wish, especially with respect to
205     trailing slashes.
206     """
207
208 class RmOptions(VDriveOptions):
209     def parseArgs(self, where):
210         self.where = where
211
212     def getSynopsis(self):
213         return "%s rm VDRIVE_FILE" % (os.path.basename(sys.argv[0]),)
214
215 class MvOptions(VDriveOptions):
216     def parseArgs(self, frompath, topath):
217         self.from_file = frompath
218         self.to_file = topath
219
220     def getSynopsis(self):
221         return "%s mv FROM TO" % (os.path.basename(sys.argv[0]),)
222
223 class LnOptions(VDriveOptions):
224     def parseArgs(self, frompath, topath):
225         self.from_file = frompath
226         self.to_file = topath
227
228     def getSynopsis(self):
229         return "%s ln FROM TO" % (os.path.basename(sys.argv[0]),)
230
231 class BackupConfigurationError(Exception):
232     pass
233
234 class BackupOptions(VDriveOptions):
235     optFlags = [
236         ("verbose", "v", "Be noisy about what is happening."),
237         ("ignore-timestamps", None, "Do not use backupdb timestamps to decide if a local file is unchanged."),
238         ]
239
240     vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore', '.svn',
241                    '.arch-ids','{arch}', '=RELEASE-ID', '=meta-update', '=update',
242                    '.bzr', '.bzrignore', '.bzrtags', '.hg', '.hgignore', '_darcs')
243
244     def __init__(self):
245         super(BackupOptions, self).__init__()
246         self['exclude'] = set()
247
248     def parseArgs(self, localdir, topath):
249         self.from_dir = localdir
250         self.to_dir = topath
251
252     def getSynopsis(Self):
253         return "%s backup FROM ALIAS:TO" % os.path.basename(sys.argv[0])
254
255     def opt_exclude(self, pattern):
256         """Ignore files matching a glob pattern. You may give multiple
257         '--exclude' options."""
258         g = pattern.strip()
259         if g:
260             exclude = self['exclude']
261             exclude.add(g)
262
263     def opt_exclude_from(self, filepath):
264         """Ignore file matching glob patterns listed in file, one per
265         line."""
266         try:
267             exclude_file = file(filepath)
268         except:
269             raise BackupConfigurationError('Error opening exclude file %r.' % filepath)
270         try:
271             for line in exclude_file:
272                 self.opt_exclude(line)
273         finally:
274             exclude_file.close()
275
276     def opt_exclude_vcs(self):
277         """Exclude files and directories used by following version
278         control systems: 'CVS', 'RCS', 'SCCS', 'SVN', 'Arch',
279         'Bazaar', 'Mercurial', and 'Darcs'."""
280         for pattern in self.vcs_patterns:
281             self.opt_exclude(pattern)
282
283     def filter_listdir(self, listdir):
284         """Yields non-excluded childpaths in path."""
285         exclude = self['exclude']
286         exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
287         for filename in listdir:
288             for regexp in exclude_regexps:
289                 if regexp.match(filename):
290                     break
291             else:
292                 yield filename
293
294     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'."""
295
296 class WebopenOptions(VDriveOptions):
297     def parseArgs(self, where=''):
298         self.where = where
299
300     def getSynopsis(self):
301         return "%s webopen [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
302
303     longdesc = """Opens a webbrowser to the contents of some portion of the virtual drive."""
304
305 class ManifestOptions(VDriveOptions):
306     optFlags = [
307         ("storage-index", "s", "Only print storage index strings, not pathname+cap"),
308         ("verify-cap", None, "Only print verifycap, not pathname+cap"),
309         ("repair-cap", None, "Only print repaircap, not pathname+cap"),
310         ("raw", "r", "Display raw JSON data instead of parsed"),
311         ]
312     def parseArgs(self, where=''):
313         self.where = where
314
315     def getSynopsis(self):
316         return "%s manifest [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
317
318     longdesc = """Print a list of all files/directories reachable from the given starting point."""
319
320 class StatsOptions(VDriveOptions):
321     optFlags = [
322         ("raw", "r", "Display raw JSON data instead of parsed"),
323         ]
324     def parseArgs(self, where=''):
325         self.where = where
326
327     def getSynopsis(self):
328         return "%s stats [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
329
330     longdesc = """Print statistics about of all files/directories reachable from the given starting point."""
331
332 class CheckOptions(VDriveOptions):
333     optFlags = [
334         ("raw", None, "Display raw JSON data instead of parsed"),
335         ("verify", None, "Verify all hashes, instead of merely querying share presence"),
336         ("repair", None, "Automatically repair any problems found"),
337         ("add-lease", None, "Add/renew lease on all shares"),
338         ]
339     def parseArgs(self, where=''):
340         self.where = where
341
342     def getSynopsis(self):
343         return "%s check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
344
345     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."""
346
347 class DeepCheckOptions(VDriveOptions):
348     optFlags = [
349         ("raw", None, "Display raw JSON data instead of parsed"),
350         ("verify", None, "Verify all hashes, instead of merely querying share presence"),
351         ("repair", None, "Automatically repair any problems found"),
352         ("add-lease", None, "Add/renew lease on all shares"),
353         ("verbose", "v", "Be noisy about what is happening."),
354         ]
355     def parseArgs(self, where=''):
356         self.where = where
357
358     def getSynopsis(self):
359         return "%s deep-check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
360
361     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."""
362
363 subCommands = [
364     ["mkdir", None, MakeDirectoryOptions, "Create a new directory"],
365     ["add-alias", None, AddAliasOptions, "Add a new alias cap"],
366     ["create-alias", None, CreateAliasOptions, "Create a new alias cap"],
367     ["list-aliases", None, ListAliasOptions, "List all alias caps"],
368     ["ls", None, ListOptions, "List a directory"],
369     ["get", None, GetOptions, "Retrieve a file from the virtual drive."],
370     ["put", None, PutOptions, "Upload a file into the virtual drive."],
371     ["cp", None, CpOptions, "Copy one or more files."],
372     ["rm", None, RmOptions, "Unlink a file or directory in the virtual drive."],
373     ["mv", None, MvOptions, "Move a file within the virtual drive."],
374     ["ln", None, LnOptions, "Make an additional link to an existing file."],
375     ["backup", None, BackupOptions, "Make target dir look like local dir."],
376     ["webopen", None, WebopenOptions, "Open a webbrowser to the root_dir"],
377     ["manifest", None, ManifestOptions, "List all files/dirs in a subtree"],
378     ["stats", None, StatsOptions, "Print statistics about all files/dirs in a subtree"],
379     ["check", None, CheckOptions, "Check a single file or directory"],
380     ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point"],
381     ]
382
383 def mkdir(options):
384     from allmydata.scripts import tahoe_mkdir
385     rc = tahoe_mkdir.mkdir(options)
386     return rc
387
388 def add_alias(options):
389     from allmydata.scripts import tahoe_add_alias
390     rc = tahoe_add_alias.add_alias(options)
391     return rc
392
393 def create_alias(options):
394     from allmydata.scripts import tahoe_add_alias
395     rc = tahoe_add_alias.create_alias(options)
396     return rc
397
398 def list_aliases(options):
399     from allmydata.scripts import tahoe_add_alias
400     rc = tahoe_add_alias.list_aliases(options)
401     return rc
402
403 def list(options):
404     from allmydata.scripts import tahoe_ls
405     rc = tahoe_ls.list(options)
406     return rc
407
408 def get(options):
409     from allmydata.scripts import tahoe_get
410     rc = tahoe_get.get(options)
411     if rc == 0:
412         if options.to_file is None:
413             # be quiet, since the file being written to stdout should be
414             # proof enough that it worked, unless the user is unlucky
415             # enough to have picked an empty file
416             pass
417         else:
418             print >>options.stderr, "%s retrieved and written to %s" % \
419                   (options.from_file, options.to_file)
420     return rc
421
422 def put(options):
423     from allmydata.scripts import tahoe_put
424     rc = tahoe_put.put(options)
425     return rc
426
427 def cp(options):
428     from allmydata.scripts import tahoe_cp
429     rc = tahoe_cp.copy(options)
430     return rc
431
432 def rm(options):
433     from allmydata.scripts import tahoe_rm
434     rc = tahoe_rm.rm(options)
435     return rc
436
437 def mv(options):
438     from allmydata.scripts import tahoe_mv
439     rc = tahoe_mv.mv(options, mode="move")
440     return rc
441
442 def ln(options):
443     from allmydata.scripts import tahoe_mv
444     rc = tahoe_mv.mv(options, mode="link")
445     return rc
446
447 def backup(options):
448     from allmydata.scripts import tahoe_backup
449     rc = tahoe_backup.backup(options)
450     return rc
451
452 def webopen(options, opener=None):
453     from allmydata.scripts import tahoe_webopen
454     rc = tahoe_webopen.webopen(options, opener=opener)
455     return rc
456
457 def manifest(options):
458     from allmydata.scripts import tahoe_manifest
459     rc = tahoe_manifest.manifest(options)
460     return rc
461
462 def stats(options):
463     from allmydata.scripts import tahoe_manifest
464     rc = tahoe_manifest.stats(options)
465     return rc
466
467 def check(options):
468     from allmydata.scripts import tahoe_check
469     rc = tahoe_check.check(options)
470     return rc
471
472 def deepcheck(options):
473     from allmydata.scripts import tahoe_check
474     rc = tahoe_check.deepcheck(options)
475     return rc
476
477 dispatch = {
478     "mkdir": mkdir,
479     "add-alias": add_alias,
480     "create-alias": create_alias,
481     "list-aliases": list_aliases,
482     "ls": list,
483     "get": get,
484     "put": put,
485     "cp": cp,
486     "rm": rm,
487     "mv": mv,
488     "ln": ln,
489     "backup": backup,
490     "webopen": webopen,
491     "manifest": manifest,
492     "stats": stats,
493     "check": check,
494     "deep-check": deepcheck,
495     }