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