]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - 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
index 66b7df0830fc20ca92325df2e4727def9bd4f5a7..ef8d3bae0a21ecd42e996479b4e211c86d448b05 100644 (file)
@@ -6,14 +6,19 @@ from twisted.internet import defer
 from nevow import url, rend
 from nevow.inevow import IRequest
 
-from allmydata.interfaces import ExistingChildError
+from allmydata.interfaces import ExistingChildError, SDMF_VERSION, MDMF_VERSION
 from allmydata.monitor import Monitor
 from allmydata.immutable.upload import FileHandle
+from allmydata.mutable.publish import MutableFileHandle
+from allmydata.mutable.common import MODE_READ
 from allmydata.util import log, base32
+from allmydata.util.encodingutil import quote_output
+from allmydata.blacklist import FileProhibited, ProhibitedNode
 
 from allmydata.web.common import text_plain, WebError, RenderMixin, \
      boolean_of_arg, get_arg, should_create_intermediate_directories, \
-     MyExceptionHandler, parse_replace_arg
+     MyExceptionHandler, parse_replace_arg, parse_offset_arg, \
+     parse_mutable_type_arg
 from allmydata.web.check_results import CheckResults, \
      CheckAndRepairResults, LiteralCheckResults
 from allmydata.web.info import MoreInfo
@@ -23,9 +28,13 @@ class ReplaceMeMixin:
         # a new file is being uploaded in our place.
         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
         if mutable:
-            req.content.seek(0)
-            data = req.content.read()
-            d = client.create_mutable_file(data)
+            arg = get_arg(req, "mutable-type", None)
+            mutable_type = parse_mutable_type_arg(arg)
+            if mutable_type is "invalid":
+                raise WebError("Unknown type: %s" % arg, http.BAD_REQUEST)
+
+            data = MutableFileHandle(req.content)
+            d = client.create_mutable_file(data, version=mutable_type)
             def _uploaded(newnode):
                 d2 = self.parentnode.set_node(self.name, newnode,
                                               overwrite=replace)
@@ -58,21 +67,20 @@ class ReplaceMeMixin:
         d.addCallback(lambda res: childnode.get_uri())
         return d
 
-    def _read_data_from_formpost(self, req):
-        # SDMF: files are small, and we can only upload data, so we read
-        # the whole file into memory before uploading.
-        contents = req.fields["file"]
-        contents.file.seek(0)
-        data = contents.file.read()
-        return data
 
     def replace_me_with_a_formpost(self, req, client, replace):
         # create a new file, maybe mutable, maybe immutable
         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
 
+        # create an immutable file
+        contents = req.fields["file"]
         if mutable:
-            data = self._read_data_from_formpost(req)
-            d = client.create_mutable_file(data)
+            arg = get_arg(req, "mutable-type", None)
+            mutable_type = parse_mutable_type_arg(arg)
+            if mutable_type is "invalid":
+                raise WebError("Unknown type: %s" % arg, http.BAD_REQUEST)
+            uploadable = MutableFileHandle(contents.file)
+            d = client.create_mutable_file(uploadable, version=mutable_type)
             def _uploaded(newnode):
                 d2 = self.parentnode.set_node(self.name, newnode,
                                               overwrite=replace)
@@ -80,13 +88,13 @@ class ReplaceMeMixin:
                 return d2
             d.addCallback(_uploaded)
             return d
-        # create an immutable file
-        contents = req.fields["file"]
+
         uploadable = FileHandle(contents.file, convergence=client.convergence)
         d = self.parentnode.add_file(self.name, uploadable, overwrite=replace)
         d.addCallback(lambda newnode: newnode.get_uri())
         return d
 
+
 class PlaceHolderNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
     def __init__(self, client, parentnode, name):
         rend.Page.__init__(self)
@@ -146,11 +154,13 @@ class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
 
     def childFactory(self, ctx, name):
         req = IRequest(ctx)
+        if isinstance(self.node, ProhibitedNode):
+            raise FileProhibited(self.node.reason)
         if should_create_intermediate_directories(req):
-            raise WebError("Cannot create directory '%s', because its "
-                           "parent is a file, not a directory" % name)
-        raise WebError("Files have no children, certainly not named '%s'"
-                       % name)
+            raise WebError("Cannot create directory %s, because its "
+                           "parent is a file, not a directory" % quote_output(name, encoding='utf-8'))
+        raise WebError("Files have no children, certainly not named %s"
+                       % quote_output(name, encoding='utf-8'))
 
     def render_GET(self, ctx):
         req = IRequest(ctx)
@@ -169,18 +179,27 @@ class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
             # properly. So we assume that at least the browser will agree
             # with itself, and echo back the same bytes that we were given.
             filename = get_arg(req, "filename", self.name) or "unknown"
-            if self.node.is_mutable():
-                # some day: d = self.node.get_best_version()
-                d = makeMutableDownloadable(self.node)
-            else:
-                d = defer.succeed(self.node)
+            d = self.node.get_best_readable_version()
             d.addCallback(lambda dn: FileDownloader(dn, filename))
             return d
         if t == "json":
-            if self.parentnode and self.name:
-                d = self.parentnode.get_metadata_for(self.name)
+            # We do this to make sure that fields like size and
+            # mutable-type (which depend on the file on the grid and not
+            # just on the cap) are filled in. The latter gets used in
+            # tests, in particular.
+            #
+            # TODO: Make it so that the servermap knows how to update in
+            # a mode specifically designed to fill in these fields, and
+            # then update it in that mode.
+            if self.node.is_mutable():
+                d = self.node.get_servermap(MODE_READ)
             else:
                 d = defer.succeed(None)
