]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
9fd44022a61404a2ac843140a2a0bb313676623b
[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, CannotPackUnknownNodeError
10 from allmydata.monitor import Monitor
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.unknown import UnknownNode
13 from allmydata.util import log, base32
14
15 from allmydata.web.common import text_plain, WebError, RenderMixin, \
16      boolean_of_arg, get_arg, should_create_intermediate_directories, \
17      MyExceptionHandler, parse_replace_arg
18 from allmydata.web.check_results import CheckResults, \
19      CheckAndRepairResults, LiteralCheckResults
20 from allmydata.web.info import MoreInfo
21
22 class ReplaceMeMixin:
23
24     def replace_me_with_a_child(self, req, client, replace):
25         # a new file is being uploaded in our place.
26         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
27         if mutable:
28             req.content.seek(0)
29             data = req.content.read()
30             d = client.create_mutable_file(data)
31             def _uploaded(newnode):
32                 d2 = self.parentnode.set_node(self.name, newnode,
33                                               overwrite=replace)
34                 d2.addCallback(lambda res: newnode)
35                 return d2
36             d.addCallback(_uploaded)
37         else:
38             uploadable = FileHandle(req.content, convergence=client.convergence)
39             d = self.parentnode.add_file(self.name, uploadable,
40                                          overwrite=replace)
41         def _done(filenode):
42             log.msg("webish upload complete",
43                     facility="tahoe.webish", level=log.NOISY)
44             if self.node:
45                 # we've replaced an existing file (or modified a mutable
46                 # file), so the response code is 200
47                 req.setResponseCode(http.OK)
48             else:
49                 # we've created a new file, so the code is 201
50                 req.setResponseCode(http.CREATED)
51             return filenode.get_uri()
52         d.addCallback(_done)
53         return d
54
55     def replace_me_with_a_childcap(self, req, client, replace):
56         req.content.seek(0)
57         childcap = req.content.read()
58         childnode = client.create_node_from_uri(childcap, childcap+"readonly")
59         if isinstance(childnode, UnknownNode):
60             # don't be willing to pack unknown nodes: we might accidentally
61             # put some write-authority into the rocap slot because we don't
62             # know how to diminish the URI they gave us. We don't even know
63             # if they gave us a readcap or a writecap.
64             msg = "cannot attach unknown node as child %s" % str(self.name)
65             raise CannotPackUnknownNodeError(msg)
66         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
67         d.addCallback(lambda res: childnode.get_uri())
68         return d
69
70     def _read_data_from_formpost(self, req):
71         # SDMF: files are small, and we can only upload data, so we read
72         # the whole file into memory before uploading.
73         contents = req.fields["file"]
74         contents.file.seek(0)
75         data = contents.file.read()
76         return data
77
78     def replace_me_with_a_formpost(self, req, client, replace):
79         # create a new file, maybe mutable, maybe immutable
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, client, parentnode, name):
101         rend.Page.__init__(self)
102         self.client = client
103         assert parentnode
104         self.parentnode = parentnode
105         self.name = name
106         self.node = None
107
108     def render_PUT(self, ctx):
109         req = IRequest(ctx)
110         t = get_arg(req, "t", "").strip()
111         replace = parse_replace_arg(get_arg(req, "replace", "true"))
112
113         assert self.parentnode and self.name
114         if req.getHeader("content-range"):
115             raise WebError("Content-Range in PUT not yet supported",
116                            http.NOT_IMPLEMENTED)
117         if not t:
118             return self.replace_me_with_a_child(req, self.client, replace)
119         if t == "uri":
120             return self.replace_me_with_a_childcap(req, self.client, replace)
121
122         raise WebError("PUT to a file: bad t=%s" % t)
123
124     def render_POST(self, ctx):
125         req = IRequest(ctx)
126         t = get_arg(req, "t", "").strip()
127         replace = boolean_of_arg(get_arg(req, "replace", "true"))
128         if t == "upload":
129             # like PUT, but get the file data from an HTML form's input field.
130             # We could get here from POST /uri/mutablefilecap?t=upload,
131             # or POST /uri/path/file?t=upload, or
132             # POST /uri/path/dir?t=upload&name=foo . All have the same
133             # behavior, we just ignore any name= argument
134             d = self.replace_me_with_a_formpost(req, self.client, replace)
135         else:
136             # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
137             # there are no other t= values left to be handled by the
138             # placeholder.
139             raise WebError("POST to a file: bad t=%s" % t)
140
141         when_done = get_arg(req, "when_done", None)
142         if when_done:
143             d.addCallback(lambda res: url.URL.fromString(when_done))
144         return d
145
146
147 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
148     def __init__(self, client, node, parentnode=None, name=None):
149         rend.Page.__init__(self)
150         self.client = client
151         assert node
152         self.node = node
153         self.parentnode = parentnode
154         self.name = name
155
156     def childFactory(self, ctx, name):
157         req = IRequest(ctx)
158         if should_create_intermediate_directories(req):
159             raise WebError("Cannot create directory '%s', because its "
160                            "parent is a file, not a directory" % name)
161         raise WebError("Files have no children, certainly not named '%s'"
162                        % name)
163
164     def render_GET(self, ctx):
165         req = IRequest(ctx)
166         t = get_arg(req, "t", "").strip()
167         if not t:
168             # just get the contents
169             # the filename arrives as part of the URL or in a form input
170             # element, and will be sent back in a Content-Disposition header.
171             # Different browsers use various character sets for this name,
172             # sometimes depending upon how language environment is
173             # configured. Firefox sends the equivalent of
174             # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
175             # latin-1. Browsers cannot agree on how to interpret the name
176             # they see in the Content-Disposition header either, despite some
177             # 11-year old standards (RFC2231) that explain how to do it
178             # properly. So we assume that at least the browser will agree
179             # with itself, and echo back the same bytes that we were given.
180             filename = get_arg(req, "filename", self.name) or "unknown"
181             if self.node.is_mutable():
182                 # some day: d = self.node.get_best_version()
183                 d = makeMutableDownloadable(self.node)
184             else:
185                 d = defer.succeed(self.node)
186             d.addCallback(lambda dn: FileDownloader(dn, filename))
187             return d
188         if t == "json":
189             if self.parentnode and self.name:
190                 d = self.parentnode.get_metadata_for(self.name)
191             else:
192                 d = defer.succeed(None)
193             d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
194             return d
195         if t == "info":
196             return MoreInfo(self.node)
197         if t == "uri":
198             return FileURI(ctx, self.node)
199         if t == "readonly-uri":
200             return FileReadOnlyURI(ctx, self.node)
201         raise WebError("GET file: bad t=%s" % t)
202
203     def render_HEAD(self, ctx):
204         req = IRequest(ctx)
205         t = get_arg(req, "t", "").strip()
206         if t:
207             raise WebError("GET file: bad t=%s" % t)
208         filename = get_arg(req, "filename", self.name) or "unknown"
209         if self.node.is_mutable():
210             # some day: d = self.node.get_best_version()
211             d = makeMutableDownloadable(self.node)
212         else:
213             d = defer.succeed(self.node)
214         d.addCallback(lambda dn: FileDownloader(dn, filename))
215         return d
216
217     def render_PUT(self, ctx):
218         req = IRequest(ctx)
219         t = get_arg(req, "t", "").strip()
220         replace = parse_replace_arg(get_arg(req, "replace", "true"))
221
222         if not t:
223             if self.node.is_mutable():
224                 return self.replace_my_contents(req)
225             if not replace:
226                 # this is the early trap: if someone else modifies the
227                 # directory while we're uploading, the add_file(overwrite=)
228                 # call in replace_me_with_a_child will do the late trap.
229                 raise ExistingChildError()
230             assert self.parentnode and self.name
231             return self.replace_me_with_a_child(req, self.client, replace)
232         if t == "uri":
233             if not replace:
234                 raise ExistingChildError()
235             assert self.parentnode and self.name
236             return self.replace_me_with_a_childcap(req, self.client, replace)
237
238         raise WebError("PUT to a file: bad t=%s" % t)
239
240     def render_POST(self, ctx):
241         req = IRequest(ctx)
242         t = get_arg(req, "t", "").strip()
243         replace = boolean_of_arg(get_arg(req, "replace", "true"))
244         if t == "check":
245             d = self._POST_check(req)
246         elif t == "upload":
247             # like PUT, but get the file data from an HTML form's input field
248             # We could get here from POST /uri/mutablefilecap?t=upload,
249             # or POST /uri/path/file?t=upload, or
250             # POST /uri/path/dir?t=upload&name=foo . All have the same
251             # behavior, we just ignore any name= argument
252             if self.node.is_mutable():
253                 d = self.replace_my_contents_with_a_formpost(req)
254             else:
255                 if not replace:
256                     raise ExistingChildError()
257                 assert self.parentnode and self.name
258                 d = self.replace_me_with_a_formpost(req, self.client, replace)
259         else:
260             raise WebError("POST to file: bad t=%s" % t)
261
262         when_done = get_arg(req, "when_done", None)
263         if when_done:
264             d.addCallback(lambda res: url.URL.fromString(when_done))
265         return d
266
267     def _maybe_literal(self, res, Results_Class):
268         if res:
269             return Results_Class(self.client, res)
270         return LiteralCheckResults(self.client)
271
272     def _POST_check(self, req):
273         verify = boolean_of_arg(get_arg(req, "verify", "false"))
274         repair = boolean_of_arg(get_arg(req, "repair", "false"))
275         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
276         if repair:
277             d = self.node.check_and_repair(Monitor(), verify, add_lease)
278             d.addCallback(self._maybe_literal, CheckAndRepairResults)
279         else:
280             d = self.node.check(Monitor(), verify, add_lease)
281             d.addCallback(self._maybe_literal, CheckResults)
282         return d
283
284     def render_DELETE(self, ctx):
285         assert self.parentnode and self.name
286         d = self.parentnode.delete(self.name)
287         d.addCallback(lambda res: self.node.get_uri())
288         return d
289
290     def replace_my_contents(self, req):
291         req.content.seek(0)
292         new_contents = req.content.read()
293         d = self.node.overwrite(new_contents)
294         d.addCallback(lambda res: self.node.get_uri())
295         return d
296
297     def replace_my_contents_with_a_formpost(self, req):
298         # we have a mutable file. Get the data from the formpost, and replace
299         # the mutable file's contents with it.
300         new_contents = self._read_data_from_formpost(req)
301         d = self.node.overwrite(new_contents)
302         d.addCallback(lambda res: self.node.get_uri())
303         return d
304
305 class MutableDownloadable:
306     #implements(IDownloadable)
307     def __init__(self, size, node):
308         self.size = size
309         self.node = node
310     def get_size(self):
311         return self.size
312     def is_mutable(self):
313         return True
314     def read(self, consumer, offset=0, size=None):
315         d = self.node.download_best_version()
316         d.addCallback(self._got_data, consumer, offset, size)
317         return d
318     def _got_data(self, contents, consumer, offset, size):
319         start = offset
320         if size is not None:
321             end = offset+size
322         else:
323             end = self.size
324         # SDMF: we can write the whole file in one big chunk
325         consumer.write(contents[start:end])
326         return consumer
327
328 def makeMutableDownloadable(n):
329     d = defer.maybeDeferred(n.get_size_of_best_version)
330     d.addCallback(MutableDownloadable, n)
331     return d
332
333 class FileDownloader(rend.Page):
334     # since we override the rendering process (to let the tahoe Downloader
335     # drive things), we must inherit from regular old twisted.web.resource
336     # instead of nevow.rend.Page . Nevow will use adapters to wrap a
337     # nevow.appserver.OldResourceAdapter around any
338     # twisted.web.resource.IResource that it is given. TODO: it looks like
339     # that wrapper would allow us to return a Deferred from render(), which
340     # might could simplify the implementation of WebDownloadTarget.
341
342     def __init__(self, filenode, filename):
343         rend.Page.__init__(self)
344         self.filenode = filenode
345         self.filename = filename
346
347     def renderHTTP(self, ctx):
348         req = IRequest(ctx)
349         gte = static.getTypeAndEncoding
350         ctype, encoding = gte(self.filename,
351                               static.File.contentTypes,
352                               static.File.contentEncodings,
353                               defaultType="text/plain")
354         req.setHeader("content-type", ctype)
355         if encoding:
356             req.setHeader("content-encoding", encoding)
357
358         save_to_filename = None
359         if boolean_of_arg(get_arg(req, "save", "False")):
360             # tell the browser to save the file rather display it we don't
361             # try to encode the filename, instead we echo back the exact same
362             # bytes we were given in the URL. See the comment in
363             # FileNodeHandler.render_GET for the sad details.
364             req.setHeader("content-disposition",
365                           'attachment; filename="%s"' % self.filename)
366
367         filesize = self.filenode.get_size()
368         assert isinstance(filesize, (int,long)), filesize
369         offset, size = 0, None
370         contentsize = filesize
371         req.setHeader("accept-ranges", "bytes")
372         if not self.filenode.is_mutable():
373             # TODO: look more closely at Request.setETag and how it interacts
374             # with a conditional "if-etag-equals" request, I think this may
375             # need to occur after the setResponseCode below
376             si = self.filenode.get_storage_index()
377             if si:
378                 req.setETag(base32.b2a(si))
379         # TODO: for mutable files, use the roothash. For LIT, hash the data.
380         # or maybe just use the URI for CHK and LIT.
381         rangeheader = req.getHeader('range')
382         if rangeheader:
383             # adapted from nevow.static.File
384             bytesrange = rangeheader.split('=')
385             if bytesrange[0] != 'bytes':
386                 raise WebError("Syntactically invalid http range header!")
387             start, end = bytesrange[1].split('-')
388             if start:
389                 offset = int(start)
390                 if not end:
391                     # RFC 2616 says:
392                     #
393                     # "If the last-byte-pos value is absent, or if the value is
394                     # greater than or equal to the current length of the
395                     # entity-body, last-byte-pos is taken to be equal to one less
396                     # than the current length of the entity- body in bytes."
397                     end = filesize - 1
398                 size = int(end) - offset + 1
399             req.setResponseCode(http.PARTIAL_CONTENT)
400             req.setHeader('content-range',"bytes %s-%s/%s" %
401                           (str(offset), str(offset+size-1), str(filesize)))
402             contentsize = size
403         req.setHeader("content-length", str(contentsize))
404         if req.method == "HEAD":
405             return ""
406         d = self.filenode.read(req, offset, size)
407         def _error(f):
408             if req.startedWriting:
409                 # The content-type is already set, and the response code has
410                 # already been sent, so we can't provide a clean error
411                 # indication. We can emit text (which a browser might
412                 # interpret as something else), and if we sent a Size header,
413                 # they might notice that we've truncated the data. Keep the
414                 # error message small to improve the chances of having our
415                 # error response be shorter than the intended results.
416                 #
417                 # We don't have a lot of options, unfortunately.
418                 req.write("problem during download\n")
419                 req.finish()
420             else:
421                 # We haven't written anything yet, so we can provide a
422                 # sensible error message.
423                 eh = MyExceptionHandler()
424                 eh.renderHTTP_exception(ctx, f)
425         d.addCallbacks(lambda ign: req.finish(), _error)
426         return req.deferred
427
428
429 def FileJSONMetadata(ctx, filenode, edge_metadata):
430     if filenode.is_readonly():
431         rw_uri = None
432         ro_uri = filenode.get_uri()
433     else:
434         rw_uri = filenode.get_uri()
435         ro_uri = filenode.get_readonly_uri()
436     data = ("filenode", {})
437     data[1]['size'] = filenode.get_size()
438     if ro_uri:
439         data[1]['ro_uri'] = ro_uri
440     if rw_uri:
441         data[1]['rw_uri'] = rw_uri
442     verifycap = filenode.get_verify_cap()
443     if verifycap:
444         data[1]['verify_uri'] = verifycap.to_string()
445     data[1]['mutable'] = filenode.is_mutable()
446     if edge_metadata is not None:
447         data[1]['metadata'] = edge_metadata
448     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
449
450 def FileURI(ctx, filenode):
451     return text_plain(filenode.get_uri(), ctx)
452
453 def FileReadOnlyURI(ctx, filenode):
454     if filenode.is_readonly():
455         return text_plain(filenode.get_uri(), ctx)
456     return text_plain(filenode.get_readonly_uri(), ctx)
457
458 class FileNodeDownloadHandler(FileNodeHandler):
459     def childFactory(self, ctx, name):
460         return FileNodeDownloadHandler(self.client, self.node, name=name)