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
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
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
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"))
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)
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,
39 d2.addCallback(lambda res: newnode)
41 d.addCallback(_uploaded)
43 uploadable = FileHandle(req.content, convergence=client.convergence)
44 d = self.parentnode.add_file(self.name, uploadable,
47 log.msg("webish upload complete",
48 facility="tahoe.webish", level=log.NOISY, umid="TCjBGQ")
50 # we've replaced an existing file (or modified a mutable
51 # file), so the response code is 200
52 req.setResponseCode(http.OK)
54 # we've created a new file, so the code is 201
55 req.setResponseCode(http.CREATED)
56 return filenode.get_uri()
60 def replace_me_with_a_childcap(self, req, client, replace):
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())
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"))
73 # create an immutable file
74 contents = req.fields["file"]
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,
85 d2.addCallback(lambda res: newnode.get_uri())
87 d.addCallback(_uploaded)
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())
96 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
97 def __init__(self, client, parentnode, name):
98 rend.Page.__init__(self)
101 self.parentnode = parentnode
105 def render_PUT(self, ctx):
107 t = get_arg(req, "t", "").strip()
108 replace = parse_replace_arg(get_arg(req, "replace", "true"))
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)
115 return self.replace_me_with_a_child(req, self.client, replace)
117 return self.replace_me_with_a_childcap(req, self.client, replace)
119 raise WebError("PUT to a file: bad t=%s" % t)
121 def render_POST(self, ctx):
123 t = get_arg(req, "t", "").strip()
124 replace = boolean_of_arg(get_arg(req, "replace", "true"))
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)
133 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
134 # there are no other t= values left to be handled by the
136 raise WebError("POST to a file: bad t=%s" % t)
138 when_done = get_arg(req, "when_done", None)
140 d.addCallback(lambda res: url.URL.fromString(when_done))
144 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
145 def __init__(self, client, node, parentnode=None, name=None):
146 rend.Page.__init__(self)
150 self.parentnode = parentnode
153 def childFactory(self, ctx, name):
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'"
161 def render_GET(self, ctx):
163 t = get_arg(req, "t", "").strip()
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))
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.
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)
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))
198 d.addCallback(lambda ignored: None)
199 d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
202 return MoreInfo(self.node)
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)
209 def render_HEAD(self, ctx):
211 t = get_arg(req, "t", "").strip()
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))
219 def render_PUT(self, 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))
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()
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")
239 return self.replace_my_contents(req)
242 return self.update_my_contents(req, offset)
244 raise WebError("PUT to a mutable file: Invalid offset")
247 if offset is not None:
248 raise WebError("PUT to a file: append operation invoked "
249 "on an immutable cap")
251 assert self.parentnode and self.name
252 return self.replace_me_with_a_child(req, self.client, replace)
256 raise ExistingChildError()
257 assert self.parentnode and self.name
258 return self.replace_me_with_a_childcap(req, self.client, replace)
260 raise WebError("PUT to a file: bad t=%s" % t)
262 def render_POST(self, ctx):
264 t = get_arg(req, "t", "").strip()
265 replace = boolean_of_arg(get_arg(req, "replace", "true"))
267 d = self._POST_check(req)
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)
278 raise ExistingChildError()
279 assert self.parentnode and self.name
280 d = self.replace_me_with_a_formpost(req, self.client, replace)
282 raise WebError("POST to file: bad t=%s" % t)
284 when_done = get_arg(req, "when_done", None)
286 d.addCallback(lambda res: url.URL.fromString(when_done))
289 def _maybe_literal(self, res, Results_Class):
291 return Results_Class(self.client, res)
292 return LiteralCheckResults(self.client)
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"))
299 d = self.node.check_and_repair(Monitor(), verify, add_lease)
300 d.addCallback(self._maybe_literal, CheckAndRepairResults)
302 d = self.node.check(Monitor(), verify, add_lease)
303 d.addCallback(self._maybe_literal, CheckResults)
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())
312 def replace_my_contents(self, req):
314 new_contents = MutableFileHandle(req.content)
315 d = self.node.overwrite(new_contents)
316 d.addCallback(lambda res: self.node.get_uri())
320 def update_my_contents(self, req, offset):
322 added_contents = MutableFileHandle(req.content)
324 d = self.node.get_best_mutable_version()
325 d.addCallback(lambda mv:
326 mv.update(added_contents, offset))
327 d.addCallback(lambda ignored:
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)
338 d = self.node.overwrite(new_contents)
339 d.addCallback(lambda res: self.node.get_uri())
343 class FileDownloader(rend.Page):
344 def __init__(self, filenode, filename):
345 rend.Page.__init__(self)
346 self.filenode = filenode
347 self.filename = filename
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.
355 filesize = self.filenode.get_size()
356 assert isinstance(filesize, (int,long)), filesize
359 # byte-ranges-specifier
360 units, rangeset = range.split('=', 1)
362 return None # nothing else supported
365 first, last = r.split('-', 1)
368 # suffix-byte-range-spec
369 first = filesize - long(last)
390 # Note: the spec uses "1#" for the list of ranges, which
391 # implicitly allows whitespace around the ',' separators,
393 return [ parse_range(r.strip()) for r in rangeset.split(',') ]
397 def renderHTTP(self, 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)
406 req.setHeader("content-encoding", encoding)
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)
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()
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')
432 ranges = self.parse_range_header(rangeheader)
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]
441 if first >= filesize:
442 raise WebError('First beyond end of file',
443 http.REQUESTED_RANGE_NOT_SATISFIABLE)
445 first = max(0, first)
446 last = min(filesize-1, last)
448 req.setResponseCode(http.PARTIAL_CONTENT)
449 req.setHeader('content-range',"bytes %s-%s/%s" %
450 (str(first), str(last),
452 contentsize = last - first + 1
455 req.setHeader("content-length", str(contentsize))
456 if req.method == "HEAD":
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.
462 def _request_finished(ign):
463 finished.append(True)
464 if hasattr(req, "notifyFinish"):
465 req.notifyFinish().addBoth(_request_finished)
467 d = self.filenode.read(req, first, size)
473 lp = log.msg("error during GET", facility="tahoe.webish", failure=f,
474 level=log.UNUSUAL, umid="xSiF3w")
476 log.msg("but it's too late to tell them", parent=lp,
477 level=log.UNUSUAL, umid="j1xIbw")
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.
489 # We don't have a lot of options, unfortunately.
490 req.write("problem during download\n")
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)
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()
507 data[1]['ro_uri'] = ro_uri
509 data[1]['rw_uri'] = rw_uri
510 verifycap = filenode.get_verify_cap()
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
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"
523 mutable_type = "sdmf"
524 data[1]['mutable-type'] = mutable_type
526 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
528 def FileURI(ctx, filenode):
529 return text_plain(filenode.get_uri(), ctx)
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)
536 class FileNodeDownloadHandler(FileNodeHandler):
537 def childFactory(self, ctx, name):
538 return FileNodeDownloadHandler(self.client, self.node, name=name)