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