]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/commitdiff
vdrive: switch to URI:DIR and URI:DIR-RO, providing transitive readonlyness
authorBrian Warner <warner@lothar.com>
Mon, 25 Jun 2007 20:23:51 +0000 (13:23 -0700)
committerBrian Warner <warner@lothar.com>
Mon, 25 Jun 2007 20:23:51 +0000 (13:23 -0700)
13 files changed:
src/allmydata/client.py
src/allmydata/filetable.py
src/allmydata/interfaces.py
src/allmydata/introducer_and_vdrive.py
src/allmydata/scripts/runner.py
src/allmydata/test/test_filetable.py
src/allmydata/test/test_system.py
src/allmydata/test/test_vdrive.py
src/allmydata/uri.py
src/allmydata/util/hashutil.py
src/allmydata/vdrive.py
src/allmydata/web/directory.xhtml
src/allmydata/webish.py

index bdd9189e5da3b392510d5b740c1e3c8fdbd707cc..70a47addaaf681f5dd2c894c21dd6fbd20bad1bb 100644 (file)
@@ -2,8 +2,8 @@
 import os, sha, stat, time
 from foolscap import Referenceable, SturdyRef
 from zope.interface import implements
-from allmydata.interfaces import RIClient
-from allmydata import node
+from allmydata.interfaces import RIClient, IDirectoryNode
+from allmydata import node, vdrive, uri
 
 from twisted.internet import defer, reactor
 from twisted.application.internet import TimerService
@@ -13,7 +13,6 @@ from allmydata.Crypto.Util.number import bytes_to_long
 from allmydata.storageserver import StorageServer
 from allmydata.upload import Uploader
 from allmydata.download import Downloader
-from allmydata.vdrive import DirectoryNode
 from allmydata.webish import WebishServer
 from allmydata.control import ControlServer
 from allmydata.introducer import IntroducerClient
@@ -28,7 +27,7 @@ class Client(node.Node, Referenceable):
     GLOBAL_VDRIVE_FURL_FILE = "vdrive.furl"
     MY_FURL_FILE = "myself.furl"
     SUICIDE_PREVENTION_HOTLINE_FILE = "suicide_prevention_hotline"
-    MY_VDRIVE_FURL_FILE = "my_vdrive.furl"
+    MY_VDRIVE_URI_FILE = "my_vdrive.uri"
 
     # we're pretty narrow-minded right now
     OLDEST_SUPPORTED_VERSION = allmydata.__version__
@@ -115,16 +114,20 @@ class Client(node.Node, Referenceable):
     def _got_vdrive(self, vdrive_server):
         # vdrive_server implements RIVirtualDriveServer
         self.log("connected to vdrive server")
-        d = vdrive_server.callRemote("get_public_root_furl")
-        d.addCallback(self._got_vdrive_root_furl, vdrive_server)
-        d.addCallback(self._create_my_vdrive)
+        d = vdrive_server.callRemote("get_public_root_uri")
+        d.addCallback(self._got_vdrive_uri)
+        d.addCallback(self._got_vdrive_rootnode)
+        d.addCallback(self._create_my_vdrive, vdrive_server)
         d.addCallback(self._got_my_vdrive)
 
-    def _got_vdrive_root_furl(self, vdrive_root_furl, vdrive_server):
-        root = DirectoryNode(vdrive_root_furl, self)
+    def _got_vdrive_uri(self, root_uri):
+        furl, wk = uri.unpack_dirnode_uri(root_uri)
+        self._vdrive_furl = furl
+        return vdrive.create_directory_node(self, root_uri)
+
+    def _got_vdrive_rootnode(self, rootnode):
         self.log("got vdrive root")
-        self._vdrive_server = vdrive_server
-        self._vdrive_root = root
+        self._vdrive_root = rootnode
         self._connected_to_vdrive = True
 
         #vdrive = self.getServiceNamed("vdrive")
@@ -133,34 +136,34 @@ class Client(node.Node, Referenceable):
 
         if "webish" in self.namedServices:
             webish = self.getServiceNamed("webish")
-            webish.set_vdrive_root(root)
+            webish.set_vdrive_rootnode(rootnode)
 
-    def _create_my_vdrive(self, ignored=None):
-        MY_VDRIVE_FURL_FILE = os.path.join(self.basedir,
-                                           self.MY_VDRIVE_FURL_FILE)
+    def _create_my_vdrive(self, ignored, vdrive_server):
+        MY_VDRIVE_URI_FILE = os.path.join(self.basedir,
+                                           self.MY_VDRIVE_URI_FILE)
         try:
-            f = open(MY_VDRIVE_FURL_FILE, "r")
-            my_vdrive_furl = f.read().strip()
+            f = open(MY_VDRIVE_URI_FILE, "r")
+            my_vdrive_uri = f.read().strip()
             f.close()
-            return defer.succeed(DirectoryNode(my_vdrive_furl, self))
+            return vdrive.create_directory_node(self, my_vdrive_uri)
         except EnvironmentError:
-            d = self._vdrive_server.callRemote("create_directory")
+            assert self._vdrive_furl
+            d = vdrive.create_directory(self, self._vdrive_furl)
             def _got_directory(dirnode):
-                f = open(MY_VDRIVE_FURL_FILE, "w")
-                f.write(dirnode.furl + "\n")
+                f = open(MY_VDRIVE_URI_FILE, "w")
+                f.write(dirnode.get_uri() + "\n")
                 f.close()
-                dirnode._set_client(self)
                 return dirnode
             d.addCallback(_got_directory)
             return d
 
     def _got_my_vdrive(self, my_vdrive):
-        assert isinstance(my_vdrive, DirectoryNode), my_vdrive
+        IDirectoryNode(my_vdrive)
         self._my_vdrive = my_vdrive
 
         if "webish" in self.namedServices:
             webish = self.getServiceNamed("webish")
-            webish.set_my_vdrive_root(my_vdrive)
+            webish.set_my_vdrive_rootnode(my_vdrive)
 
 
     def remote_get_versions(self):
index c948ded96e82bcd79ba58a430fe628696efa1119..99b8777f624f6a4b9099f8148bc4b71aa48bebc2 100644 (file)
 
 import os
 from zope.interface import implements
-from foolscap import Referenceable
-from allmydata.interfaces import RIVirtualDriveServer, RIMutableDirectoryNode
-from allmydata.vdrive import FileNode, DirectoryNode
-from allmydata.util import bencode, idlib
 from twisted.application import service
-from twisted.python import log
-
-class BadNameError(Exception):
-    """Bad filename component"""
+from foolscap import Referenceable
+from allmydata.interfaces import RIVirtualDriveServer
+from allmydata.util import bencode, idlib, hashutil, fileutil
+from allmydata import uri
 
-class BadFileError(Exception):
+class BadWriteEnablerError(Exception):
     pass
-
-class BadDirectoryError(Exception):
+class ChildAlreadyPresentError(Exception):
     pass
 
-class MutableDirectoryNode(Referenceable):
-    """I represent a single directory.
-
-    I am associated with a file on disk, using a randomly-generated (and
-    hopefully unique) name. This file contains a serialized dictionary which
-    maps child names to 'child specifications'. These specifications are
-    tuples, either of ('file', URI), or ('subdir', FURL).
-    """
-
-    implements(RIMutableDirectoryNode)
-
-    def __init__(self, basedir, name=None):
-        self._basedir = basedir
-        if name:
-            self._name = name
-            # for well-known nodes, make sure they exist
-            try:
-                ignored = self._read_from_file()
-            except EnvironmentError:
-                self._write_to_file({})
-        else:
-            self._name = self.create_filename()
-            self._write_to_file({}) # start out empty
-
-    def _read_from_file(self):
-        data = open(os.path.join(self._basedir, self._name), "rb").read()
-        children = bencode.bdecode(data)
-        child_nodes = {}
-        for k,v in children.iteritems():
-            if v[0] == "file":
-                child_nodes[k] = FileNode(v[1])
-            elif v[0] == "subdir":
-                child_nodes[k] = DirectoryNode(v[1])
-            else:
-                raise RuntimeError("unknown child spec '%s'" % (v[0],))
-        return child_nodes
-
-    def _write_to_file(self, children):
-        child_nodes = {}
-        for k,v in children.iteritems():
-            if isinstance(v, FileNode):
-                child_nodes[k] = ("file", v.uri)
-            elif isinstance(v, DirectoryNode):
-                child_nodes[k] = ("subdir", v.furl)
-            else:
-                raise RuntimeError("unknown child[%s] node '%s'" % (k,v))
-        data = bencode.bencode(child_nodes)
-        f = open(os.path.join(self._basedir, self._name), "wb")
-        f.write(data)
-        f.close()
-
-
-    def create_filename(self):
-        return idlib.b2a(os.urandom(8))
-
-    def validate_name(self, name):
-        if name == "." or name == ".." or "/" in name:
-            raise BadNameError("'%s' is not cool" % name)
-
-    # these are the public methods, available to anyone who holds a reference
-
-    def list(self):
-        log.msg("Dir(%s).list()" % self._name)
-        children = self._read_from_file()
-        results = list(children.items())
-        return sorted(results)
-    remote_list = list
-
-    def get(self, name):
-        log.msg("Dir(%s).get(%s)" % (self._name, name))
-        self.validate_name(name)
-        children = self._read_from_file()
-        if name not in children:
-            raise BadFileError("no such child")
-        return children[name]
-    remote_get = get
-
-    def add(self, name, child):
-        self.validate_name(name)
-        children = self._read_from_file()
-        if name in children:
-            raise BadNameError("the child already existed")
-        children[name] = child
-        self._write_to_file(children)
-        return child
-    remote_add = add
-
-    def remove(self, name):
-        self.validate_name(name)
-        children = self._read_from_file()
-        if name not in children:
-            raise BadFileError("cannot remove non-existent child")
-        child = children[name]
-        del children[name]
-        self._write_to_file(children)
-        return child
-    remote_remove = remove
-
-
 class NoPublicRootError(Exception):
     pass
 
 class VirtualDriveServer(service.MultiService, Referenceable):
     implements(RIVirtualDriveServer)
     name = "filetable"
