4 from zope.interface import implements
5 from twisted.internet.interfaces import IConsumer
6 from twisted.web import http, static, resource, server
7 from twisted.internet import defer
8 from nevow import url, rend
9 from nevow.inevow import IRequest
11 from allmydata.interfaces import IDownloadTarget, ExistingChildError
12 from allmydata.immutable.upload import FileHandle
13 from allmydata.immutable.filenode import LiteralFileNode
14 from allmydata.util import log
16 from allmydata.web.common import text_plain, WebError, IClient, RenderMixin, \
17 boolean_of_arg, get_arg, should_create_intermediate_directories
18 from allmydata.web.checker_results import CheckerResults, \
19 CheckAndRepairResults, LiteralCheckerResults
20 from allmydata.web.info import MoreInfo
24 def replace_me_with_a_child(self, ctx, replace):
25 # a new file is being uploaded in our place.
28 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
31 data = req.content.read()
32 d = client.create_mutable_file(data)
33 def _uploaded(newnode):
34 d2 = self.parentnode.set_node(self.name, newnode,
36 d2.addCallback(lambda res: newnode)
38 d.addCallback(_uploaded)
40 uploadable = FileHandle(req.content, convergence=client.convergence)
41 d = self.parentnode.add_file(self.name, uploadable,
44 log.msg("webish upload complete",
45 facility="tahoe.webish", level=log.NOISY)
47 # we've replaced an existing file (or modified a mutable
48 # file), so the response code is 200
49 req.setResponseCode(http.OK)
51 # we've created a new file, so the code is 201
52 req.setResponseCode(http.CREATED)
53 return filenode.get_uri()
57 def replace_me_with_a_childcap(self, ctx, replace):
60 childcap = req.content.read()
62 childnode = client.create_node_from_uri(childcap)
63 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
64 d.addCallback(lambda res: childnode.get_uri())
67 def _read_data_from_formpost(self, req):
68 # SDMF: files are small, and we can only upload data, so we read
69 # the whole file into memory before uploading.
70 contents = req.fields["file"]
72 data = contents.file.read()
75 def replace_me_with_a_formpost(self, ctx, replace):
76 # create a new file, maybe mutable, maybe immutable
79 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
82 data = self._read_data_from_formpost(req)
83 d = client.create_mutable_file(data)
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)
91 # create an immutable file
92 contents = req.fields["file"]
93 uploadable = FileHandle(contents.file, convergence=client.convergence)
94 d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
95 d.addCallback(lambda newnode: newnode.get_uri())
98 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
99 def __init__(self, parentnode, name):
100 rend.Page.__init__(self)
102 self.parentnode = parentnode
106 def render_PUT(self, ctx):
108 t = get_arg(req, "t", "").strip()
109 replace = boolean_of_arg(get_arg(req, "replace", "true"))
110 assert self.parentnode and self.name
112 return self.replace_me_with_a_child(ctx, replace)
114 return self.replace_me_with_a_childcap(ctx, replace)
116 raise WebError("PUT to a file: bad t=%s" % t)
118 def render_POST(self, ctx):
120 t = get_arg(req, "t", "").strip()
121 replace = boolean_of_arg(get_arg(req, "replace", "true"))
123 # like PUT, but get the file data from an HTML form's input field.
124 # We could get here from POST /uri/mutablefilecap?t=upload,
125 # or POST /uri/path/file?t=upload, or
126 # POST /uri/path/dir?t=upload&name=foo . All have the same
127 # behavior, we just ignore any name= argument
128 d = self.replace_me_with_a_formpost(ctx, replace)
130 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
131 # there are no other t= values left to be handled by the
133 raise WebError("POST to a file: bad t=%s" % t)
135 when_done = get_arg(req, "when_done", None)
137 d.addCallback(lambda res: url.URL.fromString(when_done))
141 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
142 def __init__(self, node, parentnode=None, name=None):
143 rend.Page.__init__(self)
146 self.parentnode = parentnode
149 def childFactory(self, ctx, name):
151 if should_create_intermediate_directories(req):
152 raise WebError("Cannot create directory '%s', because its "
153 "parent is a file, not a directory" % name)
154 raise WebError("Files have no children, certainly not named '%s'"
157 def render_GET(self, ctx):
159 t = get_arg(req, "t", "").strip()
161 # just get the contents
162 save_to_file = boolean_of_arg(get_arg(req, "save", "False"))
163 # the filename arrives as part of the URL or in a form input
164 # element, and will be sent back in a Content-Disposition header.
165 # Different browsers use various character sets for this name,
166 # sometimes depending upon how language environment is
167 # configured. Firefox sends the equivalent of
168 # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
169 # latin-1. Browsers cannot agree on how to interpret the name
170 # they see in the Content-Disposition header either, despite some
171 # 11-year old standards (RFC2231) that explain how to do it
172 # properly. So we assume that at least the browser will agree
173 # with itself, and echo back the same bytes that we were given.
174 filename = get_arg(req, "filename", self.name) or "unknown"
175 return FileDownloader(self.node, filename, save_to_file)
177 return FileJSONMetadata(ctx, self.node)
179 return MoreInfo(self.node)
181 return FileURI(ctx, self.node)
182 if t == "readonly-uri":
183 return FileReadOnlyURI(ctx, self.node)
184 raise WebError("GET file: bad t=%s" % t)
186 def render_HEAD(self, ctx):
188 t = get_arg(req, "t", "").strip()
190 raise WebError("GET file: bad t=%s" % t)
191 # if we have a filename, use it to get the content-type
192 filename = get_arg(req, "filename", self.name) or "unknown"
193 gte = static.getTypeAndEncoding
194 ctype, encoding = gte(filename,
195 static.File.contentTypes,
196 static.File.contentEncodings,
197 defaultType="text/plain")
198 req.setHeader("content-type", ctype)
200 req.setHeader("content-encoding", encoding)
201 if self.node.is_mutable():
202 d = self.node.get_size_of_best_version()
203 # otherwise, we can get the size from the URI
205 d = defer.succeed(self.node.get_size())
206 def _got_length(length):
207 req.setHeader("content-length", length)
209 d.addCallback(_got_length)
212 def render_PUT(self, ctx):
214 t = get_arg(req, "t", "").strip()
215 replace = boolean_of_arg(get_arg(req, "replace", "true"))
217 if self.node.is_mutable():
218 return self.replace_my_contents(ctx)
220 # this is the early trap: if someone else modifies the
221 # directory while we're uploading, the add_file(overwrite=)
222 # call in replace_me_with_a_child will do the late trap.
223 raise ExistingChildError()
224 assert self.parentnode and self.name
225 return self.replace_me_with_a_child(ctx, replace)
228 raise ExistingChildError()
229 assert self.parentnode and self.name
230 return self.replace_me_with_a_childcap(ctx, replace)
232 raise WebError("PUT to a file: bad t=%s" % t)
234 def render_POST(self, ctx):
236 t = get_arg(req, "t", "").strip()
237 replace = boolean_of_arg(get_arg(req, "replace", "true"))
239 d = self._POST_check(req)
241 # like PUT, but get the file data from an HTML form's input field
242 # We could get here from POST /uri/mutablefilecap?t=upload,
243 # or POST /uri/path/file?t=upload, or
244 # POST /uri/path/dir?t=upload&name=foo . All have the same
245 # behavior, we just ignore any name= argument
246 if self.node.is_mutable():
247 d = self.replace_my_contents_with_a_formpost(ctx)
250 raise ExistingChildError()
251 assert self.parentnode and self.name
252 d = self.replace_me_with_a_formpost(ctx, replace)
254 raise WebError("POST to file: bad t=%s" % t)
256 when_done = get_arg(req, "when_done", None)
258 d.addCallback(lambda res: url.URL.fromString(when_done))
261 def _POST_check(self, req):
262 verify = boolean_of_arg(get_arg(req, "verify", "false"))
263 repair = boolean_of_arg(get_arg(req, "repair", "false"))
264 if isinstance(self.node, LiteralFileNode):
265 return defer.succeed(LiteralCheckerResults())
267 d = self.node.check_and_repair(verify)
268 d.addCallback(lambda res: CheckAndRepairResults(res))
270 d = self.node.check(verify)
271 d.addCallback(lambda res: CheckerResults(res))
274 def render_DELETE(self, ctx):
275 assert self.parentnode and self.name
276 d = self.parentnode.delete(self.name)
277 d.addCallback(lambda res: self.node.get_uri())
280 def replace_my_contents(self, ctx):
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, ctx):
289 # we have a mutable file. Get the data from the formpost, and replace
290 # the mutable file's contents with it.
292 new_contents = self._read_data_from_formpost(req)
293 d = self.node.overwrite(new_contents)
294 d.addCallback(lambda res: self.node.get_uri())
298 class WebDownloadTarget:
299 implements(IDownloadTarget, IConsumer)
300 def __init__(self, req, content_type, content_encoding, save_to_filename):
302 self._content_type = content_type
303 self._content_encoding = content_encoding
305 self._producer = None
306 self._save_to_filename = save_to_filename
308 def registerProducer(self, producer, streaming):
309 self._req.registerProducer(producer, streaming)
310 def unregisterProducer(self):
311 self._req.unregisterProducer()
313 def open(self, size):
315 self._req.setHeader("content-type", self._content_type)
316 if self._content_encoding:
317 self._req.setHeader("content-encoding", self._content_encoding)
318 self._req.setHeader("content-length", str(size))
319 if self._save_to_filename is not None:
320 # tell the browser to save the file rather display it we don't
321 # try to encode the filename, instead we echo back the exact same
322 # bytes we were given in the URL. See the comment in
323 # FileNodeHandler.render_GET for the sad details.
324 filename = self._save_to_filename
325 self._req.setHeader("content-disposition",
326 'attachment; filename="%s"' % filename)
328 def write(self, data):
329 self._req.write(data)
335 # The content-type is already set, and the response code
336 # has already been sent, so we can't provide a clean error
337 # indication. We can emit text (which a browser might interpret
338 # as something else), and if we sent a Size header, they might
339 # notice that we've truncated the data. Keep the error message
340 # small to improve the chances of having our error response be
341 # shorter than the intended results.
343 # We don't have a lot of options, unfortunately.
344 self._req.write("problem during download\n")
346 # We haven't written anything yet, so we can provide a sensible
349 msg.replace("\n", "|")
350 self._req.setResponseCode(http.GONE, msg)
351 self._req.setHeader("content-type", "text/plain")
352 # TODO: HTML-formatted exception?
353 self._req.write(str(why))
356 def register_canceller(self, cb):
361 class FileDownloader(resource.Resource):
362 # since we override the rendering process (to let the tahoe Downloader
363 # drive things), we must inherit from regular old twisted.web.resource
364 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
365 # nevow.appserver.OldResourceAdapter around any
366 # twisted.web.resource.IResource that it is given. TODO: it looks like
367 # that wrapper would allow us to return a Deferred from render(), which
368 # might could simplify the implementation of WebDownloadTarget.
370 def __init__(self, filenode, filename, save_to_file):
371 resource.Resource.__init__(self)
372 self.filenode = filenode
373 self.filename = filename
374 self.save_to_file = save_to_file
375 def render(self, req):
376 gte = static.getTypeAndEncoding
377 ctype, encoding = gte(self.filename,
378 static.File.contentTypes,
379 static.File.contentEncodings,
380 defaultType="text/plain")
381 save_to_filename = None
382 if self.save_to_file:
383 save_to_filename = self.filename
384 wdt = WebDownloadTarget(req, ctype, encoding, save_to_filename)
385 d = self.filenode.download(wdt)
386 # exceptions during download are handled by the WebDownloadTarget
387 d.addErrback(lambda why: None)
388 return server.NOT_DONE_YET
390 def FileJSONMetadata(ctx, filenode):
391 if filenode.is_readonly():
393 ro_uri = filenode.get_uri()
395 rw_uri = filenode.get_uri()
396 ro_uri = filenode.get_readonly_uri()
397 data = ("filenode", {})
398 data[1]['size'] = filenode.get_size()
400 data[1]['ro_uri'] = ro_uri
402 data[1]['rw_uri'] = rw_uri
403 data[1]['mutable'] = filenode.is_mutable()
404 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
406 def FileURI(ctx, filenode):
407 return text_plain(filenode.get_uri(), ctx)
409 def FileReadOnlyURI(ctx, filenode):
410 if filenode.is_readonly():
411 return text_plain(filenode.get_uri(), ctx)
412 return text_plain(filenode.get_readonly_uri(), ctx)
414 class FileNodeDownloadHandler(FileNodeHandler):
415 def childFactory(self, ctx, name):
416 return FileNodeDownloadHandler(self.node, name=name)