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

Last change on this file was e1839ff3, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-03-24T15:45:56Z

Fix lints.

  • 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        ]
449    def parseArgs(self, *locations):
450        self.locations = list(map(argv_to_unicode, locations))
451
452    synopsis = "[options] [ALIAS:PATH]"
453    description = """
454    Check a single file or directory: count how many shares are available and
455    verify their hashes. Optionally repair the file if any problems were
456    found."""
457
458class DeepCheckOptions(FileStoreOptions):
459    optFlags = [
460        ("raw", None, "Display raw JSON data instead of parsed."),
461        ("verify", None, "Verify all hashes, instead of merely querying share presence."),
462        ("repair", None, "Automatically repair any problems found."),
463        ("add-lease", None, "Add/renew lease on all shares."),
464        ("verbose", "v", "Be noisy about what is happening."),
465        ]
466    def parseArgs(self, *locations):
467        self.locations = list(map(argv_to_unicode, locations))
468
469    synopsis = "[options] [ALIAS:PATH]"
470    description = """
471    Check all files and directories reachable from the given starting point
472    (which must be a directory), like 'tahoe check' but for multiple files.
473    Optionally repair any problems found."""
474
475subCommands : SubCommands = [
476    ("mkdir", None, MakeDirectoryOptions, "Create a new directory."),
477    ("add-alias", None, AddAliasOptions, "Add a new alias cap."),
478    ("create-alias", None, CreateAliasOptions, "Create a new alias cap."),
479    ("list-aliases", None, ListAliasesOptions, "List all alias caps."),
480    ("ls", None, ListOptions, "List a directory."),
481    ("get", None, GetOptions, "Retrieve a file from the grid."),
482    ("put", None, PutOptions, "Upload a file into the grid."),
483    ("cp", None, CpOptions, "Copy one or more files or directories."),
484    ("unlink", None, UnlinkOptions, "Unlink a file or directory on the grid."),
485    ("mv", None, MvOptions, "Move a file within the grid."),
486    ("ln", None, LnOptions, "Make an additional link to an existing file or directory."),
487    ("backup", None, BackupOptions, "Make target dir look like local dir."),
488    ("webopen", None, WebopenOptions, "Open a web browser to a grid file or directory."),
489    ("manifest", None, ManifestOptions, "List all files/directories in a subtree."),
490    ("stats", None, StatsOptions, "Print statistics about all files/directories in a subtree."),
491    ("check", None, CheckOptions, "Check a single file or directory."),
492    ("deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point."),
493    ("status", None, TahoeStatusCommand, "Various status information."),
494    ]
495
496def mkdir(options):
497    from allmydata.scripts import tahoe_mkdir
498    rc = tahoe_mkdir.mkdir(options)
499    return rc
500
501def add_alias(options):
502    from allmydata.scripts import tahoe_add_alias
503    rc = tahoe_add_alias.add_alias(options)
504    return rc
505
506def create_alias(options):
507    from allmydata.scripts import tahoe_add_alias
508    rc = tahoe_add_alias.create_alias(options)
509    return rc
510
511def list_aliases(options):
512    from allmydata.scripts import tahoe_add_alias
513    rc = tahoe_add_alias.list_aliases(options)
514    return rc
515
516def list_(options):
517    from allmydata.scripts import tahoe_ls
518    rc = tahoe_ls.ls(options)
519    return rc
520
521def get(options):
522    from allmydata.scripts import tahoe_get
523    rc = tahoe_get.get(options)
524    if rc == 0:
525        if options.to_file is None:
526            # be quiet, since the file being written to stdout should be
527            # proof enough that it worked, unless the user is unlucky
528            # enough to have picked an empty file
529            pass
530        else:
531            print("%s retrieved and written to %s" % \
532                  (options.from_file, options.to_file), file=options.stderr)
533    return rc
534
535def put(options):
536    from allmydata.scripts import tahoe_put
537    rc = tahoe_put.put(options)
538    return rc
539
540def cp(options):
541    from allmydata.scripts import tahoe_cp
542    rc = tahoe_cp.copy(options)
543    return rc
544
545def unlink(options, command="unlink"):
546    from allmydata.scripts import tahoe_unlink
547    rc = tahoe_unlink.unlink(options, command=command)
548    return rc
549
550def rm(options):
551    return unlink(options, command="rm")
552
553def mv(options):
554    from allmydata.scripts import tahoe_mv
555    rc = tahoe_mv.mv(options, mode="move")
556    return rc
557
558def ln(options):
559    from allmydata.scripts import tahoe_mv
560    rc = tahoe_mv.mv(options, mode="link")
561    return rc
562
563def backup(options):
564    from allmydata.scripts import tahoe_backup
565    rc = tahoe_backup.backup(options)
566    return rc
567
568def webopen(options, opener=None):
569    from allmydata.scripts import tahoe_webopen
570    rc = tahoe_webopen.webopen(options, opener=opener)
571    return rc
572
573def manifest(options):
574    from allmydata.scripts import tahoe_manifest
575    rc = tahoe_manifest.manifest(options)
576    return rc
577
578def stats(options):
579    from allmydata.scripts import tahoe_manifest
580    rc = tahoe_manifest.stats(options)
581    return rc
582
583def check(options):
584    from allmydata.scripts import tahoe_check
585    rc = tahoe_check.check(options)
586    return rc
587
588def deepcheck(options):
589    from allmydata.scripts import tahoe_check
590    rc = tahoe_check.deepcheck(options)
591    return rc
592
593def status(options):
594    from allmydata.scripts import tahoe_status
595    return tahoe_status.do_status(options)
596
597dispatch = {
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    "status": status,
617    }
Note: See TracBrowser for help on using the repository browser.