-    VDRIVEDIR = "vdrive"
 
-    def __init__(self, basedir=".", offer_public_root=True):
+    def __init__(self, basedir, offer_public_root=True):
         service.MultiService.__init__(self)
-        vdrive_dir = os.path.join(basedir, self.VDRIVEDIR)
-        if not os.path.exists(vdrive_dir):
-            os.mkdir(vdrive_dir)
-        self._vdrive_dir = vdrive_dir
+        self._basedir = os.path.abspath(basedir)
+        fileutil.make_dirs(self._basedir)
         self._root = None
         if offer_public_root:
-            self._root = MutableDirectoryNode(vdrive_dir, "root")
-
-    def startService(self):
-        service.MultiService.startService(self)
-        # _register_all_dirnodes exists to avoid hacking our Tub to
-        # automatically translate inbound your-reference names
-        # (Tub.getReferenceForName) into MutableDirectoryNode instances by
-        # looking in our basedir for them. Without that hack, we have to
-        # register all nodes at startup to make sure they'll be available to
-        # all callers. In addition, we must register any new ones that we
-        # create later on.
-        tub = self.parent.tub
-        self._root_furl = tub.registerReference(self._root, "root")
-        self._register_all_dirnodes(tub)
-
-    def _register_all_dirnodes(self, tub):
-        for name in os.listdir(self._vdrive_dir):
-            node = MutableDirectoryNode(self._vdrive_dir, name)
-            ignored_furl = tub.registerReference(node, name)
-
-    def get_public_root_furl(self):
+            rootfile = os.path.join(self._basedir, "root")
+            if not os.path.exists(rootfile):
+                write_key = hashutil.random_key()
+                (wk, we, rk, index) = \
+                     hashutil.generate_dirnode_keys_from_writekey(write_key)
+                self.create_directory(index, we)
+                f = open(rootfile, "wb")
+                f.write(wk)
+                f.close()
+                self._root = wk
+            else:
+                f = open(rootfile, "rb")
+                self._root = f.read()
+
+    def set_furl(self, myfurl):
+        self._myfurl = myfurl
+
+    def get_public_root_uri(self):
         if self._root:
-            return self._root_furl
+            return uri.pack_dirnode_uri(self._myfurl, self._root)
         raise NoPublicRootError
-    remote_get_public_root_furl = get_public_root_furl
+    remote_get_public_root_uri = get_public_root_uri
 
-    def create_directory(self):
-        node = MutableDirectoryNode(self._vdrive_dir)
-        furl = self.parent.tub.registerReference(node, node._name)
-        return DirectoryNode(furl)
+    def create_directory(self, index, write_enabler):
+        data = [write_enabler, []]
+        self._write_to_file(index, data)
+        return index
     remote_create_directory = create_directory
+
+    # the file on disk consists of the write_enabler token and a list of
+    # (H(name), E(name), E(write), E(read)) tuples.
+
+    def _read_from_file(self, index):
+        name = idlib.b2a(index)
+        data = open(os.path.join(self._basedir, name), "rb").read()
+        return bencode.bdecode(data)
+
+    def _write_to_file(self, index, data):
+        name = idlib.b2a(index)
+        f = open(os.path.join(self._basedir, name), "wb")
+        f.write(bencode.bencode(data))
+        f.close()
+
+
+    def get(self, index, key):
+        data = self._read_from_file(index)
+        for (H_key, E_key, E_write, E_read) in data[1]:
+            if H_key == key:
+                return (E_write, E_read)
+        raise IndexError("unable to find key %s" % idlib.b2a(key))
+    remote_get = get
+
+    def list(self, index):
+        data = self._read_from_file(index)
+        response = [ (E_key, E_write, E_read)
+                     for (H_key, E_key, E_write, E_read) in data[1] ]
+        return response
+    remote_list = list
+
+    def delete(self, index, write_enabler, key):
+        data = self._read_from_file(index)
+        if data[0] != write_enabler:
+            raise BadWriteEnablerError
+        for i,(H_key, E_key, E_write, E_read) in enumerate(data[1]):
+            if H_key == key:
+                del data[1][i]
+                self._write_to_file(index, data)
+                return
+        raise IndexError("unable to find key %s" % idlib.b2a(key))
+    remote_delete = delete
+
+    def set(self, index, write_enabler, key,   name, write, read):
+        data = self._read_from_file(index)
+        if data[0] != write_enabler:
+            raise BadWriteEnablerError
+        # first, see if the key is already present
+        for i,(H_key, E_key, E_write, E_read) in enumerate(data[1]):
+            if H_key == key:
+                raise ChildAlreadyPresentError
+        # now just append the data
+        data[1].append( (key, name, write, read) )
+        self._write_to_file(index, data)
+    remote_set = set
index b7a88e827035a365891a85319388e24ac1411f82..028ef26d9d535bab33abffada628f90ed0d49471 100644 (file)
@@ -126,33 +126,79 @@ RIMutableDirectoryNode_ = Any() # TODO: how can we avoid this?
 FileNode_ = Any() # TODO: foolscap needs constraints on copyables
 DirectoryNode_ = Any() # TODO: same
 AnyNode_ = ChoiceOf(FileNode_, DirectoryNode_)
+EncryptedThing = str
 
-class RIMutableDirectoryNode(RemoteInterface):
-    def list():
-        return ListOf( TupleOf(str, # name, relative to directory
-                               AnyNode_,
-                               ),
-                       maxLength=100,
-                       )
+class RIVirtualDriveServer(RemoteInterface):
+    def get_public_root_uri():
+        """Obtain the URI for this server's global publically-writable root
+        directory. This returns a read-write directory URI.
+
+        If this vdrive server does not offer a public root, this will
+        raise an exception."""
+        return URI
 
-    def get(name=str):
-        return AnyNode_
+    def create_directory(index=Hash, write_enabler=Hash):
+        """Create a new (empty) directory, unattached to anything else.
 
-    def add(name=str, what=AnyNode_):
-        return AnyNode_
+        This returns the same index that was passed in.
+        """
+        return Hash
 
-    def remove(name=str):
-        return AnyNode_
+    def get(index=Hash, key=Hash):
+        """Retrieve a named child of the given directory. 'index' specifies
+        which directory is being accessed, and is generally the hash of the
+        read key. 'key' is the hash of the read key and the child name.
 
+        This operation returns a pair of encrypted strings. The first string
+        is meant to be decrypted by the Write Key and provides read-write
+        access to the child. If this directory holds read-only access to the
+        child, this first string will be an empty string. The second string
+        is meant to be decrypted by the Read Key and provides read-only
+        access to the child.
 
-class RIVirtualDriveServer(RemoteInterface):
-    def get_public_root_furl():
-        """If this vdrive server does not offer a public root, this will
-        raise an exception."""
-        return FURL
+        When the child is a read-write directory, the encrypted URI:DIR-RO
+        will be in the read slot, and the encrypted URI:DIR will be in the
+        write slot. When the child is a read-only directory, the encrypted
+        URI:DIR-RO will be in the read slot and the write slot will be empty.
+        When the child is a CHK file, the encrypted URI:CHK will be in the
+        read slot, and the write slot will be empty.
+
+        This might raise IndexError if there is no child by the desired name.
+        """
+        return (EncryptedThing, EncryptedThing)
+
+    def list(index=Hash):
+        """List the contents of a directory.
+
+        This returns a list of (NAME, WRITE, READ) tuples. Each value is an
+        encrypted string (although the WRITE value may sometimes be an empty
+        string).
+
+        NAME: the child name, encrypted with the Read Key
+        WRITE: the child write URI, encrypted with the Write Key, or an
+               empty string if this child is read-only
+        READ: the child read URI, encrypted with the Read Key
+        """
+        return ListOf((EncryptedThing, EncryptedThing, EncryptedThing),
+                      maxLength=100,
+                      )
+
+    def set(index=Hash, write_enabler=Hash, key=Hash,
+            name=EncryptedThing, write=EncryptedThing, read=EncryptedThing):
+        """Set a child object.
+
+        This will raise IndexError if a child with the given name already
+        exists.
+        """
+        pass
+
+    def delete(index=Hash, write_enabler=Hash, key=Hash):
+        """Delete a specific child.
+
+        This uses the hashed key to locate a specific child, and deletes it.
+        """
+        pass
 
