]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
web/filenode.py: rely on Request.notifyFinish. Closes #1366.
[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, SDMF_VERSION, MDMF_VERSION
10 from allmydata.monitor import Monitor
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.mutable.publish import MutableFileHandle
13 from allmydata.mutable.common import MODE_READ
14 from allmydata.util import log, base32
15 from allmydata.util.encodingutil import quote_output
16 from allmydata.blacklist import FileProhibited, ProhibitedNode
17
18 from allmydata.web.common import text_plain, WebError, RenderMixin, \
19      boolean_of_arg, get_arg, should_create_intermediate_directories, \
20      MyExceptionHandler, parse_replace_arg, parse_offset_arg, \
21      get_format, get_mutable_type
22 from allmydata.web.check_results import CheckResults, \
23      CheckAndRepairResults, LiteralCheckResults
24 from allmydata.web.info import MoreInfo
25
26 class ReplaceMeMixin:
27     def replace_me_with_a_child(self, req, client, replace):
28         # a new file is being uploaded in our place.
29         file_format = get_format(req, "CHK")
30         mutable_type = get_mutable_type(file_format)
31         if mutable_type is not None:
32             data = MutableFileHandle(req.content)
33             d = client.create_mutable_file(data, version=mutable_type)
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             assert file_format == "CHK"
42             uploadable = FileHandle(req.content, convergence=client.convergence)
43             d = self.parentnode.add_file(self.name, uploadable,
44                                          overwrite=replace)
45         def _done(filenode):
46             log.msg("webish upload complete",
47                     facility="tahoe.webish", level=log.NOISY, umid="TCjBGQ")
48             if self.node:
49                 # we've replaced an existing file (or modified a mutable
50                 # file), so the response code is 200
51                 req.setResponseCode(http.OK)
52             else:
53                 # we've created a new file, so the code is 201
54                 req.setResponseCode(http.CREATED)
55             return filenode.get_uri()
56         d.addCallback(_done)
57         return d
58
59     def replace_me_with_a_childcap(self, req, client, replace):
60         req.content.seek(0)
61         childcap = req.content.read()
62         childnode = client.create_node_from_uri(childcap, None, name=self.name)
63         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
64         d.addCallback(lambda res: childnode.get_uri())
65         return d
66
67
68     def replace_me_with_a_formpost(self, req, client, replace):
69         # create a new file, maybe mutable, maybe immutable
70         file_format = get_format(req, "CHK")
71         contents = req.fields["file"]
72         if file_format in ("SDMF", "MDMF"):
73             mutable_type = get_mutable_type(file_format)
74             uploadable = MutableFileHandle(contents.file)
75             d = client.create_mutable_file(uploadable, version=mutable_type)
76             def _uploaded(newnode):
77                 d2 = self.parentnode.set_node(self.name, newnode,
78                                               overwrite=replace)
79                 d2.addCallback(lambda res: newnode.get_uri())
80                 return d2
81             d.addCallback(_uploaded)
82             return d
83
84         uploadable = FileHandle(contents.file, convergence=client.convergence)
85         d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
86         d.addCallback(lambda newnode: newnode.get_uri())
87         return d
88
89
90 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
91     def __init__(self, client, parentnode, name):
92         rend.Page.__init__(self)
93         self.client = client
94         assert parentnode
95         self.parentnode = parentnode
96         self.name = name
97         self.node = None
98
99     def render_PUT(self, ctx):
100         req = IRequest(ctx)
101         t = get_arg(req, "t", "").strip()
102         replace = parse_replace_arg(get_arg(req, "replace", "true"))
103
104         assert self.parentnode and self.name
105         if req.getHeader("content-range"):
106             raise WebError("Content-Range in PUT not yet supported",
107                            http.NOT_IMPLEMENTED)
108         if not t:
109             return self.replace_me_with_a_child(req, self.client, replace)
110         if t == "uri":
111             return self.replace_me_with_a_childcap(req, self.client, 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(req, self.client, 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, client, node, parentnode=None, name=None):
140         rend.Page.__init__(self)
141         self.client = client
142         assert node
143         self.node = node
144         self.parentnode = parentnode
145         self.name = name
146
147     def childFactory(self, ctx, name):
148         req = IRequest(ctx)
149         if isinstance(self.node, ProhibitedNode):
150             raise FileProhibited(self.node.reason)
151         if should_create_intermediate_directories(req):
152             raise WebError("Cannot create directory %s, because its "
153                            "parent is a file, not a directory" % quote_output(name, encoding='utf-8'))
154         raise WebError("Files have no children, certainly not named %s"
155                        % quote_output(name, encoding='utf-8'))
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             # the filename arrives as part of the URL or in a form input
163             # element, and will be sent back in a Content-Disposition header.
164             # Different browsers use various character sets for this name,
165             # sometimes depending upon how language environment is
166             # configured. Firefox sends the equivalent of
167             # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
168             # latin-1. Browsers cannot agree on how to interpret the name
169             # they see in the Content-Disposition header either, despite some
170             # 11-year old standards (RFC2231) that explain how to do it
171             # properly. So we assume that at least the browser will agree
172             # with itself, and echo back the same bytes that we were given.
173             filename = get_arg(req, "filename", self.name) or "unknown"
174             d = self.node.get_best_readable_version()
175             d.addCallback(lambda dn: FileDownloader(dn, filename))
176             return d
177         if t == "json":
178             # We do this to make sure that fields like size and
179             # mutable-type (which depend on the file on the grid and not
180             # just on the cap) are filled in. The latter gets used in
181             # tests, in particular.
182             #
183             # TODO: Make it so that the servermap knows how to update in
184             # a mode specifically designed to fill in these fields, and
185             # then update it in that mode.
186             if self.node.is_mutable():
187                 d = self.node.get_servermap(MODE_READ)
188             else:
189                 d = defer.succeed(None)
190             if self.parentnode and self.name:
191                 d.addCallback(lambda ignored:
192                     self.parentnode.get_metadata_for(self.name))
193             else:
194                 d.addCallback(lambda ignored: None)
195             d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
196             return d
197         if t == "info":
198             return MoreInfo(self.node)
199         if t == "uri":
200             return FileURI(ctx, self.node)
201         if t == "readonly-uri":
202             return FileReadOnlyURI(ctx, self.node)
203         raise WebError("GET file: bad t=%s" % t)
204
205     def render_HEAD(self, ctx):
206         req = IRequest(ctx)
207         t = get_arg(req, "t", "").strip()
208         if t:
209             raise WebError("GET file: bad t=%s" % t)
210         filename = get_arg(req, "filename", self.name) or "unknown"
211         d = self.node.get_best_readable_version()
212         d.addCallback(lambda dn: FileDownloader(dn, filename))
213         return d
214
215     def render_PUT(self, ctx):
216         req = IRequest(ctx)
217         t = get_arg(req, "t", "").strip()
218         replace = parse_replace_arg(get_arg(req, "replace", "true"))
219         offset = parse_offset_arg(get_arg(req, "offset", None))
220
221         if not t:
222             if not replace:
223                 # this is the early trap: if someone else modifies the
224                 # directory while we're uploading, the add_file(overwrite=)
225                 # call in replace_me_with_a_child will do the late trap.
226                 raise ExistingChildError()
227
228             if self.node.is_mutable():
229                 # Are we a readonly filenode? We shouldn't allow callers
230                 # to try to replace us if we are.
231                 if self.node.is_readonly():
232                     raise WebError("PUT to a mutable file: replace or update"
233                                    " requested with read-only cap")
234                 if offset is None:
235                     return self.replace_my_contents(req)
236
237                 if offset >= 0:
238                     return self.update_my_contents(req, offset)
239
240                 raise WebError("PUT to a mutable file: Invalid offset")
241
242             else:
243                 if offset is not None:
244                     raise WebError("PUT to a file: append operation invoked "
245                                    "on an immutable cap")
246
247                 assert self.parentnode and self.name
248                 return self.replace_me_with_a_child(req, self.client, replace)
249
250         if t == "uri":
251             if not replace:
252                 raise ExistingChildError()
253             assert self.parentnode and self.name
254             return self.replace_me_with_a_childcap(req, self.client, replace)
255
256         raise WebError("PUT to a file: bad t=%s" % t)
257
258     def render_POST(self, ctx):
259         req = IRequest(ctx)
260         t = get_arg(req, "t", "").strip()
261         replace = boolean_of_arg(get_arg(req, "replace", "true"))
262         if t == "check":
263             d = self._POST_check(req)
264         elif t == "upload":
265             # like PUT, but get the file data from an HTML form's input field
266             # We could get here from POST /uri/mutablefilecap?t=upload,
267             # or POST /uri/path/file?t=upload, or
268             # POST /uri/path/dir?t=upload&name=foo . All have the same
269             # behavior, we just ignore any name= argument
270             if self.node.is_mutable():
271                 d = self.replace_my_contents_with_a_formpost(req)
272             else:
273                 if not replace:
274                     raise ExistingChildError()
275                 assert self.parentnode and self.name
276                 d = self.replace_me_with_a_formpost(req, self.client, replace)
277         else:
278             raise WebError("POST to file: bad t=%s" % t)
279
280         when_done = get_arg(req, "when_done", None)
281         if when_done:
282             d.addCallback(lambda res: url.URL.fromString(when_done))
283         return d
284
285     def _maybe_literal(self, res, Results_Class):
286         if res:
287             return Results_Class(self.client, res)
288         return LiteralCheckResults(self.client)
289
290     def _POST_check(self, req):
291         verify = boolean_of_arg(get_arg(req, "verify", "false"))
292         repair = boolean_of_arg(get_arg(req, "repair", "false"))
293         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
294         if repair:
295             d = self.node.check_and_repair(Monitor(), verify, add_lease)
296             d.addCallback(self._maybe_literal, CheckAndRepairResults)
297         else:
298             d = self.node.check(Monitor(), verify, add_lease)
299             d.addCallback(self._maybe_literal, CheckResults)
300         return d
301
302     def render_DELETE(self, ctx):
303         assert self.parentnode and self.name
304         d = self.parentnode.delete(self.name)
305         d.addCallback(lambda res: self.node.get_uri())
306         return d
307
308     def replace_my_contents(self, req):
309         req.content.seek(0)
310         new_contents = MutableFileHandle(req.content)
311         d = self.node.overwrite(new_contents)
312         d.addCallback(lambda res: self.node.get_uri())
313         return d
314
315
316     def update_my_contents(self, req, offset):
317         req.content.seek(0)
318         added_contents = MutableFileHandle(req.content)
319
320         d = self.node.get_best_mutable_version()
321         d.addCallback(lambda mv:
322             mv.update(added_contents, offset))
323         d.addCallback(lambda ignored:
324             self.node.get_uri())
325         return d
326
327
328     def replace_my_contents_with_a_formpost(self, req):
329         # we have a mutable file. Get the data from the formpost, and replace
330         # the mutable file's contents with it.
331         new_contents = req.fields['file']
332         new_contents = MutableFileHandle(new_contents.file)
333
334         d = self.node.overwrite(new_contents)
335         d.addCallback(lambda res: self.node.get_uri())
336         return d
337
338
339 class FileDownloader(rend.Page):
340     def __init__(self, filenode, filename):
341         rend.Page.__init__(self)
342         self.filenode = filenode
343         self.filename = filename
344
345     def parse_range_header(self, range):
346         # Parse a byte ranges according to RFC 2616 "14.35.1 Byte
347         # Ranges".  Returns None if the range doesn't make sense so it
348         # can be ignored (per the spec).  When successful, returns a
349         # list of (first,last) inclusive range tuples.
350
351         filesize = self.filenode.get_size()
352         assert isinstance(filesize, (int,long)), filesize
353
354         try:
355             # byte-ranges-specifier
356             units, rangeset = range.split('=', 1)
357             if units != 'bytes':
358                 return None     # nothing else supported
359
360             def parse_range(r):
361                 first, last = r.split('-', 1)
362
363                 if first is '':
364                     # suffix-byte-range-spec
365                     first = filesize - long(last)
366                     last = filesize - 1
367                 else:
368                     # byte-range-spec
369
370                     # first-byte-pos
371                     first = long(first)
372
373                     # last-byte-pos
374                     if last is '':
375                         last = filesize - 1
376                     else:
377                         last = long(last)
378
379                 if last < first:
380                     raise ValueError
381
382                 return (first, last)
383
384             # byte-range-set
385             #
386             # Note: the spec uses "1#" for the list of ranges, which
387             # implicitly allows whitespace around the ',' separators,
388             # so strip it.
389             return [ parse_range(r.strip()) for r in rangeset.split(',') ]
390         except ValueError:
391             return None
392
393     def renderHTTP(self, ctx):
394         req = IRequest(ctx)
395         gte = static.getTypeAndEncoding
396         ctype, encoding = gte(self.filename,
397                               static.File.contentTypes,
398                               static.File.contentEncodings,
399                               defaultType="text/plain")
400         req.setHeader("content-type", ctype)
401         if encoding:
402             req.setHeader("content-encoding", encoding)
403
404         if boolean_of_arg(get_arg(req, "save", "False")):
405             # tell the browser to save the file rather display it we don't
406             # try to encode the filename, instead we echo back the exact same
407             # bytes we were given in the URL. See the comment in
408             # FileNodeHandler.render_GET for the sad details.
409             req.setHeader("content-disposition",
410                           'attachment; filename="%s"' % self.filename)
411
412         filesize = self.filenode.get_size()
413         assert isinstance(filesize, (int,long)), filesize
414         first, size = 0, None
415         contentsize = filesize
416         req.setHeader("accept-ranges", "bytes")
417         if not self.filenode.is_mutable():
418             # TODO: look more closely at Request.setETag and how it interacts
419             # with a conditional "if-etag-equals" request, I think this may
420             # need to occur after the setResponseCode below
421             si = self.filenode.get_storage_index()
422             if si:
423                 req.setETag(base32.b2a(si))
424         # TODO: for mutable files, use the roothash. For LIT, hash the data.
425         # or maybe just use the URI for CHK and LIT.
426         rangeheader = req.getHeader('range')
427         if rangeheader:
428             ranges = self.parse_range_header(rangeheader)
429
430             # ranges = None means the header didn't parse, so ignore
431             # the header as if it didn't exist.  If is more than one
432             # range, then just return the first for now, until we can
433             # generate multipart/byteranges.
434             if ranges is not None:
435                 first, last = ranges[0]
436
437                 if first >= filesize:
438                     raise WebError('First beyond end of file',
439                                    http.REQUESTED_RANGE_NOT_SATISFIABLE)
440                 else:
441                     first = max(0, first)
442                     last = min(filesize-1, last)
443
444                     req.setResponseCode(http.PARTIAL_CONTENT)
445                     req.setHeader('content-range',"bytes %s-%s/%s" %
446                                   (str(first), str(last),
447                                    str(filesize)))
448                     contentsize = last - first + 1
449                     size = contentsize
450
451         req.setHeader("content-length", str(contentsize))
452         if req.method == "HEAD":
453             return ""
454
455         finished = []
456         def _request_finished(ign):
457             finished.append(True)
458         req.notifyFinish().addBoth(_request_finished)
459
460         d = self.filenode.read(req, first, size)
461
462         def _finished(ign):
463             if not finished:
464                 req.finish()
465         def _error(f):
466             lp = log.msg("error during GET", facility="tahoe.webish", failure=f,
467                          level=log.UNUSUAL, umid="xSiF3w")
468             if finished:
469                 log.msg("but it's too late to tell them", parent=lp,
470                         level=log.UNUSUAL, umid="j1xIbw")
471                 return
472             req._tahoe_request_had_error = f # for HTTP-style logging
473             if req.startedWriting:
474                 # The content-type is already set, and the response code has
475                 # already been sent, so we can't provide a clean error
476                 # indication. We can emit text (which a browser might
477                 # interpret as something else), and if we sent a Size header,
478                 # they might notice that we've truncated the data. Keep the
479                 # error message small to improve the chances of having our
480                 # error response be shorter than the intended results.
481                 #
482                 # We don't have a lot of options, unfortunately.
483                 req.write("problem during download\n")
484                 req.finish()
485             else:
486                 # We haven't written anything yet, so we can provide a
487                 # sensible error message.
488                 eh = MyExceptionHandler()
489                 eh.renderHTTP_exception(ctx, f)
490         d.addCallbacks(_finished, _error)
491         return req.deferred
492
493
494 def FileJSONMetadata(ctx, filenode, edge_metadata):
495     rw_uri = filenode.get_write_uri()
496     ro_uri = filenode.get_readonly_uri()
497     data = ("filenode", {})
498     data[1]['size'] = filenode.get_size()
499     if ro_uri:
500         data[1]['ro_uri'] = ro_uri
501     if rw_uri:
502         data[1]['rw_uri'] = rw_uri
503     verifycap = filenode.get_verify_cap()
504     if verifycap:
505         data[1]['verify_uri'] = verifycap.to_string()
506     data[1]['mutable'] = filenode.is_mutable()
507     if edge_metadata is not None:
508         data[1]['metadata'] = edge_metadata
509
510     if filenode.is_mutable():
511         mutable_type = filenode.get_version()
512         assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
513         if mutable_type == MDMF_VERSION:
514             file_format = "MDMF"
515         else:
516             file_format = "SDMF"
517     else:
518         file_format = "CHK"
519     data[1]['format'] = file_format
520
521     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
522
523 def FileURI(ctx, filenode):
524     return text_plain(filenode.get_uri(), ctx)
525
526 def FileReadOnlyURI(ctx, filenode):
527     if filenode.is_readonly():
528         return text_plain(filenode.get_uri(), ctx)
529     return text_plain(filenode.get_readonly_uri(), ctx)
530
531 class FileNodeDownloadHandler(FileNodeHandler):
532     def childFactory(self, ctx, name):
533         return FileNodeDownloadHandler(self.client, self.node, name=name)