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