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, RenderMixin, \
16 boolean_of_arg, get_arg, should_create_intermediate_directories, \
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)
59 d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
60 d.addCallback(lambda res: childnode.get_uri())
63 def _read_data_from_formpost(self, req):
64 # SDMF: files are small, and we can only upload data, so we read
65 # the whole file into memory before uploading.
66 contents = req.fields["file"]
68 data = contents.file.read()
71 def replace_me_with_a_formpost(self, req, client, replace):
72 # create a new file, maybe mutable, maybe immutable
73 mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
76 data = self._read_data_from_formpost(req)
77 d = client.create_mutable_file(data)
78 def _uploaded(newnode):
79 d2 = self.parentnode.set_node(self.name, newnode,
81 d2.addCallback(lambda res: newnode.get_uri())
83 d.addCallback(_uploaded)
85 # create an immutable file
86 contents = req.fields["file"]
87 uploadable = FileHandle(contents.file, convergence=client.convergence)
88 d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
89 d.addCallback(lambda newnode: newnode.get_uri())
92 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
93 def __init__(self, client, parentnode, name):
94 rend.Page.__init__(self)
97 self.parentnode = parentnode
101 def render_PUT(self, ctx):
103 t = get_arg(req, "t", "").strip()
104 replace = boolean_of_arg(get_arg(req, "replace", "true"))
105 assert self.parentnode and self.name
106 if req.getHeader("content-range"):
107 raise WebError("Content-Range in PUT not yet supported",
108 http.NOT_IMPLEMENTED)
110 return self.replace_me_with_a_child(req, self.client, replace)
112 return self.replace_me_with_a_childcap(req, self.client, replace)
114 raise WebError("PUT to a file: bad t=%s" % t)
116 def render_POST(self, ctx):
118 t = get_arg(req, "t", "").strip()
119 replace = boolean_of_arg(get_arg(req, "replace", "true"))
121 # like PUT, but get the file data from an HTML form's input field.
122 # We could get here from POST /uri/mutablefilecap?t=upload,
123 # or POST /uri/path/file?t=upload, or
124 # POST /uri/path/dir?t=upload&name=foo . All have the same
125 # behavior, we just ignore any name= argument
126 d = self.replace_me_with_a_formpost(req, self.client, replace)
128 # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
129 # there are no other t= values left to be handled by the
131 raise WebError("POST to a file: bad t=%s" % t)
133 when_done = get_arg(req, "when_done", None)
135 d.addCallback(lambda res: url.URL.fromString(when_done))
139 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
140 def __init__(self, client, node, parentnode=None, name=None):
141 rend.Page.__init__(self)
145 self.parentnode = parentnode
148 def childFactory(self, ctx, name):
150 if should_create_intermediate_directories(req):
151 raise WebError("Cannot create directory '%s', because its "
152 "parent is a file, not a directory" % name)
153 raise WebError("Files have no children, certainly not named '%s'"
156 def render_GET(self, ctx):
158 t = get_arg(req, "t", "").strip()
160 # just get the contents
161 # the filename arrives as part of the URL or in a form input
162 # element, and will be sent back in a Content-Disposition header.
163 # Different browsers use various character sets for this name,
164 # sometimes depending upon how language environment is
165 # configured. Firefox sends the equivalent of
166 # urllib.quote(name.encode("utf-8")), while IE7 sometimes does
167 # latin-1. Browsers cannot agree on how to interpret the name
168 # they see in the Content-Disposition header either, despite some
169 # 11-year old standards (RFC2231) that explain how to do it
170 # properly. So we assume that at least the browser will agree
171 # with itself, and echo back the same bytes that we were given.
172 filename = get_arg(req, "filename", self.name) or "unknown"
173 if self.node.is_mutable():
174 # some day: d = self.node.get_best_version()
175 d = makeMutableDownloadable(self.node)
177 d = defer.succeed(self.node)
178 d.addCallback(lambda dn: FileDownloader(dn, filename))
181 return FileJSONMetadata(ctx, self.node)
183 return MoreInfo(self.node)
185 return FileURI(ctx, self.node)
186 if t == "readonly-uri":
187 return FileReadOnlyURI(ctx, self.node)
188 raise WebError("GET file: bad t=%s" % t)
190 def render_HEAD(self, ctx):
192 t = get_arg(req, "t", "").strip()
194 raise WebError("GET file: bad t=%s" % t)
195 filename = get_arg(req, "filename", self.name) or "unknown"
196 if self.node.is_mutable():
197 # some day: d = self.node.get_best_version()
198 d = makeMutableDownloadable(self.node)
200 d = defer.succeed(self.node)
201 d.addCallback(lambda dn: FileDownloader(dn, filename))
204 def render_PUT(self, ctx):
206 t = get_arg(req, "t", "").strip()
207 replace = boolean_of_arg(get_arg(req, "replace", "true"))
209 if self.node.is_mutable():
210 return self.replace_my_contents(req)
212 # this is the early trap: if someone else modifies the
213 # directory while we're uploading, the add_file(overwrite=)
214 # call in replace_me_with_a_child will do the late trap.
215 raise ExistingChildError()
216 assert self.parentnode and self.name
217 return self.replace_me_with_a_child(req, self.client, replace)
220 raise ExistingChildError()
221 assert self.parentnode and self.name
222 return self.replace_me_with_a_childcap(req, self.client, replace)
224 raise WebError("PUT to a file: bad t=%s" % t)
226 def render_POST(self, ctx):
228 t = get_arg(req, "t", "").strip()
229 replace = boolean_of_arg(get_arg(req, "replace", "true"))
231 d = self._POST_check(req)
233 # like PUT, but get the file data from an HTML form's input field
234 # We could get here from POST /uri/mutablefilecap?t=upload,
235 # or POST /uri/path/file?t=upload, or
236 # POST /uri/path/dir?t=upload&name=foo . All have the same
237 # behavior, we just ignore any name= argument
238 if self.node.is_mutable():
239 d = self.replace_my_contents_with_a_formpost(req)
242 raise ExistingChildError()
243 assert self.parentnode and self.name
244 d = self.replace_me_with_a_formpost(req, self.client, replace)
246 raise WebError("POST to file: bad t=%s" % t)
248 when_done = get_arg(req, "when_done", None)
250 d.addCallback(lambda res: url.URL.fromString(when_done))
253 def _POST_check(self, req):
254 verify = boolean_of_arg(get_arg(req, "verify", "false"))
255 repair = boolean_of_arg(get_arg(req, "repair", "false"))
256 add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
257 if isinstance(self.node, LiteralFileNode):
258 return defer.succeed(LiteralCheckResults(self.client))
260 d = self.node.check_and_repair(Monitor(), verify, add_lease)
261 d.addCallback(lambda res: CheckAndRepairResults(self.client, res))
263 d = self.node.check(Monitor(), verify, add_lease)
264 d.addCallback(lambda res: CheckResults(self.client, res))
267 def render_DELETE(self, ctx):
268 assert self.parentnode and self.name
269 d = self.parentnode.delete(self.name)
270 d.addCallback(lambda res: self.node.get_uri())
273 def replace_my_contents(self, req):
275 new_contents = req.content.read()
276 d = self.node.overwrite(new_contents)
277 d.addCallback(lambda res: self.node.get_uri())
280 def replace_my_contents_with_a_formpost(self, req):
281 # we have a mutable file. Get the data from the formpost, and replace
282 # the mutable file's contents with it.
283 new_contents = self._read_data_from_formpost(req)
284 d = self.node.overwrite(new_contents)
285 d.addCallback(lambda res: self.node.get_uri())
288 class MutableDownloadable:
289 #implements(IDownloadable)
290 def __init__(self, size, node):
295 def is_mutable(self):
297 def read(self, consumer, offset=0, size=None):
298 d = self.node.download_best_version()
299 d.addCallback(self._got_data, consumer, offset, size)
301 def _got_data(self, contents, consumer, offset, size):
307 # SDMF: we can write the whole file in one big chunk
308 consumer.write(contents[start:end])
311 def makeMutableDownloadable(n):
312 d = defer.maybeDeferred(n.get_size_of_best_version)
313 d.addCallback(MutableDownloadable, n)
316 class FileDownloader(rend.Page):
317 # since we override the rendering process (to let the tahoe Downloader
318 # drive things), we must inherit from regular old twisted.web.resource
319 # instead of nevow.rend.Page . Nevow will use adapters to wrap a
320 # nevow.appserver.OldResourceAdapter around any
321 # twisted.web.resource.IResource that it is given. TODO: it looks like
322 # that wrapper would allow us to return a Deferred from render(), which
323 # might could simplify the implementation of WebDownloadTarget.
325 def __init__(self, filenode, filename):
326 rend.Page.__init__(self)
327 self.filenode = filenode
328 self.filename = filename
330 def renderHTTP(self, ctx):
332 gte = static.getTypeAndEncoding
333 ctype, encoding = gte(self.filename,
334 static.File.contentTypes,
335 static.File.contentEncodings,
336 defaultType="text/plain")
337 req.setHeader("content-type", ctype)
339 req.setHeader("content-encoding", encoding)
341 save_to_filename = None
342 if boolean_of_arg(get_arg(req, "save", "False")):
343 # tell the browser to save the file rather display it we don't
344 # try to encode the filename, instead we echo back the exact same
345 # bytes we were given in the URL. See the comment in
346 # FileNodeHandler.render_GET for the sad details.
347 req.setHeader("content-disposition",
348 'attachment; filename="%s"' % self.filename)
350 filesize = self.filenode.get_size()
351 assert isinstance(filesize, (int,long)), filesize
352 offset, size = 0, None
353 contentsize = filesize
354 req.setHeader("accept-ranges", "bytes")
355 if not self.filenode.is_mutable():
356 # TODO: look more closely at Request.setETag and how it interacts
357 # with a conditional "if-etag-equals" request, I think this may
358 # need to occur after the setResponseCode below
359 si = self.filenode.get_storage_index()
361 req.setETag(base32.b2a(si))
362 # TODO: for mutable files, use the roothash. For LIT, hash the data.
363 # or maybe just use the URI for CHK and LIT.
364 rangeheader = req.getHeader('range')
366 # adapted from nevow.static.File
367 bytesrange = rangeheader.split('=')
368 if bytesrange[0] != 'bytes':
369 raise WebError("Syntactically invalid http range header!")
370 start, end = bytesrange[1].split('-')
376 # "If the last-byte-pos value is absent, or if the value is
377 # greater than or equal to the current length of the
378 # entity-body, last-byte-pos is taken to be equal to one less
379 # than the current length of the entity- body in bytes."
381 size = int(end) - offset + 1
382 req.setResponseCode(http.PARTIAL_CONTENT)
383 req.setHeader('content-range',"bytes %s-%s/%s" %
384 (str(offset), str(offset+size-1), str(filesize)))
386 req.setHeader("content-length", str(contentsize))
387 if req.method == "HEAD":
389 d = self.filenode.read(req, offset, size)
391 if req.startedWriting:
392 # The content-type is already set, and the response code has
393 # already been sent, so we can't provide a clean error
394 # indication. We can emit text (which a browser might
395 # interpret as something else), and if we sent a Size header,
396 # they might notice that we've truncated the data. Keep the
397 # error message small to improve the chances of having our
398 # error response be shorter than the intended results.
400 # We don't have a lot of options, unfortunately.
401 req.write("problem during download\n")
404 # We haven't written anything yet, so we can provide a
405 # sensible error message.
406 eh = MyExceptionHandler()
407 eh.renderHTTP_exception(ctx, f)
408 d.addCallbacks(lambda ign: req.finish(), _error)
412 def FileJSONMetadata(ctx, filenode):
413 if filenode.is_readonly():
415 ro_uri = filenode.get_uri()
417 rw_uri = filenode.get_uri()
418 ro_uri = filenode.get_readonly_uri()
419 data = ("filenode", {})
420 data[1]['size'] = filenode.get_size()
422 data[1]['ro_uri'] = ro_uri
424 data[1]['rw_uri'] = rw_uri
425 verifycap = filenode.get_verify_cap()
427 data[1]['verify_uri'] = verifycap.to_string()
428 data[1]['mutable'] = filenode.is_mutable()
429 return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
431 def FileURI(ctx, filenode):
432 return text_plain(filenode.get_uri(), ctx)
434 def FileReadOnlyURI(ctx, filenode):
435 if filenode.is_readonly():
436 return text_plain(filenode.get_uri(), ctx)
437 return text_plain(filenode.get_readonly_uri(), ctx)
439 class FileNodeDownloadHandler(FileNodeHandler):
440 def childFactory(self, ctx, name):
441 return FileNodeDownloadHandler(self.client, self.node, name=name)