+            if self.parentnode and self.name:
+                d.addCallback(lambda ignored:
+                    self.parentnode.get_metadata_for(self.name))
+            else:
+                d.addCallback(lambda ignored: None)
             d.addCallback(lambda md: FileJSONMetadata(ctx, self.node, md))
             return d
         if t == "info":
@@ -197,11 +216,7 @@ class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         if t:
             raise WebError("GET file: bad t=%s" % t)
         filename = get_arg(req, "filename", self.name) or "unknown"
-        if self.node.is_mutable():
-            # some day: d = self.node.get_best_version()
-            d = makeMutableDownloadable(self.node)
-        else:
-            d = defer.succeed(self.node)
+        d = self.node.get_best_readable_version()
         d.addCallback(lambda dn: FileDownloader(dn, filename))
         return d
 
@@ -209,17 +224,37 @@ class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
         req = IRequest(ctx)
         t = get_arg(req, "t", "").strip()
         replace = parse_replace_arg(get_arg(req, "replace", "true"))
+        offset = parse_offset_arg(get_arg(req, "offset", None))
 
         if not t:
-            if self.node.is_mutable():
-                return self.replace_my_contents(req)
             if not replace:
                 # this is the early trap: if someone else modifies the
                 # directory while we're uploading, the add_file(overwrite=)
                 # call in replace_me_with_a_child will do the late trap.
                 raise ExistingChildError()
-            assert self.parentnode and self.name
-            return self.replace_me_with_a_child(req, self.client, replace)
+
+            if self.node.is_mutable():
+                # Are we a readonly filenode? We shouldn't allow callers
+                # to try to replace us if we are.
+                if self.node.is_readonly():
+                    raise WebError("PUT to a mutable file: replace or update"
+                                   " requested with read-only cap")
+                if offset is None:
+                    return self.replace_my_contents(req)
+
+                if offset >= 0:
+                    return self.update_my_contents(req, offset)
+
+                raise WebError("PUT to a mutable file: Invalid offset")
+
+            else:
+                if offset is not None:
+                    raise WebError("PUT to a file: append operation invoked "
+                                   "on an immutable cap")
+
+                assert self.parentnode and self.name
+                return self.replace_me_with_a_child(req, self.client, replace)
+
         if t == "uri":
             if not replace:
                 raise ExistingChildError()
@@ -280,56 +315,36 @@ class FileNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
 
     def replace_my_contents(self, req):
         req.content.seek(0)
-        new_contents = req.content.read()
+        new_contents = MutableFileHandle(req.content)
         d = self.node.overwrite(new_contents)
         d.addCallback(lambda res: self.node.get_uri())
         return d
 
+
+    def update_my_contents(self, req, offset):
+        req.content.seek(0)
+        added_contents = MutableFileHandle(req.content)
+
+        d = self.node.get_best_mutable_version()
+        d.addCallback(lambda mv:
+            mv.update(added_contents, offset))
+        d.addCallback(lambda ignored:
+            self.node.get_uri())
+        return d
+
+
     def replace_my_contents_with_a_formpost(self, req):
         # we have a mutable file. Get the data from the formpost, and replace
         # the mutable file's contents with it.
-        new_contents = self._read_data_from_formpost(req)
+        new_contents = req.fields['file']
+        new_contents = MutableFileHandle(new_contents.file)
+
         d = self.node.overwrite(new_contents)
         d.addCallback(lambda res: self.node.get_uri())
         return d
 
-class MutableDownloadable:
-    #implements(IDownloadable)
-    def __init__(self, size, node):
-        self.size = size
-        self.node = node
-    def get_size(self):
-        return self.size
-    def is_mutable(self):
-        return True
-    def read(self, consumer, offset=0, size=None):
-        d = self.node.download_best_version()
-        d.addCallback(self._got_data, consumer, offset, size)
-        return d
-    def _got_data(self, contents, consumer, offset, size):
-        start = offset
-        if size is not None:
-            end = offset+size
-        else:
-            end = self.size
-        # SDMF: we can write the whole file in one big chunk
-        consumer.write(contents[start:end])
-        return consumer
-
-def makeMutableDownloadable(n):
-    d = defer.maybeDeferred(n.get_size_of_best_version)
-    d.addCallback(MutableDownloadable, n)
-    return d
 
 class FileDownloader(rend.Page):
-    # since we override the rendering process (to let the tahoe Downloader
-    # drive things), we must inherit from regular old twisted.web.resource
-    # instead of nevow.rend.Page . Nevow will use adapters to wrap a
-    # nevow.appserver.OldResourceAdapter around any
-    # twisted.web.resource.IResource that it is given. TODO: it looks like
-    # that wrapper would allow us to return a Deferred from render(), which
-    # might could simplify the implementation of WebDownloadTarget.
-
     def __init__(self, filenode, filename):
         rend.Page.__init__(self)
         self.filenode = filenode
@@ -502,6 +517,16 @@ def FileJSONMetadata(ctx, filenode, edge_metadata):
     data[1]['mutable'] = filenode.is_mutable()
     if edge_metadata is not None:
         data[1]['metadata'] = edge_metadata
+
+    if filenode.is_mutable() and filenode.get_version() is not None:
+        mutable_type = filenode.get_version()
+        assert mutable_type in (MDMF_VERSION, SDMF_VERSION)
+        if mutable_type == MDMF_VERSION:
+            mutable_type = "mdmf"
+        else:
+            mutable_type = "sdmf"
+        data[1]['mutable-type'] = mutable_type
+
     return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
 
 def FileURI(ctx, filenode):