]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
web: fix JSON output for mutable files
[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.upload import FileHandle
12 from allmydata.interfaces import IDownloadTarget, ExistingChildError
13 from allmydata.mutable.common import MODE_READ
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
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             filename = get_arg(req, "filename", self.name) or "unknown"
160             save_to_file = boolean_of_arg(get_arg(req, "save", "False"))
161             return FileDownloader(self.node, filename, save_to_file)
162         if t == "json":
163             return FileJSONMetadata(ctx, self.node)
164         if t == "uri":
165             return FileURI(ctx, self.node)
166         if t == "readonly-uri":
167             return FileReadOnlyURI(ctx, self.node)
168         raise WebError("GET file: bad t=%s" % t)
169
170     def render_HEAD(self, ctx):
171         req = IRequest(ctx)
172         t = get_arg(req, "t", "").strip()
173         if t:
174             raise WebError("GET file: bad t=%s" % t)
175         if self.node.is_mutable():
176             # update the servermap to get the size of this file without
177             # downloading the full contents.
178             d = self.node.get_servermap(MODE_READ)
179             def _got_servermap(smap):
180                 ver = smap.best_recoverable_version()
181                 if not ver:
182                     raise WebError("Unable to recover this file",
183                                    http.NOT_FOUND)
184                 length = smap.size_of_version(ver)
185                 return length
186             d.addCallback(_got_servermap)
187         # otherwise, we can get the size from the URI
188         else:
189             d = defer.succeed(self.node.get_size())
190         def _got_length(length):
191             req.setHeader("content-length", length)
192             return ""
193         d.addCallback(_got_length)
194         return d
195
196     def render_PUT(self, ctx):
197         req = IRequest(ctx)
198         t = get_arg(req, "t", "").strip()
199         replace = boolean_of_arg(get_arg(req, "replace", "true"))
200         if not t:
201             if self.node.is_mutable():
202                 return self.replace_my_contents(ctx)
203             if not replace:
204                 # this is the early trap: if someone else modifies the
205                 # directory while we're uploading, the add_file(overwrite=)
206                 # call in replace_me_with_a_child will do the late trap.
207                 raise ExistingChildError()
208             assert self.parentnode and self.name
209             return self.replace_me_with_a_child(ctx, replace)
210         if t == "uri":
211             if not replace:
212                 raise ExistingChildError()
213             assert self.parentnode and self.name
214             return self.replace_me_with_a_childcap(ctx, replace)
215
216         raise WebError("PUT to a file: bad t=%s" % t)
217
218     def render_POST(self, ctx):
219         req = IRequest(ctx)
220         t = get_arg(req, "t", "").strip()
221         replace = boolean_of_arg(get_arg(req, "replace", "true"))
222         if t == "check":
223             d = self._POST_check(req)
224         elif t == "upload":
225             # like PUT, but get the file data from an HTML form's input field
226             # We could get here from POST /uri/mutablefilecap?t=upload,
227             # or POST /uri/path/file?t=upload, or
228             # POST /uri/path/dir?t=upload&name=foo . All have the same
229             # behavior, we just ignore any name= argument
230             if self.node.is_mutable():
231                 d = self.replace_my_contents_with_a_formpost(ctx)
232             else:
233                 if not replace:
234                     raise ExistingChildError()
235                 assert self.parentnode and self.name
236                 d = self.replace_me_with_a_formpost(ctx, replace)
237         else:
238             raise WebError("POST to file: bad t=%s" % t)
239
240         when_done = get_arg(req, "when_done", None)
241         if when_done:
242             d.addCallback(lambda res: url.URL.fromString(when_done))
243         return d
244
245     def _POST_check(self, req):
246         d = self.node.check()
247         def _done(res):
248             log.msg("checked %s, results %s" % (self.node, res),
249                     facility="tahoe.webish", level=log.NOISY)
250             return str(res)
251         d.addCallback(_done)
252         # TODO: results
253         return d
254
255     def render_DELETE(self, ctx):
256         assert self.parentnode and self.name
257         d = self.parentnode.delete(self.name)
258         d.addCallback(lambda res: self.node.get_uri())
259         return d
260
261     def replace_my_contents(self, ctx):
262         req = IRequest(ctx)
263         req.content.seek(0)
264         new_contents = req.content.read()
265         d = self.node.overwrite(new_contents)
266         d.addCallback(lambda res: self.node.get_uri())
267         return d
268
269     def replace_my_contents_with_a_formpost(self, ctx):
270         # we have a mutable file. Get the data from the formpost, and replace
271         # the mutable file's contents with it.
272         req = IRequest(ctx)
273         new_contents = self._read_data_from_formpost(req)
274         d = self.node.overwrite(new_contents)
275         d.addCallback(lambda res: self.node.get_uri())
276         return d
277
278
279 class WebDownloadTarget:
280     implements(IDownloadTarget, IConsumer)
281     def __init__(self, req, content_type, content_encoding, save_to_filename):
282         self._req = req
283         self._content_type = content_type
284         self._content_encoding = content_encoding
285         self._opened = False
286         self._producer = None
287         self._save_to_filename = save_to_filename
288
289     def registerProducer(self, producer, streaming):
290         self._req.registerProducer(producer, streaming)
291     def unregisterProducer(self):
292         self._req.unregisterProducer()
293
294     def open(self, size):
295         self._opened = True
296         self._req.setHeader("content-type", self._content_type)
297         if self._content_encoding:
298             self._req.setHeader("content-encoding", self._content_encoding)
299         self._req.setHeader("content-length", str(size))
300         if self._save_to_filename is not None:
301             # tell the browser to save the file rather display it
302             # TODO: indicate charset of filename= properly
303             filename = self._save_to_filename.encode("utf-8")
304             self._req.setHeader("content-disposition",
305                                 'attachment; filename="%s"'
306                                 % filename)
307
308     def write(self, data):
309         self._req.write(data)
310     def close(self):
311         self._req.finish()
312
313     def fail(self, why):
314         if self._opened:
315             # The content-type is already set, and the response code
316             # has already been sent, so we can't provide a clean error
317             # indication. We can emit text (which a browser might interpret
318             # as something else), and if we sent a Size header, they might
319             # notice that we've truncated the data. Keep the error message
320             # small to improve the chances of having our error response be
321             # shorter than the intended results.
322             #
323             # We don't have a lot of options, unfortunately.
324             self._req.write("problem during download\n")
325         else:
326             # We haven't written anything yet, so we can provide a sensible
327             # error message.
328             msg = str(why.type)
329             msg.replace("\n", "|")
330             self._req.setResponseCode(http.GONE, msg)
331             self._req.setHeader("content-type", "text/plain")
332             # TODO: HTML-formatted exception?
333             self._req.write(str(why))
334         self._req.finish()
335
336     def register_canceller(self, cb):
337         pass
338     def finish(self):
339         pass
340
341 class FileDownloader(resource.Resource):
342     # since we override the rendering process (to let the tahoe Downloader
343     # drive things), we must inherit from regular old twisted.web.resource
344     # instead of nevow.rend.Page . Nevow will use adapters to wrap a
345     # nevow.appserver.OldResourceAdapter around any
346     # twisted.web.resource.IResource that it is given. TODO: it looks like
347     # that wrapper would allow us to return a Deferred from render(), which
348     # might could simplify the implementation of WebDownloadTarget.
349
350     def __init__(self, filenode, filename, save_to_file):
351         resource.Resource.__init__(self)
352         self.filenode = filenode
353         self.filename = filename
354         self.save_to_file = save_to_file
355     def render(self, req):
356         gte = static.getTypeAndEncoding
357         ctype, encoding = gte(self.filename,
358                               static.File.contentTypes,
359                               static.File.contentEncodings,
360                               defaultType="text/plain")
361         save_to_filename = None
362         if self.save_to_file:
363             save_to_filename = self.filename
364         wdt = WebDownloadTarget(req, ctype, encoding, save_to_filename)
365         d = self.filenode.download(wdt)
366         # exceptions during download are handled by the WebDownloadTarget
367         d.addErrback(lambda why: None)
368         return server.NOT_DONE_YET
369
370 def FileJSONMetadata(ctx, filenode):
371     if filenode.is_readonly():
372         rw_uri = None
373         ro_uri = filenode.get_uri()
374     else:
375         rw_uri = filenode.get_uri()
376         ro_uri = filenode.get_readonly_uri()
377     data = ("filenode", {})
378     data[1]['size'] = filenode.get_size()
379     if ro_uri:
380         data[1]['ro_uri'] = ro_uri
381     if rw_uri:
382         data[1]['rw_uri'] = rw_uri
383     return text_plain(simplejson.dumps(data, indent=1), ctx)
384
385 def FileURI(ctx, filenode):
386     return text_plain(filenode.get_uri(), ctx)
387
388 def FileReadOnlyURI(ctx, filenode):
389     if filenode.is_readonly():
390         return text_plain(filenode.get_uri(), ctx)
391     return text_plain(filenode.get_readonly_uri(), ctx)
392
393 class FileNodeDownloadHandler(FileNodeHandler):
394     def childFactory(self, ctx, name):
395         return FileNodeDownloadHandler(self.node, name=name)