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.immutable.filenode import LiteralFileNode
13 from allmydata.util import log, base32
15 from allmydata.web.common import text_plain, WebError, IClient, RenderMixin, \
16 boolean_of_arg, get_arg, should_create_intermediate_directories
17 from allmydata.web.check_results import CheckResults, \
18 CheckAndRepairResults, LiteralCheckResults
19 from allmydata.web.info import MoreInfo
23 def replace_me_with_a_child(self, ctx, replace):
24 # a new file is being uploaded in our place.
27 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
30 data = req.content.read()
31 d = client.create_mutable_file(data)
32 def _uploaded(newnode):
33 d2 = self.parentnode.set_node(self.name, newnode,
35 d2.addCallback(lambda res: newnode)
37 d.addCallback(_uploaded)
39 uploadable = FileHandle(req.content, convergence=client.convergence)
40 d = self.parentnode.add_file(self.name, uploadable,
43 log.msg("webish upload complete",
44 facility="tahoe.webish", level=log.NOISY)
46 # we've replaced an existing file (or modified a mutable
47 # file), so the response code is 200
48 req.setResponseCode(http.OK)
50 # we've created a new file, so the code is 201
51 req.setResponseCode(http.CREATED)
52 return filenode.get_uri()
56 def replace_me_with_a_childcap(self, ctx, replace):
59 childcap = req.content.read()
61 childnode = client.create_node_from_uri(childcap)
62 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
63 d.addCallback(lambda res: childnode.get_uri())
66 def _read_data_from_formpost(self, req):
67 # SDMF: files are small, and we can only upload data, so we read
68 # the whole file into memory before uploading.
69 contents = req.fields["file"]
71 data = contents.file.read()
74 def replace_me_with_a_formpost(self, ctx, replace):
75 # create a new file, maybe mutable, maybe immutable
78 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
81 data = self._read_data_from_formpost(req)
82 d = client.create_mutable_file(data)
83 def _uploaded(newnode):
84 d2 = self.parentnode.set_node(self.name, newnode,
86 d2.addCallback(lambda res: newnode.get_uri())
88 d.addCallback(_uploaded)
90 # create an immutable file
91 contents = req.fields["file"]
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())
97 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
98 def __init__(self, parentnode, name):
99 rend.Page.__init__(self)
101 self.parentnode = parentnode
105 def render_PUT(self, ctx):
107 t = get_arg(req, "t", "").strip()
108 replace = boolean_of_arg(get_arg(req, "replace", "true"))
109 assert self.parentnode and self.name
110 if req.getHeader("content-range"):
111 raise WebError("Content-Range in PUT not yet supported",
112 http.NOT_IMPLEMENTED)
114 return self.replace_me_with_a_child(ctx, replace)
116 return self.replace_me_with_a_childcap(ctx, replace)
118 raise WebError("PUT to a file: bad t=%s" % t)
120 def render_POST(self, ctx):
122 t = get_arg(req, "t", "").strip()
123 replace = boolean_of_arg(get_arg(req, "replace", "true"))
125 # like PUT, but get the file data from an HTML form's input field.
126 # We could get here from POST /uri/mutablefilecap?t=upload,
127 # or POST /uri/path/file?t=upload, or
128 # POST /uri/path/dir?t=upload&name=foo . All have the same
129 # behavior, we just ignore any name= argument
130 d = self.replace_me_with_a_formpost(ctx, replace)
132 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
133 # there are no other t= values left to be handled by the
135 raise WebError("POST to a file: bad t=%s" % t)
137 when_done = get_arg(req, "when_done", None)
139 d.addCallback(lambda res: url.URL.fromString(when_done))
143 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
144 def __init__(self, node, parentnode=None, name=None):
145 rend.Page.__init__(self)
148 self.parentnode = parentnode
151 def childFactory(self, ctx, name):
153 if should_create_intermediate_directories(req):
154 raise WebError("Cannot create directory '%s', because its "
155 "parent is a file, not a directory" % name)
156 raise WebError("Files have no children, certainly not named '%s'"
159 def render_GET(self, ctx):
161 t = get_arg(req, "t", "").strip()
163 # just get the contents
164 # the filename arrives as part of the URL or in a form input
165 # element, and will be sent back in a Content-Disposition header.
166 # Different browsers use various character sets for this name,
167 # sometimes depending upon how language environment is
168 # configured. Firefox sends the equivalent of
169 # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
170 # latin-1. Browsers cannot agree on how to interpret the name
171 # they see in the Content-Disposition header either, despite some
172 # 11-year old standards (RFC2231) that explain how to do it
173 # properly. So we assume that at least the browser will agree
174 # with itself, and echo back the same bytes that we were given.
175 filename = get_arg(req, "filename", self.name) or "unknown"
176 if self.node.is_mutable():
177 # some day: d = self.node.get_best_version()
178 d = makeMutableDownloadable(self.node)
180 d = defer.succeed(self.node)
181 d.addCallback(lambda dn: FileDownloader(dn, filename))
184 return FileJSONMetadata(ctx, self.node)
186 return MoreInfo(self.node)
188 return FileURI(ctx, self.node)
189 if t == "readonly-uri":
190 return FileReadOnlyURI(ctx, self.node)
191 raise WebError("GET file: bad t=%s" % t)
193 def render_HEAD(self, ctx):
195 t = get_arg(req, "t", "").strip()
197 raise WebError("GET file: bad t=%s" % t)
198 filename = get_arg(req, "filename", self.name) or "unknown"
199 if self.node.is_mutable():
200 # some day: d = self.node.get_best_version()
201 d = makeMutableDownloadable(self.node)
203 d = defer.succeed(self.node)
204 d.addCallback(lambda dn: FileDownloader(dn, filename))
207 def render_PUT(self, ctx):
209 t = get_arg(req, "t", "").strip()
210 replace = boolean_of_arg(get_arg(req, "replace", "true"))
212 if self.node.is_mutable():
213 return self.replace_my_contents(ctx)
215 # this is the early trap: if someone else modifies the
216 # directory while we're uploading, the add_file(overwrite=)
217 # call in replace_me_with_a_child will do the late trap.
218 raise ExistingChildError()
219 assert self.parentnode and self.name
220 return self.replace_me_with_a_child(ctx, replace)
223 raise ExistingChildError()
224 assert self.parentnode and self.name
225 return self.replace_me_with_a_childcap(ctx, replace)
227 raise WebError("PUT to a file: bad t=%s" % t)
229 def render_POST(self, ctx):
231 t = get_arg(req, "t", "").strip()
232 replace = boolean_of_arg(get_arg(req, "replace", "true"))
234 d = self._POST_check(req)
236 # like PUT, but get the file data from an HTML form's input field
237 # We could get here from POST /uri/mutablefilecap?t=upload,
238 # or POST /uri/path/file?t=upload, or
239 # POST /uri/path/dir?t=upload&name=foo . All have the same
240 # behavior, we just ignore any name= argument
241 if self.node.is_mutable():
242 d = self.replace_my_contents_with_a_formpost(ctx)
245 raise ExistingChildError()
246 assert self.parentnode and self.name
247 d = self.replace_me_with_a_formpost(ctx, replace)
249 raise WebError("POST to file: bad t=%s" % t)
251 when_done = get_arg(req, "when_done", None)
253 d.addCallback(lambda res: url.URL.fromString(when_done))
256 def _POST_check(self, req):
257 verify = boolean_of_arg(get_arg(req, "verify", "false"))
258 repair = boolean_of_arg(get_arg(req, "repair", "false"))
259 add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
260 if isinstance(self.node, LiteralFileNode):
261 return defer.succeed(LiteralCheckResults())
263 d = self.node.check_and_repair(Monitor(), verify, add_lease)
264 d.addCallback(lambda res: CheckAndRepairResults(res))
266 d = self.node.check(Monitor(), verify, add_lease)
267 d.addCallback(lambda res: CheckResults(res))
270 def render_DELETE(self, ctx):
271 assert self.parentnode and self.name
272 d = self.parentnode.delete(self.name)
273 d.addCallback(lambda res: self.node.get_uri())
276 def replace_my_contents(self, ctx):
279 new_contents = req.content.read()
280 d = self.node.overwrite(new_contents)
281 d.addCallback(lambda res: self.node.get_uri())
284 def replace_my_contents_with_a_formpost(self, ctx):
285 # we have a mutable file. Get the data from the formpost, and replace
286 # the mutable file's contents with it.
288 new_contents = self._read_data_from_formpost(req)
289 d = self.node.overwrite(new_contents)
290 d.addCallback(lambda res: self.node.get_uri())
293 class MutableDownloadable:
294 #implements(IDownloadable)
295 def __init__(self, size, node):
300 def is_mutable(self):
302 def read(self, consumer, offset=0, size=None):
303 d = self.node.download_best_version()
304 d.addCallback(self._got_data, consumer, offset, size)
306 def _got_data(self, contents, consumer, offset, size):
312 # SDMF: we can write the whole file in one big chunk
313 consumer.write(contents[start:end])
316 def makeMutableDownloadable(n):
317 d = defer.maybeDeferred(n.get_size_of_best_version)
318 d.addCallback(MutableDownloadable, n)
321 class FileDownloader(rend.Page):
322 # since we override the rendering process (to let the tahoe Downloader
323 # drive things), we must inherit from regular old twisted.web.resource
324 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
325 # nevow.appserver.OldResourceAdapter around any
326 # twisted.web.resource.IResource that it is given. TODO: it looks like
327 # that wrapper would allow us to return a Deferred from render(), which
328 # might could simplify the implementation of WebDownloadTarget.
330 def __init__(self, filenode, filename):
331 rend.Page.__init__(self)
332 self.filenode = filenode
333 self.filename = filename
335 def renderHTTP(self, ctx):
337 gte = static.getTypeAndEncoding
338 ctype, encoding = gte(self.filename,
339 static.File.contentTypes,
340 static.File.contentEncodings,
341 defaultType="text/plain")
342 req.setHeader("content-type", ctype)
344 req.setHeader("content-encoding", encoding)
346 save_to_filename = None
347 if boolean_of_arg(get_arg(req, "save", "False")):
348 # tell the browser to save the file rather display it we don't
349 # try to encode the filename, instead we echo back the exact same
350 # bytes we were given in the URL. See the comment in
351 # FileNodeHandler.render_GET for the sad details.
352 req.setHeader("content-disposition",
353 'attachment; filename="%s"' % self.filename)
355 filesize = self.filenode.get_size()
356 assert isinstance(filesize, (int,long)), filesize
357 offset, size = 0, None
358 contentsize = filesize
359 req.setHeader("accept-ranges", "bytes")
360 if not self.filenode.is_mutable():
361 # TODO: look more closely at Request.setETag and how it interacts
362 # with a conditional "if-etag-equals" request, I think this may
363 # need to occur after the setResponseCode below
364 si = self.filenode.get_storage_index()
366 req.setETag(base32.b2a(si))
367 # TODO: for mutable files, use the roothash. For LIT, hash the data.
368 # or maybe just use the URI for CHK and LIT.
369 rangeheader = req.getHeader('range')
371 # adapted from nevow.static.File
372 bytesrange = rangeheader.split('=')
373 if bytesrange[0] != 'bytes':
374 raise WebError("Syntactically invalid http range header!")
375 start, end = bytesrange[1].split('-')
381 # "If the last-byte-pos value is absent, or if the value is
382 # greater than or equal to the current length of the
383 # entity-body, last-byte-pos is taken to be equal to one less
384 # than the current length of the entity- body in bytes."
386 size = int(end) - offset + 1
387 req.setResponseCode(http.PARTIAL_CONTENT)
388 req.setHeader('content-range',"bytes %s-%s/%s" %
389 (str(offset), str(offset+size-1), str(filesize)))
391 req.setHeader("content-length", str(contentsize))
392 if req.method == "HEAD":
394 d = self.filenode.read(req, offset, size)
396 if req.startedWriting:
397 # The content-type is already set, and the response code has
398 # already been sent, so we can't provide a clean error
399 # indication. We can emit text (which a browser might
400 # interpret as something else), and if we sent a Size header,
401 # they might notice that we've truncated the data. Keep the
402 # error message small to improve the chances of having our
403 # error response be shorter than the intended results.
405 # We don't have a lot of options, unfortunately.
406 req.write("problem during download\n")
408 # We haven't written anything yet, so we can provide a
409 # sensible error message.
411 msg.replace("\n", "|")
412 req.setResponseCode(http.GONE, msg)
413 req.setHeader("content-type", "text/plain")
414 req.responseHeaders.setRawHeaders("content-encoding", [])
415 req.responseHeaders.setRawHeaders("content-disposition", [])
416 # TODO: HTML-formatted exception?
419 d.addBoth(lambda ign: req.finish())
423 def FileJSONMetadata(ctx, filenode):
424 if filenode.is_readonly():
426 ro_uri = filenode.get_uri()
428 rw_uri = filenode.get_uri()
429 ro_uri = filenode.get_readonly_uri()
430 data = ("filenode", {})
431 data[1]['size'] = filenode.get_size()
433 data[1]['ro_uri'] = ro_uri
435 data[1]['rw_uri'] = rw_uri
436 verifycap = filenode.get_verify_cap()
438 data[1]['verify_uri'] = verifycap.to_string()
439 data[1]['mutable'] = filenode.is_mutable()
440 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
442 def FileURI(ctx, filenode):
443 return text_plain(filenode.get_uri(), ctx)
445 def FileReadOnlyURI(ctx, filenode):
446 if filenode.is_readonly():
447 return text_plain(filenode.get_uri(), ctx)
448 return text_plain(filenode.get_readonly_uri(), ctx)
450 class FileNodeDownloadHandler(FileNodeHandler):
451 def childFactory(self, ctx, name):
452 return FileNodeDownloadHandler(self.node, name=name)