-    def create_directory():
-        return DirectoryNode_
 
 class IFileNode(Interface):
     def download(target):
@@ -161,23 +207,66 @@ class IFileNode(Interface):
         pass
 
 class IDirectoryNode(Interface):
+    def is_mutable():
+        """Return True if this directory is mutable, False if it is read-only.
+        """
+    def get_uri():
+        """Return the directory URI that can be used by others to get access
+        to this directory node. If this node is read-only, the URI will only
+        offer read-only access. If this node is read-write, the URI will
+        offer read-write acess.
+
+        If you have read-write access to a directory and wish to share merely
+        read-only access with others, use get_immutable_uri().
+
+        The dirnode ('1') URI returned by this method can be used in set() on
+        a different directory ('2') to 'mount' a reference to this directory
+        ('1') under the other ('2'). This URI is just a string, so it can be
+        passed around through email or other out-of-band protocol.
+        """
+
+    def get_immutable_uri():
+        """Return the directory URI that can be used by others to get
+        read-only access to this directory node. The result is a read-only
+        URI, regardless of whether this dirnode is read-only or read-write.
+
+        If you have merely read-only access to this dirnode,
+        get_immutable_uri() will return the same thing as get_uri().
+        """
+
     def list():
-        """I return a Deferred that fires with a"""
-        pass
+        """I return a Deferred that fires with a dictionary mapping child
+        name to an IFileNode or IDirectoryNode."""
 
     def get(name):
-        """I return a Deferred that fires with a specific named child."""
-        pass
+        """I return a Deferred that fires with a specific named child node,
+        either an IFileNode or an IDirectoryNode."""
+
+    def set_uri(name, child_uri):
+        """I add a child (by URI) at the specific name. I return a Deferred
+        that fires when the operation finishes.
 
-    def add(name, child):
+        The child_uri could be for a file, or for a directory (either
+        read-write or read-only, using a URI that came from get_uri() ).
+
+        If this directory node is read-only, the Deferred will errback with a
+        NotMutableError."""
+
+    def set_node(name, child):
         """I add a child at the specific name. I return a Deferred that fires
-        when the operation finishes."""
+        when the operation finishes. This Deferred will fire with the child
+        node that was just added.
+
+        If this directory node is read-only, the Deferred will errback with a
+        NotMutableError."""
 
     def add_file(name, uploadable):
         """I upload a file (using the given IUploadable), then attach the
-        resulting FileNode to the directory at the given name."""
+        resulting FileNode to the directory at the given name. I return a
+        Deferred that fires (with the IFileNode of the uploaded file) when
+        the operation completes."""
 
-    def remove(name):
+    def delete(name):
         """I remove the child at the specific name. I return a Deferred that
         fires when the operation finishes."""
 
@@ -185,18 +274,6 @@ class IDirectoryNode(Interface):
         """I create and attach an empty directory at the given name. I return
         a Deferred that fires when the operation finishes."""
 
-    def attach_shared_directory(name, furl):
-        """I attach a directory that was shared (possibly by someone else)
-        with IDirectoryNode.get_furl to this parent at the given name. I
-        return a Deferred that fires when the operation finishes."""
-
-    def get_shared_directory_furl():
-        """I return a FURL that can be used to attach this directory to
-        somewhere else. The FURL is just a string, so it can be passed
-        through email or other out-of-band protocol. Use it by passing it in
-        to attach_shared_directory(). I return a Deferred that fires when the
-        operation finishes."""
-
     def move_child_to(current_child_name, new_parent, new_child_name=None):
         """I take one of my children and move them to a new parent. The child
         is referenced by name. On the new parent, the child will live under
index 5209facd22eee5e178620df6acee002bd601c979..2eeb1db22f83266063f404ebe71011db92d7f302 100644 (file)
@@ -8,6 +8,7 @@ from allmydata.introducer import Introducer
 class IntroducerAndVdrive(node.Node):
     PORTNUMFILE = "introducer.port"
     NODETYPE = "introducer"
+    VDRIVEDIR = "vdrive"
 
     def __init__(self, basedir="."):
         node.Node.__init__(self, basedir)
@@ -21,8 +22,11 @@ class IntroducerAndVdrive(node.Node):
         f.write(self.urls["introducer"] + "\n")
         f.close()
 
-        vds = self.add_service(VirtualDriveServer(self.basedir))
-        self.urls["vdrive"] = self.tub.registerReference(vds, "vdrive")
+        vdrive_dir = os.path.join(self.basedir, self.VDRIVEDIR)
+        vds = self.add_service(VirtualDriveServer(vdrive_dir))
+        vds_furl = self.tub.registerReference(vds, "vdrive")
+        vds.set_furl(vds_furl)
+        self.urls["vdrive"] = vds_furl
         self.log(" vdrive is at %s" % self.urls["vdrive"])
         f = open(os.path.join(self.basedir, "vdrive.furl"), "w")
         f.write(self.urls["vdrive"] + "\n")
index a52e0f60d7e1ebc35c84224986b8d9ce426887a0..9c25d8c38d41632138c9915332f11a379c9e4fad 100644 (file)
@@ -129,6 +129,30 @@ class DumpOptions(usage.Options):
         if not self['filename']:
             raise usage.UsageError("<filename> parameter is required")
 
+class DumpRootDirnodeOptions(BasedirMixin, usage.Options):
+    optParameters = [
+        ["basedir", "C", None, "the vdrive-server's base directory"],
+        ]
+
+class DumpDirnodeOptions(BasedirMixin, usage.Options):
+    optParameters = [
+        ["uri", "u", None, "the URI of the dirnode to dump."],
+        ["basedir", "C", None, "which directory to create the introducer in"],
+        ]
+    optFlags = [
+        ["verbose", "v", "be extra noisy (show encrypted data)"],
+        ]
+    def parseArgs(self, *args):
+        if len(args) == 1:
+            self['uri'] = args[-1]
+            args = args[:-1]
+        BasedirMixin.parseArgs(self, *args)
+
+    def postOptions(self):
+        BasedirMixin.postOptions(self)
+        if not self['uri']:
+            raise usage.UsageError("<uri> parameter is required")
+
 client_tac = """
 # -*- python -*-
 
@@ -164,7 +188,9 @@ class Options(usage.Options):
         ["restart", None, RestartOptions, "Restart a node."],
         ["dump-uri-extension", None, DumpOptions,
          "Unpack and display the contents of a uri_extension file."],
-        ["dump-directory-node", None, DumpOptions,
+        ["dump-root-dirnode", None, DumpRootDirnodeOptions,
+         "Compute most of the URI for the vdrive server's root dirnode."],
+        ["dump-dirnode", None, DumpDirnodeOptions,
          "Unpack and display the contents of a vdrive DirectoryNode."],
         ]
 
@@ -211,8 +237,10 @@ def runner(argv, run_by_human=True):
             rc = start(basedir, so) or rc
     elif command == "dump-uri-extension":
         rc = dump_uri_extension(so)
-    elif command == "dump-directory-node":
-        rc = dump_directory_node(so)
+    elif command == "dump-root-dirnode":
+        rc = dump_root_dirnode(so.basedirs[0], so)
+    elif command == "dump-dirnode":
+        rc = dump_directory_node(so.basedirs[0], so)
     return rc
 
 def run():
@@ -329,30 +357,71 @@ def dump_uri_extension(config):
     print
     return 0
 
-def dump_directory_node(config):
-    from allmydata import filetable, vdrive
-    filename = config['filename']
+def dump_root_dirnode(basedir, config):
+    from allmydata import uri
 
