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