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, CannotPackUnknownNodeError
10 from allmydata.monitor import Monitor
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.unknown import UnknownNode
13 from allmydata.util import log, base32
15 from allmydata.web.common import text_plain, WebError, RenderMixin, \
16 boolean_of_arg, get_arg, should_create_intermediate_directories, \
17 MyExceptionHandler, parse_replace_arg
18 from allmydata.web.check_results import CheckResults, \
19 CheckAndRepairResults, LiteralCheckResults
20 from allmydata.web.info import MoreInfo
24 def replace_me_with_a_child(self, req, client, replace):
25 # a new file is being uploaded in our place.
26 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
29 data = req.content.read()
30 d = client.create_mutable_file(data)
31 def _uploaded(newnode):
32 d2 = self.parentnode.set_node(self.name, newnode,
34 d2.addCallback(lambda res: newnode)
36 d.addCallback(_uploaded)
38 uploadable = FileHandle(req.content, convergence=client.convergence)
39 d = self.parentnode.add_file(self.name, uploadable,
42 log.msg("webish upload complete",
43 facility="tahoe.webish", level=log.NOISY)
45 # we've replaced an existing file (or modified a mutable
46 # file), so the response code is 200
47 req.setResponseCode(http.OK)
49 # we've created a new file, so the code is 201
50 req.setResponseCode(http.CREATED)
51 return filenode.get_uri()
55 def replace_me_with_a_childcap(self, req, client, replace):
57 childcap = req.content.read()
58 childnode = client.create_node_from_uri(childcap, childcap+"readonly")
59 if isinstance(childnode, UnknownNode):
60 # don't be willing to pack unknown nodes: we might accidentally
61 # put some write-authority into the rocap slot because we don't
62 # know how to diminish the URI they gave us. We don't even know
63 # if they gave us a readcap or a writecap.
64 msg = "cannot attach unknown node as child %s" % str(self.name)
65 raise CannotPackUnknownNodeError(msg)
66 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
67 d.addCallback(lambda res: childnode.get_uri())
70 def _read_data_from_formpost(self, req):
71 # SDMF: files are small, and we can only upload data, so we read
72 # the whole file into memory before uploading.
73 contents = req.fields["file"]
75 data = contents.file.read()
78 def replace_me_with_a_formpost(self, req, client, replace):
79 # create a new file, maybe mutable, maybe immutable
80 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
83 data = self._read_data_from_formpost(req)
84 d = client.create_mutable_file(data)
85 def _uploaded(newnode):
86 d2 = self.parentnode.set_node(self.name, newnode,
88 d2.addCallback(lambda res: newnode.get_uri())
90 d.addCallback(_uploaded)
92 # create an immutable file
93 contents = req.fields["file"]
94 uploadable = FileHandle(contents.file, convergence=client.convergence)
95 d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
96 d.addCallback(lambda newnode: newnode.get_uri())
99 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
100 def __init__(self, client, parentnode, name):
101 rend.Page.__init__(self)
104 self.parentnode = parentnode
108 def render_PUT(self, ctx):
110 t = get_arg(req, "t", "").strip()
111 replace = parse_replace_arg(get_arg(req, "replace", "true"))
113 assert self.parentnode and self.name
114 if req.getHeader("content-range"):
115 raise WebError("Content-Range in PUT not yet supported",
116 http.NOT_IMPLEMENTED)
118 return self.replace_me_with_a_child(req, self.client, replace)
120 return self.replace_me_with_a_childcap(req, self.client, replace)
122 raise WebError("PUT to a file: bad t=%s" % t)
124 def render_POST(self, ctx):
126 t = get_arg(req, "t", "").strip()
127 replace = boolean_of_arg(get_arg(req, "replace", "true"))
129 # like PUT, but get the file data from an HTML form's input field.
130 # We could get here from POST /uri/mutablefilecap?t=upload,
131 # or POST /uri/path/file?t=upload, or
132 # POST /uri/path/dir?t=upload&name=foo . All have the same
133 # behavior, we just ignore any name= argument
134 d = self.replace_me_with_a_formpost(req, self.client, replace)
136 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
137 # there are no other t= values left to be handled by the
139 raise WebError("POST to a file: bad t=%s" % t)
141 when_done = get_arg(req, "when_done", None)
143 d.addCallback(lambda res: url.URL.fromString(when_done))
147 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
148 def __init__(self, client, node, parentnode=None, name=None):
149 rend.Page.__init__(self)
153 self.parentnode = parentnode
156 def childFactory(self, ctx, name):
158 if should_create_intermediate_directories(req):
159 raise WebError("Cannot create directory '%s', because its "
160 "parent is a file, not a directory" % name)
161 raise WebError("Files have no children, certainly not named '%s'"
164 def render_GET(self, ctx):
166 t = get_arg(req, "t", "").strip()
168 # just get the contents
169 # the filename arrives as part of the URL or in a form input
170 # element, and will be sent back in a Content-Disposition header.
171 # Different browsers use various character sets for this name,
172 # sometimes depending upon how language environment is
173 # configured. Firefox sends the equivalent of
174 # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
175 # latin-1. Browsers cannot agree on how to interpret the name
176 # they see in the Content-Disposition header either, despite some
177 # 11-year old standards (RFC2231) that explain how to do it
178 # properly. So we assume that at least the browser will agree
179 # with itself, and echo back the same bytes that we were given.
180 filename = get_arg(req, "filename", self.name) or "unknown"
181 if self.node.is_mutable():
182 # some day: d = self.node.get_best_version()
183 d = makeMutableDownloadable(self.node)
185 d = defer.succeed(self.node)
186 d.addCallback(lambda dn: FileDownloader(dn, filename))
189 if self.parentnode and self.name:
190 d = self.parentnode.get_metadata_for(self.name)
192 d = defer.succeed(None)
193 d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
196 return MoreInfo(self.node)
198 return FileURI(ctx, self.node)
199 if t == "readonly-uri":
200 return FileReadOnlyURI(ctx, self.node)
201 raise WebError("GET file: bad t=%s" % t)
203 def render_HEAD(self, ctx):
205 t = get_arg(req, "t", "").strip()
207 raise WebError("GET file: bad t=%s" % t)
208 filename = get_arg(req, "filename", self.name) or "unknown"
209 if self.node.is_mutable():
210 # some day: d = self.node.get_best_version()
211 d = makeMutableDownloadable(self.node)
213 d = defer.succeed(self.node)
214 d.addCallback(lambda dn: FileDownloader(dn, filename))
217 def render_PUT(self, ctx):
219 t = get_arg(req, "t", "").strip()
220 replace = parse_replace_arg(get_arg(req, "replace", "true"))
223 if self.node.is_mutable():
224 return self.replace_my_contents(req)
226 # this is the early trap: if someone else modifies the
227 # directory while we're uploading, the add_file(overwrite=)
228 # call in replace_me_with_a_child will do the late trap.
229 raise ExistingChildError()
230 assert self.parentnode and self.name
231 return self.replace_me_with_a_child(req, self.client, replace)
234 raise ExistingChildError()
235 assert self.parentnode and self.name
236 return self.replace_me_with_a_childcap(req, self.client, replace)
238 raise WebError("PUT to a file: bad t=%s" % t)
240 def render_POST(self, ctx):
242 t = get_arg(req, "t", "").strip()
243 replace = boolean_of_arg(get_arg(req, "replace", "true"))
245 d = self._POST_check(req)
247 # like PUT, but get the file data from an HTML form's input field
248 # We could get here from POST /uri/mutablefilecap?t=upload,
249 # or POST /uri/path/file?t=upload, or
250 # POST /uri/path/dir?t=upload&name=foo . All have the same
251 # behavior, we just ignore any name= argument
252 if self.node.is_mutable():
253 d = self.replace_my_contents_with_a_formpost(req)
256 raise ExistingChildError()
257 assert self.parentnode and self.name
258 d = self.replace_me_with_a_formpost(req, self.client, replace)
260 raise WebError("POST to file: bad t=%s" % t)
262 when_done = get_arg(req, "when_done", None)
264 d.addCallback(lambda res: url.URL.fromString(when_done))
267 def _maybe_literal(self, res, Results_Class):
269 return Results_Class(self.client, res)
270 return LiteralCheckResults(self.client)
272 def _POST_check(self, req):
273 verify = boolean_of_arg(get_arg(req, "verify", "false"))
274 repair = boolean_of_arg(get_arg(req, "repair", "false"))
275 add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
277 d = self.node.check_and_repair(Monitor(), verify, add_lease)
278 d.addCallback(self._maybe_literal, CheckAndRepairResults)
280 d = self.node.check(Monitor(), verify, add_lease)
281 d.addCallback(self._maybe_literal, CheckResults)
284 def render_DELETE(self, ctx):
285 assert self.parentnode and self.name
286 d = self.parentnode.delete(self.name)
287 d.addCallback(lambda res: self.node.get_uri())
290 def replace_my_contents(self, req):
292 new_contents = req.content.read()
293 d = self.node.overwrite(new_contents)
294 d.addCallback(lambda res: self.node.get_uri())
297 def replace_my_contents_with_a_formpost(self, req):
298 # we have a mutable file. Get the data from the formpost, and replace
299 # the mutable file's contents with it.
300 new_contents = self._read_data_from_formpost(req)
301 d = self.node.overwrite(new_contents)
302 d.addCallback(lambda res: self.node.get_uri())
305 class MutableDownloadable:
306 #implements(IDownloadable)
307 def __init__(self, size, node):
312 def is_mutable(self):
314 def read(self, consumer, offset=0, size=None):
315 d = self.node.download_best_version()
316 d.addCallback(self._got_data, consumer, offset, size)
318 def _got_data(self, contents, consumer, offset, size):
324 # SDMF: we can write the whole file in one big chunk
325 consumer.write(contents[start:end])
328 def makeMutableDownloadable(n):
329 d = defer.maybeDeferred(n.get_size_of_best_version)
330 d.addCallback(MutableDownloadable, n)
333 class FileDownloader(rend.Page):
334 # since we override the rendering process (to let the tahoe Downloader
335 # drive things), we must inherit from regular old twisted.web.resource
336 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
337 # nevow.appserver.OldResourceAdapter around any
338 # twisted.web.resource.IResource that it is given. TODO: it looks like
339 # that wrapper would allow us to return a Deferred from render(), which
340 # might could simplify the implementation of WebDownloadTarget.
342 def __init__(self, filenode, filename):
343 rend.Page.__init__(self)
344 self.filenode = filenode
345 self.filename = filename
347 def renderHTTP(self, ctx):
349 gte = static.getTypeAndEncoding
350 ctype, encoding = gte(self.filename,
351 static.File.contentTypes,
352 static.File.contentEncodings,
353 defaultType="text/plain")
354 req.setHeader("content-type", ctype)
356 req.setHeader("content-encoding", encoding)
358 save_to_filename = None
359 if boolean_of_arg(get_arg(req, "save", "False")):
360 # tell the browser to save the file rather display it we don't
361 # try to encode the filename, instead we echo back the exact same
362 # bytes we were given in the URL. See the comment in
363 # FileNodeHandler.render_GET for the sad details.
364 req.setHeader("content-disposition",
365 'attachment; filename="%s"' % self.filename)
367 filesize = self.filenode.get_size()
368 assert isinstance(filesize, (int,long)), filesize
369 offset, size = 0, None
370 contentsize = filesize
371 req.setHeader("accept-ranges", "bytes")
372 if not self.filenode.is_mutable():
373 # TODO: look more closely at Request.setETag and how it interacts
374 # with a conditional "if-etag-equals" request, I think this may
375 # need to occur after the setResponseCode below
376 si = self.filenode.get_storage_index()
378 req.setETag(base32.b2a(si))
379 # TODO: for mutable files, use the roothash. For LIT, hash the data.
380 # or maybe just use the URI for CHK and LIT.
381 rangeheader = req.getHeader('range')
383 # adapted from nevow.static.File
384 bytesrange = rangeheader.split('=')
385 if bytesrange[0] != 'bytes':
386 raise WebError("Syntactically invalid http range header!")
387 start, end = bytesrange[1].split('-')
393 # "If the last-byte-pos value is absent, or if the value is
394 # greater than or equal to the current length of the
395 # entity-body, last-byte-pos is taken to be equal to one less
396 # than the current length of the entity- body in bytes."
398 size = int(end) - offset + 1
399 req.setResponseCode(http.PARTIAL_CONTENT)
400 req.setHeader('content-range',"bytes %s-%s/%s" %
401 (str(offset), str(offset+size-1), str(filesize)))
403 req.setHeader("content-length", str(contentsize))
404 if req.method == "HEAD":
406 d = self.filenode.read(req, offset, size)
408 if req.startedWriting:
409 # The content-type is already set, and the response code has
410 # already been sent, so we can't provide a clean error
411 # indication. We can emit text (which a browser might
412 # interpret as something else), and if we sent a Size header,
413 # they might notice that we've truncated the data. Keep the
414 # error message small to improve the chances of having our
415 # error response be shorter than the intended results.
417 # We don't have a lot of options, unfortunately.
418 req.write("problem during download\n")
421 # We haven't written anything yet, so we can provide a
422 # sensible error message.
423 eh = MyExceptionHandler()
424 eh.renderHTTP_exception(ctx, f)
425 d.addCallbacks(lambda ign: req.finish(), _error)
429 def FileJSONMetadata(ctx, filenode, edge_metadata):
430 if filenode.is_readonly():
432 ro_uri = filenode.get_uri()
434 rw_uri = filenode.get_uri()
435 ro_uri = filenode.get_readonly_uri()
436 data = ("filenode", {})
437 data[1]['size'] = filenode.get_size()
439 data[1]['ro_uri'] = ro_uri
441 data[1]['rw_uri'] = rw_uri
442 verifycap = filenode.get_verify_cap()
444 data[1]['verify_uri'] = verifycap.to_string()
445 data[1]['mutable'] = filenode.is_mutable()
446 if edge_metadata is not None:
447 data[1]['metadata'] = edge_metadata
448 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
450 def FileURI(ctx, filenode):
451 return text_plain(filenode.get_uri(), ctx)
453 def FileReadOnlyURI(ctx, filenode):
454 if filenode.is_readonly():
455 return text_plain(filenode.get_uri(), ctx)
456 return text_plain(filenode.get_readonly_uri(), ctx)
458 class FileNodeDownloadHandler(FileNodeHandler):
459 def childFactory(self, ctx, name):
460 return FileNodeDownloadHandler(self.client, self.node, name=name)