source: trunk/src/allmydata/scripts/cli.py

Last change on this file was fd28160, checked in by JW Jacobson <116485484+jwjacobson@…>, at 2024-11-04T16:31:45Z

WIP 1101 Add verbose flag to check command

This commit adds the verbose option to the command 'check' so that if
you choose verbose, you do not get an error message. The full verbose
output is not yet implemented. When verbose is fully supported, the
option should return more detailed output.

modified: src/allmydata/scripts/cli.py
modified: src/allmydata/test/cli/test_check.py

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