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