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.util import log, base32
14 from allmydata.web.common import text_plain, WebError, RenderMixin, \
15 boolean_of_arg, get_arg, should_create_intermediate_directories, \
16 MyExceptionHandler, parse_replace_arg
17 from allmydata.web.check_results import CheckResults, \
18 CheckAndRepairResults, LiteralCheckResults
19 from allmydata.web.info import MoreInfo
22 def replace_me_with_a_child(self, req, client, replace):
23 # a new file is being uploaded in our place.
24 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
27 data = req.content.read()
28 d = client.create_mutable_file(data)
29 def _uploaded(newnode):
30 d2 = self.parentnode.set_node(self.name, newnode,
32 d2.addCallback(lambda res: newnode)
34 d.addCallback(_uploaded)
36 uploadable = FileHandle(req.content, convergence=client.convergence)
37 d = self.parentnode.add_file(self.name, uploadable,
40 log.msg("webish upload complete",
41 facility="tahoe.webish", level=log.NOISY, umid="TCjBGQ")
43 # we've replaced an existing file (or modified a mutable
44 # file), so the response code is 200
45 req.setResponseCode(http.OK)
47 # we've created a new file, so the code is 201
48 req.setResponseCode(http.CREATED)
49 return filenode.get_uri()
53 def replace_me_with_a_childcap(self, req, client, replace):
55 childcap = req.content.read()
56 childnode = client.create_node_from_uri(childcap, None, name=self.name)
57 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
58 d.addCallback(lambda res: childnode.get_uri())
61 def _read_data_from_formpost(self, req):
62 # SDMF: files are small, and we can only upload data, so we read
63 # the whole file into memory before uploading.
64 contents = req.fields["file"]
66 data = contents.file.read()
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"))
74 data = self._read_data_from_formpost(req)
75 d = client.create_mutable_file(data)
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)
83 # create an immutable file
84 contents = req.fields["file"]
85 uploadable = FileHandle(contents.file, convergence=client.convergence)
86 d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
87 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 should_create_intermediate_directories(req):
150 raise WebError("Cannot create directory '%s', because its "
151 "parent is a file, not a directory" % name)
152 raise WebError("Files have no children, certainly not named '%s'"
155 def render_GET(self, ctx):
157 t = get_arg(req, "t", "").strip()
159 # just get the contents
160 # the filename arrives as part of the URL or in a form input
161 # element, and will be sent back in a Content-Disposition header.
162 # Different browsers use various character sets for this name,
163 # sometimes depending upon how language environment is
164 # configured. Firefox sends the equivalent of
165 # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
166 # latin-1. Browsers cannot agree on how to interpret the name
167 # they see in the Content-Disposition header either, despite some
168 # 11-year old standards (RFC2231) that explain how to do it
169 # properly. So we assume that at least the browser will agree
170 # with itself, and echo back the same bytes that we were given.
171 filename = get_arg(req, "filename", self.name) or "unknown"
172 if self.node.is_mutable():
173 # some day: d = self.node.get_best_version()
174 d = makeMutableDownloadable(self.node)
176 d = defer.succeed(self.node)
177 d.addCallback(lambda dn: FileDownloader(dn, filename))
180 if self.parentnode and self.name:
181 d = self.parentnode.get_metadata_for(self.name)
183 d = defer.succeed(None)
184 d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
187 return MoreInfo(self.node)
189 return FileURI(ctx, self.node)
190 if t == "readonly-uri":
191 return FileReadOnlyURI(ctx, self.node)
192 raise WebError("GET file: bad t=%s" % t)
194 def render_HEAD(self, ctx):
196 t = get_arg(req, "t", "").strip()
198 raise WebError("GET file: bad t=%s" % t)
199 filename = get_arg(req, "filename", self.name) or "unknown"
200 if self.node.is_mutable():
201 # some day: d = self.node.get_best_version()
202 d = makeMutableDownloadable(self.node)
204 d = defer.succeed(self.node)
205 d.addCallback(lambda dn: FileDownloader(dn, filename))
208 def render_PUT(self, ctx):
210 t = get_arg(req, "t", "").strip()
211 replace = parse_replace_arg(get_arg(req, "replace", "true"))
214 if self.node.is_mutable():
215 return self.replace_my_contents(req)
217 # this is the early trap: if someone else modifies the
218 # directory while we're uploading, the add_file(overwrite=)
219 # call in replace_me_with_a_child will do the late trap.
220 raise ExistingChildError()
221 assert self.parentnode and self.name
222 return self.replace_me_with_a_child(req, self.client, replace)
225 raise ExistingChildError()
226 assert self.parentnode and self.name
227 return self.replace_me_with_a_childcap(req, self.client, replace)
229 raise WebError("PUT to a file: bad t=%s" % t)
231 def render_POST(self, ctx):
233 t = get_arg(req, "t", "").strip()
234 replace = boolean_of_arg(get_arg(req, "replace", "true"))
236 d = self._POST_check(req)
238 # like PUT, but get the file data from an HTML form's input field
239 # We could get here from POST /uri/mutablefilecap?t=upload,
240 # or POST /uri/path/file?t=upload, or
241 # POST /uri/path/dir?t=upload&name=foo . All have the same
242 # behavior, we just ignore any name= argument
243 if self.node.is_mutable():
244 d = self.replace_my_contents_with_a_formpost(req)
247 raise ExistingChildError()
248 assert self.parentnode and self.name
249 d = self.replace_me_with_a_formpost(req, self.client, replace)
251 raise WebError("POST to file: bad t=%s" % t)
253 when_done = get_arg(req, "when_done", None)
255 d.addCallback(lambda res: url.URL.fromString(when_done))
258 def _maybe_literal(self, res, Results_Class):
260 return Results_Class(self.client, res)
261 return LiteralCheckResults(self.client)
263 def _POST_check(self, req):
264 verify = boolean_of_arg(get_arg(req, "verify", "false"))
265 repair = boolean_of_arg(get_arg(req, "repair", "false"))
266 add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
268 d = self.node.check_and_repair(Monitor(), verify, add_lease)
269 d.addCallback(self._maybe_literal, CheckAndRepairResults)
271 d = self.node.check(Monitor(), verify, add_lease)
272 d.addCallback(self._maybe_literal, CheckResults)
275 def render_DELETE(self, ctx):
276 assert self.parentnode and self.name
277 d = self.parentnode.delete(self.name)
278 d.addCallback(lambda res: self.node.get_uri())
281 def replace_my_contents(self, req):
283 new_contents = req.content.read()
284 d = self.node.overwrite(new_contents)
285 d.addCallback(lambda res: self.node.get_uri())
288 def replace_my_contents_with_a_formpost(self, req):
289 # we have a mutable file. Get the data from the formpost, and replace
290 # the mutable file's contents with it.
291 new_contents = self._read_data_from_formpost(req)
292 d = self.node.overwrite(new_contents)
293 d.addCallback(lambda res: self.node.get_uri())
296 class MutableDownloadable:
297 #implements(IDownloadable)
298 def __init__(self, size, node):
303 def is_mutable(self):
305 def read(self, consumer, offset=0, size=None):
306 d = self.node.download_best_version()
307 d.addCallback(self._got_data, consumer, offset, size)
309 def _got_data(self, contents, consumer, offset, size):
315 # SDMF: we can write the whole file in one big chunk
316 consumer.write(contents[start:end])
319 def makeMutableDownloadable(n):
320 d = defer.maybeDeferred(n.get_size_of_best_version)
321 d.addCallback(MutableDownloadable, n)
324 class FileDownloader(rend.Page):
325 # since we override the rendering process (to let the tahoe Downloader
326 # drive things), we must inherit from regular old twisted.web.resource
327 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
328 # nevow.appserver.OldResourceAdapter around any
329 # twisted.web.resource.IResource that it is given. TODO: it looks like
330 # that wrapper would allow us to return a Deferred from render(), which
331 # might could simplify the implementation of WebDownloadTarget.
333 def __init__(self, filenode, filename):
334 rend.Page.__init__(self)
335 self.filenode = filenode
336 self.filename = filename
338 def parse_range_header(self, range):
339 # Parse a byte ranges according to RFC 2616 "14.35.1 Byte
340 # Ranges". Returns None if the range doesn't make sense so it
341 # can be ignored (per the spec). When successful, returns a
342 # list of (first,last) inclusive range tuples.
344 filesize = self.filenode.get_size()
345 assert isinstance(filesize, (int,long)), filesize
348 # byte-ranges-specifier
349 units, rangeset = range.split('=', 1)
351 return None # nothing else supported
354 first, last = r.split('-', 1)
357 # suffix-byte-range-spec
358 first = filesize - long(last)
379 # Note: the spec uses "1#" for the list of ranges, which
380 # implicitly allows whitespace around the ',' separators,
382 return [ parse_range(r.strip()) for r in rangeset.split(',') ]
386 def renderHTTP(self, ctx):
388 gte = static.getTypeAndEncoding
389 ctype, encoding = gte(self.filename,
390 static.File.contentTypes,
391 static.File.contentEncodings,
392 defaultType="text/plain")
393 req.setHeader("content-type", ctype)
395 req.setHeader("content-encoding", encoding)
397 if boolean_of_arg(get_arg(req, "save", "False")):
398 # tell the browser to save the file rather display it we don't
399 # try to encode the filename, instead we echo back the exact same
400 # bytes we were given in the URL. See the comment in
401 # FileNodeHandler.render_GET for the sad details.
402 req.setHeader("content-disposition",
403 'attachment; filename="%s"' % self.filename)
405 filesize = self.filenode.get_size()
406 assert isinstance(filesize, (int,long)), filesize
407 first, size = 0, None
408 contentsize = filesize
409 req.setHeader("accept-ranges", "bytes")
410 if not self.filenode.is_mutable():
411 # TODO: look more closely at Request.setETag and how it interacts
412 # with a conditional "if-etag-equals" request, I think this may
413 # need to occur after the setResponseCode below
414 si = self.filenode.get_storage_index()
416 req.setETag(base32.b2a(si))
417 # TODO: for mutable files, use the roothash. For LIT, hash the data.
418 # or maybe just use the URI for CHK and LIT.
419 rangeheader = req.getHeader('range')
421 ranges = self.parse_range_header(rangeheader)
423 # ranges = None means the header didn't parse, so ignore
424 # the header as if it didn't exist. If is more than one
425 # range, then just return the first for now, until we can
426 # generate multipart/byteranges.
427 if ranges is not None:
428 first, last = ranges[0]
430 if first >= filesize:
431 raise WebError('First beyond end of file',
432 http.REQUESTED_RANGE_NOT_SATISFIABLE)
434 first = max(0, first)
435 last = min(filesize-1, last)
437 req.setResponseCode(http.PARTIAL_CONTENT)
438 req.setHeader('content-range',"bytes %s-%s/%s" %
439 (str(first), str(last),
441 contentsize = last - first + 1
444 req.setHeader("content-length", str(contentsize))
445 if req.method == "HEAD":
447 d = self.filenode.read(req, first, size)
449 log.msg("error during GET", facility="tahoe.webish", failure=f,
450 level=log.UNUSUAL, umid="xSiF3w")
451 req._tahoe_request_had_error = f # for HTTP-style logging
452 if req.startedWriting:
453 # The content-type is already set, and the response code has
454 # already been sent, so we can't provide a clean error
455 # indication. We can emit text (which a browser might
456 # interpret as something else), and if we sent a Size header,
457 # they might notice that we've truncated the data. Keep the
458 # error message small to improve the chances of having our
459 # error response be shorter than the intended results.
461 # We don't have a lot of options, unfortunately.
462 req.write("problem during download\n")
465 # We haven't written anything yet, so we can provide a
466 # sensible error message.
467 eh = MyExceptionHandler()
468 eh.renderHTTP_exception(ctx, f)
469 d.addCallbacks(lambda ign: req.finish(), _error)
473 def FileJSONMetadata(ctx, filenode, edge_metadata):
474 rw_uri = filenode.get_write_uri()
475 ro_uri = filenode.get_readonly_uri()
476 data = ("filenode", {})
477 data[1]['size'] = filenode.get_size()
479 data[1]['ro_uri'] = ro_uri
481 data[1]['rw_uri'] = rw_uri
482 verifycap = filenode.get_verify_cap()
484 data[1]['verify_uri'] = verifycap.to_string()
485 data[1]['mutable'] = filenode.is_mutable()
486 if edge_metadata is not None:
487 data[1]['metadata'] = edge_metadata
488 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
490 def FileURI(ctx, filenode):
491 return text_plain(filenode.get_uri(), ctx)
493 def FileReadOnlyURI(ctx, filenode):
494 if filenode.is_readonly():
495 return text_plain(filenode.get_uri(), ctx)
496 return text_plain(filenode.get_readonly_uri(), ctx)
498 class FileNodeDownloadHandler(FileNodeHandler):
499 def childFactory(self, ctx, name):
500 return FileNodeDownloadHandler(self.client, self.node, name=name)