]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
rename "checker results" to "check results", because it is more parallel to "check...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / filenode.py
1
2 import simplejson
3
4 from twisted.web import http, static
5 from twisted.internet import defer
6 from nevow import url, rend
7 from nevow.inevow import IRequest
8
9 from allmydata.interfaces import ExistingChildError
10 from allmydata.monitor import Monitor
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.immutable.filenode import LiteralFileNode
13 from allmydata.util import log, base32
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.check_results import CheckerResults, \
18      CheckAndRepairResults, LiteralCheckerResults
19 from allmydata.web.info import MoreInfo
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 req.getHeader("content-range"):
111             raise WebError("Content-Range in PUT not yet supported",
112                            http.NOT_IMPLEMENTED)
113         if not t:
114             return self.replace_me_with_a_child(ctx, replace)
115         if t == "uri":
116             return self.replace_me_with_a_childcap(ctx, replace)
117
118         raise WebError("PUT to a file: bad t=%s" % t)
119
120     def render_POST(self, ctx):
121         req = IRequest(ctx)
122         t = get_arg(req, "t", "").strip()
123         replace = boolean_of_arg(get_arg(req, "replace", "true"))
124         if t == "upload":
125             # like PUT, but get the file data from an HTML form's input field.
126             # We could get here from POST /uri/mutablefilecap?t=upload,
127             # or POST /uri/path/file?t=upload, or
128             # POST /uri/path/dir?t=upload&name=foo . All have the same
129             # behavior, we just ignore any name= argument
130             d = self.replace_me_with_a_formpost(ctx, replace)
131         else:
132             # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
133             # there are no other t= values left to be handled by the
134             # placeholder.
135             raise WebError("POST to a file: bad t=%s" % t)
136
137         when_done = get_arg(req, "when_done", None)
138         if when_done:
139             d.addCallback(lambda res: url.URL.fromString(when_done))
140         return d
141
142
143 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
144     def __init__(self, node, parentnode=None, name=None):
145         rend.Page.__init__(self)
146         assert node
147         self.node = node
148         self.parentnode = parentnode
149         self.name = name
150
151     def childFactory(self, ctx, name):
152         req = IRequest(ctx)
153         if should_create_intermediate_directories(req):
154             raise WebError("Cannot create directory '%s', because its "
155                            "parent is a file, not a directory" % name)
156         raise WebError("Files have no children, certainly not named '%s'"
157                        % name)
158
159     def render_GET(self, ctx):
160         req = IRequest(ctx)
161         t = get_arg(req, "t", "").strip()
162         if not t:
163             # just get the contents
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             if self.node.is_mutable():
177                 # some day: d = self.node.get_best_version()
178                 d = makeMutableDownloadable(self.node)
179             else:
180                 d = defer.succeed(self.node)
181             d.addCallback(lambda dn: FileDownloader(dn, filename))
182             return d
183         if t == "json":
184             return FileJSONMetadata(ctx, self.node)
185         if t == "info":
186             return MoreInfo(self.node)
187         if t == "uri":
188             return FileURI(ctx, self.node)
189         if t == "readonly-uri":
190             return FileReadOnlyURI(ctx, self.node)
191         raise WebError("GET file: bad t=%s" % t)
192
193     def render_HEAD(self, ctx):
194         req = IRequest(ctx)
195         t = get_arg(req, "t", "").strip()
196         if t:
197             raise WebError("GET file: bad t=%s" % t)
198         filename = get_arg(req, "filename", self.name) or "unknown"
199         if self.node.is_mutable():
200             # some day: d = self.node.get_best_version()
201             d = makeMutableDownloadable(self.node)
202         else:
203             d = defer.succeed(self.node)
204         d.addCallback(lambda dn: FileDownloader(dn, filename))
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         if isinstance(self.node, LiteralFileNode):
260             return defer.succeed(LiteralCheckerResults())
261         if repair:
262             d = self.node.check_and_repair(Monitor(), verify)
263             d.addCallback(lambda res: CheckAndRepairResults(res))
264         else:
265             d = self.node.check(Monitor(), verify)
266             d.addCallback(lambda res: CheckerResults(res))
267         return d
268
269     def render_DELETE(self, ctx):
270         assert self.parentnode and self.name
271         d = self.parentnode.delete(self.name)
272         d.addCallback(lambda res: self.node.get_uri())
273         return d
274
275     def replace_my_contents(self, ctx):
276         req = IRequest(ctx)
277         req.content.seek(0)
278         new_contents = req.content.read()
279         d = self.node.overwrite(new_contents)
280         d.addCallback(lambda res: self.node.get_uri())
281         return d
282
283     def replace_my_contents_with_a_formpost(self, ctx):
284         # we have a mutable file. Get the data from the formpost, and replace
285         # the mutable file's contents with it.
286         req = IRequest(ctx)
287         new_contents = self._read_data_from_formpost(req)
288         d = self.node.overwrite(new_contents)
289         d.addCallback(lambda res: self.node.get_uri())
290         return d
291
292 class MutableDownloadable:
293     #implements(IDownloadable)
294     def __init__(self, size, node):
295         self.size = size
296         self.node = node
297     def get_size(self):
298         return self.size
299     def is_mutable(self):
300         return True
301     def read(self, consumer, offset=0, size=None):
302         d = self.node.download_best_version()
303         d.addCallback(self._got_data, consumer, offset, size)
304         return d
305     def _got_data(self, contents, consumer, offset, size):
306         start = offset
307         if size is not None:
308             end = offset+size
309         else:
310             end = self.size
311         # SDMF: we can write the whole file in one big chunk
312         consumer.write(contents[start:end])
313         return consumer
314
315 def makeMutableDownloadable(n):
316     d = defer.maybeDeferred(n.get_size_of_best_version)
317     d.addCallback(MutableDownloadable, n)
318     return d
319
320 class FileDownloader(rend.Page):
321     # since we override the rendering process (to let the tahoe Downloader
322     # drive things), we must inherit from regular old twisted.web.resource
323     # instead of nevow.rend.Page . Nevow will use adapters to wrap a
324     # nevow.appserver.OldResourceAdapter around any
325     # twisted.web.resource.IResource that it is given. TODO: it looks like
326     # that wrapper would allow us to return a Deferred from render(), which
327     # might could simplify the implementation of WebDownloadTarget.
328
329     def __init__(self, filenode, filename):
330         rend.Page.__init__(self)
331         self.filenode = filenode
332         self.filename = filename
333
334     def renderHTTP(self, ctx):
335         req = IRequest(ctx)
336         gte = static.getTypeAndEncoding
337         ctype, encoding = gte(self.filename,
338                               static.File.contentTypes,
339                               static.File.contentEncodings,
340                               defaultType="text/plain")
341         req.setHeader("content-type", ctype)
342         if encoding:
343             req.setHeader("content-encoding", encoding)
344
345         save_to_filename = None
346         if boolean_of_arg(get_arg(req, "save", "False")):
347             # tell the browser to save the file rather display it we don't
348             # try to encode the filename, instead we echo back the exact same
349             # bytes we were given in the URL. See the comment in
350             # FileNodeHandler.render_GET for the sad details.
351             req.setHeader("content-disposition",
352                           'attachment; filename="%s"' % self.filename)
353
354         filesize = self.filenode.get_size()
355         assert isinstance(filesize, (int,long)), filesize
356         offset, size = 0, None
357         contentsize = filesize
358         req.setHeader("accept-ranges", "bytes")
359         if not self.filenode.is_mutable():
360             # TODO: look more closely at Request.setETag and how it interacts
361             # with a conditional "if-etag-equals" request, I think this may
362             # need to occur after the setResponseCode below
363             si = self.filenode.get_storage_index()
364             if si:
365                 req.setETag(base32.b2a(si))
366         # TODO: for mutable files, use the roothash. For LIT, hash the data.
367         # or maybe just use the URI for CHK and LIT.
368         rangeheader = req.getHeader('range')
369         if rangeheader:
370             # adapted from nevow.static.File
371             bytesrange = rangeheader.split('=')
372             if bytesrange[0] != 'bytes':
373                 raise WebError("Syntactically invalid http range header!")
374             start, end = bytesrange[1].split('-')
375             if start:
376                 offset = int(start)
377                 if not end:
378                     # RFC 2616 says:
379                     #
380                     # "If the last-byte-pos value is absent, or if the value is
381                     # greater than or equal to the current length of the
382                     # entity-body, last-byte-pos is taken to be equal to one less
383                     # than the current length of the entity- body in bytes."
384                     end = filesize - 1
385                 size = int(end) - offset + 1
386             req.setResponseCode(http.PARTIAL_CONTENT)
387             req.setHeader('content-range',"bytes %s-%s/%s" %
388                           (str(offset), str(offset+size-1), str(filesize)))
389             contentsize = size
390         req.setHeader("content-length", str(contentsize))
391         if req.method == "HEAD":
392             return ""
393         d = self.filenode.read(req, offset, size)
394         def _error(f):
395             if req.startedWriting:
396                 # The content-type is already set, and the response code has
397                 # already been sent, so we can't provide a clean error
398                 # indication. We can emit text (which a browser might
399                 # interpret as something else), and if we sent a Size header,
400                 # they might notice that we've truncated the data. Keep the
401                 # error message small to improve the chances of having our
402                 # error response be shorter than the intended results.
403                 #
404                 # We don't have a lot of options, unfortunately.
405                 req.write("problem during download\n")
406             else:
407                 # We haven't written anything yet, so we can provide a
408                 # sensible error message.
409                 msg = str(f.type)
410                 msg.replace("\n", "|")
411                 req.setResponseCode(http.GONE, msg)
412                 req.setHeader("content-type", "text/plain")
413                 req.responseHeaders.setRawHeaders("content-encoding", [])
414                 req.responseHeaders.setRawHeaders("content-disposition", [])
415                 # TODO: HTML-formatted exception?
416                 req.write(str(f))
417         d.addErrback(_error)
418         d.addBoth(lambda ign: req.finish())
419         return req.deferred
420
421
422 def FileJSONMetadata(ctx, filenode):
423     if filenode.is_readonly():
424         rw_uri = None
425         ro_uri = filenode.get_uri()
426     else:
427         rw_uri = filenode.get_uri()
428         ro_uri = filenode.get_readonly_uri()
429     data = ("filenode", {})
430     data[1]['size'] = filenode.get_size()
431     if ro_uri:
432         data[1]['ro_uri'] = ro_uri
433     if rw_uri:
434         data[1]['rw_uri'] = rw_uri
435     data[1]['mutable'] = filenode.is_mutable()
436     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
437
438 def FileURI(ctx, filenode):
439     return text_plain(filenode.get_uri(), ctx)
440
441 def FileReadOnlyURI(ctx, filenode):
442     if filenode.is_readonly():
443         return text_plain(filenode.get_uri(), ctx)
444     return text_plain(filenode.get_readonly_uri(), ctx)
445
446 class FileNodeDownloadHandler(FileNodeHandler):
447     def childFactory(self, ctx, name):
448         return FileNodeDownloadHandler(self.node, name=name)