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.util import log
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.checker_results import CheckerResults
21 def replace_me_with_a_child(self, ctx, replace):
22 # a new file is being uploaded in our place.
25 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
28 data = req.content.read()
29 d = client.create_mutable_file(data)
30 def _uploaded(newnode):
31 d2 = self.parentnode.set_node(self.name, newnode,
33 d2.addCallback(lambda res: newnode)
35 d.addCallback(_uploaded)
37 uploadable = FileHandle(req.content, convergence=client.convergence)
38 d = self.parentnode.add_file(self.name, uploadable,
41 log.msg("webish upload complete",
42 facility="tahoe.webish", level=log.NOISY)
44 # we've replaced an existing file (or modified a mutable
45 # file), so the response code is 200
46 req.setResponseCode(http.OK)
48 # we've created a new file, so the code is 201
49 req.setResponseCode(http.CREATED)
50 return filenode.get_uri()
54 def replace_me_with_a_childcap(self, ctx, replace):
57 childcap = req.content.read()
59 childnode = client.create_node_from_uri(childcap)
60 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
61 d.addCallback(lambda res: childnode.get_uri())
64 def _read_data_from_formpost(self, req):
65 # SDMF: files are small, and we can only upload data, so we read
66 # the whole file into memory before uploading.
67 contents = req.fields["file"]
69 data = contents.file.read()
72 def replace_me_with_a_formpost(self, ctx, replace):
73 # create a new file, maybe mutable, maybe immutable
76 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
79 data = self._read_data_from_formpost(req)
80 d = client.create_mutable_file(data)
81 def _uploaded(newnode):
82 d2 = self.parentnode.set_node(self.name, newnode,
84 d2.addCallback(lambda res: newnode.get_uri())
86 d.addCallback(_uploaded)
88 # create an immutable file
89 contents = req.fields["file"]
90 uploadable = FileHandle(contents.file, convergence=client.convergence)
91 d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
92 d.addCallback(lambda newnode: newnode.get_uri())
95 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
96 def __init__(self, parentnode, name):
97 rend.Page.__init__(self)
99 self.parentnode = parentnode
103 def render_PUT(self, ctx):
105 t = get_arg(req, "t", "").strip()
106 replace = boolean_of_arg(get_arg(req, "replace", "true"))
107 assert self.parentnode and self.name
109 return self.replace_me_with_a_child(ctx, replace)
111 return self.replace_me_with_a_childcap(ctx, 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(ctx, 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, node, parentnode=None, name=None):
140 rend.Page.__init__(self)
143 self.parentnode = parentnode
146 def childFactory(self, ctx, name):
148 if should_create_intermediate_directories(req):
149 raise WebError("Cannot create directory '%s', because its "
150 "parent is a file, not a directory" % name)
151 raise WebError("Files have no children, certainly not named '%s'"
154 def render_GET(self, ctx):
156 t = get_arg(req, "t", "").strip()
158 # just get the contents
159 save_to_file = boolean_of_arg(get_arg(req, "save", "False"))
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 return FileDownloader(self.node, filename, save_to_file)
174 return FileJSONMetadata(ctx, self.node)
176 return FileURI(ctx, self.node)
177 if t == "readonly-uri":
178 return FileReadOnlyURI(ctx, self.node)
179 raise WebError("GET file: bad t=%s" % t)
181 def render_HEAD(self, ctx):
183 t = get_arg(req, "t", "").strip()
185 raise WebError("GET file: bad t=%s" % t)
186 # if we have a filename, use it to get the content-type
187 filename = get_arg(req, "filename", self.name) or "unknown"
188 gte = static.getTypeAndEncoding
189 ctype, encoding = gte(filename,
190 static.File.contentTypes,
191 static.File.contentEncodings,
192 defaultType="text/plain")
193 req.setHeader("content-type", ctype)
195 req.setHeader("content-encoding", encoding)
196 if self.node.is_mutable():
197 d = self.node.get_size_of_best_version()
198 # otherwise, we can get the size from the URI
200 d = defer.succeed(self.node.get_size())
201 def _got_length(length):
202 req.setHeader("content-length", length)
204 d.addCallback(_got_length)
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 d = self.node.check(verify, repair)
260 d.addCallback(lambda res: CheckerResults(res))
263 def render_DELETE(self, ctx):
264 assert self.parentnode and self.name
265 d = self.parentnode.delete(self.name)
266 d.addCallback(lambda res: self.node.get_uri())
269 def replace_my_contents(self, ctx):
272 new_contents = req.content.read()
273 d = self.node.overwrite(new_contents)
274 d.addCallback(lambda res: self.node.get_uri())
277 def replace_my_contents_with_a_formpost(self, ctx):
278 # we have a mutable file. Get the data from the formpost, and replace
279 # the mutable file's contents with it.
281 new_contents = self._read_data_from_formpost(req)
282 d = self.node.overwrite(new_contents)
283 d.addCallback(lambda res: self.node.get_uri())
287 class WebDownloadTarget:
288 implements(IDownloadTarget, IConsumer)
289 def __init__(self, req, content_type, content_encoding, save_to_filename):
291 self._content_type = content_type
292 self._content_encoding = content_encoding
294 self._producer = None
295 self._save_to_filename = save_to_filename
297 def registerProducer(self, producer, streaming):
298 self._req.registerProducer(producer, streaming)
299 def unregisterProducer(self):
300 self._req.unregisterProducer()
302 def open(self, size):
304 self._req.setHeader("content-type", self._content_type)
305 if self._content_encoding:
306 self._req.setHeader("content-encoding", self._content_encoding)
307 self._req.setHeader("content-length", str(size))
308 if self._save_to_filename is not None:
309 # tell the browser to save the file rather display it we don't
310 # try to encode the filename, instead we echo back the exact same
311 # bytes we were given in the URL. See the comment in
312 # FileNodeHandler.render_GET for the sad details.
313 filename = self._save_to_filename
314 self._req.setHeader("content-disposition",
315 'attachment; filename="%s"' % filename)
317 def write(self, data):
318 self._req.write(data)
324 # The content-type is already set, and the response code
325 # has already been sent, so we can't provide a clean error
326 # indication. We can emit text (which a browser might interpret
327 # as something else), and if we sent a Size header, they might
328 # notice that we've truncated the data. Keep the error message
329 # small to improve the chances of having our error response be
330 # shorter than the intended results.
332 # We don't have a lot of options, unfortunately.
333 self._req.write("problem during download\n")
335 # We haven't written anything yet, so we can provide a sensible
338 msg.replace("\n", "|")
339 self._req.setResponseCode(http.GONE, msg)
340 self._req.setHeader("content-type", "text/plain")
341 # TODO: HTML-formatted exception?
342 self._req.write(str(why))
345 def register_canceller(self, cb):
350 class FileDownloader(resource.Resource):
351 # since we override the rendering process (to let the tahoe Downloader
352 # drive things), we must inherit from regular old twisted.web.resource
353 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
354 # nevow.appserver.OldResourceAdapter around any
355 # twisted.web.resource.IResource that it is given. TODO: it looks like
356 # that wrapper would allow us to return a Deferred from render(), which
357 # might could simplify the implementation of WebDownloadTarget.
359 def __init__(self, filenode, filename, save_to_file):
360 resource.Resource.__init__(self)
361 self.filenode = filenode
362 self.filename = filename
363 self.save_to_file = save_to_file
364 def render(self, req):
365 gte = static.getTypeAndEncoding
366 ctype, encoding = gte(self.filename,
367 static.File.contentTypes,
368 static.File.contentEncodings,
369 defaultType="text/plain")
370 save_to_filename = None
371 if self.save_to_file:
372 save_to_filename = self.filename
373 wdt = WebDownloadTarget(req, ctype, encoding, save_to_filename)
374 d = self.filenode.download(wdt)
375 # exceptions during download are handled by the WebDownloadTarget
376 d.addErrback(lambda why: None)
377 return server.NOT_DONE_YET
379 def FileJSONMetadata(ctx, filenode):
380 if filenode.is_readonly():
382 ro_uri = filenode.get_uri()
384 rw_uri = filenode.get_uri()
385 ro_uri = filenode.get_readonly_uri()
386 data = ("filenode", {})
387 data[1]['size'] = filenode.get_size()
389 data[1]['ro_uri'] = ro_uri
391 data[1]['rw_uri'] = rw_uri
392 data[1]['mutable'] = filenode.is_mutable()
393 return text_plain(simplejson.dumps(data, indent=1), ctx)
395 def FileURI(ctx, filenode):
396 return text_plain(filenode.get_uri(), ctx)
398 def FileReadOnlyURI(ctx, filenode):
399 if filenode.is_readonly():
400 return text_plain(filenode.get_uri(), ctx)
401 return text_plain(filenode.get_readonly_uri(), ctx)
403 class FileNodeDownloadHandler(FileNodeHandler):
404 def childFactory(self, ctx, name):
405 return FileNodeDownloadHandler(self.node, name=name)