]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
web: full patch for HTML-vs-plaintext traceback renderings, improve test coverage...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / filenode.py
1
2 import simplejson
3
4 from twisted.web import http, static
5 from twisted.internet import defer
6 from nevow import url, rend
7 from nevow.inevow import IRequest
8
9 from allmydata.interfaces import ExistingChildError
10 from allmydata.monitor import Monitor
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.immutable.filenode import LiteralFileNode
13 from allmydata.util import log, base32
14
15 from allmydata.web.common import text_plain, WebError, RenderMixin, \
16      boolean_of_arg, get_arg, should_create_intermediate_directories, \
17      MyExceptionHandler
18 from allmydata.web.check_results import CheckResults, \
19      CheckAndRepairResults, LiteralCheckResults
20 from allmydata.web.info import MoreInfo
21
22 class ReplaceMeMixin:
23
24     def replace_me_with_a_child(self, req, client, replace):
25         # a new file is being uploaded in our place.
26         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
27         if mutable:
28             req.content.seek(0)
29             data = req.content.read()
30             d = client.create_mutable_file(data)
31             def _uploaded(newnode):
32                 d2 = self.parentnode.set_node(self.name, newnode,
33                                               overwrite=replace)
34                 d2.addCallback(lambda res: newnode)
35                 return d2
36             d.addCallback(_uploaded)
37         else:
38             uploadable = FileHandle(req.content, convergence=client.convergence)
39             d = self.parentnode.add_file(self.name, uploadable,
40                                          overwrite=replace)
41         def _done(filenode):
42             log.msg("webish upload complete",
43                     facility="tahoe.webish", level=log.NOISY)
44             if self.node:
45                 # we've replaced an existing file (or modified a mutable
46                 # file), so the response code is 200
47                 req.setResponseCode(http.OK)
48             else:
49                 # we've created a new file, so the code is 201
50                 req.setResponseCode(http.CREATED)
51             return filenode.get_uri()
52         d.addCallback(_done)
53         return d
54
55     def replace_me_with_a_childcap(self, req, client, replace):
56         req.content.seek(0)
57         childcap = req.content.read()
58         childnode = client.create_node_from_uri(childcap)
59         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
60         d.addCallback(lambda res: childnode.get_uri())
61         return d
62
63     def _read_data_from_formpost(self, req):
64         # SDMF: files are small, and we can only upload data, so we read
65         # the whole file into memory before uploading.
66         contents = req.fields["file"]
67         contents.file.seek(0)
68         data = contents.file.read()
69         return data
70
71     def replace_me_with_a_formpost(self, req, client, replace):
72         # create a new file, maybe mutable, maybe immutable
73         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
74
75         if mutable:
76             data = self._read_data_from_formpost(req)
77             d = client.create_mutable_file(data)
78             def _uploaded(newnode):
79                 d2 = self.parentnode.set_node(self.name, newnode,
80                                               overwrite=replace)
81                 d2.addCallback(lambda res: newnode.get_uri())
82                 return d2
83             d.addCallback(_uploaded)
84             return d
85         # create an immutable file
86         contents = req.fields["file"]
87         uploadable = FileHandle(contents.file, convergence=client.convergence)
88         d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
89         d.addCallback(lambda newnode: newnode.get_uri())
90         return d
91
92 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
93     def __init__(self, client, parentnode, name):
94         rend.Page.__init__(self)
95         self.client = client
96         assert parentnode
97         self.parentnode = parentnode
98         self.name = name
99         self.node = None
100
101     def render_PUT(self, ctx):
102         req = IRequest(ctx)
103         t = get_arg(req, "t", "").strip()
104         replace = boolean_of_arg(get_arg(req, "replace", "true"))
105         assert self.parentnode and self.name
106         if req.getHeader("content-range"):
107             raise WebError("Content-Range in PUT not yet supported",
108                            http.NOT_IMPLEMENTED)
109         if not t:
110             return self.replace_me_with_a_child(req, self.client, replace)
111         if t == "uri":
112             return self.replace_me_with_a_childcap(req, self.client, replace)
113
114         raise WebError("PUT to a file: bad t=%s" % t)
115
116     def render_POST(self, ctx):
117         req = IRequest(ctx)
118         t = get_arg(req, "t", "").strip()
119         replace = boolean_of_arg(get_arg(req, "replace", "true"))
120         if t == "upload":
121             # like PUT, but get the file data from an HTML form's input field.
122             # We could get here from POST /uri/mutablefilecap?t=upload,
123             # or POST /uri/path/file?t=upload, or
124             # POST /uri/path/dir?t=upload&name=foo . All have the same
125             # behavior, we just ignore any name= argument
126             d = self.replace_me_with_a_formpost(req, self.client, replace)
127         else:
128             # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
129             # there are no other t= values left to be handled by the
130             # placeholder.
131             raise WebError("POST to a file: bad t=%s" % t)
132
133         when_done = get_arg(req, "when_done", None)
134         if when_done:
135             d.addCallback(lambda res: url.URL.fromString(when_done))
136         return d
137
138
139 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
140     def __init__(self, client, node, parentnode=None, name=None):
141         rend.Page.__init__(self)
142         self.client = client
143         assert node
144         self.node = node
145         self.parentnode = parentnode
146         self.name = name
147
148     def childFactory(self, ctx, name):
149         req = IRequest(ctx)
150         if should_create_intermediate_directories(req):
151             raise WebError("Cannot create directory '%s', because its "
152                            "parent is a file, not a directory" % name)
153         raise WebError("Files have no children, certainly not named '%s'"
154                        % name)
155
156     def render_GET(self, ctx):
157         req = IRequest(ctx)
158         t = get_arg(req, "t", "").strip()
159         if not t:
160             # just get the contents
161             # the filename arrives as part of the URL or in a form input
162             # element, and will be sent back in a Content-Disposition header.
163             # Different browsers use various character sets for this name,
164             # sometimes depending upon how language environment is
165             # configured. Firefox sends the equivalent of
166             # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
167             # latin-1. Browsers cannot agree on how to interpret the name
168             # they see in the Content-Disposition header either, despite some
169             # 11-year old standards (RFC2231) that explain how to do it
170             # properly. So we assume that at least the browser will agree
171             # with itself, and echo back the same bytes that we were given.
172             filename = get_arg(req, "filename", self.name) or "unknown"
173             if self.node.is_mutable():
174                 # some day: d = self.node.get_best_version()
175                 d = makeMutableDownloadable(self.node)
176             else:
177                 d = defer.succeed(self.node)
178             d.addCallback(lambda dn: FileDownloader(dn, filename))
179             return d
180         if t == "json":
181             return FileJSONMetadata(ctx, self.node)
182         if t == "info":
183             return MoreInfo(self.node)
184         if t == "uri":
185             return FileURI(ctx, self.node)
186         if t == "readonly-uri":
187             return FileReadOnlyURI(ctx, self.node)
188         raise WebError("GET file: bad t=%s" % t)
189
190     def render_HEAD(self, ctx):
191         req = IRequest(ctx)
192         t = get_arg(req, "t", "").strip()
193         if t:
194             raise WebError("GET file: bad t=%s" % t)
195         filename = get_arg(req, "filename", self.name) or "unknown"
196         if self.node.is_mutable():
197             # some day: d = self.node.get_best_version()
198             d = makeMutableDownloadable(self.node)
199         else:
200             d = defer.succeed(self.node)
201         d.addCallback(lambda dn: FileDownloader(dn, filename))
202         return d
203
204     def render_PUT(self, ctx):
205         req = IRequest(ctx)
206         t = get_arg(req, "t", "").strip()
207         replace = boolean_of_arg(get_arg(req, "replace", "true"))
208         if not t:
209             if self.node.is_mutable():
210                 return self.replace_my_contents(req)
211             if not replace:
212                 # this is the early trap: if someone else modifies the
213                 # directory while we're uploading, the add_file(overwrite=)
214                 # call in replace_me_with_a_child will do the late trap.
215                 raise ExistingChildError()
216             assert self.parentnode and self.name
217             return self.replace_me_with_a_child(req, self.client, replace)
218         if t == "uri":
219             if not replace:
220                 raise ExistingChildError()
221             assert self.parentnode and self.name
222             return self.replace_me_with_a_childcap(req, self.client, replace)
223
224         raise WebError("PUT to a file: bad t=%s" % t)
225
226     def render_POST(self, ctx):
227         req = IRequest(ctx)
228         t = get_arg(req, "t", "").strip()
229         replace = boolean_of_arg(get_arg(req, "replace", "true"))
230         if t == "check":
231             d = self._POST_check(req)
232         elif t == "upload":
233             # like PUT, but get the file data from an HTML form's input field
234             # We could get here from POST /uri/mutablefilecap?t=upload,
235             # or POST /uri/path/file?t=upload, or
236             # POST /uri/path/dir?t=upload&name=foo . All have the same
237             # behavior, we just ignore any name= argument
238             if self.node.is_mutable():
239                 d = self.replace_my_contents_with_a_formpost(req)
240             else:
241                 if not replace:
242                     raise ExistingChildError()
243                 assert self.parentnode and self.name
244                 d = self.replace_me_with_a_formpost(req, self.client, replace)
245         else:
246             raise WebError("POST to file: bad t=%s" % t)
247
248         when_done = get_arg(req, "when_done", None)
249         if when_done:
250             d.addCallback(lambda res: url.URL.fromString(when_done))
251         return d
252
253     def _POST_check(self, req):
254         verify = boolean_of_arg(get_arg(req, "verify", "false"))
255         repair = boolean_of_arg(get_arg(req, "repair", "false"))
256         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
257         if isinstance(self.node, LiteralFileNode):
258             return defer.succeed(LiteralCheckResults(self.client))
259         if repair:
260             d = self.node.check_and_repair(Monitor(), verify, add_lease)
261             d.addCallback(lambda res: CheckAndRepairResults(self.client, res))
262         else:
263             d = self.node.check(Monitor(), verify, add_lease)
264             d.addCallback(lambda res: CheckResults(self.client, res))
265         return d
266
267     def render_DELETE(self, ctx):
268         assert self.parentnode and self.name
269         d = self.parentnode.delete(self.name)
270         d.addCallback(lambda res: self.node.get_uri())
271         return d
272
273     def replace_my_contents(self, req):
274         req.content.seek(0)
275         new_contents = req.content.read()
276         d = self.node.overwrite(new_contents)
277         d.addCallback(lambda res: self.node.get_uri())
278         return d
279
280     def replace_my_contents_with_a_formpost(self, req):
281         # we have a mutable file. Get the data from the formpost, and replace
282         # the mutable file's contents with it.
283         new_contents = self._read_data_from_formpost(req)
284         d = self.node.overwrite(new_contents)
285         d.addCallback(lambda res: self.node.get_uri())
286         return d
287
288 class MutableDownloadable:
289     #implements(IDownloadable)
290     def __init__(self, size, node):
291         self.size = size
292         self.node = node
293     def get_size(self):
294         return self.size
295     def is_mutable(self):
296         return True
297     def read(self, consumer, offset=0, size=None):
298         d = self.node.download_best_version()
299         d.addCallback(self._got_data, consumer, offset, size)
300         return d
301     def _got_data(self, contents, consumer, offset, size):
302         start = offset
303         if size is not None:
304             end = offset+size
305         else:
306             end = self.size
307         # SDMF: we can write the whole file in one big chunk
308         consumer.write(contents[start:end])
309         return consumer
310
311 def makeMutableDownloadable(n):
312     d = defer.maybeDeferred(n.get_size_of_best_version)
313     d.addCallback(MutableDownloadable, n)
314     return d
315
316 class FileDownloader(rend.Page):
317     # since we override the rendering process (to let the tahoe Downloader
318     # drive things), we must inherit from regular old twisted.web.resource
319     # instead of nevow.rend.Page . Nevow will use adapters to wrap a
320     # nevow.appserver.OldResourceAdapter around any
321     # twisted.web.resource.IResource that it is given. TODO: it looks like
322     # that wrapper would allow us to return a Deferred from render(), which
323     # might could simplify the implementation of WebDownloadTarget.
324
325     def __init__(self, filenode, filename):
326         rend.Page.__init__(self)
327         self.filenode = filenode
328         self.filename = filename
329
330     def renderHTTP(self, ctx):
331         req = IRequest(ctx)
332         gte = static.getTypeAndEncoding
333         ctype, encoding = gte(self.filename,
334                               static.File.contentTypes,
335                               static.File.contentEncodings,
336                               defaultType="text/plain")
337         req.setHeader("content-type", ctype)
338         if encoding:
339             req.setHeader("content-encoding", encoding)
340
341         save_to_filename = None
342         if boolean_of_arg(get_arg(req, "save", "False")):
343             # tell the browser to save the file rather display it we don't
344             # try to encode the filename, instead we echo back the exact same
345             # bytes we were given in the URL. See the comment in
346             # FileNodeHandler.render_GET for the sad details.
347             req.setHeader("content-disposition",
348                           'attachment; filename="%s"' % self.filename)
349
350         filesize = self.filenode.get_size()
351         assert isinstance(filesize, (int,long)), filesize
352         offset, size = 0, None
353         contentsize = filesize
354         req.setHeader("accept-ranges", "bytes")
355         if not self.filenode.is_mutable():
356             # TODO: look more closely at Request.setETag and how it interacts
357             # with a conditional "if-etag-equals" request, I think this may
358             # need to occur after the setResponseCode below
359             si = self.filenode.get_storage_index()
360             if si:
361                 req.setETag(base32.b2a(si))
362         # TODO: for mutable files, use the roothash. For LIT, hash the data.
363         # or maybe just use the URI for CHK and LIT.
364         rangeheader = req.getHeader('range')
365         if rangeheader:
366             # adapted from nevow.static.File
367             bytesrange = rangeheader.split('=')
368             if bytesrange[0] != 'bytes':
369                 raise WebError("Syntactically invalid http range header!")
370             start, end = bytesrange[1].split('-')
371             if start:
372                 offset = int(start)
373                 if not end:
374                     # RFC 2616 says:
375                     #
376                     # "If the last-byte-pos value is absent, or if the value is
377                     # greater than or equal to the current length of the
378                     # entity-body, last-byte-pos is taken to be equal to one less
379                     # than the current length of the entity- body in bytes."
380                     end = filesize - 1
381                 size = int(end) - offset + 1
382             req.setResponseCode(http.PARTIAL_CONTENT)
383             req.setHeader('content-range',"bytes %s-%s/%s" %
384                           (str(offset), str(offset+size-1), str(filesize)))
385             contentsize = size
386         req.setHeader("content-length", str(contentsize))
387         if req.method == "HEAD":
388             return ""
389         d = self.filenode.read(req, offset, size)
390         def _error(f):
391             if req.startedWriting:
392                 # The content-type is already set, and the response code has
393                 # already been sent, so we can't provide a clean error
394                 # indication. We can emit text (which a browser might
395                 # interpret as something else), and if we sent a Size header,
396                 # they might notice that we've truncated the data. Keep the
397                 # error message small to improve the chances of having our
398                 # error response be shorter than the intended results.
399                 #
400                 # We don't have a lot of options, unfortunately.
401                 req.write("problem during download\n")
402                 req.finish()
403             else:
404                 # We haven't written anything yet, so we can provide a
405                 # sensible error message.
406                 eh = MyExceptionHandler()
407                 eh.renderHTTP_exception(ctx, f)
408         d.addCallbacks(lambda ign: req.finish(), _error)
409         return req.deferred
410
411
412 def FileJSONMetadata(ctx, filenode):
413     if filenode.is_readonly():
414         rw_uri = None
415         ro_uri = filenode.get_uri()
416     else:
417         rw_uri = filenode.get_uri()
418         ro_uri = filenode.get_readonly_uri()
419     data = ("filenode", {})
420     data[1]['size'] = filenode.get_size()
421     if ro_uri:
422         data[1]['ro_uri'] = ro_uri
423     if rw_uri:
424         data[1]['rw_uri'] = rw_uri
425     verifycap = filenode.get_verify_cap()
426     if verifycap:
427         data[1]['verify_uri'] = verifycap.to_string()
428     data[1]['mutable'] = filenode.is_mutable()
429     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
430
431 def FileURI(ctx, filenode):
432     return text_plain(filenode.get_uri(), ctx)
433
434 def FileReadOnlyURI(ctx, filenode):
435     if filenode.is_readonly():
436         return text_plain(filenode.get_uri(), ctx)
437     return text_plain(filenode.get_readonly_uri(), ctx)
438
439 class FileNodeDownloadHandler(FileNodeHandler):
440     def childFactory(self, ctx, name):
441         return FileNodeDownloadHandler(self.client, self.node, name=name)