filetable: switch to new approach with anonymous nodes
authorBrian Warner <warner@allmydata.com>
Fri, 15 Jun 2007 00:24:56 +0000 (17:24 -0700)
committerBrian Warner <warner@allmydata.com>
Fri, 15 Jun 2007 00:24:56 +0000 (17:24 -0700)
src/allmydata/filetable.py
src/allmydata/test/test_filetable.py

index 53263bba4c376fbc9a897cc56f3553729a8e9d62..601cc4b724798702569530e1ece6eefea084a9a1 100644 (file)
@@ -1,29 +1,85 @@
 
-import os, shutil
+import os
 from zope.interface import implements
 from foolscap import Referenceable
 from allmydata.interfaces import RIMutableDirectoryNode
+from allmydata.util import bencode, idlib
+from allmydata.util.assertutil import _assert
 from twisted.application import service
 from twisted.python import log
 
-class DeadDirectoryNodeError(Exception):
-    """The directory referenced by this node has been deleted."""
-
-class BadDirectoryError(Exception):
-    """There was a problem with the directory being referenced."""
-class BadFileError(Exception):
-    """The file being referenced does not exist."""
 class BadNameError(Exception):
     """Bad filename component"""
 
+class BadFileError(Exception):
+    pass
+
+class BadDirectoryError(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', nodename).
+    """
+
     implements(RIMutableDirectoryNode)
 
-    def __init__(self, basedir):
+    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 make_subnode(self, name=None):
+        return self.__class__(self._basedir, name)
+
+    def _read_from_file(self):
+        f = open(os.path.join(self._basedir, self._name), "rb")
+        data = f.read()
+        f.close()
+        children_specifications = bencode.bdecode(data)
+        children = {}
+        for k,v in children_specifications.items():
+            nodetype = v[0]
+            if nodetype == "file":
+                (uri, ) = v[1:]
+                child = uri
+            elif nodetype == "subdir":
+                (nodename, ) = v[1:]
+                child = self.make_subnode(nodename)
+            else:
+                _assert("Unknown nodetype in node specification %s" % (v,))
+            children[k] = child
+        return children
+
+    def _write_to_file(self, children):
+        children_specifications = {}
+        for k,v in children.items():
+            if isinstance(v, MutableDirectoryNode):
+                child = ("subdir", v._name)
+            else:
+                assert isinstance(v, str)
+                child = ("file", v) # URI
+            children_specifications[k] = child
+        data = bencode.bencode(children_specifications)
+        f = open(os.path.join(self._basedir, self._name), "wb")
+        f.write(data)
+        f.close()
 
-    def make_subnode(self, basedir):
-        return self.__class__(basedir)
+
+    def create_filename(self):
+        return idlib.b2a(os.urandom(8))
 
     def validate_name(self, name):
         if name == "." or name == ".." or "/" in name:
@@ -32,66 +88,47 @@ class MutableDirectoryNode(Referenceable):
     # these are the public methods, available to anyone who holds a reference
 
     def list(self):
-        log.msg("Dir(%s).list" % self._basedir)
-        results = []
-        if not os.path.isdir(self._basedir):
-            raise DeadDirectoryNodeError("This directory has been deleted")
-        for name in os.listdir(self._basedir):
-            absname = os.path.join(self._basedir, name)
-            if os.path.isdir(absname):
-                results.append( (name, self.make_subnode(absname)) )
-            elif os.path.isfile(absname):
-                f = open(absname, "rb")
-                data = f.read()
-                f.close()
-                results.append( (name, data) )
-            # anything else is ignored
+        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)
-        absname = os.path.join(self._basedir, name)
-        if os.path.isdir(absname):
-            return self.make_subnode(absname)
-        elif os.path.isfile(absname):
-            f = open(absname, "rb")
-            data = f.read()
-            f.close()
-            return data
-        else:
-            raise BadFileError("there is nothing named '%s' in this directory"
-                               % name)
+        children = self._read_from_file()
+        if name not in children:
+            raise BadFileError("no such child")
+        return children[name]
     remote_get = get
 
     def add_directory(self, name):
         self.validate_name(name)
-        absname = os.path.join(self._basedir, name)
-        if os.path.isdir(absname):
-            raise BadDirectoryError("the directory '%s' already exists" % name)
-        if os.path.exists(absname):
-            raise BadDirectoryError("the directory '%s' already exists "
-                                    "(but isn't a directory)" % name)
-        os.mkdir(absname)
-        return self.make_subnode(absname)
+        children = self._read_from_file()
+        if name in children:
+            raise BadDirectoryError("the directory already existed")
+        children[name] = child = self.make_subnode()
+        self._write_to_file(children)
+        return child
     remote_add_directory = add_directory
 
     def add_file(self, name, uri):
         self.validate_name(name)
-        f = open(os.path.join(self._basedir, name), "wb")
-        f.write(uri)
-        f.close()
+        children = self._read_from_file()
+        children[name] = uri
+        self._write_to_file(children)
     remote_add_file = add_file
 
     def remove(self, name):
         self.validate_name(name)
-        absname = os.path.join(self._basedir, name)
-        if os.path.isdir(absname):
-            shutil.rmtree(absname)
-        elif os.path.isfile(absname):
-            os.unlink(absname)
-        else:
-            raise BadFileError("Cannot delete non-existent file '%s'" % name)
+        children = self._read_from_file()
+        if name not in children:
+            raise BadFileError("cannot remove non-existent child")
+        dead_child = children[name]
+        del children[name]
+        self._write_to_file(children)
+        #return dead_child
     remote_remove = remove
 
 
@@ -104,7 +141,7 @@ class GlobalVirtualDrive(service.MultiService):
         vdrive_dir = os.path.join(basedir, self.VDRIVEDIR)
         if not os.path.exists(vdrive_dir):
             os.mkdir(vdrive_dir)
-        self._root = MutableDirectoryNode(vdrive_dir)
+        self._root = MutableDirectoryNode(vdrive_dir, "root")
 
     def get_root(self):
         return self._root
index f4a5a66235f0d13df0c822f641239dad39a3e890..56a590404398488f98d374d48eafa92e328b7e49 100644 (file)
@@ -1,14 +1,14 @@
 
 import os
 from twisted.trial import unittest
-from allmydata.filetable import (MutableDirectoryNode, DeadDirectoryNodeError,
+from allmydata.filetable import (MutableDirectoryNode,
                                  BadDirectoryError, BadFileError, BadNameError)
 
 
 class FileTable(unittest.TestCase):
     def test_files(self):
         os.mkdir("filetable")
-        root = MutableDirectoryNode(os.path.abspath("filetable"))
+        root = MutableDirectoryNode(os.path.abspath("filetable"), "root")
         self.failUnlessEqual(root.list(), [])
         root.add_file("one", "vid-one")
         root.add_file("two", "vid-two")
@@ -54,7 +54,5 @@ class FileTable(unittest.TestCase):
         root.remove("subdir1")
         self.failUnlessEqual(root.list(), [("one", "vid-one")])
 
-        # should our (orphaned) subdir1/subdir2 node still be able to do
-        # anything?
-        self.failUnlessRaises(DeadDirectoryNodeError, subdir1.list)
+