]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/filenode.py
add --add-lease to 'tahoe check', 'tahoe deep-check', and webapi.
[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.immutable.filenode import LiteralFileNode
13 from allmydata.util import log, base32
14
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.check_results import CheckResults, \
18      CheckAndRepairResults, LiteralCheckResults
19 from allmydata.web.info import MoreInfo
20
21 class ReplaceMeMixin:
22
23     def replace_me_with_a_child(self, ctx, replace):
24         # a new file is being uploaded in our place.
25         req = IRequest(ctx)
26         client = IClient(ctx)
27         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
28         if mutable:
29             req.content.seek(0)
30             data = req.content.read()
31             d = client.create_mutable_file(data)
32             def _uploaded(newnode):
33                 d2 = self.parentnode.set_node(self.name, newnode,
34                                               overwrite=replace)
35                 d2.addCallback(lambda res: newnode)
36                 return d2
37             d.addCallback(_uploaded)
38         else:
39             uploadable = FileHandle(req.content, convergence=client.convergence)
40             d = self.parentnode.add_file(self.name, uploadable,
41                                          overwrite=replace)
42         def _done(filenode):
43             log.msg("webish upload complete",
44                     facility="tahoe.webish", level=log.NOISY)
45             if self.node:
46                 # we've replaced an existing file (or modified a mutable
47                 # file), so the response code is 200
48                 req.setResponseCode(http.OK)
49             else:
50                 # we've created a new file, so the code is 201
51                 req.setResponseCode(http.CREATED)
52             return filenode.get_uri()
53         d.addCallback(_done)
54         return d
55
56     def replace_me_with_a_childcap(self, ctx, replace):
57         req = IRequest(ctx)
58         req.content.seek(0)
59         childcap = req.content.read()
60         client = IClient(ctx)
61         childnode = client.create_node_from_uri(childcap)
62         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
63         d.addCallback(lambda res: childnode.get_uri())
64         return d
65
66     def _read_data_from_formpost(self, req):
67         # SDMF: files are small, and we can only upload data, so we read
68         # the whole file into memory before uploading.
69         contents = req.fields["file"]
70         contents.file.seek(0)
71         data = contents.file.read()
72         return data
73
74     def replace_me_with_a_formpost(self, ctx, replace):
75         # create a new file, maybe mutable, maybe immutable
76         req = IRequest(ctx)
77         client = IClient(ctx)
78         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
79
80         if mutable:
81             data = self._read_data_from_formpost(req)
82             d = client.create_mutable_file(data)
83             def _uploaded(newnode):
84                 d2 = self.parentnode.set_node(self.name, newnode,
85                                               overwrite=replace)
86                 d2.addCallback(lambda res: newnode.get_uri())
87                 return d2
88             d.addCallback(_uploaded)
89             return d
90         # create an immutable file
91         contents = req.fields["file"]
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 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
98     def __init__(self, parentnode, name):
99         rend.Page.__init__(self)
100         assert parentnode
101         self.parentnode = parentnode
102         self.name = name
103         self.node = None
104
105     def render_PUT(self, ctx):
106         req = IRequest(ctx)
107         t = get_arg(req, "t", "").strip()
108         replace = boolean_of_arg(get_arg(req, "replace", "true"))
109         assert self.parentnode and self.name
110         if req.getHeader("content-range"):
111             raise WebError("Content-Range in PUT not yet supported",
112                            http.NOT_IMPLEMENTED)
113         if not t:
114             return self.replace_me_with_a_child(ctx, replace)
115         if t == "uri":
116             return self.replace_me_with_a_childcap(ctx, replace)
117
118         raise WebError("PUT to a file: bad t=%s" % t)
119
120     def render_POST(self, ctx):
121         req = IRequest(ctx)
122         t = get_arg(req, "t", "").strip()
123         replace = boolean_of_arg(get_arg(req, "replace", "true"))
124         if t == "upload":
125             # like PUT, but get the file data from an HTML form's input field.
126             # We could get here from POST /uri/mutablefilecap?t=upload,
127             # or POST /uri/path/file?t=upload, or
128             # POST /uri/path/dir?t=upload&name=foo . All have the same
129             # behavior, we just ignore any name= argument
130             d = self.replace_me_with_a_formpost(ctx, replace)
131         else:
132             # t=mkdir is handled in DirectoryNodeHandler._POST_mkdir, so
133             # there are no other t= values left to be handled by the
134             # placeholder.
135             raise WebError("POST to a file: bad t=%s" % t)
136
137         when_done = get_arg(req, "when_done", None)
138         if when_done:
139             d.addCallback(lambda res: url.URL.fromString(when_done))
140         return d
141
142
143 class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
144     def __init__(self, node, parentnode=None, name=None):
145         rend.Page.__init__(self)
146         assert node
147         self.node = node
148         self.parentnode = parentnode
149         self.name = name
150
151     def childFactory(self, ctx, name):
152         req = IRequest(ctx)
153         if should_create_intermediate_directories(req):
154             raise WebError("Cannot create directory '%s', because its "
155                            "parent is a file, not a directory" % name)
156         raise WebError("Files have no children, certainly not named '%s'"
157                        % name)
158
159     def render_GET(self, ctx):
160         req = IRequest(ctx)
161         t = get_arg(req, "t", "").strip()
162         if not t:
163             # just get the contents
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             if self.node.is_mutable():
177                 # some day: d = self.node.get_best_version()
178                 d = makeMutableDownloadable(self.node)
179             else:
180                 d = defer.succeed(self.node)
181             d.addCallback(lambda dn: FileDownloader(dn, filename))
182             return d
183         if t == "json":
184             return FileJSONMetadata(ctx, self.node)
185         if t == "info":
186             return MoreInfo(self.node)
187         if t == "uri":
188             return FileURI(ctx, self.node)
189         if t == "readonly-uri":
190             return FileReadOnlyURI(ctx, self.node)
191         raise WebError("GET file: bad t=%s" % t)
192
193     def render_HEAD(self, ctx):
194         req = IRequest(ctx)
195         t = get_arg(req, "t", "").strip()
196         if t:
197             raise WebError("GET file: bad t=%s" % t)
198         filename = get_arg(req, "filename", self.name) or "unknown"
199         if self.node.is_mutable():
200             # some day: d = self.node.get_best_version()
201             d = makeMutableDownloadable(self.node)
202         else:
203             d = defer.succeed(self.node)
204         d.addCallback(lambda dn: FileDownloader(dn, filename))
205         return d
206
207     def render_PUT(self, ctx):
208         req = IRequest(ctx)
209         t = get_arg(req, "t", "").strip()
210         replace = boolean_of_arg(get_arg(req, "replace", "true"))
211         if not t:
212             if self.node.is_mutable():
213                 return self.replace_my_contents(ctx)
214             if not replace:
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)
221         if t == "uri":
222             if not replace:
223                 raise ExistingChildError()
224             assert self.parentnode and self.name
225             return self.replace_me_with_a_childcap(ctx, replace)
226
227         raise WebError("PUT to a file: bad t=%s" % t)
228
229     def render_POST(self, ctx):
230         req = IRequest(ctx)
231         t = get_arg(req, "t", "").strip()
232         replace = boolean_of_arg(get_arg(req, "replace", "true"))
233         if t == "check":
234             d = self._POST_check(req)
235         elif t == "upload":
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)
243             else:
244                 if not replace:
245                     raise ExistingChildError()
246                 assert self.parentnode and self.name
247                 d = self.replace_me_with_a_formpost(ctx, replace)
248         else:
249             raise WebError("POST to file: bad t=%s" % t)
250
251         when_done = get_arg(req, "when_done", None)
252         if when_done:
253             d.addCallback(lambda res: url.URL.fromString(when_done))
254         return d
255
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         add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
260         if isinstance(self.node, LiteralFileNode):
261             return defer.succeed(LiteralCheckResults())
262         if repair:
263             d = self.node.check_and_repair(Monitor(), verify, add_lease)
264             d.addCallback(lambda res: CheckAndRepairResults(res))
265         else:
266             d = self.node.check(Monitor(), verify, add_lease)
267             d.addCallback(lambda res: CheckResults(res))
268         return d
269
270     def render_DELETE(self, ctx):
271         assert self.parentnode and self.name
272         d = self.parentnode.delete(self.name)
273         d.addCallback(lambda res: self.node.get_uri())
274         return d
275
276     def replace_my_contents(self, ctx):
277         req = IRequest(ctx)
278         req.content.seek(0)
279         new_contents = req.content.read()
280         d = self.node.overwrite(new_contents)
281         d.addCallback(lambda res: self.node.get_uri())
282         return d
283
284     def replace_my_contents_with_a_formpost(self, ctx):
285         # we have a mutable file. Get the data from the formpost, and replace
286         # the mutable file's contents with it.
287         req = IRequest(ctx)
288         new_contents = self._read_data_from_formpost(req)
289         d = self.node.overwrite(new_contents)
290         d.addCallback(lambda res: self.node.get_uri())
291         return d
292
293 class MutableDownloadable:
294     #implements(IDownloadable)
295     def __init__(self, size, node):
296         self.size = size
297         self.node = node
298     def get_size(self):
299         return self.size
300     def is_mutable(self):
301         return True
302     def read(self, consumer, offset=0, size=None):
303         d = self.node.download_best_version()
304         d.addCallback(self._got_data, consumer, offset, size)
305         return d
306     def _got_data(self, contents, consumer, offset, size):
307         start = offset
308         if size is not None:
309             end = offset+size
310         else:
311             end = self.size
312         # SDMF: we can write the whole file in one big chunk
313         consumer.write(contents[start:end])
314         return consumer
315
316 def makeMutableDownloadable(n):
317     d = defer.maybeDeferred(n.get_size_of_best_version)
318     d.addCallback(MutableDownloadable, n)
319     return d
320
321 class FileDownloader(rend.Page):
322     # since we override the rendering process (to let the tahoe Downloader
323     # drive things), we must inherit from regular old twisted.web.resource
324     # instead of nevow.rend.Page . Nevow will use adapters to wrap a
325     # nevow.appserver.OldResourceAdapter around any
326     # twisted.web.resource.IResource that it is given. TODO: it looks like
327     # that wrapper would allow us to return a Deferred from render(), which
328     # might could simplify the implementation of WebDownloadTarget.
329
330     def __init__(self, filenode, filename):
331         rend.Page.__init__(self)
332         self.filenode = filenode
333         self.filename = filename
334
335     def renderHTTP(self, ctx):
336         req = IRequest(ctx)
337         gte = static.getTypeAndEncoding
338         ctype, encoding = gte(self.filename,
339                               static.File.contentTypes,
340                               static.File.contentEncodings,
341                               defaultType="text/plain")
342         req.setHeader("content-type", ctype)
343         if encoding:
344             req.setHeader("content-encoding", encoding)
345
346         save_to_filename = None
347         if boolean_of_arg(get_arg(req, "save", "False")):
348             # tell the browser to save the file rather display it we don't
349             # try to encode the filename, instead we echo back the exact same
350             # bytes we were given in the URL. See the comment in
351             # FileNodeHandler.render_GET for the sad details.
352             req.setHeader("content-disposition",
353                           'attachment; filename="%s"' % self.filename)
354
355         filesize = self.filenode.get_size()
356         assert isinstance(filesize, (int,long)), filesize
357         offset, size = 0, None
358         contentsize = filesize
359         req.setHeader("accept-ranges", "bytes")
360         if not self.filenode.is_mutable():
361             # TODO: look more closely at Request.setETag and how it interacts
362             # with a conditional "if-etag-equals" request, I think this may
363             # need to occur after the setResponseCode below
364             si = self.filenode.get_storage_index()
365             if si:
366                 req.setETag(base32.b2a(si))
367         # TODO: for mutable files, use the roothash. For LIT, hash the data.
368         # or maybe just use the URI for CHK and LIT.
369         rangeheader = req.getHeader('range')
370         if rangeheader:
371             # adapted from nevow.static.File
372             bytesrange = rangeheader.split('=')
373             if bytesrange[0] != 'bytes':
374                 raise WebError("Syntactically invalid http range header!")
375             start, end = bytesrange[1].split('-')
376             if start:
377                 offset = int(start)
378                 if not end:
379                     # RFC 2616 says:
380                     #
381                     # "If the last-byte-pos value is absent, or if the value is
382                     # greater than or equal to the current length of the
383                     # entity-body, last-byte-pos is taken to be equal to one less
384                     # than the current length of the entity- body in bytes."
385                     end = filesize - 1
386                 size = int(end) - offset + 1
387             req.setResponseCode(http.PARTIAL_CONTENT)
388             req.setHeader('content-range',"bytes %s-%s/%s" %
389                           (str(offset), str(offset+size-1), str(filesize)))
390             contentsize = size
391         req.setHeader("content-length", str(contentsize))
392         if req.method == "HEAD":
393             return ""
394         d = self.filenode.read(req, offset, size)
395         def _error(f):
396             if req.startedWriting:
397                 # The content-type is already set, and the response code has
398                 # already been sent, so we can't provide a clean error
399                 # indication. We can emit text (which a browser might
400                 # interpret as something else), and if we sent a Size header,
401                 # they might notice that we've truncated the data. Keep the
402                 # error message small to improve the chances of having our
403                 # error response be shorter than the intended results.
404                 #
405                 # We don't have a lot of options, unfortunately.
406                 req.write("problem during download\n")
407             else:
408                 # We haven't written anything yet, so we can provide a
409                 # sensible error message.
410                 msg = str(f.type)
411                 msg.replace("\n", "|")
412                 req.setResponseCode(http.GONE, msg)
413                 req.setHeader("content-type", "text/plain")
414                 req.responseHeaders.setRawHeaders("content-encoding", [])
415                 req.responseHeaders.setRawHeaders("content-disposition", [])
416                 # TODO: HTML-formatted exception?
417                 req.write(str(f))
418         d.addErrback(_error)
419         d.addBoth(lambda ign: req.finish())
420         return req.deferred
421
422
423 def FileJSONMetadata(ctx, filenode):
424     if filenode.is_readonly():
425         rw_uri = None
426         ro_uri = filenode.get_uri()
427     else:
428         rw_uri = filenode.get_uri()
429         ro_uri = filenode.get_readonly_uri()
430     data = ("filenode", {})
431     data[1]['size'] = filenode.get_size()
432     if ro_uri:
433         data[1]['ro_uri'] = ro_uri
434     if rw_uri:
435         data[1]['rw_uri'] = rw_uri
436     verifycap = filenode.get_verify_cap()
437     if verifycap:
438         data[1]['verify_uri'] = verifycap.to_string()
439     data[1]['mutable'] = filenode.is_mutable()
440     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
441
442 def FileURI(ctx, filenode):
443     return text_plain(filenode.get_uri(), ctx)
444
445 def FileReadOnlyURI(ctx, filenode):
446     if filenode.is_readonly():
447         return text_plain(filenode.get_uri(), ctx)
448     return text_plain(filenode.get_readonly_uri(), ctx)
449
450 class FileNodeDownloadHandler(FileNodeHandler):
451     def childFactory(self, ctx, name):
452         return FileNodeDownloadHandler(self.node, name=name)