-    basedir, name = os.path.split(filename)
-    dirnode = filetable.MutableDirectoryNode(basedir, name)
+    root_dirnode_file = os.path.join(basedir, "vdrive", "root")
+    try:
+        f = open(root_dirnode_file, "rb")
+        key = f.read()
+        rooturi = uri.pack_dirnode_uri("fakeFURL", key)
+        print rooturi
+        return 0
+    except EnvironmentError:
+        print "unable to read root dirnode file from %s" % root_dirnode_file
+        return 1
+
+def dump_directory_node(basedir, config):
+    from allmydata import filetable, vdrive, uri
+    from allmydata.util import hashutil, idlib
+    dir_uri = config['uri']
+    verbose = config['verbose']
+
+    furl, key = uri.unpack_dirnode_uri(dir_uri)
+    if uri.is_mutable_dirnode_uri(dir_uri):
+        wk, we, rk, index = hashutil.generate_dirnode_keys_from_writekey(key)
+    else:
+        wk, we, rk, index = hashutil.generate_dirnode_keys_from_readkey(key)
+
+    filename = os.path.join(basedir, "vdrive", idlib.b2a(index))
 
     print
-    print "DirectoryNode at %s" % name
-    print
+    print "dirnode uri: %s" % dir_uri
+    print "filename : %s" % filename
+    print "index        : %s" % idlib.b2a(index)
+    if wk:
+        print "writekey     : %s" % idlib.b2a(wk)
+        print "write_enabler: %s" % idlib.b2a(we)
+    else:
+        print "writekey     : None"
+        print "write_enabler: None"
+    print "readkey      : %s" % idlib.b2a(rk)
 
-    children = dirnode._read_from_file()
-    names = sorted(children.keys())
-    for name in names:
-        v = children[name]
-        if isinstance(v, vdrive.FileNode):
-            value = "File (uri=%s...)" % v.uri[:40]
-        elif isinstance(v, vdrive.DirectoryNode):
-            lastslash = v.furl.rindex("/")
-            furlname = v.furl[lastslash+1:lastslash+1+15]
-            value = "Directory (furl=%s.../%s...)" % (v.furl[:15], furlname)
-        else:
-            value = "weird: %s" % (v,)
-        print "%20s: %s" % (name, value)
     print
-    return 0
 
+    vds = filetable.VirtualDriveServer(os.path.join(basedir, "vdrive"), False)
+    data = vds._read_from_file(index)
+    if we:
+        if we != data[0]:
+            print "ERROR: write_enabler does not match"
+
+    for (H_key, E_key, E_write, E_read) in data[1]:
+        if verbose:
+            print " H_key %s" % idlib.b2a(H_key)
+            print " E_key %s" % idlib.b2a(E_key)
+            print " E_write %s" % idlib.b2a(E_write)
+            print " E_read %s" % idlib.b2a(E_read)
+        key = vdrive.decrypt(rk, E_key)
+        print " key %s" % key
+        if hashutil.dir_name_hash(rk, key) != H_key:
+            print "  ERROR: H_key does not match"
+        if wk and E_write:
+            if len(E_write) < 14:
+                print "  ERROR: write data is short:", idlib.b2a(E_write)
+            write = vdrive.decrypt(wk, E_write)
+            print "   write: %s" % write
+        read = vdrive.decrypt(rk, E_read)
+        print "   read: %s" % read
+        print
+
+    return 0
index 12a26ac53c1483b697a5baf5335c115b74cecfe5..4886941bf1b7f10f040848f35861739af9f9dd93 100644 (file)
@@ -1,54 +1,86 @@
 
-import os
 from twisted.trial import unittest
-from allmydata.filetable import (MutableDirectoryNode,
-                                 BadFileError, BadNameError)
-from allmydata.vdrive import FileNode, DirectoryNode
+from allmydata import filetable, uri
+from allmydata.util import hashutil
 
 
 class FileTable(unittest.TestCase):
-    def test_files(self):
-        os.mkdir("filetable")
-        basedir = os.path.abspath("filetable")
-        root = MutableDirectoryNode(basedir, "root")
-        self.failUnlessEqual(root.list(), [])
-        root.add("one", FileNode("vid-one"))
-        root.add("two", FileNode("vid-two"))
-        self.failUnlessEqual(root.list(), [("one", FileNode("vid-one")),
-                                           ("two", FileNode("vid-two"))])
-        root.remove("two")
-        self.failUnlessEqual(root.list(), [("one", FileNode("vid-one"))])
-        self.failUnlessRaises(BadFileError, root.remove, "two")
-        self.failUnlessRaises(BadFileError, root.remove, "three")
-
-        self.failUnlessEqual(root.get("one"), FileNode("vid-one"))
-        self.failUnlessRaises(BadFileError, root.get, "missing")
-        self.failUnlessRaises(BadNameError, root.get, "/etc/passwd") # evil
-        self.failUnlessRaises(BadNameError, root.get, "..") # sneaky
-        self.failUnlessRaises(BadNameError, root.get, ".") # dumb
-
-        # now play with directories
-        subdir1 = root.add("subdir1", DirectoryNode("subdir1.furl"))
-        self.failUnless(isinstance(subdir1, DirectoryNode))
-        subdir1a = root.get("subdir1")
-        self.failUnless(isinstance(subdir1a, DirectoryNode))
-        self.failUnlessEqual(subdir1a, subdir1)
-        entries = root.list()
-        self.failUnlessEqual(len(entries), 2)
-        one_index = entries.index( ("one", FileNode("vid-one")) )
-        subdir_index = 1 - one_index
-        self.failUnlessEqual(entries[subdir_index][0], "subdir1")
-        subdir2 = entries[subdir_index][1]
-        self.failUnless(isinstance(subdir2, DirectoryNode))
-
-        self.failUnlessEqual(len(root.list()), 2)
-
-        self.failUnlessRaises(BadNameError, # replacing an existing child
-                              root.add,
-                              "subdir1", DirectoryNode("subdir1.furl"))
-
-        root.remove("subdir1")
-        self.failUnlessEqual(root.list(), [("one", FileNode("vid-one"))])
+    def test_vdrive_server(self):
+        basedir = "filetable/FileTable/test_vdrive_server"
+        vds = filetable.VirtualDriveServer(basedir)
+        vds.set_furl("myFURL")
 
+        root_uri = vds.get_public_root_uri()
+        self.failUnless(uri.is_dirnode_uri(root_uri))
+        self.failUnless(uri.is_mutable_dirnode_uri(root_uri))
+        furl, key = uri.unpack_dirnode_uri(root_uri)
+        self.failUnlessEqual(furl, "myFURL")
+        self.failUnlessEqual(len(key), hashutil.KEYLEN)
 
+        wk, we, rk, index = hashutil.generate_dirnode_keys_from_writekey(key)
+        empty_list = vds.list(index)
+        self.failUnlessEqual(empty_list, [])
 
+        vds.set(index, we, "key1", "name1", "write1", "read1")
+        vds.set(index, we, "key2", "name2", "", "read2")
+
+        self.failUnlessRaises(filetable.ChildAlreadyPresentError,
+                              vds.set,
+                              index, we, "key2", "name2", "write2", "read2")
+
+        self.failUnlessRaises(filetable.BadWriteEnablerError,
+                              vds.set,
+                              index, "not the write enabler",
+                              "key2", "name2", "write2", "read2")
+
+        self.failUnlessEqual(vds.get(index, "key1"),
+                             ("write1", "read1"))
+        self.failUnlessEqual(vds.get(index, "key2"),
+                             ("", "read2"))
+        self.failUnlessRaises(IndexError,
+                              vds.get, index, "key3")
+
+        self.failUnlessEqual(sorted(vds.list(index)),
+                             [ ("name1", "write1", "read1"),
+                               ("name2", "", "read2"),
+                               ])
+
+        self.failUnlessRaises(filetable.BadWriteEnablerError,
+                              vds.delete,
+                              index, "not the write enabler", "name1")
+        self.failUnlessEqual(sorted(vds.list(index)),
+                             [ ("name1", "write1", "read1"),
+                               ("name2", "", "read2"),
+                               ])
+        self.failUnlessRaises(IndexError,
+                              vds.delete,
+                              index, we, "key3")
+
+        vds.delete(index, we, "key1")
+        self.failUnlessEqual(sorted(vds.list(index)),
+                             [ ("name2", "", "read2"),
+                               ])
+        self.failUnlessRaises(IndexError,
+                              vds.get, index, "key1")
+        self.failUnlessEqual(vds.get(index, "key2"),
+                             ("", "read2"))
+
+
+        vds2 = filetable.VirtualDriveServer(basedir)
+        vds2.set_furl("myFURL")
+        root_uri2 = vds.get_public_root_uri()
+        self.failUnless(uri.is_mutable_dirnode_uri(root_uri2))
+        furl2, key2 = uri.unpack_dirnode_uri(root_uri2)
+        (wk2, we2, rk2, index2) = \
+              hashutil.generate_dirnode_keys_from_writekey(key2)
+        self.failUnlessEqual(sorted(vds2.list(index2)),
+                             [ ("name2", "", "read2"),
+                               ])
+
+    def test_no_root(self):
+        basedir = "FileTable/test_no_root"
+        vds = filetable.VirtualDriveServer(basedir, offer_public_root=False)
+        vds.set_furl("myFURL")
+
+        self.failUnlessRaises(filetable.NoPublicRootError,
+                              vds.get_public_root_uri)
index 84839abbb08fc0537723ca1dcb02a1bc1b27f742..af1bda46f9a27ff2b3d178044cb66dfd6db01263 100644 (file)
@@ -240,7 +240,7 @@ class SystemTest(testutil.SignalMixin, unittest.TestCase):
             d1.addCallback(lambda subdir1_node:
                            subdir1_node.add_file("mydata567", ut))
             def _stash_uri(filenode):
