]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
src/allmydata/web/filenode.py: delete a stale comment that was made incorrect by...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / filenode.py
1
2 import simplejson
3
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
8
9 from allmydata.interfaces import ExistingChildError
10 from allmydata.monitor import Monitor
11 from allmydata.immutable.upload import FileHandle
12 from allmydata.util import log, base32
13
14 from allmydata.web.common import text_plain, WebError, RenderMixin, \
15      boolean_of_arg, get_arg, should_create_intermediate_directories, \
16      MyExceptionHandler, parse_replace_arg
17 from allmydata.web.check_results import CheckResults, \
18      CheckAndRepairResults, LiteralCheckResults
19 from allmydata.web.info import MoreInfo
20
21 class ReplaceMeMixin:
22     def replace_me_with_a_child(self, req, client, replace):
23         # a new file is being uploaded in our place.
24         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
25         if mutable:
26             req.content.seek(0)
27             data = req.content.read()
28             d = client.create_mutable_file(data)
29             def _uploaded(newnode):
30                 d2 = self.parentnode.set_node(self.name, newnode,
31                                               overwrite=replace)
32                 d2.addCallback(lambda res: newnode)
33                 return d2
34             d.addCallback(_uploaded)
35         else:
36             uploadable = FileHandle(req.content, convergence=client.convergence)
37             d = self.parentnode.add_file(self.name, uploadable,
38                                          overwrite=replace)
39         def _done(filenode):
40             log.msg("webish upload complete",
41                     facility="tahoe.webish", level=log.NOISY, umid="TCjBGQ")
42             if self.node:
43                 # we've replaced an existing file (or modified a mutable
44                 # file), so the response code is 200
45                 req.setResponseCode(http.OK)
46             else:
47                 # we've created a new file, so the code is 201
48                 req.setResponseCode(http.CREATED)
49             return filenode.get_uri()
50         d.addCallback(_done)
51         return d
52
53     def replace_me_with_a_childcap(self, req, client, replace):
54         req.content.seek(0)
55         childcap = req.content.read()
56         childnode = client.create_node_from_uri(childcap, None, name=self.name)
57         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
58         d.addCallback(lambda res: childnode.get_uri())
59         return d
60
61     def _read_data_from_formpost(self, req):
62         # SDMF: files are small, and we can only upload data, so we read
63         # the whole file into memory before uploading.
64         contents = req.fields["file"]
65         contents.file.seek(0)
66         data = contents.file.read()
67         return data
68
69     def replace_me_with_a_formpost(self, req, client, replace):
70         # create a new file, maybe mutable, maybe immutable
71         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
72
73         if mutable:
74             data = self._read_data_from_formpost(req)
75             d = client.create_mutable_file(data)
76             def _uploaded(newnode):
77                 d2 = self.parentnode.set_node(self.name, newnode,
78                                               overwrite=replace)
79                 d2.addCallback(lambda res: newnode.get_uri())
80                 return d2
81             d.addCallback(_uploaded)
82             return d
83         # create an immutable file
84         contents = req.fields["file"]
85         uploadable = FileHandle(contents.file, convergence=client.convergence)
86         d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
87         d.addCallback(lambda newnode: newnode.get_uri())
88         return d
89
90 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
91     def __init__(self, client, parentnode, name):
92         rend.Page.__init__(self)
93         self.client = client
94         assert parentnode
95         self.parentnode = parentnode
96         self.name = name
97         self.node = None
98
99     def render_PUT(self, ctx):
100         req = IRequest(ctx)
101         t = get_arg(req, "t", "").strip()
102         replace = parse_replace_arg(get_arg(req, "replace", "true"))
103
104         assert self.parentnode and self.name
105         if req.getHeader("content-range"):
106             raise WebError("Content-Range in PUT not yet supported",
107                            http.NOT_IMPLEMENTED)
108         if not t:
109             return self.replace_me_with_a_child(req, self.client, replace)
110         if t == "uri":
111             return self.replace_me_with_a_childcap(req, self.client, replace)
112
113         raise WebError("PUT to a file: bad t=%s" % t)
114
115     def render_POST(self, ctx):
116         req = IRequest(ctx)
117         t = get_arg(req, "t", "").strip()
118         replace = boolean_of_arg(get_arg(req, "replace", "true"))
119         if t == "upload":
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(req, self.client, replace)
126         else:
127             # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
128             # there are no other t= values left to be handled by the
129             # placeholder.
130             raise WebError("POST to a file: bad t=%s" % t)
131
132         when_done = get_arg(req, "when_done", None)
133         if when_done:
134             d.addCallback(lambda res: url.URL.fromString(when_done))
135         return d
136
137
138 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
139     def __init__(self, client, node, parentnode=None, name=None):
140         rend.Page.__init__(self)
141         self.client = client
142         assert node
143         self.node = node
144         self.parentnode = parentnode
145         self.name = name
146
147     def childFactory(self, ctx, name):
148         req = IRequest(ctx)
149         if should_create_intermediate_directories(req):
150             raise WebError("Cannot create directory '%s', because its "
151                            "parent is a file, not a directory" % name)
152         raise WebError("Files have no children, certainly not named '%s'"
153                        % name)
154
155     def render_GET(self, ctx):
156         req = IRequest(ctx)
157         t = get_arg(req, "t", "").strip()
158         if not t:
159             # just get the contents
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             if self.node.is_mutable():
173                 # some day: d = self.node.get_best_version()
174                 d = makeMutableDownloadable(self.node)
175             else:
176                 d = defer.succeed(self.node)
177             d.addCallback(lambda dn: FileDownloader(dn, filename))
178             return d
179         if t == "json":
180             if self.parentnode and self.name:
181                 d = self.parentnode.get_metadata_for(self.name)
182             else:
183                 d = defer.succeed(None)
184             d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
185             return d
186         if t == "info":
187             return MoreInfo(self.node)
188         if t == "uri":
189             return FileURI(ctx, self.node)
190         if t == "readonly-uri":
191             return FileReadOnlyURI(ctx, self.node)
192         raise WebError("GET file: bad t=%s" % t)
193
194     def render_HEAD(self, ctx):
195         req = IRequest(ctx)
196         t = get_arg(req, "t", "").strip()
197         if t:
198             raise WebError("GET file: bad t=%s" % t)
199         filename = get_arg(req, "filename", self.name) or "unknown"
200         if self.node.is_mutable():
201             # some day: d = self.node.get_best_version()
202             d = makeMutableDownloadable(self.node)
203         else:
204             d = defer.succeed(self.node)
205         d.addCallback(lambda dn: FileDownloader(dn, filename))
206         return d
207
208     def render_PUT(self, ctx):
209         req = IRequest(ctx)
210         t = get_arg(req, "t", "").strip()
211         replace = parse_replace_arg(get_arg(req, "replace", "true"))
212
213         if not t:
214             if self.node.is_mutable():
215                 return self.replace_my_contents(req)
216             if not replace:
217                 # this is the early trap: if someone else modifies the
218                 # directory while we're uploading, the add_file(overwrite=)
219                 # call in replace_me_with_a_child will do the late trap.
220                 raise ExistingChildError()
221             assert self.parentnode and self.name
222             return self.replace_me_with_a_child(req, self.client, replace)
223         if t == "uri":
224             if not replace:
225                 raise ExistingChildError()
226             assert self.parentnode and self.name
227             return self.replace_me_with_a_childcap(req, self.client, replace)
228
229         raise WebError("PUT to a file: bad t=%s" % t)
230
231     def render_POST(self, ctx):
232         req = IRequest(ctx)
233         t = get_arg(req, "t", "").strip()
234         replace = boolean_of_arg(get_arg(req, "replace", "true"))
235         if t == "check":
236             d = self._POST_check(req)
237         elif t == "upload":
238             # like PUT, but get the file data from an HTML form's input field
239             # We could get here from POST /uri/mutablefilecap?t=upload,
240             # or POST /uri/path/file?t=upload, or
241             # POST /uri/path/dir?t=upload&name=foo . All have the same
242             # behavior, we just ignore any name= argument
243             if self.node.is_mutable():
244                 d = self.replace_my_contents_with_a_formpost(req)
245             else:
246                 if not replace:
247                     raise ExistingChildError()
248                 assert self.parentnode and self.name
249                 d = self.replace_me_with_a_formpost(req, self.client, replace)
250         else:
251             raise WebError("POST to file: bad t=%s" % t)
252
253         when_done = get_arg(req, "when_done", None)
254         if when_done:
255             d.addCallback(lambda res: url.URL.fromString(when_done))
256         return d
257
258     def _maybe_literal(self, res, Results_Class):
259         if res:
260             return Results_Class(self.client, res)
261         return LiteralCheckResults(self.client)
262
263     def _POST_check(self, req):
264         verify = boolean_of_arg(get_arg(req, "verify", "false"))
265         repair = boolean_of_arg(get_arg(req, "repair", "false"))
266         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
267         if repair:
268             d = self.node.check_and_repair(Monitor(), verify, add_lease)
269             d.addCallback(self._maybe_literal, CheckAndRepairResults)
270         else:
271             d = self.node.check(Monitor(), verify, add_lease)
272             d.addCallback(self._maybe_literal, CheckResults)
273         return d
274
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())
279         return d
280
281     def replace_my_contents(self, req):
282         req.content.seek(0)
283         new_contents = req.content.read()
284         d = self.node.overwrite(new_contents)
285         d.addCallback(lambda res: self.node.get_uri())
286         return d
287
288     def replace_my_contents_with_a_formpost(self, req):
289         # we have a mutable file. Get the data from the formpost, and replace
290         # the mutable file's contents with it.
291         new_contents = self._read_data_from_formpost(req)
292         d = self.node.overwrite(new_contents)
293         d.addCallback(lambda res: self.node.get_uri())
294         return d
295
296 class MutableDownloadable:
297     #implements(IDownloadable)
298     def __init__(self, size, node):
299         self.size = size
300         self.node = node
301     def get_size(self):
302         return self.size
303     def is_mutable(self):
304         return True
305     def read(self, consumer, offset=0, size=None):
306         d = self.node.download_best_version()
307         d.addCallback(self._got_data, consumer, offset, size)
308         return d
309     def _got_data(self, contents, consumer, offset, size):
310         start = offset
311         if size is not None:
312             end = offset+size
313         else:
314             end = self.size
315         # SDMF: we can write the whole file in one big chunk
316         consumer.write(contents[start:end])
317         return consumer
318
319 def makeMutableDownloadable(n):
320     d = defer.maybeDeferred(n.get_size_of_best_version)
321     d.addCallback(MutableDownloadable, n)
322     return d
323
324 class FileDownloader(rend.Page):
325     def __init__(self, filenode, filename):
326         rend.Page.__init__(self)
327         self.filenode = filenode
328         self.filename = filename
329
330     def parse_range_header(self, range):
331         # Parse a byte ranges according to RFC 2616 "14.35.1 Byte
332         # Ranges".  Returns None if the range doesn't make sense so it
333         # can be ignored (per the spec).  When successful, returns a
334         # list of (first,last) inclusive range tuples.
335
336         filesize = self.filenode.get_size()
337         assert isinstance(filesize, (int,long)), filesize
338
339         try:
340             # byte-ranges-specifier
341             units, rangeset = range.split('=', 1)
342             if units != 'bytes':
343                 return None     # nothing else supported
344
345             def parse_range(r):
346                 first, last = r.split('-', 1)
347
348                 if first is '':
349                     # suffix-byte-range-spec
350                     first = filesize - long(last)
351                     last = filesize - 1
352                 else:
353                     # byte-range-spec
354
355                     # first-byte-pos
356                     first = long(first)
357
358                     # last-byte-pos
359                     if last is '':
360                         last = filesize - 1
361                     else:
362                         last = long(last)
363
364                 if last < first:
365                     raise ValueError
366
367                 return (first, last)
368
369             # byte-range-set
370             #
371             # Note: the spec uses "1#" for the list of ranges, which
372             # implicitly allows whitespace around the ',' separators,
373             # so strip it.
374             return [ parse_range(r.strip()) for r in rangeset.split(',') ]
375         except ValueError:
376             return None
377
378     def renderHTTP(self, ctx):
379         req = IRequest(ctx)
380         gte = static.getTypeAndEncoding
381         ctype, encoding = gte(self.filename,
382                               static.File.contentTypes,
383                               static.File.contentEncodings,
384                               defaultType="text/plain")
385         req.setHeader("content-type", ctype)
386         if encoding:
387             req.setHeader("content-encoding", encoding)
388
389         if boolean_of_arg(get_arg(req, "save", "False")):
390             # tell the browser to save the file rather display it we don't
391             # try to encode the filename, instead we echo back the exact same
392             # bytes we were given in the URL. See the comment in
393             # FileNodeHandler.render_GET for the sad details.
394             req.setHeader("content-disposition",
395                           'attachment; filename="%s"' % self.filename)
396
397         filesize = self.filenode.get_size()
398         assert isinstance(filesize, (int,long)), filesize
399         first, size = 0, None
400         contentsize = filesize
401         req.setHeader("accept-ranges", "bytes")
402         if not self.filenode.is_mutable():
403             # TODO: look more closely at Request.setETag and how it interacts
404             # with a conditional "if-etag-equals" request, I think this may
405             # need to occur after the setResponseCode below
406             si = self.filenode.get_storage_index()
407             if si:
408                 req.setETag(base32.b2a(si))
409         # TODO: for mutable files, use the roothash. For LIT, hash the data.
410         # or maybe just use the URI for CHK and LIT.
411         rangeheader = req.getHeader('range')
412         if rangeheader:
413             ranges = self.parse_range_header(rangeheader)
414
415             # ranges = None means the header didn't parse, so ignore
416             # the header as if it didn't exist.  If is more than one
417             # range, then just return the first for now, until we can
418             # generate multipart/byteranges.
419             if ranges is not None:
420                 first, last = ranges[0]
421
422                 if first >= filesize:
423                     raise WebError('First beyond end of file',
424                                    http.REQUESTED_RANGE_NOT_SATISFIABLE)
425                 else:
426                     first = max(0, first)
427                     last = min(filesize-1, last)
428
429                     req.setResponseCode(http.PARTIAL_CONTENT)
430                     req.setHeader('content-range',"bytes %s-%s/%s" %
431                                   (str(first), str(last),
432                                    str(filesize)))
433                     contentsize = last - first + 1
434                     size = contentsize
435
436         req.setHeader("content-length", str(contentsize))
437         if req.method == "HEAD":
438             return ""
439
440         # Twisted >=9.0 throws an error if we call req.finish() on a closed
441         # HTTP connection. It also has req.notifyFinish() to help avoid it.
442         finished = []
443         def _request_finished(ign):
444             finished.append(True)
445         if hasattr(req, "notifyFinish"):
446             req.notifyFinish().addBoth(_request_finished)
447
448         d = self.filenode.read(req, first, size)
449
450         def _finished(ign):
451             if not finished:
452                 req.finish()
453         def _error(f):
454             lp = log.msg("error during GET", facility="tahoe.webish", failure=f,
455                          level=log.UNUSUAL, umid="xSiF3w")
456             if finished:
457                 log.msg("but it's too late to tell them", parent=lp,
458                         level=log.UNUSUAL, umid="j1xIbw")
459                 return
460             req._tahoe_request_had_error = f # for HTTP-style logging
461             if req.startedWriting:
462                 # The content-type is already set, and the response code has
463                 # already been sent, so we can't provide a clean error
464                 # indication. We can emit text (which a browser might
465                 # interpret as something else), and if we sent a Size header,
466                 # they might notice that we've truncated the data. Keep the
467                 # error message small to improve the chances of having our
468                 # error response be shorter than the intended results.
469                 #
470                 # We don't have a lot of options, unfortunately.
471                 req.write("problem during download\n")
472                 req.finish()
473             else:
474                 # We haven't written anything yet, so we can provide a
475                 # sensible error message.
476                 eh = MyExceptionHandler()
477                 eh.renderHTTP_exception(ctx, f)
478         d.addCallbacks(_finished, _error)
479         return req.deferred
480
481
482 def FileJSONMetadata(ctx, filenode, edge_metadata):
483     rw_uri = filenode.get_write_uri()
484     ro_uri = filenode.get_readonly_uri()
485     data = ("filenode", {})
486     data[1]['size'] = filenode.get_size()
487     if ro_uri:
488         data[1]['ro_uri'] = ro_uri
489     if rw_uri:
490         data[1]['rw_uri'] = rw_uri
491     verifycap = filenode.get_verify_cap()
492     if verifycap:
493         data[1]['verify_uri'] = verifycap.to_string()
494     data[1]['mutable'] = filenode.is_mutable()
495     if edge_metadata is not None:
496         data[1]['metadata'] = edge_metadata
497     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
498
499 def FileURI(ctx, filenode):
500     return text_plain(filenode.get_uri(), ctx)
501
502 def FileReadOnlyURI(ctx, filenode):
503     if filenode.is_readonly():
504         return text_plain(filenode.get_uri(), ctx)
505     return text_plain(filenode.get_readonly_uri(), ctx)
506
507 class FileNodeDownloadHandler(FileNodeHandler):
508     def childFactory(self, ctx, name):
509         return FileNodeDownloadHandler(self.client, self.node, name=name)