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