-                self.uri = filenode.uri
+                self.uri = filenode.get_uri()
                 return filenode
             d1.addCallback(_stash_uri)
             return d1
index 536f1c0c2038af1d8033e96c1860cc2400fcfce1..472bffcdc05f29ac69e31a29600a77eaf12d428d 100644 (file)
 
-import os
 from twisted.trial import unittest
 from twisted.internet import defer
-from allmydata import vdrive, filetable
+from twisted.python import failure
+from allmydata import vdrive, filetable, uri
+from allmydata.interfaces import IDirectoryNode
+from foolscap import eventual
 
-class LocalDirNode(filetable.MutableDirectoryNode):
+class LocalReference:
+    def __init__(self, target):
+        self.target = target
     def callRemote(self, methname, *args, **kwargs):
-        def _call():
-            meth = getattr(self, methname)
+        def _call(ignored):
+            meth = getattr(self.target, methname)
             return meth(*args, **kwargs)
-        return defer.maybeDeferred(_call)
+        d = eventual.fireEventually(None)
+        d.addCallback(_call)
+        return d
+
+class MyTub:
+    def __init__(self, vds, myfurl):
+        self.vds = vds
+        self.myfurl = myfurl
+    def getReference(self, furl):
+        assert furl == self.myfurl
+        return eventual.fireEventually(LocalReference(self.vds))
+
+class MyClient:
+    def __init__(self, vds, myfurl):
+        self.tub = MyTub(vds, myfurl)
+
+class Test(unittest.TestCase):
+    def test_create_directory(self):
+        basedir = "vdrive/test_create_directory"
+        vds = filetable.VirtualDriveServer(basedir)
+        vds.set_furl("myFURL")
+        self.client = client = MyClient(vds, "myFURL")
+        d = vdrive.create_directory(client, "myFURL")
+        def _created(node):
+            self.failUnless(IDirectoryNode.providedBy(node))
+            self.failUnless(node.is_mutable())
+        d.addCallback(_created)
+        return d
+
+    def test_one(self):
+        basedir = "vdrive/test_one"
+        vds = filetable.VirtualDriveServer(basedir)
+        vds.set_furl("myFURL")
+        root_uri = vds.get_public_root_uri()
+
+        self.client = client = MyClient(vds, "myFURL")
+        d1 = vdrive.create_directory_node(client, root_uri)
+        d2 = vdrive.create_directory_node(client, root_uri)
+        d = defer.gatherResults( [d1,d2] )
+        d.addCallback(self._test_one_1)
+        return d
+
+    def _test_one_1(self, (rootnode1, rootnode2) ):
+        self.failUnlessEqual(rootnode1, rootnode2)
+        self.failIfEqual(rootnode1, "not")
+
+        self.rootnode = rootnode = rootnode1
+        self.failUnless(rootnode.is_mutable())
+        self.readonly_uri = rootnode.get_immutable_uri()
+        d = vdrive.create_directory_node(self.client, self.readonly_uri)
+        d.addCallback(self._test_one_2)
+        return d
+
+    def _test_one_2(self, ro_rootnode):
+        self.ro_rootnode = ro_rootnode
+        self.failIf(ro_rootnode.is_mutable())
+        self.failUnlessEqual(ro_rootnode.get_immutable_uri(),
+                             self.readonly_uri)
+
+        rootnode = self.rootnode
+
+        ignored = rootnode.dump()
+
+        # root/
+        d = rootnode.list()
+        def _listed(res):
+            self.failUnlessEqual(res, {})
+        d.addCallback(_listed)
+
+        file1 = uri.pack_uri("i"*32, "k"*16, "e"*32, 25, 100, 12345)
+        file2 = uri.pack_uri("i"*31 + "2", "k"*16, "e"*32, 25, 100, 12345)
+        file2_node = vdrive.FileNode(file2, None)
+        d.addCallback(lambda res: rootnode.set_uri("foo", file1))
+        # root/
+        # root/foo =file1
+
+        d.addCallback(lambda res: rootnode.list())
+        def _listed2(res):
+            self.failUnlessEqual(res.keys(), ["foo"])
+            file1_node = res["foo"]
+            self.failUnless(isinstance(file1_node, vdrive.FileNode))
+            self.failUnlessEqual(file1_node.uri, file1)
+        d.addCallback(_listed2)
+
+        d.addCallback(lambda res: rootnode.get("foo"))
+        def _got_foo(res):
+            self.failUnless(isinstance(res, vdrive.FileNode))
+            self.failUnlessEqual(res.uri, file1)
+        d.addCallback(_got_foo)
+
+        d.addCallback(lambda res: rootnode.get("missing"))
+        # this should raise an exception
+        d.addBoth(self.shouldFail, IndexError, "get('missing')",
+                  "unable to find child named 'missing'")
+
+        d.addCallback(lambda res: rootnode.create_empty_directory("bar"))
+        # root/
+        # root/foo =file1
+        # root/bar/
+
+        d.addCallback(lambda res: rootnode.list())
+        d.addCallback(self.failUnlessKeysMatch, ["foo", "bar"])
+        def _listed3(res):
+            self.failIfEqual(res["foo"], res["bar"])
+            self.failIfEqual(res["bar"], res["foo"])
+            self.failIfEqual(res["foo"], "not")
+            self.failIfEqual(res["bar"], self.rootnode)
+            self.failUnlessEqual(res["foo"], res["foo"])
+            # make sure the objects can be used as dict keys
+            testdict = {res["foo"]: 1, res["bar"]: 2}
+            bar_node = res["bar"]
+            self.failUnless(isinstance(bar_node, vdrive.MutableDirectoryNode))
+            self.bar_node = bar_node
+            bar_ro_uri = bar_node.get_immutable_uri()
+            return rootnode.set_uri("bar-ro", bar_ro_uri)
+        d.addCallback(_listed3)
+        # root/
+        # root/foo =file1
+        # root/bar/
+        # root/bar-ro/  (read-only)
+
+        d.addCallback(lambda res: rootnode.list())
+        d.addCallback(self.failUnlessKeysMatch, ["foo", "bar", "bar-ro"])
+        def _listed4(res):
+            self.failIf(res["bar-ro"].is_mutable())
+            self.bar_node_readonly = res["bar-ro"]
+
+            # add another file to bar/
+            bar = res["bar"]
+            return bar.set_node("file2", file2_node)
+        d.addCallback(_listed4)
+        d.addCallback(self.failUnlessIdentical, file2_node)
+        # and a directory
+        d.addCallback(lambda res: self.bar_node.create_empty_directory("baz"))
+        # root/
+        # root/foo =file1
+        # root/bar/
+        # root/bar/file2 =file2
+        # root/bar/baz/
+        # root/bar-ro/  (read-only)
+        # root/bar-ro/file2 =file2
+        # root/bar-ro/baz/
+
+        d.addCallback(lambda res: self.bar_node.list())
+        d.addCallback(self.failUnlessKeysMatch, ["file2", "baz"])
+        d.addCallback(lambda res:
+                      self.failUnless(res["baz"].is_mutable()))
+
+        d.addCallback(lambda res: self.bar_node_readonly.list())
+        d.addCallback(self.failUnlessKeysMatch, ["file2", "baz"])
+        d.addCallback(lambda res:
+                      self.failIf(res["baz"].is_mutable()))
+
+        # try to add a file to bar-ro, should get exception
+        d.addCallback(lambda res:
+                      self.bar_node_readonly.set_uri("file3", file2))
+        d.addBoth(self.shouldFail, vdrive.NotMutableError,
+                  "bar-ro.set('file3')")
+
+        # try to delete a file from bar-ro, should get exception
+        d.addCallback(lambda res: self.bar_node_readonly.delete("file2"))
+        d.addBoth(self.shouldFail, vdrive.NotMutableError,
+                  "bar-ro.delete('file2')")
+
+        # try to mkdir in bar-ro, should get exception
+        d.addCallback(lambda res:
+                      self.bar_node_readonly.create_empty_directory("boffo"))
+        d.addBoth(self.shouldFail, vdrive.NotMutableError,
+                  "bar-ro.mkdir('boffo')")
+
+        d.addCallback(lambda res: rootnode.delete("foo"))
+        # root/
+        # root/bar/
+        # root/bar/file2 =file2
+        # root/bar/baz/
+        # root/bar-ro/  (read-only)
+        # root/bar-ro/file2 =file2
+        # root/bar-ro/baz/
+
+        d.addCallback(lambda res: rootnode.list())
+        d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro"])
+
+        d.addCallback(lambda res:
+                      self.bar_node.move_child_to("file2",
+                                                  self.rootnode, "file4"))
+        # root/
+        # root/file4 = file4
+        # root/bar/
+        # root/bar/baz/
+        # root/bar-ro/  (read-only)
+        # root/bar-ro/baz/
+
+        d.addCallback(lambda res: rootnode.list())
+        d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro", "file4"])
+        d.addCallback(lambda res:self.bar_node.list())
+        d.addCallback(self.failUnlessKeysMatch, ["baz"])
+        d.addCallback(lambda res:self.bar_node_readonly.list())
+        d.addCallback(self.failUnlessKeysMatch, ["baz"])
+
+
+        d.addCallback(lambda res:
+                      rootnode.move_child_to("file4",
+                                             self.bar_node_readonly, "boffo"))
+        d.addBoth(self.shouldFail, vdrive.NotMutableError,
+                  "mv root/file4 root/bar-ro/boffo")
+
+        d.addCallback(lambda res: rootnode.list())
+        d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro", "file4"])
+        d.addCallback(lambda res:self.bar_node.list())
+        d.addCallback(self.failUnlessKeysMatch, ["baz"])
+        d.addCallback(lambda res:self.bar_node_readonly.list())
+        d.addCallback(self.failUnlessKeysMatch, ["baz"])
+
+
+        d.addCallback(lambda res:
+                      rootnode.move_child_to("file4", self.bar_node))
+
+        d.addCallback(lambda res: rootnode.list())
+        d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro"])
+        d.addCallback(lambda res:self.bar_node.list())
+        d.addCallback(self.failUnlessKeysMatch, ["baz", "file4"])
+        d.addCallback(lambda res:self.bar_node_readonly.list())
+        d.addCallback(self.failUnlessKeysMatch, ["baz", "file4"])
+
+        return d
+
+    def shouldFail(self, res, expected_failure, which, substring=None):
+        if isinstance(res, failure.Failure):
+            res.trap(expected_failure)
+            if substring:
+                self.failUnless(substring in str(res),
+                                "substring '%s' not in '%s'"
+                                % (substring, str(res)))
+        else:
+            self.fail("%s was supposed to raise %s, not get '%s'" %
+                      (which, expected_failure, res))
+
+    def failUnlessKeysMatch(self, res, expected_keys):
+        self.failUnlessEqual(sorted(res.keys()),
+                             sorted(expected_keys))
+        return res
 
 
+"""
 class Traverse(unittest.TestCase):
     def make_tree(self, basedir):
         os.makedirs(basedir)
@@ -67,3 +312,4 @@ class Traverse(unittest.TestCase):
                                            ["2.a", "2.b", "d2.1"]))
         return d
 del Traverse
+"""
index 308f64de6a66960428f94c3b77094fe62c388f92..80ff5b3c60776b4ef862258de4efbfd61980ee21 100644 (file)
@@ -86,3 +86,31 @@ def unpack_extension_readable(data):
         if "hash" in k:
             unpacked[k] = idlib.b2a(unpacked[k])
     return unpacked
