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