From 53f7d2c7feda0cb3b407fa4d772ffbd504b83974 Mon Sep 17 00:00:00 2001
From: david-sarah <david-sarah@jacaranda.org>
Date: Mon, 31 May 2010 21:54:28 -0700
Subject: [PATCH] dirnode.py: Fix #1034 (MetadataSetter does not enforce
 restriction on setting 'tahoe' subkeys), and expose the metadata updater for
 use by SFTP. Also, support diminishing a child cap to read-only if 'no-write'
 is set in the metadata.

---
 src/allmydata/dirnode.py           | 128 ++++++++++++++++++-----------
 src/allmydata/test/test_dirnode.py |  11 ++-
 2 files changed, 87 insertions(+), 52 deletions(-)

diff --git a/src/allmydata/dirnode.py b/src/allmydata/dirnode.py
index fca772b9..51f40c25 100644
--- a/src/allmydata/dirnode.py
+++ b/src/allmydata/dirnode.py
@@ -24,6 +24,50 @@ from pycryptopp.cipher.aes import AES
 from allmydata.util.dictutil import AuxValueDict
 
 
+def update_metadata(metadata, new_metadata, now):
+    """Updates 'metadata' in-place with the information in 'new_metadata'.
+    Timestamps are set according to the time 'now'."""
+
+    if metadata is None:
+        metadata = {"ctime": now,
+                    "mtime": now,
+                    "tahoe": {
+                        "linkcrtime": now,
+                        "linkmotime": now,
+                        }
+                    }
+
+    if new_metadata is not None:
+        # Overwrite all metadata.
+        newmd = new_metadata.copy()
+
+        # Except 'tahoe'.
+        if newmd.has_key('tahoe'):
+            del newmd['tahoe']
+        if metadata.has_key('tahoe'):
+            newmd['tahoe'] = metadata['tahoe']
+
+        metadata = newmd
+    else:
+        # For backwards compatibility with Tahoe < 1.4.0:
+        if "ctime" not in metadata:
+            metadata["ctime"] = now
+        metadata["mtime"] = now
+
+    # update timestamps
+    sysmd = metadata.get('tahoe', {})
+    if not 'linkcrtime' in sysmd:
+        if "ctime" in metadata:
+            # In Tahoe < 1.4.0 we used the word "ctime" to mean what Tahoe >= 1.4.0
+            # calls "linkcrtime".
+            sysmd["linkcrtime"] = metadata["ctime"]
+        else:
+            sysmd["linkcrtime"] = now
+    sysmd["linkmotime"] = now
+
+    return metadata
+
+
 # TODO: {Deleter,MetadataSetter,Adder}.modify all start by unpacking the
 # contents and end by repacking them. It might be better to apply them to
 # the unpacked contents.
@@ -47,28 +91,38 @@ class Deleter:
 
 
 class MetadataSetter:
-    def __init__(self, node, name, metadata):
+    def __init__(self, node, name, metadata, create_readonly_node=None):
         self.node = node
         self.name = name
         self.metadata = metadata
+        self.create_readonly_node = create_readonly_node
 
     def modify(self, old_contents, servermap, first_time):
         children = self.node._unpack_contents(old_contents)
-        if self.name not in children:
-            raise NoSuchChildError(self.name)
-        children[self.name] = (children[self.name][0], self.metadata)
+        name = self.name
+        if name not in children:
+            raise NoSuchChildError(name)
+
+        now = time.time()
+        metadata = update_metadata(children[name][1].copy(), self.metadata, now)
+        child = children[name][0]
+        if self.create_readonly_node and metadata and metadata.get('no-write', False):
+            child = self.create_readonly_node(child, name)
+
+        children[name] = (child, metadata)
         new_contents = self.node._pack_contents(children)
         return new_contents
 
 
 class Adder:
-    def __init__(self, node, entries=None, overwrite=True):
+    def __init__(self, node, entries=None, overwrite=True, create_readonly_node=None):
         self.node = node
         if entries is None:
             entries = {}
         precondition(isinstance(entries, dict), entries)
         self.entries = entries
         self.overwrite = overwrite
+        self.create_readonly_node = create_readonly_node
 
     def set_node(self, name, node, metadata):
         precondition(isinstance(name, unicode), name)
@@ -86,6 +140,7 @@ class Adder:
             # error again in pack_children.
             child.raise_error()
 
+            metadata = None
             if name in children:
                 if not self.overwrite:
                     raise ExistingChildError("child '%s' already exists" % name)
@@ -93,44 +148,11 @@ class Adder:
                 if self.overwrite == "only-files" and IDirectoryNode.providedBy(children[name][0]):
                     raise ExistingChildError("child '%s' already exists" % name)
                 metadata = children[name][1].copy()