+
+def is_dirnode_uri(uri):
+    return uri.startswith("URI:DIR:") or uri.startswith("URI:DIR-RO:")
+def is_mutable_dirnode_uri(uri):
+    return uri.startswith("URI:DIR:")
+def unpack_dirnode_uri(uri):
+    assert is_dirnode_uri(uri)
+    # URI:DIR:furl:key
+    #  but note that the furl contains colons
+    for prefix in ("URI:DIR:", "URI:DIR-RO:"):
+        if uri.startswith(prefix):
+            uri = uri[len(prefix):]
+            break
+    else:
+        assert 0
+    colon = uri.rindex(":")
+    furl = uri[:colon]
+    key = uri[colon+1:]
+    return furl, idlib.a2b(key)
+
+def make_immutable_dirnode_uri(mutable_uri):
+    assert is_mutable_dirnode_uri(mutable_uri)
+    furl, writekey = unpack_dirnode_uri(mutable_uri)
+    readkey = hashutil.dir_read_key_hash(writekey)
+    return "URI:DIR-RO:%s:%s" % (furl, idlib.b2a(readkey))
+
+def pack_dirnode_uri(furl, writekey):
+    return "URI:DIR:%s:%s" % (furl, idlib.b2a(writekey))
index c3d967b9684cab0cb14b5f4c50f59d1a185a07ff..d44003c54f9430cd3fc02cf8b606c2656cf59239 100644 (file)
@@ -1,4 +1,5 @@
 from allmydata.Crypto.Hash import SHA256
+import os
 
 def netstring(s):
     return "%d:%s," % (len(s), s,)
@@ -56,3 +57,25 @@ def key_hash(data):
 def key_hasher():
     return tagged_hasher("allmydata_encryption_key_v1")
 
+KEYLEN = 16
+def random_key():
+    return os.urandom(KEYLEN)
+
+def dir_write_enabler_hash(write_key):
+    return tagged_hash("allmydata_dir_write_enabler_v1", write_key)
+def dir_read_key_hash(write_key):
+    return tagged_hash("allmydata_dir_read_key_v1", write_key)[:KEYLEN]
+def dir_index_hash(read_key):
+    return tagged_hash("allmydata_dir_index_v1", read_key)
+def dir_name_hash(readkey, name):
+    return tagged_pair_hash("allmydata_dir_name_v1", readkey, name)
+
+def generate_dirnode_keys_from_writekey(write_key):
+    readkey = dir_read_key_hash(write_key)
+    write_enabler = dir_write_enabler_hash(write_key)
+    index = dir_index_hash(readkey)
+    return write_key, write_enabler, readkey, index
+
+def generate_dirnode_keys_from_readkey(read_key):
+    index = dir_index_hash(read_key)
+    return None, None, read_key, index
index 7e8672d081867cff1798b31334416cc06d780442..bd11576c599ea6355489d6ad000130c46ed4fa5f 100644 (file)
@@ -1,11 +1,18 @@
 
 """This is the client-side facility to manipulate virtual drives."""
 
+import os.path
+from zope.interface import implements
 from twisted.application import service
 from twisted.internet import defer
 from twisted.python import log
-from allmydata import upload, download
-from foolscap import Copyable, RemoteCopy
+from allmydata import upload, download, uri
+from allmydata.Crypto.Cipher import AES
+from allmydata.util import hashutil, idlib
+from allmydata.interfaces import IDirectoryNode, IFileNode
+
+class NotMutableError(Exception):
+    pass
 
 class VDrive(service.MultiService):
     name = "vdrive"
@@ -181,99 +188,243 @@ class VDrive(service.MultiService):
         return self.get_file(from_where, download.FileHandle(filehandle))
 
 
-class DirectoryNode(Copyable, RemoteCopy):
-    """I have either a .furl attribute or a .get(tub) method."""
-    typeToCopy = "allmydata.com/tahoe/interfaces/DirectoryNode/v1"
-    copytype = typeToCopy
-    def __init__(self, furl=None, client=None):
-        # RemoteCopy subclasses are always called without arguments
-        self.furl = furl
-        self._set_client(client)
-    def _set_client(self, client):
+def create_directory_node(client, diruri):
+    assert uri.is_dirnode_uri(diruri)
+    if uri.is_mutable_dirnode_uri(diruri):
+        dirnode_class = MutableDirectoryNode
+    else:
+        dirnode_class = ImmutableDirectoryNode
+    (furl, key) = uri.unpack_dirnode_uri(diruri)
+    d = client.tub.getReference(furl)
+    def _got(rref):
+        dirnode = dirnode_class(diruri, client, rref, key)
+        return dirnode
+    d.addCallback(_got)
+    return d
+
+def encrypt(key, data):
+    # TODO: add the hmac
+    IV = os.urandom(14)
+    counterstart = IV + "\x00"*2
+    assert len(counterstart) == 16, len(counterstart)
+    cryptor = AES.new(key=key, mode=AES.MODE_CTR, counterstart=counterstart)
+    crypttext = cryptor.encrypt(data)
+    return IV + crypttext
+
+def decrypt(key, data):
+    # TODO: validate the hmac
+    assert len(data) >= 14, len(data)
+    IV = data[:14]
+    counterstart = IV + "\x00"*2
+    assert len(counterstart) == 16, len(counterstart)
+    cryptor = AES.new(key=key, mode=AES.MODE_CTR, counterstart=counterstart)
+    plaintext = cryptor.decrypt(data[14:])
+    return plaintext
+
+
+class ImmutableDirectoryNode:
+    implements(IDirectoryNode)
+
+    def __init__(self, myuri, client, rref, readkey):
+        self._uri = myuri
         self._client = client
