1 import os.path, re, sys, fnmatch
2 from twisted.python import usage
3 from allmydata.scripts.common import BaseOptions, get_aliases
5 NODEURL_RE=re.compile("http://([^:]*)(:([1-9][0-9]*))?")
7 class VDriveOptions(BaseOptions, usage.Options):
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."
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."]
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()
30 self['node-directory'] = os.path.expanduser(self['node-directory'])
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" %
37 raise usage.UsageError(msg)
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'] += "/"
44 aliases = get_aliases(self['node-directory'])
46 aliases["tahoe"] = self['dir-cap']
47 self.aliases = aliases # maps alias name to dircap
50 class MakeDirectoryOptions(VDriveOptions):
51 def parseArgs(self, where=""):
53 longdesc = """Create a new directory, either unlinked or as a subdirectory."""
55 class AddAliasOptions(VDriveOptions):
56 def parseArgs(self, alias, cap):
60 def getSynopsis(self):
61 return "%s add-alias ALIAS DIRCAP" % (os.path.basename(sys.argv[0]),)
63 longdesc = """Add a new alias for an existing directory."""
65 class CreateAliasOptions(VDriveOptions):
66 def parseArgs(self, alias):
69 def getSynopsis(self):
70 return "%s create-alias ALIAS" % (os.path.basename(sys.argv[0]),)
72 longdesc = """Create a new directory and add an alias for it."""
74 class ListAliasOptions(VDriveOptions):
75 longdesc = """Display a table of all configured aliases."""
77 class ListOptions(VDriveOptions):
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"),
85 def parseArgs(self, where=""):
88 longdesc = """List the contents of some portion of the grid."""
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
99 if self.to_file == "-":
102 def getSynopsis(self):
103 return "%s get REMOTE_FILE LOCAL_FILE" % (os.path.basename(sys.argv[0]),)
106 Retrieve a file from the grid and write it to the local filesystem. If
107 LOCAL_FILE is omitted or '-', the contents of the file will be written to
110 def getUsage(self, width=None):
111 t = VDriveOptions.getUsage(self, width)
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
121 class PutOptions(VDriveOptions):
123 ("mutable", "m", "Create a mutable file instead of an immutable one."),
126 def parseArgs(self, arg1=None, arg2=None):
129 if arg1 is not None and arg2 is not None:
130 self.from_file = arg1
132 elif arg1 is not None and arg2 is None:
133 self.from_file = arg1 # might be "-"
136 self.from_file = None
138 if self.from_file == "-":
139 self.from_file = None
141 def getSynopsis(self):
142 return "%s put LOCAL_FILE REMOTE_FILE" % (os.path.basename(sys.argv[0]),)
145 Put a file into the grid, copying its contents from the local filesystem.
146 If REMOTE_FILE is missing, upload the file but do not link it into a
147 directory; also print the new filecap to stdout. If LOCAL_FILE is missing
148 or '-', data will be copied from stdin. REMOTE_FILE is assumed to start
149 with tahoe: unless otherwise specified."""
151 def getUsage(self, width=None):
152 t = VDriveOptions.getUsage(self, width)
155 % cat FILE | tahoe put # create unlinked file from stdin
156 % cat FILE | tahoe - # same
157 % tahoe put bar # create unlinked file from local 'bar'
158 % cat FILE | tahoe put - FOO # create tahoe:FOO from stdin
159 % tahoe put bar FOO # copy local 'bar' to tahoe:FOO
160 % tahoe put bar tahoe:FOO # same
161 % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
165 class CpOptions(VDriveOptions):
167 ("recursive", "r", "Copy source directory recursively."),
168 ("verbose", "v", "Be noisy about what is happening."),
170 "When copying to local files, write out filecaps instead of actual "
171 "data (only useful for debugging and tree-comparison purposes)."),
173 def parseArgs(self, *args):
175 raise usage.UsageError("cp requires at least two arguments")
176 self.sources = args[:-1]
177 self.destination = args[-1]
178 def getSynopsis(self):
179 return "Usage: tahoe [options] cp FROM.. TO"
181 Use 'tahoe cp' to copy files between a local filesystem and a Tahoe grid.
182 Any FROM/TO arguments that begin with an alias indicate Tahoe-side
183 files or non-file arguments. Directories will be copied recursively.
184 New Tahoe-side directories will be created when necessary. Assuming that
185 you have previously set up an alias 'home' with 'tahoe create-alias home',
186 here are some examples:
188 tahoe cp ~/foo.txt home: # creates tahoe-side home:foo.txt
190 tahoe cp ~/foo.txt /tmp/bar.txt home: # copies two files to home:
192 tahoe cp ~/Pictures home:stuff/my-pictures # copies directory recursively
194 You can also use a dircap as either FROM or TO target:
196 tahoe cp URI:DIR2-RO:j74uhg25nwdpjpacl6rkat2yhm:kav7ijeft5h7r7rxdp5bgtlt3viv32yabqajkrdykozia5544jqa/wiki.html ./ # copy Zooko's wiki page to a local file
198 This command still has some limitations: symlinks, special files (device
199 nodes, named pipes), and non-ASCII filenames are not handled very well.
200 Arguments should probably not have trailing slashes. 'tahoe cp' does not
201 behave as much like /bin/cp as you would wish, especially with respect to
205 class RmOptions(VDriveOptions):
206 def parseArgs(self, where):
209 def getSynopsis(self):
210 return "%s rm REMOTE_FILE" % (os.path.basename(sys.argv[0]),)
212 class MvOptions(VDriveOptions):
213 def parseArgs(self, frompath, topath):
214 self.from_file = frompath
215 self.to_file = topath
217 def getSynopsis(self):
218 return "%s mv FROM TO" % (os.path.basename(sys.argv[0]),)
220 Use 'tahoe mv' to move files that are already on the grid elsewhere on
221 the grid, e.g., 'tahoe mv alias:some_file alias:new_file'.
223 If moving a remote file into a remote directory, you'll need to append a
224 '/' to the name of the remote directory, e.g., 'tahoe mv tahoe:file1
225 tahoe:dir/', not 'tahoe mv tahoe:file1 tahoe:dir'.
227 Note that it is not possible to use this command to move local files to
228 the grid -- use 'tahoe cp' for that.
231 class LnOptions(VDriveOptions):
232 def parseArgs(self, frompath, topath):
233 self.from_file = frompath
234 self.to_file = topath
236 def getSynopsis(self):
237 return "%s ln FROM TO" % (os.path.basename(sys.argv[0]),)
239 class BackupConfigurationError(Exception):
242 class BackupOptions(VDriveOptions):
244 ("verbose", "v", "Be noisy about what is happening."),
245 ("ignore-timestamps", None, "Do not use backupdb timestamps to decide whether a local file is unchanged."),
248 vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore',
249 '.svn', '.arch-ids','{arch}', '=RELEASE-ID',
250 '=meta-update', '=update', '.bzr', '.bzrignore',
251 '.bzrtags', '.hg', '.hgignore', '_darcs')
254 super(BackupOptions, self).__init__()
255 self['exclude'] = set()
257 def parseArgs(self, localdir, topath):
258 self.from_dir = localdir
261 def getSynopsis(Self):
262 return "%s backup FROM ALIAS:TO" % os.path.basename(sys.argv[0])
264 def opt_exclude(self, pattern):
265 """Ignore files matching a glob pattern. You may give multiple
266 '--exclude' options."""
269 exclude = self['exclude']
272 def opt_exclude_from(self, filepath):
273 """Ignore file matching glob patterns listed in file, one per
276 exclude_file = file(filepath)
278 raise BackupConfigurationError('Error opening exclude file %r.' % filepath)
280 for line in exclude_file:
281 self.opt_exclude(line)
285 def opt_exclude_vcs(self):
286 """Exclude files and directories used by following version
287 control systems: 'CVS', 'RCS', 'SCCS', 'SVN', 'Arch',
288 'Bazaar', 'Mercurial', and 'Darcs'."""
289 for pattern in self.vcs_patterns:
290 self.opt_exclude(pattern)
292 def filter_listdir(self, listdir):
293 """Yields non-excluded childpaths in path."""
294 exclude = self['exclude']
295 exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
296 for filename in listdir:
297 for regexp in exclude_regexps:
298 if regexp.match(filename):
304 Add a versioned backup of the local FROM directory to a timestamped
305 subdirectory of the TO/Archives directory on the grid, sharing as many
306 files and directories as possible with earlier backups. Create TO/Latest
307 as a reference to the latest backup. Behaves somewhat like 'rsync -a
308 --link-dest=TO/Archives/(previous) FROM TO/Archives/(new); ln -sf
309 TO/Archives/(new) TO/Latest'."""
311 class WebopenOptions(VDriveOptions):
312 def parseArgs(self, where=''):
315 def getSynopsis(self):
316 return "%s webopen [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
318 longdesc = """Open a web browser to the contents of some file or
319 directory on the grid."""
321 class ManifestOptions(VDriveOptions):
323 ("storage-index", "s", "Only print storage index strings, not pathname+cap"),
324 ("verify-cap", None, "Only print verifycap, not pathname+cap"),
325 ("repair-cap", None, "Only print repaircap, not pathname+cap"),
326 ("raw", "r", "Display raw JSON data instead of parsed"),
328 def parseArgs(self, where=''):
331 def getSynopsis(self):
332 return "%s manifest [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
334 longdesc = """Print a list of all files and directories reachable from
335 the given starting point."""
337 class StatsOptions(VDriveOptions):
339 ("raw", "r", "Display raw JSON data instead of parsed"),
341 def parseArgs(self, where=''):
344 def getSynopsis(self):
345 return "%s stats [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
347 longdesc = """Print statistics about of all files and directories
348 reachable from the given starting point."""
350 class CheckOptions(VDriveOptions):
352 ("raw", None, "Display raw JSON data instead of parsed"),
353 ("verify", None, "Verify all hashes, instead of merely querying share presence"),
354 ("repair", None, "Automatically repair any problems found"),
355 ("add-lease", None, "Add/renew lease on all shares"),
357 def parseArgs(self, where=''):
360 def getSynopsis(self):
361 return "%s check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
364 Check a single file or directory: count how many shares are available and
365 verify their hashes. Optionally repair the file if any problems were
368 class DeepCheckOptions(VDriveOptions):
370 ("raw", None, "Display raw JSON data instead of parsed"),
371 ("verify", None, "Verify all hashes, instead of merely querying share presence"),
372 ("repair", None, "Automatically repair any problems found"),
373 ("add-lease", None, "Add/renew lease on all shares"),
374 ("verbose", "v", "Be noisy about what is happening."),
376 def parseArgs(self, where=''):
379 def getSynopsis(self):
380 return "%s deep-check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
383 Check all files and directories reachable from the given starting point
384 (which must be a directory), like 'tahoe check' but for multiple files.
385 Optionally repair any problems found."""
388 ["mkdir", None, MakeDirectoryOptions, "Create a new directory"],
389 ["add-alias", None, AddAliasOptions, "Add a new alias cap"],
390 ["create-alias", None, CreateAliasOptions, "Create a new alias cap"],
391 ["list-aliases", None, ListAliasOptions, "List all alias caps"],
392 ["ls", None, ListOptions, "List a directory"],
393 ["get", None, GetOptions, "Retrieve a file from the grid."],
394 ["put", None, PutOptions, "Upload a file into the grid."],
395 ["cp", None, CpOptions, "Copy one or more files."],
396 ["rm", None, RmOptions, "Unlink a file or directory on the grid."],
397 ["mv", None, MvOptions, "Move a file within the grid."],
398 ["ln", None, LnOptions, "Make an additional link to an existing file."],
399 ["backup", None, BackupOptions, "Make target dir look like local dir."],
400 ["webopen", None, WebopenOptions, "Open a web browser to a grid file or directory."],
401 ["manifest", None, ManifestOptions, "List all files/directories in a subtree"],
402 ["stats", None, StatsOptions, "Print statistics about all files/directories in a subtree"],
403 ["check", None, CheckOptions, "Check a single file or directory"],
404 ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point"],
408 from allmydata.scripts import tahoe_mkdir
409 rc = tahoe_mkdir.mkdir(options)
412 def add_alias(options):
413 from allmydata.scripts import tahoe_add_alias
414 rc = tahoe_add_alias.add_alias(options)
417 def create_alias(options):
418 from allmydata.scripts import tahoe_add_alias
419 rc = tahoe_add_alias.create_alias(options)
422 def list_aliases(options):
423 from allmydata.scripts import tahoe_add_alias
424 rc = tahoe_add_alias.list_aliases(options)
428 from allmydata.scripts import tahoe_ls
429 rc = tahoe_ls.list(options)
433 from allmydata.scripts import tahoe_get
434 rc = tahoe_get.get(options)
436 if options.to_file is None:
437 # be quiet, since the file being written to stdout should be
438 # proof enough that it worked, unless the user is unlucky
439 # enough to have picked an empty file
442 print >>options.stderr, "%s retrieved and written to %s" % \
443 (options.from_file, options.to_file)
447 from allmydata.scripts import tahoe_put
448 rc = tahoe_put.put(options)
452 from allmydata.scripts import tahoe_cp
453 rc = tahoe_cp.copy(options)
457 from allmydata.scripts import tahoe_rm
458 rc = tahoe_rm.rm(options)
462 from allmydata.scripts import tahoe_mv
463 rc = tahoe_mv.mv(options, mode="move")
467 from allmydata.scripts import tahoe_mv
468 rc = tahoe_mv.mv(options, mode="link")
472 from allmydata.scripts import tahoe_backup
473 rc = tahoe_backup.backup(options)
476 def webopen(options, opener=None):
477 from allmydata.scripts import tahoe_webopen
478 rc = tahoe_webopen.webopen(options, opener=opener)
481 def manifest(options):
482 from allmydata.scripts import tahoe_manifest
483 rc = tahoe_manifest.manifest(options)
487 from allmydata.scripts import tahoe_manifest
488 rc = tahoe_manifest.stats(options)
492 from allmydata.scripts import tahoe_check
493 rc = tahoe_check.check(options)
496 def deepcheck(options):
497 from allmydata.scripts import tahoe_check
498 rc = tahoe_check.deepcheck(options)
503 "add-alias": add_alias,
504 "create-alias": create_alias,
505 "list-aliases": list_aliases,
515 "manifest": manifest,
518 "deep-check": deepcheck,