-            else:
-                metadata = {"ctime": now,
-                            "mtime": now,
-                            "tahoe": {
-                                "linkcrtime": now,
-                                "linkmotime": now,
-                                }
-                            }
-
-            if new_metadata is not None:
-                # Overwrite all metadata.
-                newmd = new_metadata.copy()
-
-                # Except 'tahoe'.
-                if newmd.has_key('tahoe'):
-                    del newmd['tahoe']
-                if metadata.has_key('tahoe'):
-                    newmd['tahoe'] = metadata['tahoe']
-
-                metadata = newmd
-            else:
-                # For backwards compatibility with Tahoe < 1.4.0:
-                if "ctime" not in metadata:
-                    metadata["ctime"] = now
-                metadata["mtime"] = now
-
-            # update timestamps
-            sysmd = metadata.get('tahoe', {})
-            if not 'linkcrtime' in sysmd:
-                if "ctime" in metadata:
-                    # In Tahoe < 1.4.0 we used the word "ctime" to mean what Tahoe >= 1.4.0
-                    # calls "linkcrtime".
-                    sysmd["linkcrtime"] = metadata["ctime"]
-                else:
-                    sysmd["linkcrtime"] = now
-            sysmd["linkmotime"] = now
 
-            children[name] = (child, metadata)
+            if self.create_readonly_node and metadata and metadata.get('no-write', False):
+                child = self.create_readonly_node(child, name)
+
+            children[name] = (child, update_metadata(metadata, new_metadata, now))
         new_contents = self.node._pack_contents(children)
         return new_contents
 
@@ -248,6 +270,11 @@ class DirectoryNode:
         node.raise_error()
         return node
 
+    def _create_readonly_node(self, node, name):
+        if not node.is_unknown() and node.is_readonly():
+            return node
+        return self._create_and_validate_node(None, node.get_readonly_uri(), name=name)
+
     def _unpack_contents(self, data):
         # the directory is serialized as a list of netstrings, one per child.
         # Each child is serialized as a list of four netstrings: (name, ro_uri,
@@ -407,7 +434,8 @@ class DirectoryNode:
         if self.is_readonly():
             return defer.fail(NotWriteableError())
         assert isinstance(metadata, dict)
-        s = MetadataSetter(self, name, metadata)
+        s = MetadataSetter(self, name, metadata,
+                           create_readonly_node=self._create_readonly_node)
         d = self._node.modify(s.modify)
         d.addCallback(lambda res: self)
         return d
@@ -453,7 +481,7 @@ class DirectoryNode:
         precondition(isinstance(name, unicode), name)
         precondition(isinstance(writecap, (str,type(None))), writecap)
         precondition(isinstance(readcap, (str,type(None))), readcap)
-            
+
         # We now allow packing unknown nodes, provided they are valid
         # for this type of directory.
         child_node = self._create_and_validate_node(writecap, readcap, name)
@@ -463,7 +491,8 @@ class DirectoryNode:
 
     def set_children(self, entries, overwrite=True):
         # this takes URIs
-        a = Adder(self, overwrite=overwrite)
+        a = Adder(self, overwrite=overwrite,
+                  create_readonly_node=self._create_readonly_node)
         for (name, e) in entries.iteritems():
             assert isinstance(name, unicode)
             if len(e) == 2:
@@ -498,7 +527,8 @@ class DirectoryNode:
             return defer.fail(NotWriteableError())
         assert isinstance(name, unicode)
         assert IFilesystemNode.providedBy(child), child
-        a = Adder(self, overwrite=overwrite)
+        a = Adder(self, overwrite=overwrite,
+                  create_readonly_node=self._create_readonly_node)
         a.set_node(name, child, metadata)
         d = self._node.modify(a.modify)
         d.addCallback(lambda res: child)
@@ -508,7 +538,8 @@ class DirectoryNode:
         precondition(isinstance(entries, dict), entries)
         if self.is_readonly():
             return defer.fail(NotWriteableError())
-        a = Adder(self, entries, overwrite=overwrite)
+        a = Adder(self, entries, overwrite=overwrite,
+                  create_readonly_node=self._create_readonly_node)
         d = self._node.modify(a.modify)
         d.addCallback(lambda res: self)
         return d
@@ -551,7 +582,8 @@ class DirectoryNode:
             d = self._nodemaker.create_immutable_directory(initial_children)
         def _created(child):
             entries = {name: (child, None)}
-            a = Adder(self, entries, overwrite=overwrite)
+            a = Adder(self, entries, overwrite=overwrite,
+                      create_readonly_node=self._create_readonly_node)
             d = self._node.modify(a.modify)
             d.addCallback(lambda res: child)
             return d
diff --git a/src/allmydata/test/test_dirnode.py b/src/allmydata/test/test_dirnode.py
index 8f610a6e..5a326ee4 100644
--- a/src/allmydata/test/test_dirnode.py
+++ b/src/allmydata/test/test_dirnode.py
@@ -894,11 +894,14 @@ class Dirnode(GridTestMixin, unittest.TestCase,
 
             d.addCallback(lambda res:
                           n.set_metadata_for(u"child",
-                                             {"tags": ["web2.0-compatible"]}))
+                                             {"tags": ["web2.0-compatible"], "tahoe": {"bad": "mojo"}}))
             d.addCallback(lambda n1: n1.get_metadata_for(u"child"))
-            d.addCallback(lambda metadata:
-                          self.failUnlessEqual(metadata,
-                                               {"tags": ["web2.0-compatible"]}))
+            def _check_metadata(md):
+                self.failUnless("tags" in md, md)
+                self.failUnlessEqual(md["tags"], ["web2.0-compatible"])
+                self.failUnless("tahoe" in md, md)
+                self.failIf("bad" in md["tahoe"], md)
+            d.addCallback(_check_metadata)
 
             def _start(res):
                 self._start_timestamp = time.time()
-- 
2.45.2