-        return self
-    def getStateToCopy(self):
-        return {"furl": self.furl }
-    def setCopyableState(self, state):
-        self.furl = state['furl']
+        self._tub = client.tub
+        self._rref = rref
+        self._readkey = readkey
+        self._writekey = None
+        self._write_enabler = None
+        self._index = hashutil.dir_index_hash(self._readkey)
+        self._mutable = False
+
+    def dump(self):
+        return ["URI: %s" % self._uri,
+                "rk: %s" % idlib.b2a(self._readkey),
+                "index: %s" % idlib.b2a(self._index),
+                ]
+
+    def is_mutable(self):
+        return self._mutable
+
+    def get_uri(self):
+        return self._uri
+
+    def get_immutable_uri(self):
+        # return the dirnode URI for a read-only form of this directory
+        if self._mutable:
+            return uri.make_immutable_dirnode_uri(self._uri)
+        else:
+            return self._uri
+
     def __hash__(self):
-        return hash((self.__class__, self.furl))
+        return hash((self.__class__, self._uri))
     def __cmp__(self, them):
         if cmp(type(self), type(them)):
             return cmp(type(self), type(them))
         if cmp(self.__class__, them.__class__):
             return cmp(self.__class__, them.__class__)
-        return cmp(self.furl, them.furl)
+        return cmp(self._uri, them._uri)
+
+    def _encrypt(self, key, data):
+        return encrypt(key, data)
+
+    def _decrypt(self, key, data):
+        return decrypt(key, data)
+
+    def _decrypt_child(self, E_write, E_read):
+        if E_write and self._writekey:
+            # we prefer read-write children when we can get them
+            return self._decrypt(self._writekey, E_write)
+        else:
+            return self._decrypt(self._readkey, E_read)
 
     def list(self):
-        d = self._client.tub.getReference(self.furl)
-        d.addCallback(lambda node: node.callRemote("list"))
-        d.addCallback(lambda children:
-                      [(name,child._set_client(self._client))
-                       for name,child in children])
+        d = self._rref.callRemote("list", self._index)
+        entries = {}
+        def _got(res):
+            dl = []
+            for (E_name, E_write, E_read) in res:
+                name = self._decrypt(self._readkey, E_name)
+                child_uri = self._decrypt_child(E_write, E_read)
+                d2 = self._create_node(child_uri)
+                def _created(node, name):
+                    entries[name] = node
+                d2.addCallback(_created, name)
+                dl.append(d2)
+            return defer.DeferredList(dl)
+        d.addCallback(_got)
+        d.addCallback(lambda res: entries)
         return d
 
+    def _hash_name(self, name):
+        return hashutil.dir_name_hash(self._readkey, name)
+
     def get(self, name):
-        d = self._client.tub.getReference(self.furl)
-        d.addCallback(lambda node: node.callRemote("get", name))
-        d.addCallback(lambda child: child._set_client(self._client))
+        H_name = self._hash_name(name)
+        d = self._rref.callRemote("get", self._index, H_name)
+        def _check_index_error(f):
+            f.trap(IndexError)
+            raise IndexError("get(index=%s): unable to find child named '%s'"
+                             % (idlib.b2a(self._index), name))
+        d.addErrback(_check_index_error)
+        d.addCallback(lambda (E_write, E_read):
+                      self._decrypt_child(E_write, E_read))
+        d.addCallback(self._create_node)
         return d
 
-    def add(self, name, child):
-        d = self._client.tub.getReference(self.furl)
-        d.addCallback(lambda node: node.callRemote("add", name, child))
-        d.addCallback(lambda newnode: newnode._set_client(self._client))
+    def _set(self, name, write_child, read_child):
+        if not self._mutable:
+            return defer.fail(NotMutableError())
+        H_name = self._hash_name(name)
+        E_name = self._encrypt(self._readkey, name)
+        E_write = ""
+        if self._writekey and write_child:
+            E_write = self._encrypt(self._writekey, write_child)
+        E_read = self._encrypt(self._readkey, read_child)
+        d = self._rref.callRemote("set", self._index, self._write_enabler,
+                                  H_name, E_name, E_write, E_read)
         return d
 
-    def add_file(self, name, uploadable):
-        uploader = self._client.getServiceNamed("uploader")
-        d = uploader.upload(uploadable)
-        d.addCallback(lambda uri: self.add(name, FileNode(uri, self._client)))
+    def set_uri(self, name, child_uri):
+        write, read = self._split_uri(child_uri)
+        return self._set(name, write, read)
+
+    def set_node(self, name, child):
+        d = self.set_uri(name, child.get_uri())
+        d.addCallback(lambda res: child)
         return d
 
-    def remove(self, name):
-        d = self._client.tub.getReference(self.furl)
-        d.addCallback(lambda node: node.callRemote("remove", name))
-        d.addCallback(lambda newnode: newnode._set_client(self._client))
+    def delete(self, name):
+        if not self._mutable:
+            return defer.fail(NotMutableError())
+        H_name = self._hash_name(name)
+        d = self._rref.callRemote("delete", self._index, self._write_enabler,
+                                  H_name)
         return d
 
+    def _create_node(self, child_uri):
+        if uri.is_dirnode_uri(child_uri):
+            return create_directory_node(self._client, child_uri)
+        else:
+            return defer.succeed(FileNode(child_uri, self._client))
+
+    def _split_uri(self, child_uri):
+        if uri.is_dirnode_uri(child_uri):
+            if uri.is_mutable_dirnode_uri(child_uri):
+                write = child_uri
+                read = uri.make_immutable_dirnode_uri(child_uri)
+            else:
+                write = None
+                read = child_uri
+            return (write, read)
+        return (None, child_uri) # file
+
     def create_empty_directory(self, name):
-        vdrive_server = self._client._vdrive_server
-        d = vdrive_server.callRemote("create_directory")
-        d.addCallback(lambda node: self.add(name, node))
+        if not self._mutable:
+            return defer.fail(NotMutableError())
+        child_writekey = hashutil.random_key()
+        my_furl, parent_writekey = uri.unpack_dirnode_uri(self._uri)
+        child_uri = uri.pack_dirnode_uri(my_furl, child_writekey)
+        child = MutableDirectoryNode(child_uri, self._client, self._rref,
+                                     child_writekey)
+        d = self._rref.callRemote("create_directory",
+                                  child._index, child._write_enabler)
+        d.addCallback(lambda index: self.set_node(name, child))
         return d
 
-    def attach_shared_directory(self, name, furl):
-        d = self.add(name, DirectoryNode(furl))
+    def add_file(self, name, uploadable):
+        if not self._mutable:
+            return defer.fail(NotMutableError())
+        uploader = self._client.getServiceNamed("uploader")
+        d = uploader.upload(uploadable)
+        d.addCallback(lambda uri: self.set_node(name,
+                                                FileNode(uri, self._client)))
         return d
 
-    def get_shared_directory_furl(self):
-        return defer.succeed(self.furl)
-
     def move_child_to(self, current_child_name,
                       new_parent, new_child_name=None):
+        if not (self._mutable and new_parent.is_mutable()):
+            return defer.fail(NotMutableError())
         if new_child_name is None:
             new_child_name = current_child_name
         d = self.get(current_child_name)
-        d.addCallback(lambda child: new_parent.add(new_child_name, child))
-        d.addCallback(lambda child: self.remove(current_child_name))
+        d.addCallback(lambda child: new_parent.set_node(new_child_name, child))
+        d.addCallback(lambda child: self.delete(current_child_name))
         return d
 
-class FileNode(Copyable, RemoteCopy):
-    """I have a .uri attribute."""
-    typeToCopy = "allmydata.com/tahoe/interfaces/FileNode/v1"
-    copytype = typeToCopy
-    def __init__(self, uri=None, client=None):
-        # RemoteCopy subclasses are always called without arguments
+class MutableDirectoryNode(ImmutableDirectoryNode):
+    implements(IDirectoryNode)
+
+    def __init__(self, myuri, client, rref, writekey):
+        readkey = hashutil.dir_read_key_hash(writekey)
+        ImmutableDirectoryNode.__init__(self, myuri, client, rref, readkey)
+        self._writekey = writekey
+        self._write_enabler = hashutil.dir_write_enabler_hash(writekey)
+        self._mutable = True
+
+def create_directory(client, furl):
+    write_key = hashutil.random_key()
+    (wk, we, rk, index) = \
+         hashutil.generate_dirnode_keys_from_writekey(write_key)
+    myuri = uri.pack_dirnode_uri(furl, wk)
+    d = client.tub.getReference(furl)
+    def _got_vdrive_server(vdrive_server):
+        node = MutableDirectoryNode(myuri, client, vdrive_server, wk)
+        d2 = vdrive_server.callRemote("create_directory", index, we)
+        d2.addCallback(lambda res: node)
+        return d2
+    d.addCallback(_got_vdrive_server)
+    return d
+
+class FileNode:
+    implements(IFileNode)
+
+    def __init__(self, uri, client):
         self.uri = uri
