1 | """ |
---|
2 | Implement the ``tahoe put`` command. |
---|
3 | """ |
---|
4 | from __future__ import annotations |
---|
5 | |
---|
6 | from io import BytesIO |
---|
7 | from urllib.parse import quote as url_quote |
---|
8 | from base64 import urlsafe_b64encode |
---|
9 | |
---|
10 | from cryptography.hazmat.primitives.serialization import load_pem_private_key |
---|
11 | |
---|
12 | from twisted.python.filepath import FilePath |
---|
13 | |
---|
14 | from allmydata.crypto.rsa import PrivateKey, der_string_from_signing_key |
---|
15 | from allmydata.scripts.common_http import do_http, format_http_success, format_http_error |
---|
16 | from allmydata.scripts.common import get_alias, DEFAULT_ALIAS, escape_path, \ |
---|
17 | UnknownAliasError |
---|
18 | from allmydata.util.encodingutil import quote_output |
---|
19 | |
---|
20 | def load_private_key(path: str) -> str: |
---|
21 | """ |
---|
22 | Load a private key from a file and return it in a format appropriate |
---|
23 | to include in the HTTP request. |
---|
24 | """ |
---|
25 | privkey = load_pem_private_key(FilePath(path).getContent(), password=None) |
---|
26 | assert isinstance(privkey, PrivateKey) |
---|
27 | derbytes = der_string_from_signing_key(privkey) |
---|
28 | return urlsafe_b64encode(derbytes).decode("ascii") |
---|
29 | |
---|
30 | def put(options): |
---|
31 | """ |
---|
32 | @param verbosity: 0, 1, or 2, meaning quiet, verbose, or very verbose |
---|
33 | |
---|
34 | @return: a Deferred which eventually fires with the exit code |
---|
35 | """ |
---|
36 | nodeurl = options['node-url'] |
---|
37 | aliases = options.aliases |
---|
38 | from_file = options.from_file |
---|
39 | to_file = options.to_file |
---|
40 | mutable = options['mutable'] |
---|
41 | if options["private-key-path"] is None: |
---|
42 | private_key = None |
---|
43 | else: |
---|
44 | private_key = load_private_key(options["private-key-path"]) |
---|
45 | format = options['format'] |
---|
46 | if options['quiet']: |
---|
47 | verbosity = 0 |
---|
48 | else: |
---|
49 | verbosity = 2 |
---|
50 | stdin = options.stdin |
---|
51 | stdout = options.stdout |
---|
52 | stderr = options.stderr |
---|
53 | |
---|
54 | if nodeurl[-1] != "/": |
---|
55 | nodeurl += "/" |
---|
56 | if to_file: |
---|
57 | # several possibilities for the TO_FILE argument. |
---|
58 | # <none> : unlinked upload |
---|
59 | # foo : TAHOE_ALIAS/foo |
---|
60 | # subdir/foo : TAHOE_ALIAS/subdir/foo |
---|
61 | # /oops/subdir/foo : DISALLOWED |
---|
62 | # ALIAS:foo : aliases[ALIAS]/foo |
---|
63 | # ALIAS:subdir/foo : aliases[ALIAS]/subdir/foo |
---|
64 | |
---|
65 | # ALIAS:/oops/subdir/foo : DISALLOWED |
---|
66 | # DIRCAP:./foo : DIRCAP/foo |
---|
67 | # DIRCAP:./subdir/foo : DIRCAP/subdir/foo |
---|
68 | # MUTABLE-FILE-WRITECAP : filecap |
---|
69 | |
---|
70 | # FIXME: don't hardcode cap format. |
---|
71 | if to_file.startswith("URI:MDMF:") or to_file.startswith("URI:SSK:"): |
---|
72 | url = nodeurl + "uri/%s" % url_quote(to_file) |
---|
73 | else: |
---|
74 | try: |
---|
75 | rootcap, path = get_alias(aliases, to_file, DEFAULT_ALIAS) |
---|
76 | except UnknownAliasError as e: |
---|
77 | e.display(stderr) |
---|
78 | return 1 |
---|
79 | path = str(path, "utf-8") |
---|
80 | if path.startswith("/"): |
---|
81 | suggestion = to_file.replace(u"/", u"", 1) |
---|
82 | print("Error: The remote filename must not start with a slash", file=stderr) |
---|
83 | print("Please try again, perhaps with %s" % quote_output(suggestion), file=stderr) |
---|
84 | return 1 |
---|
85 | url = nodeurl + "uri/%s/" % url_quote(rootcap) |
---|
86 | if path: |
---|
87 | url += escape_path(path) |
---|
88 | else: |
---|
89 | # unlinked upload |
---|
90 | url = nodeurl + "uri" |
---|
91 | |
---|
92 | queryargs = [] |
---|
93 | if mutable: |
---|
94 | queryargs.append("mutable=true") |
---|
95 | if private_key is not None: |
---|
96 | queryargs.append(f"private-key={private_key}") |
---|
97 | else: |
---|
98 | if private_key is not None: |
---|
99 | raise Exception("Can only supply a private key for mutables.") |
---|
100 | |
---|
101 | if format: |
---|
102 | queryargs.append("format=%s" % format) |
---|
103 | if queryargs: |
---|
104 | url += "?" + "&".join(queryargs) |
---|
105 | |
---|
106 | if from_file: |
---|
107 | infileobj = open(from_file, "rb") |
---|
108 | else: |
---|
109 | # do_http() can't use stdin directly: for one thing, we need a |
---|
110 | # Content-Length field. So we currently must copy it. |
---|
111 | if verbosity > 0: |
---|
112 | print("waiting for file data on stdin..", file=stderr) |
---|
113 | # We're uploading arbitrary files, so this had better be bytes: |
---|
114 | stdinb = stdin.buffer |
---|
115 | data = stdinb.read() |
---|
116 | infileobj = BytesIO(data) |
---|
117 | |
---|
118 | resp = do_http("PUT", url, infileobj) |
---|
119 | |
---|
120 | if resp.status in (200, 201,): |
---|
121 | print(format_http_success(resp), file=stderr) |
---|
122 | print(quote_output(resp.read(), quotemarks=False), file=stdout) |
---|
123 | return 0 |
---|
124 | |
---|
125 | print(format_http_error("Error", resp), file=stderr) |
---|
126 | return 1 |
---|