]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/cli.py
Improve docs on 'cp -r', noting the recent 2329 changes
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / cli.py
1 import os.path, re, fnmatch
2 from twisted.python import usage
3 from allmydata.scripts.common import get_aliases, get_default_nodedir, \
4      DEFAULT_ALIAS, BaseOptions
5 from allmydata.util.encodingutil import argv_to_unicode, argv_to_abspath, quote_local_unicode_path
6
7 NODEURL_RE=re.compile("http(s?)://([^:]*)(:([1-9][0-9]*))?")
8
9 _default_nodedir = get_default_nodedir()
10
11 class FilesystemOptions(BaseOptions):
12     optParameters = [
13         ["node-url", "u", None,
14          "Specify the URL of the Tahoe gateway node, such as "
15          "'http://127.0.0.1:3456'. "
16          "This overrides the URL found in the --node-directory ."],
17         ["dir-cap", None, None,
18          "Specify which dirnode URI should be used as the 'tahoe' alias."]
19         ]
20
21     def postOptions(self):
22         self["quiet"] = self.parent["quiet"]
23         if self.parent['node-directory']:
24             self['node-directory'] = argv_to_abspath(self.parent['node-directory'])
25         else:
26             self['node-directory'] = _default_nodedir
27
28         # compute a node-url from the existing options, put in self['node-url']
29         if self['node-url']:
30             if (not isinstance(self['node-url'], basestring)
31                 or not NODEURL_RE.match(self['node-url'])):
32                 msg = ("--node-url is required to be a string and look like "
33                        "\"http://HOSTNAMEORADDR:PORT\", not: %r" %
34                        (self['node-url'],))
35                 raise usage.UsageError(msg)
36         else:
37             node_url_file = os.path.join(self['node-directory'], "node.url")
38             self['node-url'] = open(node_url_file, "r").read().strip()
39         if self['node-url'][-1] != "/":
40             self['node-url'] += "/"
41
42         aliases = get_aliases(self['node-directory'])
43         if self['dir-cap']:
44             aliases[DEFAULT_ALIAS] = self['dir-cap']
45         self.aliases = aliases # maps alias name to dircap
46
47
48 class MakeDirectoryOptions(FilesystemOptions):
49     optParameters = [
50         ("format", None, None, "Create a directory with the given format: SDMF or MDMF (case-insensitive)"),
51         ]
52
53     def parseArgs(self, where=""):
54         self.where = argv_to_unicode(where)
55
56         if self['format']:
57             if self['format'].upper() not in ("SDMF", "MDMF"):
58                 raise usage.UsageError("%s is an invalid format" % self['format'])
59
60     def getSynopsis(self):
61         return "Usage:  %s [global-opts] mkdir [options] [REMOTE_DIR]" % (self.command_name,)
62
63     longdesc = """Create a new directory, either unlinked or as a subdirectory."""
64
65 class AddAliasOptions(FilesystemOptions):
66     def parseArgs(self, alias, cap):
67         self.alias = argv_to_unicode(alias)
68         if self.alias.endswith(u':'):
69             self.alias = self.alias[:-1]
70         self.cap = cap
71
72     def getSynopsis(self):
73         return "Usage:  %s [global-opts] add-alias [options] ALIAS[:] DIRCAP" % (self.command_name,)
74
75     longdesc = """Add a new alias for an existing directory."""
76
77 class CreateAliasOptions(FilesystemOptions):
78     def parseArgs(self, alias):
79         self.alias = argv_to_unicode(alias)
80         if self.alias.endswith(u':'):
81             self.alias = self.alias[:-1]
82
83     def getSynopsis(self):
84         return "Usage:  %s [global-opts] create-alias [options] ALIAS[:]" % (self.command_name,)
85
86     longdesc = """Create a new directory and add an alias for it."""
87
88 class ListAliasesOptions(FilesystemOptions):
89     def getSynopsis(self):
90         return "Usage:  %s [global-opts] list-aliases [options]" % (self.command_name,)
91
92     longdesc = """Display a table of all configured aliases."""
93
94 class ListOptions(FilesystemOptions):
95     optFlags = [
96         ("long", "l", "Use long format: show file sizes, and timestamps."),
97         ("uri", "u", "Show file/directory URIs."),
98         ("readonly-uri", None, "Show read-only file/directory URIs."),
99         ("classify", "F", "Append '/' to directory names, and '*' to mutable."),
100         ("json", None, "Show the raw JSON output."),
101         ]
102     def parseArgs(self, where=""):
103         self.where = argv_to_unicode(where)
104
105     def getSynopsis(self):
106         return "Usage:  %s [global-opts] ls [options] [PATH]" % (self.command_name,)
107
108     longdesc = """
109     List the contents of some portion of the grid.
110
111     If PATH is omitted, "tahoe:" is assumed.
112
113     When the -l or --long option is used, each line is shown in the
114     following format:
115
116     drwx <size> <date/time> <name in this directory>
117
118     where each of the letters on the left may be replaced by '-'.
119     If 'd' is present, it indicates that the object is a directory.
120     If the 'd' is replaced by a '?', the object type is unknown.
121     'rwx' is a Unix-like permissions mask: if the mask includes 'w',
122     then the object is writeable through its link in this directory
123     (note that the link might be replaceable even if the object is
124     not writeable through the current link).
125     The 'x' is a legacy of Unix filesystems. In Tahoe it is used
126     only to indicate that the contents of a directory can be listed.
127
128     Directories have no size, so their size field is shown as '-'.
129     Otherwise the size of the file, when known, is given in bytes.
130     The size of mutable files or unknown objects is shown as '?'.
131
132     The date/time shows when this link in the Tahoe filesystem was
133     last modified.
134     """
135
136 class GetOptions(FilesystemOptions):
137     def parseArgs(self, arg1, arg2=None):
138         # tahoe get FOO |less            # write to stdout
139         # tahoe get tahoe:FOO |less      # same
140         # tahoe get FOO bar              # write to local file
141         # tahoe get tahoe:FOO bar        # same
142
143         if arg2 == "-":
144             arg2 = None
145
146         self.from_file = argv_to_unicode(arg1)
147         self.to_file   = None if arg2 is None else argv_to_abspath(arg2)
148
149     def getSynopsis(self):
150         return "Usage:  %s [global-opts] get [options] REMOTE_FILE LOCAL_FILE" % (self.command_name,)
151
152     longdesc = """
153     Retrieve a file from the grid and write it to the local filesystem. If
154     LOCAL_FILE is omitted or '-', the contents of the file will be written to
155     stdout."""
156
157     def getUsage(self, width=None):
158         t = FilesystemOptions.getUsage(self, width)
159         t += """
160 Examples:
161  % tahoe get FOO |less            # write to stdout
162  % tahoe get tahoe:FOO |less      # same
163  % tahoe get FOO bar              # write to local file
164  % tahoe get tahoe:FOO bar        # same
165 """
166         return t
167
168 class PutOptions(FilesystemOptions):
169     optFlags = [
170         ("mutable", "m", "Create a mutable file instead of an immutable one (like --format=SDMF)"),
171         ]
172     optParameters = [
173         ("format", None, None, "Create a file with the given format: SDMF and MDMF for mutable, CHK (default) for immutable. (case-insensitive)"),
174         ]
175
176     def parseArgs(self, arg1=None, arg2=None):
177         # see Examples below
178
179         if arg1 == "-":
180             arg1 = None
181
182         self.from_file = None if arg1 is None else argv_to_abspath(arg1)
183         self.to_file   = None if arg2 is None else argv_to_unicode(arg2)
184
185         if self['format']:
186             if self['format'].upper() not in ("SDMF", "MDMF", "CHK"):
187                 raise usage.UsageError("%s is an invalid format" % self['format'])
188
189     def getSynopsis(self):
190         return "Usage:  %s [global-opts] put [options] LOCAL_FILE REMOTE_FILE" % (self.command_name,)
191
192     longdesc = """
193     Put a file into the grid, copying its contents from the local filesystem.
194     If REMOTE_FILE is missing, upload the file but do not link it into a
195     directory; also print the new filecap to stdout. If LOCAL_FILE is missing
196     or '-', data will be copied from stdin. REMOTE_FILE is assumed to start
197     with tahoe: unless otherwise specified.
198
199     If the destination file already exists and is mutable, it will be modified
200     in-place, whether or not --mutable is specified. (--mutable only affects
201     creation of new files.)"""
202
203     def getUsage(self, width=None):
204         t = FilesystemOptions.getUsage(self, width)
205         t += """
206 Examples:
207  % cat FILE | tahoe put                # create unlinked file from stdin
208  % cat FILE | tahoe put -              # same
209  % tahoe put bar                       # create unlinked file from local 'bar'
210  % cat FILE | tahoe put - FOO          # create tahoe:FOO from stdin
211  % tahoe put bar FOO                   # copy local 'bar' to tahoe:FOO
212  % tahoe put bar tahoe:FOO             # same
213  % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
214 """
215         return t
216
217 class CpOptions(FilesystemOptions):
218     optFlags = [
219         ("recursive", "r", "Copy source directory recursively."),
220         ("verbose", "v", "Be noisy about what is happening."),
221         ("caps-only", None,
222          "When copying to local files, write out filecaps instead of actual "
223          "data (only useful for debugging and tree-comparison purposes)."),
224         ]
225
226     def parseArgs(self, *args):
227         if len(args) < 2:
228             raise usage.UsageError("cp requires at least two arguments")
229         self.sources = map(argv_to_unicode, args[:-1])
230         self.destination = argv_to_unicode(args[-1])
231
232     def getSynopsis(self):
233         return "Usage: %s [global-opts] cp [options] FROM.. TO" % (self.command_name,)
234
235     longdesc = """
236     Use 'tahoe cp' to copy files between a local filesystem and a Tahoe grid.
237     Any FROM/TO arguments that begin with an alias indicate Tahoe-side
238     files or non-file arguments. Directories will be copied recursively.
239     New Tahoe-side directories will be created when necessary. Assuming that
240     you have previously set up an alias 'home' with 'tahoe create-alias home',
241     here are some examples:
242
243     tahoe cp ~/foo.txt home:  # creates tahoe-side home:foo.txt
244
245     tahoe cp ~/foo.txt /tmp/bar.txt home:  # copies two files to home:
246
247     tahoe cp ~/Pictures home:stuff/my-pictures  # copies directory recursively
248
249     You can also use a dircap as either FROM or TO target:
250
251     tahoe cp URI:DIR2-RO:ixqhc4kdbjxc7o65xjnveoewym:5x6lwoxghrd5rxhwunzavft2qygfkt27oj3fbxlq4c6p45z5uneq/blog.html ./   # copy Zooko's wiki page to a local file
252
253     This command still has some limitations: symlinks and special files
254     (device nodes, named pipes) are not handled very well. Arguments should
255     not have trailing slashes (they are ignored for directory arguments, but
256     trigger errors for file arguments). When copying directories, it can be
257     unclear whether you mean to copy the contents of a source directory, or
258     the source directory itself (i.e. whether the output goes under the
259     target directory, or one directory lower). Tahoe's rule is that source
260     directories with names are referring to the directory as a whole, and
261     source directories without names (e.g. a raw dircap) are referring to the
262     contents.
263     """
264
265 class UnlinkOptions(FilesystemOptions):
266     def parseArgs(self, where):
267         self.where = argv_to_unicode(where)
268
269     def getSynopsis(self):
270         return "Usage:  %s [global-opts] unlink [options] REMOTE_FILE" % (self.command_name,)
271
272 class RmOptions(UnlinkOptions):
273     def getSynopsis(self):
274         return "Usage:  %s [global-opts] rm [options] REMOTE_FILE" % (self.command_name,)
275
276 class MvOptions(FilesystemOptions):
277     def parseArgs(self, frompath, topath):
278         self.from_file = argv_to_unicode(frompath)
279         self.to_file = argv_to_unicode(topath)
280
281     def getSynopsis(self):
282         return "Usage:  %s [global-opts] mv [options] FROM TO" % (self.command_name,)
283
284     longdesc = """
285     Use 'tahoe mv' to move files that are already on the grid elsewhere on
286     the grid, e.g., 'tahoe mv alias:some_file alias:new_file'.
287
288     If moving a remote file into a remote directory, you'll need to append a
289     '/' to the name of the remote directory, e.g., 'tahoe mv tahoe:file1
290     tahoe:dir/', not 'tahoe mv tahoe:file1 tahoe:dir'.
291
292     Note that it is not possible to use this command to move local files to
293     the grid -- use 'tahoe cp' for that.
294     """
295
296 class LnOptions(FilesystemOptions):
297     def parseArgs(self, frompath, topath):
298         self.from_file = argv_to_unicode(frompath)
299         self.to_file = argv_to_unicode(topath)
300
301     def getSynopsis(self):
302         return "Usage:  %s [global-opts] ln [options] FROM_LINK TO_LINK" % (self.command_name,)
303
304     longdesc = """
305     Use 'tahoe ln' to duplicate a link (directory entry) already on the grid
306     to elsewhere on the grid. For example 'tahoe ln alias:some_file
307     alias:new_file'. causes 'alias:new_file' to point to the same object that
308     'alias:some_file' points to.
309
310     (The argument order is the same as Unix ln. To remember the order, you
311     can think of this command as copying a link, rather than copying a file
312     as 'tahoe cp' does. Then the argument order is consistent with that of
313     'tahoe cp'.)
314
315     When linking a remote file into a remote directory, you'll need to append
316     a '/' to the name of the remote directory, e.g. 'tahoe ln tahoe:file1
317     tahoe:dir/' (which is shorthand for 'tahoe ln tahoe:file1
318     tahoe:dir/file1'). If you forget the '/', e.g. 'tahoe ln tahoe:file1
319     tahoe:dir', the 'ln' command will refuse to overwrite the 'tahoe:dir'
320     directory, and will exit with an error.
321
322     Note that it is not possible to use this command to create links between
323     local and remote files.
324     """
325
326 class BackupConfigurationError(Exception):
327     pass
328
329 class BackupOptions(FilesystemOptions):
330     optFlags = [
331         ("verbose", "v", "Be noisy about what is happening."),
332         ("ignore-timestamps", None, "Do not use backupdb timestamps to decide whether a local file is unchanged."),
333         ]
334
335     vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore',
336                     '.svn', '.arch-ids','{arch}', '=RELEASE-ID',
337                     '=meta-update', '=update', '.bzr', '.bzrignore',
338                     '.bzrtags', '.hg', '.hgignore', '_darcs')
339
340     def __init__(self):
341         super(BackupOptions, self).__init__()
342         self['exclude'] = set()
343
344     def parseArgs(self, localdir, topath):
345         self.from_dir = argv_to_abspath(localdir)
346         self.to_dir = argv_to_unicode(topath)
347
348     def getSynopsis(self):
349         return "Usage:  %s [global-opts] backup [options] FROM ALIAS:TO" % (self.command_name,)
350
351     def opt_exclude(self, pattern):
352         """Ignore files matching a glob pattern. You may give multiple
353         '--exclude' options."""
354         g = argv_to_unicode(pattern).strip()
355         if g:
356             exclude = self['exclude']
357             exclude.add(g)
358
359     def opt_exclude_from(self, filepath):
360         """Ignore file matching glob patterns listed in file, one per
361         line. The file is assumed to be in the argv encoding."""
362         abs_filepath = argv_to_abspath(filepath)
363         try:
364             exclude_file = file(abs_filepath)
365         except:
366             raise BackupConfigurationError('Error opening exclude file %s.' % quote_local_unicode_path(abs_filepath))
367         try:
368             for line in exclude_file:
369                 self.opt_exclude(line)
370         finally:
371             exclude_file.close()
372
373     def opt_exclude_vcs(self):
374         """Exclude files and directories used by following version control
375         systems: CVS, RCS, SCCS, Git, SVN, Arch, Bazaar(bzr), Mercurial,
376         Darcs."""
377         for pattern in self.vcs_patterns:
378             self.opt_exclude(pattern)
379
380     def filter_listdir(self, listdir):
381         """Yields non-excluded childpaths in path."""
382         exclude = self['exclude']
383         exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
384         for filename in listdir:
385             for regexp in exclude_regexps:
386                 if regexp.match(filename):
387                     break
388             else:
389                 yield filename
390
391     longdesc = """
392     Add a versioned backup of the local FROM directory to a timestamped
393     subdirectory of the TO/Archives directory on the grid, sharing as many
394     files and directories as possible with earlier backups. Create TO/Latest
395     as a reference to the latest backup. Behaves somewhat like 'rsync -a
396     --link-dest=TO/Archives/(previous) FROM TO/Archives/(new); ln -sf
397     TO/Archives/(new) TO/Latest'."""
398
399 class WebopenOptions(FilesystemOptions):
400     optFlags = [
401         ("info", "i", "Open the t=info page for the file"),
402         ]
403     def parseArgs(self, where=''):
404         self.where = argv_to_unicode(where)
405
406     def getSynopsis(self):
407         return "Usage:  %s [global-opts] webopen [options] [ALIAS:PATH]" % (self.command_name,)
408
409     longdesc = """Open a web browser to the contents of some file or
410     directory on the grid. When run without arguments, open the Welcome
411     page."""
412
413 class ManifestOptions(FilesystemOptions):
414     optFlags = [
415         ("storage-index", "s", "Only print storage index strings, not pathname+cap."),
416         ("verify-cap", None, "Only print verifycap, not pathname+cap."),
417         ("repair-cap", None, "Only print repaircap, not pathname+cap."),
418         ("raw", "r", "Display raw JSON data instead of parsed."),
419         ]
420     def parseArgs(self, where=''):
421         self.where = argv_to_unicode(where)
422
423     def getSynopsis(self):
424         return "Usage:  %s [global-opts] manifest [options] [ALIAS:PATH]" % (self.command_name,)
425
426     longdesc = """Print a list of all files and directories reachable from
427     the given starting point."""
428
429 class StatsOptions(FilesystemOptions):
430     optFlags = [
431         ("raw", "r", "Display raw JSON data instead of parsed"),
432         ]
433     def parseArgs(self, where=''):
434         self.where = argv_to_unicode(where)
435
436     def getSynopsis(self):
437         return "Usage:  %s [global-opts] stats [options] [ALIAS:PATH]" % (self.command_name,)
438
439     longdesc = """Print statistics about of all files and directories
440     reachable from the given starting point."""
441
442 class CheckOptions(FilesystemOptions):
443     optFlags = [
444         ("raw", None, "Display raw JSON data instead of parsed."),
445         ("verify", None, "Verify all hashes, instead of merely querying share presence."),
446         ("repair", None, "Automatically repair any problems found."),
447         ("add-lease", None, "Add/renew lease on all shares."),
448         ]
449     def parseArgs(self, *locations):
450         self.locations = map(argv_to_unicode, locations)
451
452     def getSynopsis(self):
453         return "Usage:  %s [global-opts] check [options] [ALIAS:PATH]" % (self.command_name,)
454
455     longdesc = """
456     Check a single file or directory: count how many shares are available and
457     verify their hashes. Optionally repair the file if any problems were
458     found."""
459
460 class DeepCheckOptions(FilesystemOptions):
461     optFlags = [
462         ("raw", None, "Display raw JSON data instead of parsed."),
463         ("verify", None, "Verify all hashes, instead of merely querying share presence."),
464         ("repair", None, "Automatically repair any problems found."),
465         ("add-lease", None, "Add/renew lease on all shares."),
466         ("verbose", "v", "Be noisy about what is happening."),
467         ]
468     def parseArgs(self, *locations):
469         self.locations = map(argv_to_unicode, locations)
470
471     def getSynopsis(self):
472         return "Usage:  %s [global-opts] deep-check [options] [ALIAS:PATH]" % (self.command_name,)
473
474     longdesc = """
475     Check all files and directories reachable from the given starting point
476     (which must be a directory), like 'tahoe check' but for multiple files.
477     Optionally repair any problems found."""
478
479 subCommands = [
480     ["mkdir", None, MakeDirectoryOptions, "Create a new directory."],
481     ["add-alias", None, AddAliasOptions, "Add a new alias cap."],
482     ["create-alias", None, CreateAliasOptions, "Create a new alias cap."],
483     ["list-aliases", None, ListAliasesOptions, "List all alias caps."],
484     ["ls", None, ListOptions, "List a directory."],
485     ["get", None, GetOptions, "Retrieve a file from the grid."],
486     ["put", None, PutOptions, "Upload a file into the grid."],
487     ["cp", None, CpOptions, "Copy one or more files or directories."],
488     ["unlink", None, UnlinkOptions, "Unlink a file or directory on the grid."],
489     ["rm", None, RmOptions, "Unlink a file or directory on the grid (same as unlink)."],
490     ["mv", None, MvOptions, "Move a file within the grid."],
491     ["ln", None, LnOptions, "Make an additional link to an existing file or directory."],
492     ["backup", None, BackupOptions, "Make target dir look like local dir."],
493     ["webopen", None, WebopenOptions, "Open a web browser to a grid file or directory."],
494     ["manifest", None, ManifestOptions, "List all files/directories in a subtree."],
495     ["stats", None, StatsOptions, "Print statistics about all files/directories in a subtree."],
496     ["check", None, CheckOptions, "Check a single file or directory."],
497     ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point."],
498     ]
499
500 def mkdir(options):
501     from allmydata.scripts import tahoe_mkdir
502     rc = tahoe_mkdir.mkdir(options)
503     return rc
504
505 def add_alias(options):
506     from allmydata.scripts import tahoe_add_alias
507     rc = tahoe_add_alias.add_alias(options)
508     return rc
509
510 def create_alias(options):
511     from allmydata.scripts import tahoe_add_alias
512     rc = tahoe_add_alias.create_alias(options)
513     return rc
514
515 def list_aliases(options):
516     from allmydata.scripts import tahoe_add_alias
517     rc = tahoe_add_alias.list_aliases(options)
518     return rc
519
520 def list(options):
521     from allmydata.scripts import tahoe_ls
522     rc = tahoe_ls.list(options)
523     return rc
524
525 def get(options):
526     from allmydata.scripts import tahoe_get
527     rc = tahoe_get.get(options)
528     if rc == 0:
529         if options.to_file is None:
530             # be quiet, since the file being written to stdout should be
531             # proof enough that it worked, unless the user is unlucky
532             # enough to have picked an empty file
533             pass
534         else:
535             print >>options.stderr, "%s retrieved and written to %s" % \
536                   (options.from_file, options.to_file)
537     return rc
538
539 def put(options):
540     from allmydata.scripts import tahoe_put
541     rc = tahoe_put.put(options)
542     return rc
543
544 def cp(options):
545     from allmydata.scripts import tahoe_cp
546     rc = tahoe_cp.copy(options)
547     return rc
548
549 def unlink(options, command="unlink"):
550     from allmydata.scripts import tahoe_unlink
551     rc = tahoe_unlink.unlink(options, command=command)
552     return rc
553
554 def rm(options):
555     return unlink(options, command="rm")
556
557 def mv(options):
558     from allmydata.scripts import tahoe_mv
559     rc = tahoe_mv.mv(options, mode="move")
560     return rc
561
562 def ln(options):
563     from allmydata.scripts import tahoe_mv
564     rc = tahoe_mv.mv(options, mode="link")
565     return rc
566
567 def backup(options):
568     from allmydata.scripts import tahoe_backup
569     rc = tahoe_backup.backup(options)
570     return rc
571
572 def webopen(options, opener=None):
573     from allmydata.scripts import tahoe_webopen
574     rc = tahoe_webopen.webopen(options, opener=opener)
575     return rc
576
577 def manifest(options):
578     from allmydata.scripts import tahoe_manifest
579     rc = tahoe_manifest.manifest(options)
580     return rc
581
582 def stats(options):
583     from allmydata.scripts import tahoe_manifest
584     rc = tahoe_manifest.stats(options)
585     return rc
586
587 def check(options):
588     from allmydata.scripts import tahoe_check
589     rc = tahoe_check.check(options)
590     return rc
591
592 def deepcheck(options):
593     from allmydata.scripts import tahoe_check
594     rc = tahoe_check.deepcheck(options)
595     return rc
596
597 dispatch = {
598     "mkdir": mkdir,
599     "add-alias": add_alias,
600     "create-alias": create_alias,
601     "list-aliases": list_aliases,
602     "ls": list,
603     "get": get,
604     "put": put,
605     "cp": cp,
606     "unlink": unlink,
607     "rm": rm,
608     "mv": mv,
609     "ln": ln,
610     "backup": backup,
611     "webopen": webopen,
612     "manifest": manifest,
613     "stats": stats,
614     "check": check,
615     "deep-check": deepcheck,
616     }