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