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