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
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 get_format, get_mutable_type, get_filenode_metadata
22 from allmydata.web.check_results import CheckResultsRenderer, \
23 CheckAndRepairResultsRenderer, LiteralCheckResultsRenderer
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 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,
37 d2.addCallback(lambda res: newnode)
39 d.addCallback(_uploaded)
41 assert file_format == "CHK"
42 uploadable = FileHandle(req.content, convergence=client.convergence)
43 d = self.parentnode.add_file(self.name, uploadable,
46 log.msg("webish upload complete",
47 facility="tahoe.webish", level=log.NOISY, umid="TCjBGQ")
49 # we've replaced an existing file (or modified a mutable
50 # file), so the response code is 200
51 req.setResponseCode(http.OK)
53 # we've created a new file, so the code is 201
54 req.setResponseCode(http.CREATED)
55 return filenode.get_uri()
59 def replace_me_with_a_childcap(self, req, client, replace):
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())
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,
79 d2.addCallback(lambda res: newnode.get_uri())
81 d.addCallback(_uploaded)
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())
90 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
91 def __init__(self, client, parentnode, name):
92 rend.Page.__init__(self)
95 self.parentnode = parentnode
99 def render_PUT(self, ctx):
101 t = get_arg(req, "t", "").strip()
102 replace = parse_replace_arg(get_arg(req, "replace", "true"))
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)
109 return self.replace_me_with_a_child(req, self.client, replace)
111 return self.replace_me_with_a_childcap(req, self.client, replace)
113 raise WebError("PUT to a file: bad t=%s" % t)
115 def render_POST(self, ctx):
117 t = get_arg(req, "t", "").strip()
118 replace = boolean_of_arg(get_arg(req, "replace", "true"))
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)
127 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
128 # there are no other t= values left to be handled by the
130 raise WebError("POST to a file: bad t=%s" % t)
132 when_done = get_arg(req, "when_done", None)
134 d.addCallback(lambda res: url.URL.fromString(when_done))
138 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
139 def __init__(self, client, node, parentnode=None, name=None):
140 rend.Page.__init__(self)
144 self.parentnode = parentnode
147 def childFactory(self, ctx, name):
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'))
157 def render_GET(self, ctx):
159 t = get_arg(req, "t", "").strip()
161 # t=info contains variable ophandles, so is not allowed an ETag.
162 FIXED_OUTPUT_TYPES = ["", "json", "uri", "readonly-uri"]
163 if not self.node.is_mutable() and t in FIXED_OUTPUT_TYPES:
164 # if the client already has the ETag then we can
165 # short-circuit the whole process.
166 si = self.node.get_storage_index()
167 if si and req.setETag('%s-%s' % (base32.b2a(si), t or "")):
171 # just get the contents
172 # the filename arrives as part of the URL or in a form input
173 # element, and will be sent back in a Content-Disposition header.
174 # Different browsers use various character sets for this name,
175 # sometimes depending upon how language environment is
176 # configured. Firefox sends the equivalent of
177 # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
178 # latin-1. Browsers cannot agree on how to interpret the name
179 # they see in the Content-Disposition header either, despite some
180 # 11-year old standards (RFC2231) that explain how to do it
181 # properly. So we assume that at least the browser will agree
182 # with itself, and echo back the same bytes that we were given.
183 filename = get_arg(req, "filename", self.name) or "unknown"
184 d = self.node.get_best_readable_version()
185 d.addCallback(lambda dn: FileDownloader(dn, filename))
188 # We do this to make sure that fields like size and
189 # mutable-type (which depend on the file on the grid and not
190 # just on the cap) are filled in. The latter gets used in
191 # tests, in particular.
193 # TODO: Make it so that the servermap knows how to update in
194 # a mode specifically designed to fill in these fields, and
195 # then update it in that mode.
196 if self.node.is_mutable():
197 d = self.node.get_servermap(MODE_READ)
199 d = defer.succeed(None)
200 if self.parentnode and self.name:
201 d.addCallback(lambda ignored:
202 self.parentnode.get_metadata_for(self.name))
204 d.addCallback(lambda ignored: None)
205 d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
208 return MoreInfo(self.node)
210 return FileURI(ctx, self.node)
211 if t == "readonly-uri":
212 return FileReadOnlyURI(ctx, self.node)
213 raise WebError("GET file: bad t=%s" % t)
215 def render_HEAD(self, ctx):
217 t = get_arg(req, "t", "").strip()
219 raise WebError("HEAD file: bad t=%s" % t)
220 filename = get_arg(req, "filename", self.name) or "unknown"
221 d = self.node.get_best_readable_version()
222 d.addCallback(lambda dn: FileDownloader(dn, filename))
225 def render_PUT(self, ctx):
227 t = get_arg(req, "t", "").strip()
228 replace = parse_replace_arg(get_arg(req, "replace", "true"))
229 offset = parse_offset_arg(get_arg(req, "offset", None))
233 # this is the early trap: if someone else modifies the
234 # directory while we're uploading, the add_file(overwrite=)
235 # call in replace_me_with_a_child will do the late trap.
236 raise ExistingChildError()
238 if self.node.is_mutable():
239 # Are we a readonly filenode? We shouldn't allow callers
240 # to try to replace us if we are.
241 if self.node.is_readonly():
242 raise WebError("PUT to a mutable file: replace or update"
243 " requested with read-only cap")
245 return self.replace_my_contents(req)
248 return self.update_my_contents(req, offset)
250 raise WebError("PUT to a mutable file: Invalid offset")
253 if offset is not None:
254 raise WebError("PUT to a file: append operation invoked "
255 "on an immutable cap")
257 assert self.parentnode and self.name
258 return self.replace_me_with_a_child(req, self.client, replace)
262 raise ExistingChildError()
263 assert self.parentnode and self.name
264 return self.replace_me_with_a_childcap(req, self.client, replace)
266 raise WebError("PUT to a file: bad t=%s" % t)
268 def render_POST(self, ctx):
270 t = get_arg(req, "t", "").strip()
271 replace = boolean_of_arg(get_arg(req, "replace", "true"))
273 d = self._POST_check(req)
275 # like PUT, but get the file data from an HTML form's input field
276 # We could get here from POST /uri/mutablefilecap?t=upload,
277 # or POST /uri/path/file?t=upload, or
278 # POST /uri/path/dir?t=upload&name=foo . All have the same
279 # behavior, we just ignore any name= argument
280 if self.node.is_mutable():
281 d = self.replace_my_contents_with_a_formpost(req)
284 raise ExistingChildError()
285 assert self.parentnode and self.name
286 d = self.replace_me_with_a_formpost(req, self.client, replace)
288 raise WebError("POST to file: bad t=%s" % t)
290 when_done = get_arg(req, "when_done", None)
292 d.addCallback(lambda res: url.URL.fromString(when_done))
295 def _maybe_literal(self, res, Results_Class):
297 return Results_Class(self.client, res)
298 return LiteralCheckResultsRenderer(self.client)
300 def _POST_check(self, req):
301 verify = boolean_of_arg(get_arg(req, "verify", "false"))
302 repair = boolean_of_arg(get_arg(req, "repair", "false"))
303 add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
305 d = self.node.check_and_repair(Monitor(), verify, add_lease)
306 d.addCallback(self._maybe_literal, CheckAndRepairResultsRenderer)
308 d = self.node.check(Monitor(), verify, add_lease)
309 d.addCallback(self._maybe_literal, CheckResultsRenderer)
312 def render_DELETE(self, ctx):
313 assert self.parentnode and self.name
314 d = self.parentnode.delete(self.name)
315 d.addCallback(lambda res: self.node.get_uri())
318 def replace_my_contents(self, req):
320 new_contents = MutableFileHandle(req.content)
321 d = self.node.overwrite(new_contents)
322 d.addCallback(lambda res: self.node.get_uri())
326 def update_my_contents(self, req, offset):
328 added_contents = MutableFileHandle(req.content)
330 d = self.node.get_best_mutable_version()
331 d.addCallback(lambda mv:
332 mv.update(added_contents, offset))
333 d.addCallback(lambda ignored:
338 def replace_my_contents_with_a_formpost(self, req):
339 # we have a mutable file. Get the data from the formpost, and replace
340 # the mutable file's contents with it.
341 new_contents = req.fields['file']
342 new_contents = MutableFileHandle(new_contents.file)
344 d = self.node.overwrite(new_contents)
345 d.addCallback(lambda res: self.node.get_uri())
349 class FileDownloader(rend.Page):
350 def __init__(self, filenode, filename):
351 rend.Page.__init__(self)
352 self.filenode = filenode
353 self.filename = filename
355 def parse_range_header(self, range):
356 # Parse a byte ranges according to RFC 2616 "14.35.1 Byte
357 # Ranges". Returns None if the range doesn't make sense so it
358 # can be ignored (per the spec). When successful, returns a
359 # list of (first,last) inclusive range tuples.
361 filesize = self.filenode.get_size()
362 assert isinstance(filesize, (int,long)), filesize
365 # byte-ranges-specifier
366 units, rangeset = range.split('=', 1)
368 return None # nothing else supported
371 first, last = r.split('-', 1)
374 # suffix-byte-range-spec
375 first = filesize - long(last)
396 # Note: the spec uses "1#" for the list of ranges, which
397 # implicitly allows whitespace around the ',' separators,
399 return [ parse_range(r.strip()) for r in rangeset.split(',') ]
403 def renderHTTP(self, ctx):
405 gte = static.getTypeAndEncoding
406 ctype, encoding = gte(self.filename,
407 static.File.contentTypes,
408 static.File.contentEncodings,
409 defaultType="text/plain")
410 req.setHeader("content-type", ctype)
412 req.setHeader("content-encoding", encoding)
414 if boolean_of_arg(get_arg(req, "save", "False")):
415 # tell the browser to save the file rather display it we don't
416 # try to encode the filename, instead we echo back the exact same
417 # bytes we were given in the URL. See the comment in
418 # FileNodeHandler.render_GET for the sad details.
419 req.setHeader("content-disposition",
420 'attachment; filename="%s"' % self.filename)
422 filesize = self.filenode.get_size()
423 assert isinstance(filesize, (int,long)), filesize
424 first, size = 0, None
425 contentsize = filesize
426 req.setHeader("accept-ranges", "bytes")
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", b"%d" % contentsize)
456 if req.method == "HEAD":
460 def _request_finished(ign):
461 finished.append(True)
462 req.notifyFinish().addBoth(_request_finished)
464 d = self.filenode.read(req, first, size)
470 lp = log.msg("error during GET", facility="tahoe.webish", failure=f,
471 level=log.UNUSUAL, umid="xSiF3w")
473 log.msg("but it's too late to tell them", parent=lp,
474 level=log.UNUSUAL, umid="j1xIbw")
476 req._tahoe_request_had_error = f # for HTTP-style logging
477 if req.startedWriting:
478 # The content-type is already set, and the response code has
479 # already been sent, so we can't provide a clean error
480 # indication. We can emit text (which a browser might
481 # interpret as something else), and if we sent a Size header,
482 # they might notice that we've truncated the data. Keep the
483 # error message small to improve the chances of having our
484 # error response be shorter than the intended results.
486 # We don't have a lot of options, unfortunately.
487 req.write("problem during download\n")
490 # We haven't written anything yet, so we can provide a
491 # sensible error message.
492 eh = MyExceptionHandler()
493 eh.renderHTTP_exception(ctx, f)
494 d.addCallbacks(_finished, _error)
498 def FileJSONMetadata(ctx, filenode, edge_metadata):
499 rw_uri = filenode.get_write_uri()
500 ro_uri = filenode.get_readonly_uri()
501 data = ("filenode", get_filenode_metadata(filenode))
503 data[1]['ro_uri'] = ro_uri
505 data[1]['rw_uri'] = rw_uri
506 verifycap = filenode.get_verify_cap()
508 data[1]['verify_uri'] = verifycap.to_string()
509 if edge_metadata is not None:
510 data[1]['metadata'] = edge_metadata
512 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
514 def FileURI(ctx, filenode):
515 return text_plain(filenode.get_uri(), ctx)
517 def FileReadOnlyURI(ctx, filenode):
518 if filenode.is_readonly():
519 return text_plain(filenode.get_uri(), ctx)
520 return text_plain(filenode.get_readonly_uri(), ctx)
522 class FileNodeDownloadHandler(FileNodeHandler):
523 def childFactory(self, ctx, name):
524 return FileNodeDownloadHandler(self.client, self.node, name=name)