]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/common.py
Merge pull request #120 from zancas/1634-dont-return-none_5
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / common.py
1
2 import simplejson
3 from twisted.web import http, server
4 from twisted.python import log
5 from zope.interface import Interface
6 from nevow import loaders, appserver
7 from nevow.inevow import IRequest
8 from nevow.util import resource_filename
9 from allmydata import blacklist
10 from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
11      FileTooLargeError, NotEnoughSharesError, NoSharesError, \
12      EmptyPathnameComponentError, MustBeDeepImmutableError, \
13      MustBeReadonlyError, MustNotBeUnknownRWError, SDMF_VERSION, MDMF_VERSION
14 from allmydata.mutable.common import UnrecoverableFileError
15 from allmydata.util import abbreviate
16 from allmydata.util.encodingutil import to_str, quote_output
17
18
19 TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
20
21 def get_filenode_metadata(filenode):
22     metadata = {'mutable': filenode.is_mutable()}
23     if metadata['mutable']:
24         mutable_type = filenode.get_version()
25         assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
26         if mutable_type == MDMF_VERSION:
27             file_format = "MDMF"
28         else:
29             file_format = "SDMF"
30     else:
31         file_format = "CHK"
32     metadata['format'] = file_format
33     if filenode.get_size() is not None:
34         metadata['size'] = filenode.get_size()
35     return metadata
36
37 class IOpHandleTable(Interface):
38     pass
39
40 def getxmlfile(name):
41     return loaders.xmlfile(resource_filename('allmydata.web', '%s' % name))
42
43 def boolean_of_arg(arg):
44     # TODO: ""
45     if arg.lower() not in ("true", "t", "1", "false", "f", "0", "on", "off"):
46         raise WebError("invalid boolean argument: %r" % (arg,), http.BAD_REQUEST)
47     return arg.lower() in ("true", "t", "1", "on")
48
49 def parse_replace_arg(replace):
50     if replace.lower() == "only-files":
51         return replace
52     try:
53         return boolean_of_arg(replace)
54     except WebError:
55         raise WebError("invalid replace= argument: %r" % (replace,), http.BAD_REQUEST)
56
57
58 def get_format(req, default="CHK"):
59     arg = get_arg(req, "format", None)
60     if not arg:
61         if boolean_of_arg(get_arg(req, "mutable", "false")):
62             return "SDMF"
63         return default
64     if arg.upper() == "CHK":
65         return "CHK"
66     elif arg.upper() == "SDMF":
67         return "SDMF"
68     elif arg.upper() == "MDMF":
69         return "MDMF"
70     else:
71         raise WebError("Unknown format: %s, I know CHK, SDMF, MDMF" % arg,
72                        http.BAD_REQUEST)
73
74 def get_mutable_type(file_format): # accepts result of get_format()
75     if file_format == "SDMF":
76         return SDMF_VERSION
77     elif file_format == "MDMF":
78         return MDMF_VERSION
79     else:
80         # this is also used to identify which formats are mutable. Use
81         #  if get_mutable_type(file_format) is not None:
82         #      do_mutable()
83         #  else:
84         #      do_immutable()
85         return None
86
87
88 def parse_offset_arg(offset):
89     # XXX: This will raise a ValueError when invoked on something that
90     # is not an integer. Is that okay? Or do we want a better error
91     # message? Since this call is going to be used by programmers and
92     # their tools rather than users (through the wui), it is not
93     # inconsistent to return that, I guess.
94     if offset is not None:
95         offset = int(offset)
96
97     return offset
98
99
100 def get_root(ctx_or_req):
101     req = IRequest(ctx_or_req)
102     # the addSlash=True gives us one extra (empty) segment
103     depth = len(req.prepath) + len(req.postpath) - 1
104     link = "/".join([".."] * depth)
105     return link
106
107 def get_arg(ctx_or_req, argname, default=None, multiple=False):
108     """Extract an argument from either the query args (req.args) or the form
109     body fields (req.fields). If multiple=False, this returns a single value
110     (or the default, which defaults to None), and the query args take
111     precedence. If multiple=True, this returns a tuple of arguments (possibly
112     empty), starting with all those in the query args.
113     """
114     req = IRequest(ctx_or_req)
115     results = []
116     if argname in req.args:
117         results.extend(req.args[argname])
118     if req.fields and argname in req.fields:
119         results.append(req.fields[argname].value)
120     if multiple:
121         return tuple(results)
122     if results:
123         return results[0]
124     return default
125
126 def convert_children_json(nodemaker, children_json):
127     """I convert the JSON output of GET?t=json into the dict-of-nodes input
128     to both dirnode.create_subdirectory() and
129     client.create_directory(initial_children=). This is used by
130     t=mkdir-with-children and t=mkdir-immutable"""
131     children = {}
132     if children_json:
133         data = simplejson.loads(children_json)
134         for (namex, (ctype, propdict)) in data.iteritems():
135             namex = unicode(namex)
136             writecap = to_str(propdict.get("rw_uri"))
137             readcap = to_str(propdict.get("ro_uri"))
138             metadata = propdict.get("metadata", {})
139             # name= argument is just for error reporting
140             childnode = nodemaker.create_from_cap(writecap, readcap, name=namex)
141             children[namex] = (childnode, metadata)
142     return children
143
144 def abbreviate_time(data):
145     # 1.23s, 790ms, 132us
146     if data is None:
147         return ""
148     s = float(data)
149     if s >= 10:
150         return abbreviate.abbreviate_time(data)
151     if s >= 1.0:
152         return "%.2fs" % s
153     if s >= 0.01:
154         return "%.0fms" % (1000*s)
155     if s >= 0.001:
156         return "%.1fms" % (1000*s)
157     return "%.0fus" % (1000000*s)
158
159 def compute_rate(bytes, seconds):
160     if bytes is None:
161       return None
162
163     if seconds is None or seconds == 0:
164       return None
165
166     # negative values don't make sense here
167     assert bytes > -1
168     assert seconds > 0
169
170     return 1.0 * bytes / seconds
171
172 def abbreviate_rate(data):
173     # 21.8kBps, 554.4kBps 4.37MBps
174     if data is None:
175         return ""
176     r = float(data)
177     if r > 1000000:
178         return "%1.2fMBps" % (r/1000000)
179     if r > 1000:
180         return "%.1fkBps" % (r/1000)
181     return "%.0fBps" % r
182
183 def abbreviate_size(data):
184     # 21.8kB, 554.4kB 4.37MB
185     if data is None:
186         return ""
187     r = float(data)
188     if r > 1000000000:
189         return "%1.2fGB" % (r/1000000000)
190     if r > 1000000:
191         return "%1.2fMB" % (r/1000000)
192     if r > 1000:
193         return "%.1fkB" % (r/1000)
194     return "%.0fB" % r
195
196 def plural(sequence_or_length):
197     if isinstance(sequence_or_length, int):
198         length = sequence_or_length
199     else:
200         length = len(sequence_or_length)
201     if length == 1:
202         return ""
203     return "s"
204
205 def text_plain(text, ctx):
206     req = IRequest(ctx)
207     req.setHeader("content-type", "text/plain")
208     req.setHeader("content-length", len(text))
209     return text
210
211 class WebError(Exception):
212     def __init__(self, text, code=http.BAD_REQUEST):
213         self.text = text
214         self.code = code
215
216 # XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
217 # Internal Server Error, we either need to do that ICanHandleException trick,
218 # or make sure that childFactory returns a WebErrorResource (and never an
219 # actual exception). The latter is growing increasingly annoying.
220
221 def should_create_intermediate_directories(req):
222     t = get_arg(req, "t", "").strip()
223     return bool(req.method in ("PUT", "POST") and
224                 t not in ("delete", "rename", "rename-form", "check"))
225
226 def humanize_failure(f):
227     # return text, responsecode
228     if f.check(EmptyPathnameComponentError):
229         return ("The webapi does not allow empty pathname components, "
230                 "i.e. a double slash", http.BAD_REQUEST)
231     if f.check(ExistingChildError):
232         return ("There was already a child by that name, and you asked me "
233                 "to not replace it.", http.CONFLICT)
234     if f.check(NoSuchChildError):
235         quoted_name = quote_output(f.value.args[0], encoding="utf-8", quotemarks=False)
236         return ("No such child: %s" % quoted_name, http.NOT_FOUND)
237     if f.check(NotEnoughSharesError):
238         t = ("NotEnoughSharesError: This indicates that some "
239              "servers were unavailable, or that shares have been "
240              "lost to server departure, hard drive failure, or disk "
241              "corruption. You should perform a filecheck on "
242              "this object to learn more.\n\nThe full error message is:\n"
243              "%s") % str(f.value)
244         return (t, http.GONE)
245     if f.check(NoSharesError):
246         t = ("NoSharesError: no shares could be found. "
247              "Zero shares usually indicates a corrupt URI, or that "
248              "no servers were connected, but it might also indicate "
249              "severe corruption. You should perform a filecheck on "
250              "this object to learn more.\n\nThe full error message is:\n"
251              "%s") % str(f.value)
252         return (t, http.GONE)
253     if f.check(UnrecoverableFileError):
254         t = ("UnrecoverableFileError: the directory (or mutable file) could "
255              "not be retrieved, because there were insufficient good shares. "
256              "This might indicate that no servers were connected, "
257              "insufficient servers were connected, the URI was corrupt, or "
258              "that shares have been lost due to server departure, hard drive "
259              "failure, or disk corruption. You should perform a filecheck on "
260              "this object to learn more.")
261         return (t, http.GONE)
262     if f.check(MustNotBeUnknownRWError):
263         quoted_name = quote_output(f.value.args[1], encoding="utf-8")
264         immutable = f.value.args[2]
265         if immutable:
266             t = ("MustNotBeUnknownRWError: an operation to add a child named "
267                  "%s to a directory was given an unknown cap in a write slot.\n"
268                  "If the cap is actually an immutable readcap, then using a "
269                  "webapi server that supports a later version of Tahoe may help.\n\n"
270                  "If you are using the webapi directly, then specifying an immutable "
271                  "readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
272                  "omitting the write slot (rw_uri), would also work in this "
273                  "case.") % quoted_name
274         else:
275             t = ("MustNotBeUnknownRWError: an operation to add a child named "
276                  "%s to a directory was given an unknown cap in a write slot.\n"
277                  "Using a webapi server that supports a later version of Tahoe "
278                  "may help.\n\n"
279                  "If you are using the webapi directly, specifying a readcap in "
280                  "the read slot (ro_uri) of the JSON PROPDICT, as well as a "
281                  "writecap in the write slot if desired, would also work in this "
282                  "case.") % quoted_name
283         return (t, http.BAD_REQUEST)
284     if f.check(MustBeDeepImmutableError):
285         quoted_name = quote_output(f.value.args[1], encoding="utf-8")
286         t = ("MustBeDeepImmutableError: a cap passed to this operation for "
287              "the child named %s, needed to be immutable but was not. Either "
288              "the cap is being added to an immutable directory, or it was "
289              "originally retrieved from an immutable directory as an unknown "
290              "cap.") % quoted_name
291         return (t, http.BAD_REQUEST)
292     if f.check(MustBeReadonlyError):
293         quoted_name = quote_output(f.value.args[1], encoding="utf-8")
294         t = ("MustBeReadonlyError: a cap passed to this operation for "
295              "the child named '%s', needed to be read-only but was not. "
296              "The cap is being passed in a read slot (ro_uri), or was retrieved "
297              "from a read slot as an unknown cap.") % quoted_name
298         return (t, http.BAD_REQUEST)
299     if f.check(blacklist.FileProhibited):
300         t = "Access Prohibited: %s" % quote_output(f.value.reason, encoding="utf-8", quotemarks=False)
301         return (t, http.FORBIDDEN)
302     if f.check(WebError):
303         return (f.value.text, f.value.code)
304     if f.check(FileTooLargeError):
305         return (f.getTraceback(), http.REQUEST_ENTITY_TOO_LARGE)
306     return (str(f), None)
307
308 class MyExceptionHandler(appserver.DefaultExceptionHandler):
309     def simple(self, ctx, text, code=http.BAD_REQUEST):
310         req = IRequest(ctx)
311         req.setResponseCode(code)
312         #req.responseHeaders.setRawHeaders("content-encoding", [])
313         #req.responseHeaders.setRawHeaders("content-disposition", [])
314         req.setHeader("content-type", "text/plain;charset=utf-8")
315         if isinstance(text, unicode):
316             text = text.encode("utf-8")
317         req.setHeader("content-length", str(len(text)))
318         req.write(text)
319         # TODO: consider putting the requested URL here
320         req.finishRequest(False)
321
322     def renderHTTP_exception(self, ctx, f):
323         try:
324             text, code = humanize_failure(f)
325         except:
326             log.msg("exception in humanize_failure")
327             log.msg("argument was %s" % (f,))
328             log.err()
329             text, code = str(f), None
330         if code is not None:
331             return self.simple(ctx, text, code)
332         if f.check(server.UnsupportedMethod):
333             # twisted.web.server.Request.render() has support for transforming
334             # this into an appropriate 501 NOT_IMPLEMENTED or 405 NOT_ALLOWED
335             # return code, but nevow does not.
336             req = IRequest(ctx)
337             method = req.method
338             return self.simple(ctx,
339                                "I don't know how to treat a %s request." % method,
340                                http.NOT_IMPLEMENTED)
341         req = IRequest(ctx)
342         accept = req.getHeader("accept")
343         if not accept:
344             accept = "*/*"
345         if "*/*" in accept or "text/*" in accept or "text/html" in accept:
346             super = appserver.DefaultExceptionHandler
347             return super.renderHTTP_exception(self, ctx, f)
348         # use plain text
349         traceback = f.getTraceback()
350         return self.simple(ctx, traceback, http.INTERNAL_SERVER_ERROR)
351
352 class NeedOperationHandleError(WebError):
353     pass
354
355 class RenderMixin:
356
357     def renderHTTP(self, ctx):
358         request = IRequest(ctx)
359
360         # if we were using regular twisted.web Resources (and the regular
361         # twisted.web.server.Request object) then we could implement
362         # render_PUT and render_GET. But Nevow's request handler
363         # (NevowRequest.gotPageContext) goes directly to renderHTTP. Copy
364         # some code from the Resource.render method that Nevow bypasses, to
365         # do the same thing.
366         m = getattr(self, 'render_' + request.method, None)
367         if not m:
368             from twisted.web.server import UnsupportedMethod
369             raise UnsupportedMethod(getattr(self, 'allowedMethods', ()))
370         return m(ctx)