-        self._set_client(client)
-    def _set_client(self, client):
         self._client = client
-        return self
-    def getStateToCopy(self):
-        return {"uri": self.uri }
-    def setCopyableState(self, state):
-        self.uri = state['uri']
+
+    def get_uri(self):
+        return self.uri
+
     def __hash__(self):
         return hash((self.__class__, self.uri))
     def __cmp__(self, them):
index 03f26d8df0cc1a348b1161d31ba8665785ab7c80..a4b73b7ccc07ce3056892870792c3998f245a547 100644 (file)
 
 <div><a href=".">Refresh this view</a></div>
 <div><a href="..">Parent Directory</a></div>
-<div>To share this directory, paste the following FURL string into an
+<div>To share this directory, paste the following URI string into an
   "Add Shared Directory" box:
-  <pre class="overflow" n:render="string" n:data="share_url" /></div>
+  <pre class="overflow" n:render="string" n:data="share_uri" /></div>
+<div>To share a transitively read-only copy, use the following URI instead:
+  <pre class="overflow" n:render="string" n:data="share_readonly_uri" /></div>
 
 <table n:render="sequence" n:data="children" border="1">
   <tr n:pattern="header">
index d16653ad70af33e392191ffcdc3ad387ce7cf76c..e8b518c6d40772f30d8cfdc78414056c841b2a7a 100644 (file)
@@ -6,8 +6,8 @@ from nevow import inevow, rend, loaders, appserver, url, tags as T
 from nevow.static import File as nevow_File # TODO: merge with static.File?
 from allmydata.util import idlib
 from allmydata.uri import unpack_uri
-from allmydata.interfaces import IDownloadTarget
-from allmydata.vdrive import FileNode, DirectoryNode
+from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode
+from allmydata.vdrive import FileNode
 from allmydata import upload
 from zope.interface import implements, Interface
 import urllib
@@ -120,10 +120,10 @@ class Directory(rend.Page):
             dirname = self._dirname + "/" + name
         d = self._dirnode.get(name)
         def _got_child(res):
-            if isinstance(res, FileNode):
+            if IFileNode.providedBy(res):
                 dl = get_downloader_service(ctx)
                 return Downloader(dl, name, res)
-            elif isinstance(res, DirectoryNode):
+            elif IDirectoryNode.providedBy(res):
                 return Directory(res, dirname)
             else:
                 raise RuntimeError("what is this %s" % res)
@@ -134,18 +134,39 @@ class Directory(rend.Page):
         return ctx.tag["Directory of '%s':" % self._dirname]
 
     def render_header(self, ctx, data):
-        return "Directory of '%s':" % self._dirname
+        header = "Directory of '%s':" % self._dirname
+        if not self._dirnode.is_mutable():
+            header += " (readonly)"
+        return header
 
-    def data_share_url(self, ctx, data):
-        return self._dirnode.furl
+    def data_share_uri(self, ctx, data):
+        return self._dirnode.get_uri()
+    def data_share_readonly_uri(self, ctx, data):
+        return self._dirnode.get_immutable_uri()
 
     def data_children(self, ctx, data):
         d = self._dirnode.list()
+        d.addCallback(lambda dict: sorted(dict.items()))
         return d
 
     def render_row(self, ctx, data):
         name, target = data
-        if isinstance(target, FileNode):
+
+        if self._dirnode.is_mutable():
+            # this creates a button which will cause our child__delete method
+            # to be invoked, which deletes the file and then redirects the
+            # browser back to this directory
+            del_url = url.here.child("_delete")
+            #del_url = del_url.add("uri", target.uri)
+            del_url = del_url.add("name", name)
+            delete = T.form(action=del_url, method="post")[
+                T.input(type='submit', value='del', name="del"),
+                ]
+        else:
+            delete = "-"
+        ctx.fillSlots("delete", delete)
+
+        if IFileNode.providedBy(target):
             # file
             dlurl = urllib.quote(name)
             ctx.fillSlots("filename",
@@ -160,17 +181,7 @@ class Directory(rend.Page):
             #extract and display file size
             ctx.fillSlots("size", unpack_uri(uri)['size'])
 
-            # this creates a button which will cause our child__delete method
-            # to be invoked, which deletes the file and then redirects the
-            # browser back to this directory
-            del_url = url.here.child("_delete")
-            #del_url = del_url.add("uri", target.uri)
-            del_url = del_url.add("name", name)
-            delete = T.form(action=del_url, method="post")[
-                T.input(type='submit', value='del', name="del"),
-                ]
-            ctx.fillSlots("delete", delete)
-        elif isinstance(target, DirectoryNode):
+        elif IDirectoryNode.providedBy(target):
             # directory
             subdir_url = urllib.quote(name)
             ctx.fillSlots("filename",
@@ -178,19 +189,14 @@ class Directory(rend.Page):
             ctx.fillSlots("type", "DIR")
             ctx.fillSlots("size", "-")
             ctx.fillSlots("uri", "-")
-
-            del_url = url.here.child("_delete")
-            del_url = del_url.add("name", name)
-            delete = T.form(action=del_url, method="post")[
-                T.input(type='submit', value='del', name="del"),
-                ]
-            ctx.fillSlots("delete", delete)
         else:
             raise RuntimeError("unknown thing %s" % (target,))
         return ctx.tag
 
     def render_forms(self, ctx, data):
-        return webform.renderForms()
+        if self._dirnode.is_mutable():
+            return webform.renderForms()
+        return T.div["No upload forms: directory is immutable"]
 
     def render_results(self, ctx, data):
         req = inevow.IRequest(ctx)
@@ -235,14 +241,13 @@ class Directory(rend.Page):
         log.msg("starting webish upload")
 
         uploader = get_uploader_service(ctx)
-        d = uploader.upload(upload.FileHandle(contents.file))
+        uploadable = upload.FileHandle(contents.file)
         name = contents.filename
-        def _uploaded(uri):
-            if privateupload:
-                return self.uploadprivate(name, uri)
-            else:
-                return self._dirnode.add(name, FileNode(uri))
-        d.addCallback(_uploaded)
+        if privateupload:
+            d = uploader.upload(uploadable)
+            d.addCallback(lambda uri: self.uploadprivate(name, uri))
+        else:
+            d = self._dirnode.add_file(name, uploadable)
         def _done(res):
             log.msg("webish upload complete")
             return res
@@ -271,15 +276,15 @@ class Directory(rend.Page):
     def bind_mount(self, ctx):
         namearg = annotate.Argument("name",
                                     annotate.String("Name to place incoming directory: "))
-        furlarg = annotate.Argument("furl",
-                                    annotate.String("FURL of Shared Directory"))
-        meth = annotate.Method(arguments=[namearg, furlarg],
+        uriarg = annotate.Argument("uri",
+                                   annotate.String("URI of Shared Directory"))
+        meth = annotate.Method(arguments=[namearg, uriarg],
                                label="Add Shared Directory")
         return annotate.MethodBinding("mount", meth,
                                       action="Mount Shared Directory")
 
-    def mount(self, name, furl):
-        d = self._dirnode.attach_shared_directory(name, furl)
+    def mount(self, name, uri):
+        d = self._dirnode.set_uri(name, uri)
         #d.addCallback(lambda done: url.here.child(name))
         return d
 
@@ -287,7 +292,7 @@ class Directory(rend.Page):
         # perform the delete, then redirect back to the directory page
         args = inevow.IRequest(ctx).args
         name = args["name"][0]
-        d = self._dirnode.remove(name)
+        d = self._dirnode.delete(name)
         d.addCallback(lambda done: url.here.up())
         return d
 
@@ -324,7 +329,7 @@ class Downloader(resource.Resource):
     def __init__(self, downloader, name, filenode):
         self._downloader = downloader
         self._name = name
-        assert isinstance(filenode, FileNode)
+        IFileNode(filenode)
         self._filenode = filenode
 
     def render(self, ctx):
@@ -397,14 +402,14 @@ class WebishServer(service.MultiService):
         # apparently 'ISite' does not exist
         #self.site._client = self.parent
 
-    def set_vdrive_root(self, root):
+    def set_vdrive_rootnode(self, root):
         self.root.putChild("global_vdrive", Directory(root, "/"))
         self.root.child_welcome.has_global_vdrive = True
         # I tried doing it this way and for some reason it didn't seem to work
         #print "REMEMBERING", self.site, dl, IDownloader
         #self.site.remember(dl, IDownloader)
 
-    def set_my_vdrive_root(self, my_vdrive):
+    def set_my_vdrive_rootnode(self, my_vdrive):
         self.root.putChild("my_vdrive", Directory(my_vdrive, "~"))
         self.root.child_welcome.has_my_vdrive = True