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