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