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.monitor import Monitor
13 from allmydata.immutable.upload import FileHandle
14 from allmydata.immutable.filenode import LiteralFileNode
15 from allmydata.util import log
17 from allmydata.web.common import text_plain, WebError, IClient, RenderMixin, \
18 boolean_of_arg, get_arg, should_create_intermediate_directories
19 from allmydata.web.checker_results import CheckerResults, \
20 CheckAndRepairResults, LiteralCheckerResults
21 from allmydata.web.info import MoreInfo
25 def replace_me_with_a_child(self, ctx, replace):
26 # a new file is being uploaded in our place.
29 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
32 data = req.content.read()
33 d = client.create_mutable_file(data)
34 def _uploaded(newnode):
35 d2 = self.parentnode.set_node(self.name, newnode,
37 d2.addCallback(lambda res: newnode)
39 d.addCallback(_uploaded)
41 uploadable = FileHandle(req.content, convergence=client.convergence)
42 d = self.parentnode.add_file(self.name, uploadable,
45 log.msg("webish upload complete",
46 facility="tahoe.webish", level=log.NOISY)
48 # we've replaced an existing file (or modified a mutable
49 # file), so the response code is 200
50 req.setResponseCode(http.OK)
52 # we've created a new file, so the code is 201
53 req.setResponseCode(http.CREATED)
54 return filenode.get_uri()
58 def replace_me_with_a_childcap(self, ctx, replace):
61 childcap = req.content.read()
63 childnode = client.create_node_from_uri(childcap)
64 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
65 d.addCallback(lambda res: childnode.get_uri())
68 def _read_data_from_formpost(self, req):
69 # SDMF: files are small, and we can only upload data, so we read
70 # the whole file into memory before uploading.
71 contents = req.fields["file"]
73 data = contents.file.read()
76 def replace_me_with_a_formpost(self, ctx, replace):
77 # 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, parentnode, name):
101 rend.Page.__init__(self)
103 self.parentnode = parentnode
107 def render_PUT(self, ctx):
109 t = get_arg(req, "t", "").strip()
110 replace = boolean_of_arg(get_arg(req, "replace", "true"))
111 assert self.parentnode and self.name
113 return self.replace_me_with_a_child(ctx, replace)
115 return self.replace_me_with_a_childcap(ctx, replace)
117 raise WebError("PUT to a file: bad t=%s" % t)
119 def render_POST(self, ctx):
121 t = get_arg(req, "t", "").strip()
122 replace = boolean_of_arg(get_arg(req, "replace", "true"))
124 # like PUT, but get the file data from an HTML form's input field.
125 # We could get here from POST /uri/mutablefilecap?t=upload,
126 # or POST /uri/path/file?t=upload, or
127 # POST /uri/path/dir?t=upload&name=foo . All have the same
128 # behavior, we just ignore any name= argument
129 d = self.replace_me_with_a_formpost(ctx, replace)
131 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
132 # there are no other t= values left to be handled by the
134 raise WebError("POST to a file: bad t=%s" % t)
136 when_done = get_arg(req, "when_done", None)
138 d.addCallback(lambda res: url.URL.fromString(when_done))
142 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
143 def __init__(self, node, parentnode=None, name=None):
144 rend.Page.__init__(self)
147 self.parentnode = parentnode
150 def childFactory(self, ctx, name):
152 if should_create_intermediate_directories(req):
153 raise WebError("Cannot create directory '%s', because its "
154 "parent is a file, not a directory" % name)
155 raise WebError("Files have no children, certainly not named '%s'"
158 def render_GET(self, ctx):
160 t = get_arg(req, "t", "").strip()
162 # just get the contents
163 save_to_file = boolean_of_arg(get_arg(req, "save", "False"))
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 return FileDownloader(self.node, filename, save_to_file)
178 return FileJSONMetadata(ctx, self.node)
180 return MoreInfo(self.node)
182 return FileURI(ctx, self.node)
183 if t == "readonly-uri":
184 return FileReadOnlyURI(ctx, self.node)
185 raise WebError("GET file: bad t=%s" % t)
187 def render_HEAD(self, ctx):
189 t = get_arg(req, "t", "").strip()
191 raise WebError("GET file: bad t=%s" % t)
192 # if we have a filename, use it to get the content-type
193 filename = get_arg(req, "filename", self.name) or "unknown"
194 gte = static.getTypeAndEncoding
195 ctype, encoding = gte(filename,
196 static.File.contentTypes,
197 static.File.contentEncodings,
198 defaultType="text/plain")
199 req.setHeader("content-type", ctype)
201 req.setHeader("content-encoding", encoding)
202 if self.node.is_mutable():
203 d = self.node.get_size_of_best_version()
204 # otherwise, we can get the size from the URI
206 d = defer.succeed(self.node.get_size())
207 def _got_length(length):
208 req.setHeader("content-length", length)
210 d.addCallback(_got_length)
213 def render_PUT(self, ctx):
215 t = get_arg(req, "t", "").strip()
216 replace = boolean_of_arg(get_arg(req, "replace", "true"))
218 if self.node.is_mutable():
219 return self.replace_my_contents(ctx)
221 # this is the early trap: if someone else modifies the
222 # directory while we're uploading, the add_file(overwrite=)
223 # call in replace_me_with_a_child will do the late trap.
224 raise ExistingChildError()
225 assert self.parentnode and self.name
226 return self.replace_me_with_a_child(ctx, replace)
229 raise ExistingChildError()
230 assert self.parentnode and self.name
231 return self.replace_me_with_a_childcap(ctx, replace)
233 raise WebError("PUT to a file: bad t=%s" % t)
235 def render_POST(self, ctx):
237 t = get_arg(req, "t", "").strip()
238 replace = boolean_of_arg(get_arg(req, "replace", "true"))
240 d = self._POST_check(req)
242 # like PUT, but get the file data from an HTML form's input field
243 # We could get here from POST /uri/mutablefilecap?t=upload,
244 # or POST /uri/path/file?t=upload, or
245 # POST /uri/path/dir?t=upload&name=foo . All have the same
246 # behavior, we just ignore any name= argument
247 if self.node.is_mutable():
248 d = self.replace_my_contents_with_a_formpost(ctx)
251 raise ExistingChildError()
252 assert self.parentnode and self.name
253 d = self.replace_me_with_a_formpost(ctx, replace)
255 raise WebError("POST to file: bad t=%s" % t)
257 when_done = get_arg(req, "when_done", None)
259 d.addCallback(lambda res: url.URL.fromString(when_done))
262 def _POST_check(self, req):
263 verify = boolean_of_arg(get_arg(req, "verify", "false"))
264 repair = boolean_of_arg(get_arg(req, "repair", "false"))
265 if isinstance(self.node, LiteralFileNode):
266 return defer.succeed(LiteralCheckerResults())
268 d = self.node.check_and_repair(Monitor(), verify)
269 d.addCallback(lambda res: CheckAndRepairResults(res))
271 d = self.node.check(Monitor(), verify)
272 d.addCallback(lambda res: CheckerResults(res))
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, ctx):
284 new_contents = req.content.read()
285 d = self.node.overwrite(new_contents)
286 d.addCallback(lambda res: self.node.get_uri())
289 def replace_my_contents_with_a_formpost(self, ctx):
290 # we have a mutable file. Get the data from the formpost, and replace
291 # the mutable file's contents with it.
293 new_contents = self._read_data_from_formpost(req)
294 d = self.node.overwrite(new_contents)
295 d.addCallback(lambda res: self.node.get_uri())
299 class WebDownloadTarget:
300 implements(IDownloadTarget, IConsumer)
301 def __init__(self, req, content_type, content_encoding, save_to_filename):
303 self._content_type = content_type
304 self._content_encoding = content_encoding
306 self._producer = None
307 self._save_to_filename = save_to_filename
309 def registerProducer(self, producer, streaming):
310 self._req.registerProducer(producer, streaming)
311 def unregisterProducer(self):
312 self._req.unregisterProducer()
314 def open(self, size):
316 self._req.setHeader("content-type", self._content_type)
317 if self._content_encoding:
318 self._req.setHeader("content-encoding", self._content_encoding)
319 self._req.setHeader("content-length", str(size))
320 if self._save_to_filename is not None:
321 # tell the browser to save the file rather display it we don't
322 # try to encode the filename, instead we echo back the exact same
323 # bytes we were given in the URL. See the comment in
324 # FileNodeHandler.render_GET for the sad details.
325 filename = self._save_to_filename
326 self._req.setHeader("content-disposition",
327 'attachment; filename="%s"' % filename)
329 def write(self, data):
330 self._req.write(data)
336 # The content-type is already set, and the response code
337 # has already been sent, so we can't provide a clean error
338 # indication. We can emit text (which a browser might interpret
339 # as something else), and if we sent a Size header, they might
340 # notice that we've truncated the data. Keep the error message
341 # small to improve the chances of having our error response be
342 # shorter than the intended results.
344 # We don't have a lot of options, unfortunately.
345 self._req.write("problem during download\n")
347 # We haven't written anything yet, so we can provide a sensible
350 msg.replace("\n", "|")
351 self._req.setResponseCode(http.GONE, msg)
352 self._req.setHeader("content-type", "text/plain")
353 # TODO: HTML-formatted exception?
354 self._req.write(str(why))
357 def register_canceller(self, cb):
362 class FileDownloader(resource.Resource):
363 # since we override the rendering process (to let the tahoe Downloader
364 # drive things), we must inherit from regular old twisted.web.resource
365 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
366 # nevow.appserver.OldResourceAdapter around any
367 # twisted.web.resource.IResource that it is given. TODO: it looks like
368 # that wrapper would allow us to return a Deferred from render(), which
369 # might could simplify the implementation of WebDownloadTarget.
371 def __init__(self, filenode, filename, save_to_file):
372 resource.Resource.__init__(self)
373 self.filenode = filenode
374 self.filename = filename
375 self.save_to_file = save_to_file
376 def render(self, req):
377 gte = static.getTypeAndEncoding
378 ctype, encoding = gte(self.filename,
379 static.File.contentTypes,
380 static.File.contentEncodings,
381 defaultType="text/plain")
382 save_to_filename = None
383 if self.save_to_file:
384 save_to_filename = self.filename
385 wdt = WebDownloadTarget(req, ctype, encoding, save_to_filename)
386 d = self.filenode.download(wdt)
387 # exceptions during download are handled by the WebDownloadTarget
388 d.addErrback(lambda why: None)
389 return server.NOT_DONE_YET
391 def FileJSONMetadata(ctx, filenode):
392 if filenode.is_readonly():
394 ro_uri = filenode.get_uri()
396 rw_uri = filenode.get_uri()
397 ro_uri = filenode.get_readonly_uri()
398 data = ("filenode", {})
399 data[1]['size'] = filenode.get_size()
401 data[1]['ro_uri'] = ro_uri
403 data[1]['rw_uri'] = rw_uri
404 data[1]['mutable'] = filenode.is_mutable()
405 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
407 def FileURI(ctx, filenode):
408 return text_plain(filenode.get_uri(), ctx)
410 def FileReadOnlyURI(ctx, filenode):
411 if filenode.is_readonly():
412 return text_plain(filenode.get_uri(), ctx)
413 return text_plain(filenode.get_readonly_uri(), ctx)
415 class FileNodeDownloadHandler(FileNodeHandler):
416 def childFactory(self, ctx, name):
417 return FileNodeDownloadHandler(self.node, name=name)