[Zope-CVS] CVS: Products/Ape/lib/apelib/fs - annotated.py:1.1.2.1 classification.py:1.3.2.4 connection.py:1.5.2.3 interfaces.py:1.2.2.2 properties.py:1.3.4.2 security.py:1.2.6.3 structure.py:1.4.2.3 cache.py:NONE exceptions.py:NONE

Shane Hathaway shane at zope.com
Wed Jan 21 00:21:43 EST 2004


Update of /cvs-repository/Products/Ape/lib/apelib/fs
In directory cvs.zope.org:/tmp/cvs-serv21277/fs

Modified Files:
      Tag: ape-0_8-branch
	classification.py connection.py interfaces.py properties.py 
	security.py structure.py 
Added Files:
      Tag: ape-0_8-branch
	annotated.py 
Removed Files:
      Tag: ape-0_8-branch
	cache.py exceptions.py 
Log Message:
Restructured filesystem connections to make them easier to understand.

- Used the word annotation in place of section, since it's possible to
  plug in other strategies for storing file/directory annotations, and
  'section' doesn't make that clear.

- Separated the file annotation implementation from the filesystem connection
  class, again making it clearer that you can plug in other implementations.

- Moved the exception declaration and cache implementations into other
  modules, reducing the number of modules.


=== Added File Products/Ape/lib/apelib/fs/annotated.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""AnnotatedFilesystem class.

$Id: annotated.py,v 1.1.2.1 2004/01/21 05:21:12 shane Exp $
"""

import re
from time import time
from types import StringType


# This expression matches "\n[sectionname]...\n", where len(sectionname) > 0.
section_re = re.compile(r'^\[([^\[\]\n]+)\][^\r\n]*(?:\r\n|\r|\n)',
                      re.MULTILINE)

properties_ext = 'properties'
remainder_ext = 'remainder'

# Match 'foo.properties', 'foo.remainder', 'properties', or 'remainder'.
# This is for filtering out annotation filenames.
annotation_re = re.compile('(|.+[.])(%s|%s)$' % (
    properties_ext, remainder_ext))

# Names of annotations handled by this module
remainder_ann = 'remainder'         # The value is a binary string.
object_names_ann = 'object_names'


class AnnotatedFilesystem:
    """Filesystem abstraction that adds annotations and automatic extensions.

    Annotations are stored in .properties and .remainder files.
    """

    def __init__(self, ops, annotation_prefix='.', hidden_filenames='_'):
        self.ops = ops
        self.annotation_prefix = annotation_prefix
        self.hidden_re = re.compile(hidden_filenames)
        # _anns_cache: { path -> annotations }
        self._anns_cache = ShortLivedCache()
        # _dir_cache: { path -> directory info }
        self._dir_cache = ShortLivedCache()

    def clearCache(self):
        """Clears the cache of annotations and automatic filename extensions.

        Useful after writing to the filesystem.
        """
        self._anns_cache.clear()
        self._dir_cache.clear()

    def invalidate(self, path):
        """Invalidates info about a path being written.
        """
        self._anns_cache.invalidate(path)
        self._dir_cache.invalidate(path)

    def getAnnotationPaths(self, path):
        """Returns the property and remainder paths for a path.
        """
        ops = self.ops
        if ops.isdir(path):
            base_fn = ops.join(path, self.annotation_prefix)
        else:
            dirname, filename = ops.split(path)
            base_fn = ops.join(dirname, '%s%s.' % (
                self.annotation_prefix, filename))
        return (base_fn + properties_ext, base_fn + remainder_ext)

    def getAnnotations(self, path):
        """Reads the annotations for a path."""
        res = self._anns_cache.get(path)
        if res is not None:
            return res
        props_fn, rem_fn = self.getAnnotationPaths(path)
        res = {}
        try:
            data = self.ops.readfile(rem_fn, 0)
        except IOError:
            # The remainder file apparently does not exist
            pass
        else:
            res[remainder_ann] = data
            # Note properties file can override the remainder.
        try:
            data = self.ops.readfile(props_fn, 1)
        except IOError:
            # The properties file apparently does not exist
            self._anns_cache.set(path, res)
            return res
        pos = 0
        prev_section_name = None
        while 1:
            match = section_re.search(data, pos)
            if match is None:
                endpos = len(data)
            else:
                endpos = match.start()
            if prev_section_name is not None:
                # get the data and decode.
                section = data[pos:endpos].replace('[[', '[')
                res[prev_section_name] = section
            if match is None:
                break
            else:
                prev_section_name = match.group(1)
                pos = match.end()
        self._anns_cache.set(path, res)
        return res

    def checkAnnotationName(self, ann_name):
        if (not isinstance(ann_name, StringType)
            or not ann_name
            or '[' in ann_name
            or ']' in ann_name
            or '\n' in ann_name):
            raise ValueError(ann_name)

    def writeAnnotations(self, path, anns):
        props_fn, rem_fn = self.getAnnotationPaths(path)
        props_data = ''
        rem_data = ''
        items = anns.items()
        items.sort()
        for name, value in items:
            if name == remainder_ann:
                # Write to the remainder file.
                rem_data = value
            else:
                # Write a section of the properties file.
                props_data += self.formatSection(name, value)
        self.writeOrRemove(props_fn, 1, props_data)
        self.writeOrRemove(rem_fn, 0, rem_data)
        self._anns_cache.invalidate(path)
        # The file might be new, so invalidate the directory.
        self._dir_cache.invalidate(self.ops.dirname(path))

    def formatSection(self, name, text):
        s = '[%s]\n%s\n' % (name, text.replace('[', '[['))
        if not text.endswith('\n'):
            s += '\n'
        return s

    def writeOrRemove(self, fn, as_text, data):
        """If data is provided, write it.  Otherwise remove the file.
        """
        ops = self.ops
        if data:
            ops.writefile(fn, as_text, data)
        else:
            if ops.exists(fn):
                ops.remove(fn)

    def isLegalFilename(self, fn):
        ap = self.annotation_prefix
        if (not fn or
            (fn.startswith(ap) and annotation_re.match(fn, len(ap)))
            or self.hidden_re.match(fn) is not None):
            return 0
        return 1

    def computeDirectoryContents(self, path, allow_missing=0):
        """Returns the name translations for a directory.  Caches the results.

        Returns ({filename: name}, {name: filename}).
        """
        res = self._dir_cache.get(path)
        if res is not None:
            return res

        try:
            fns = self.ops.listdir(path)
        except OSError:
            if allow_missing:
                return {}, {}
            raise
        
        obj_list = []   # [name]
        trans = {}     # { base name -> filename with extension or None }
        filenames = filter(self.isLegalFilename, fns)
        anns = self.getAnnotations(path)
        text = anns.get(object_names_ann)
        if text:
            # Prepare a dictionary of translations from basename to filename.
            for fn in filenames:
                if '.' in fn:
                    base, ext = fn.split('.', 1)
                    if trans.has_key(base):
                        # Name collision: two or more files have the same base
                        # name.  Don't strip the extensions for any of them.
                        trans[base] = None
                    else:
                        trans[base] = fn
                else:
                    trans[fn] = None
            obj_list = [line.strip() for line in text.split('\n')]
            for obj_name in obj_list:
                if '.' in obj_name:
                    # An object name uses an extension.  Don't translate
                    # any name that uses the same base name.
                    base, ext = obj_name.split('.', 1)
                    trans[base] = None

        fn_to_name = {}
        for fn in filenames:
            fn_to_name[fn] = fn
        # Translate the file names to object names.
        for obj_name in obj_list:
            fn = trans.get(obj_name)
            if fn:
                fn_to_name[fn] = obj_name
        name_to_fn = {}
        for fn, name in fn_to_name.items():
            name_to_fn[name] = fn
        res = (fn_to_name, name_to_fn)
        self._dir_cache.set(path, res)
        return res


class ShortLivedCache:
    """Simple short-lived object cache.
    """
    def __init__(self, lifetime=1):
        # The default lifetime is 1 second.
        self.lifetime = lifetime
        self.data = {}
        self.expiration = time() + lifetime

    def get(self, key, default=None):
        now = time()
        if now >= self.expiration:
            self.data.clear()
            return default
        res = self.data.get(key, default)
        return res

    def set(self, key, value):
        now = time()
        if now >= self.expiration:
            self.data.clear()
            self.expiration = now + self.lifetime
        self.data[key] = value

    def invalidate(self, key):
        try:
            del self.data[key]
        except KeyError:
            pass

    def clear(self):
        self.data.clear()


=== Products/Ape/lib/apelib/fs/classification.py 1.3.2.3 => 1.3.2.4 ===
--- Products/Ape/lib/apelib/fs/classification.py:1.3.2.3	Thu Dec 25 23:22:33 2003
+++ Products/Ape/lib/apelib/fs/classification.py	Wed Jan 21 00:21:12 2004
@@ -11,7 +11,7 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""Filesystem classification section.
+"""Filesystem classification annotation.
 
 $Id$
 """
@@ -22,7 +22,7 @@
 from base import FSGatewayBase
 
 
-class FSClassificationSection(FSGatewayBase):
+class FSClassificationAnnotation(FSGatewayBase):
     """Gateway for storing classification data."""
 
     __implements__ = IGateway
@@ -33,7 +33,7 @@
         fs_conn = self.getConnection(event)
         p = event.oid
         classification = {'node_type': fs_conn.readNodeType(p)}
-        text = fs_conn.readSection(p, 'classification', '')
+        text = fs_conn.readAnnotation(p, 'classification', '')
         if text:
             lines = text.split('\n')
             for line in lines:
@@ -66,5 +66,5 @@
             else:
                 text.append('%s=%s' % (k, v))
         text = '\n'.join(text)
-        fs_conn.writeSection(p, 'classification', text)
+        fs_conn.writeAnnotation(p, 'classification', text)
         return text.strip()


=== Products/Ape/lib/apelib/fs/connection.py 1.5.2.2 => 1.5.2.3 ===
--- Products/Ape/lib/apelib/fs/connection.py:1.5.2.2	Sat Dec 20 23:24:04 2003
+++ Products/Ape/lib/apelib/fs/connection.py	Wed Jan 21 00:21:12 2004
@@ -16,151 +16,53 @@
 $Id$
 """
 
-import re
-from types import StringType
-
 from apelib.core.interfaces import ITPCConnection, ISourceRepository, LoadError
-from interfaces import IFSConnection
-from exceptions import FSWriteError
-from cache import ShortLivedCache
+from interfaces import IFSConnection, FSWriteError
 from fileops import StandardFileOperations
+from annotated import AnnotatedFilesystem, object_names_ann
+
+# For a node_type_ann, the value is 'f' (file) or 'd' (directory)
+node_type_ann = '@node_type'
+
+# data_ann holds the content of a file.  It is not valid for directories.
+data_ann = '@data'
 
+# file_list_ann holds the content of a directory.  It is not valid for files.
+file_list_ann = '@files'
 
-# Try to decipher this regular expression ;-)
-# It basically matches "\n[sectionname]...\n", where len(sectionname) > 0.
-section_re = re.compile(r'^\[([^\[\]\n]+)\][^\r\n]*(?:\r\n|\r|\n)',
-                      re.MULTILINE)
-
-# For a NODE_TYPE_SECTION, the value is 'f' (file) or 'd' (directory)
-NODE_TYPE_SECTION = '@node_type'
-
-# For a DATA_SECTION, the value is a two-item tuple containing a
-# string (file) or list of names (directory) and the as_text flag.
-DATA_SECTION = '@data'
-
-SUGGESTED_EXTENSION_SECTION = '@s_ext'  # The suggested filename extension.
-OBJECT_NAMES_SECTION = 'object_names'   # For directories.  The value is text.
-REMAINDER_SECTION = 'remainder'         # The value is a binary string.
-
-PROPERTIES_EXTENSION = 'properties'
-REMAINDER_EXTENSION = 'remainder'
-
-# Match 'foo.properties', 'foo.remainder', 'properties', or 'remainder'.
-# This is for filtering out metadata filenames.
-metadata_re = re.compile('(|.+[.])(%s|%s)$' % (
-    PROPERTIES_EXTENSION, REMAINDER_EXTENSION))
+# The suggested filename extension.
+suggested_extension_ann = '@s_ext'
 
 
 class FSConnection:
-    """Reads / writes files with 'sections'.
+    """Reads / writes files with annotations.
 
-    The required 'type' section specifies whether the object is a file or
-    a directory.  The optional 'data' section specifies either the main
-    file contents or the names of the files in the directory.  All other
-    sections get stored in a '.properties' file.  The properties file uses
-    square-bracket section headers and encodes sections by doubling
-    left-square brackets.
+    The required 'type' annotation specifies whether the object is a file
+    or a directory.  The optional 'data' annotation specifies either the
+    main file contents or the names of the files in the directory.
+    All other annotations get stored in the '.properties' and
+    '.remainder' files.  The properties file uses square-bracket
+    annotation headers and encodes annotations by doubling left-square
+    brackets.
     """
     __implements__ = IFSConnection, ITPCConnection, ISourceRepository
 
     basepath = ''
 
-    def __init__(self, basepath, metadata_prefix='.', hidden_filenames='_',
+    def __init__(self, basepath, annotation_prefix='.', hidden_filenames='_',
                  ops=None):
         self.basepath = basepath
-        self.metadata_prefix = metadata_prefix
-        self.hidden_re = re.compile(hidden_filenames)
-        self._final = 0
-        # _pending holds the data to be written.
-        # _pending: { subpath string -> { section_name -> data } }
-        self._pending = {}
-        self._props_cache = ShortLivedCache()
-        self._dir_cache = ShortLivedCache()
         if ops is None:
             ops = StandardFileOperations()
         self.ops = ops
+        self.afs = AnnotatedFilesystem(
+            ops, annotation_prefix, hidden_filenames)
+        self._final = 0
+        # _pending holds the data to be written.
+        # _pending: { subpath string -> { annotation_name -> data } }
+        self._pending = {}
 
-    def _clearCache(self):
-        self._props_cache.clear()
-        self._dir_cache.clear()
-
-    def _isLegalFilename(self, fn):
-        mp = self.metadata_prefix
-        if (not fn or
-            (fn.startswith(mp) and metadata_re.match(fn, len(mp)))
-            or self.hidden_re.match(fn) is not None):
-            return 0
-        return 1
-        
-
-    def _computeDirectoryContents(self, path, ignore_error=0):
-        """Computes and returns intermediate directory contents info.
-
-        Returns (filenames, object_names, translations).  The results
-        are cached for a short time.
-        """
-        res = self._dir_cache.get(path)
-        if res is not None:
-            return res
-
-        obj_names = []
-        trans = {}     # { base name -> filename with extension or None }
-        try:
-            fns = self.ops.listdir(path)
-        except OSError:
-            if ignore_error:
-                return ([], obj_names, trans)
-            raise
-        
-        filenames = filter(self._isLegalFilename, fns)
-        props = self._getPropertiesFromFile(path)
-        text = props.get(OBJECT_NAMES_SECTION)
-        if text:
-            # Prepare a dictionary of translations.
-            for fn in filenames:
-                if '.' in fn:
-                    base, ext = fn.split('.', 1)
-                    if trans.has_key(base):
-                        # Name collision: two or more files have the same base
-                        # name.  Don't use an extension for this name.
-                        trans[base] = None
-                    else:
-                        trans[base] = fn
-                else:
-                    trans[fn] = None
-            obj_names = [line.strip() for line in text.split('\n')]
-            for obj_name in obj_names:
-                if '.' in obj_name:
-                    base, ext = obj_name.split('.', 1)
-                    trans[base] = None
-
-        res = (filenames, obj_names, trans)
-        self._dir_cache.set(path, res)
-        return res
-
-
-    def _listDirectoryAsMapping(self, path, ignore_error=0):
-        """Returns the translated filenames at path.
-
-        The ignore_error flag makes this method return an empty
-        dictionary if the directory is not found.
-
-        Returns {filename -> obj_name}.
-        """
-        filenames, obj_names, trans = self._computeDirectoryContents(
-            path, ignore_error)
-        res = {}
-        for fn in filenames:
-            res[fn] = fn
-        # Translate names.
-        for obj_name in obj_names:
-            fn = trans.get(obj_name)
-            if fn:
-                res[fn] = obj_name
-        return res
-
-
-    def _expandPath(self, subpath):
+    def getPath(self, subpath):
         if self.basepath:
             while subpath.startswith('/') or subpath.startswith('\\'):
                 subpath = subpath[1:]
@@ -172,64 +74,22 @@
             dir_path, obj_name = self.ops.split(path)
             if '.' not in obj_name:
                 # This object might have an automatic filename extension.
-                filenames, obj_names, trans = self._computeDirectoryContents(
-                    dir_path, 1)
-                fn = trans.get(obj_name)
-                if fn is not None:
+                contents = self.afs.computeDirectoryContents(dir_path, 1)
+                fn_to_name, name_to_fn = contents
+                fn = name_to_fn.get(obj_name)
+                if fn:
                     # Use the filename with an extension.
                     path = self.ops.join(dir_path, fn)
         return path
 
-
-    def _checkSectionName(self, section_name):
-        if (not isinstance(section_name, StringType)
-            or not section_name
-            or '[' in section_name
-            or ']' in section_name
-            or '\n' in section_name
-            or section_name.startswith('@')
-            or section_name == OBJECT_NAMES_SECTION):
-            raise ValueError, section_name
-
-
-    def writeSection(self, subpath, section_name, data):
-        self._checkSectionName(section_name)
-        self._queue(subpath, section_name, data)
-
-
-    def writeNodeType(self, subpath, data):
-        self._queue(subpath, NODE_TYPE_SECTION, data)
-
-
-    def writeData(self, subpath, data, as_text=0):
-        self._queue(subpath, DATA_SECTION, (data, as_text))
-
-
-    def suggestExtension(self, subpath, ext):
-        self._queue(subpath, SUGGESTED_EXTENSION_SECTION, ext)
-
-
-    def readSection(self, subpath, section_name, default=None):
-        self._checkSectionName(section_name)
-        path = self._expandPath(subpath)
-        sections = self._getPropertiesFromFile(path)
-        return sections.get(section_name, default)
-
-
     def readNodeType(self, subpath):
-        path = self._expandPath(subpath)
+        path = self.getPath(subpath)
         if not self.ops.exists(path):
             raise LoadError("%s does not exist" % path)
         return self.ops.isdir(path) and 'd' or 'f'
 
-
     def readData(self, subpath, allow_missing=0, as_text=0):
-        path = self._expandPath(subpath)
-        isdir = self.ops.isdir(path)
-        # Read either the directory listing or the file contents.
-        if isdir:
-            # Return a sequence of object names.
-            return self._listDirectoryAsMapping(path).values()
+        path = self.getPath(subpath)
         # Return a string.
         try:
             return self.ops.readfile(path, as_text)
@@ -238,12 +98,39 @@
                 return None
             raise
 
+    def readDirectory(self, subpath, allow_missing=0):
+        path = self.getPath(subpath)
+        # Return a sequence of object names.
+        contents = self.afs.computeDirectoryContents(path, allow_missing)
+        fn_to_name, name_to_fn = contents
+        return name_to_fn.keys()
+
+    def readAnnotation(self, subpath, name, default=None):
+        self.afs.checkAnnotationName(name)
+        path = self.getPath(subpath)
+        annotations = self.afs.getAnnotations(path)
+        return annotations.get(name, default)
+
+    def writeNodeType(self, subpath, data):
+        self._queue(subpath, node_type_ann, data)
+
+    def writeData(self, subpath, data, as_text=0):
+        self._queue(subpath, data_ann, (data, as_text))
+
+    def writeDirectory(self, subpath, names):
+        self._queue(subpath, file_list_ann, names)
+
+    def writeAnnotation(self, subpath, name, data):
+        self.afs.checkAnnotationName(name)
+        self._queue(subpath, name, data)
 
     def getExtension(self, subpath):
-        path = self._expandPath(subpath)
+        path = self.getPath(subpath)
         stuff, ext = self.ops.splitext(path)
         return ext
 
+    def suggestExtension(self, subpath, ext):
+        self._queue(subpath, suggested_extension_ann, ext)
 
     def getModTime(self, subpath, default=0):
         """Returns the time an object was last modified.
@@ -252,10 +139,10 @@
         implementation returns the modification time of the most
         recently modified of the three.
         """
-        path = self._expandPath(subpath)
-        props, remainder = self._getPropertyPaths(path)
+        path = self.getPath(subpath)
+        extra = self.afs.getAnnotationPaths(path)
         maxtime = -1
-        for p in (path, props, remainder):
+        for p in (path,) + tuple(extra):
             try:
                 t = self.ops.getmtime(p)
             except OSError:
@@ -268,70 +155,12 @@
         return maxtime
 
 
-    def _getPropertyPaths(self, path):
-        """Returns the property and remainder paths for a path."""
-        if self.ops.isdir(path):
-            base_fn = self.ops.join(path, self.metadata_prefix)
-        else:
-            dirname, filename = self.ops.split(path)
-            base_fn = self.ops.join(dirname, '%s%s.' % (
-                self.metadata_prefix, filename))
-        return (base_fn + PROPERTIES_EXTENSION, base_fn + REMAINDER_EXTENSION)
-
-
-    def _getPropertiesFromFile(self, path):
-        """Reads the properties and remainder for a path."""
-        res = self._props_cache.get(path)
-        if res is not None:
-            return res
-
-        props_fn, rem_fn = self._getPropertyPaths(path)
-
-        res = {}
-        try:
-            data = self.ops.readfile(rem_fn, 0)
-        except IOError:
-            # The remainder file apparently does not exist
-            pass
-        else:
-            res[REMAINDER_SECTION] = data
-            # Note that the remainder can be overridden by the properties
-            # file.  Perhaps that should be prevented in the future.
-
-        try:
-            data = self.ops.readfile(props_fn, 1)
-        except IOError:
-            # The properties file apparently does not exist
-            self._props_cache.set(path, res)
-            return res
-
-        pos = 0
-        prev_section_name = None
-        while 1:
-            match = section_re.search(data, pos)
-            if match is None:
-                endpos = len(data)
-            else:
-                endpos = match.start()
-            if prev_section_name is not None:
-                # get the data and decode.
-                section = data[pos:endpos].replace('[[', '[')
-                res[prev_section_name] = section
-            if match is None:
-                break
-            else:
-                prev_section_name = match.group(1)
-                pos = match.end()
-
-        self._props_cache.set(path, res)
-        return res
-
-
-    def _writeFinal(self, subpath, sections):
-        """Performs an actual write of a file or directory to disk."""
-        # sections is a mapping.
-        path = self._expandPath(subpath)
-        t = sections[NODE_TYPE_SECTION]
+    def _writeFinal(self, subpath, anns):
+        """Performs an actual write of a file or directory to disk.
+        """
+        # anns is a mapping.
+        path = self.getPath(subpath)
+        t = anns[node_type_ann]
         if not self.ops.exists(path):
             if t == 'd':
                 self.ops.mkdir(path)
@@ -339,7 +168,7 @@
                 fn = self.ops.split(path)[1]
                 if '.' not in fn:
                     # This object has no extension and doesn't yet exist.
-                    ext = sections.get(SUGGESTED_EXTENSION_SECTION)
+                    ext = anns.get(suggested_extension_ann)
                     if ext:
                         # Try to use the suggested extension.
                         if not ext.startswith('.'):
@@ -349,59 +178,24 @@
                             # No file is in the way.
                             # Use the suggested extension.
                             path = p
-        props_fn, rem_fn = self._getPropertyPaths(path)
-        props_data = ''
-        rem_data = ''
-        items = sections.items()
-        items.sort()
-        try:
-            for name, value in items:
-                if name == NODE_TYPE_SECTION:
-                    continue
-                elif name == DATA_SECTION:
-                    data, as_text = value
-                    if t == 'd':
-                        # Change the list of subobjects.
-                        self._removeUnlinkedItems(path, data)
-                        props_data += self._formatSection(
-                            OBJECT_NAMES_SECTION, '\n'.join(data))
-                        self._disableConflictingExtensions(subpath, data)
-                        self._dir_cache.invalidate(path)
-                    else:
-                        # Change the file contents.
-                        self.ops.writefile(path, as_text, data)
-                elif name == SUGGESTED_EXTENSION_SECTION:
-                    # This doesn't need to be written.
-                    pass
-                elif name == REMAINDER_SECTION:
-                    # Write to the remainder file.
-                    rem_data = value
-                else:
-                    # Write a metadata section.
-                    props_data += self._formatSection(name, value)
-        finally:
-            self._writeOrRemove(props_fn, 1, props_data)
-            self._writeOrRemove(rem_fn, 0, rem_data)
-            self._props_cache.invalidate(path)
-            # The file might be new, so invalidate the directory.
-            self._dir_cache.invalidate(self.ops.dirname(path))
-
-
-    def _formatSection(self, name, text):
-        s = '[%s]\n%s\n' % (name, text.replace('[', '[['))
-        if not text.endswith('\n'):
-            s += '\n'
-        return s
-
-
-    def _writeOrRemove(self, fn, as_text, data):
-        """If data is provided, write it.  Otherwise remove the file.
-        """
-        if data:
-            self.ops.writefile(fn, as_text, data)
-        else:
-            if self.ops.exists(fn):
-                self.ops.remove(fn)
+        to_write = {}
+        for name, value in anns.items():
+            if (name == node_type_ann
+                or name == suggested_extension_ann):
+                # Doesn't need to be written.
+                continue
+            elif name == data_ann:
+                data, as_text = value
+                self.ops.writefile(path, as_text, data)
+            elif name == file_list_ann:
+                # Change the list of subobjects.
+                self._removeUnlinkedItems(path, value)
+                to_write[object_names_ann] = '\n'.join(value)
+                self._disableConflictingExtensions(subpath, value)
+                self.afs.invalidate(path)
+            else:
+                to_write[name] = value
+        self.afs.writeAnnotations(path, to_write)
 
 
     def _removeUnlinkedItems(self, path, names):
@@ -409,26 +203,26 @@
         linked = {}
         for name in names:
             linked[name] = 1
-        for fn, obj_name in self._listDirectoryAsMapping(path).items():
+        fn_to_name, name_to_fn = self.afs.computeDirectoryContents(path)
+        for fn, obj_name in fn_to_name.items():
             if not linked.get(obj_name):
                 item_fn = self.ops.join(path, fn)
                 if self.ops.isdir(item_fn):
                     self.ops.rmtree(item_fn)
                 else:
                     self.ops.remove(item_fn)
-                    props_fn, rem_fn = self._getPropertyPaths(item_fn)
-                    if self.ops.exists(props_fn):
-                        self.ops.remove(props_fn)
-                    if self.ops.exists(rem_fn):
-                        self.ops.remove(rem_fn)
+                    extra_paths = self.afs.getAnnotationPaths(item_fn)
+                    for p in extra_paths:
+                        if self.ops.exists(p):
+                            self.ops.remove(p)
 
 
     def _disableConflictingExtensions(self, subpath, obj_names):
         """Fixes collisions before writing files in a directory.
 
-        Enforces the rule: if 'foo.*' is in the
-        database, 'foo' may not have an automatic extension.
-        Enforces by un-queuing suggested extensions.
+        Enforces the rule: if 'foo.*' is in the database, 'foo' may
+        not have an automatic extension.  Enforces by un-queuing
+        suggested extensions.
         """
         reserved = {}  # { object name without extension -> 1 }
         for obj_name in obj_names:
@@ -438,14 +232,13 @@
         if not reserved:
             # No objects have extensions.
             return
-
         while subpath.endswith('/'):
             subpath = subpath[:-1]
         for obj_name in obj_names:
             if reserved.has_key(obj_name):
                 # Prevent obj_name from using an automatic extension.
                 child_subpath = '%s/%s' % (subpath, obj_name)
-                self._queue(child_subpath, SUGGESTED_EXTENSION_SECTION,
+                self._queue(child_subpath, suggested_extension_ann,
                             '', force=1)
 
 
@@ -456,44 +249,52 @@
         transaction commit.
         """
         non_containers = {}
-        for subpath, sections in items:
-            path = self._expandPath(subpath)
+        for subpath, anns in items:
+            path = self.getPath(subpath)
             exists = self.ops.exists(path)
             if exists and not self.ops.canwrite(path):
                 raise FSWriteError(
                     "Can't get write access to %s" % subpath)
             # type must be provided and must always be either 'd' or 'f'.
-            if (not sections.has_key(NODE_TYPE_SECTION)
-                or not sections.has_key(DATA_SECTION)):
+            if not anns.has_key(node_type_ann):
                 raise FSWriteError(
-                    'Data or node type not specified for %s' % subpath)
-            t = sections[NODE_TYPE_SECTION]
+                    'Node type not specified for %s' % subpath)
+            t = anns[node_type_ann]
             dir = self.ops.dirname(subpath)
             if non_containers.get(dir):
                 raise FSWriteError(
                     "Not a directory: %s" % dir)
-            data, as_text = sections[DATA_SECTION]
             if t == 'f':
+                data, as_text = anns[data_ann]
+                if anns.has_key(file_list_ann):
+                    raise FSWriteError(
+                        "Files can't have directory contents. %s"
+                        % subpath)
                 if exists and self.ops.isdir(path):
                     raise FSWriteError(
                         "Can't write file data to directory at %s"
                         % subpath)
                 non_containers[subpath] = 1
-                if not isinstance(data, StringType):
+                if not isinstance(data, type('')):
                     raise FSWriteError(
                         'Data for a file must be a string at %s'
                         % subpath)
             elif t == 'd':
+                data = anns[file_list_ann]
+                if anns.has_key(data_ann):
+                    raise FSWriteError(
+                        "Directories can't have file data. %s"
+                        % subpath)
                 if exists and not self.ops.isdir(path):
                     raise FSWriteError(
                         "Can't write directory contents to file at %s"
                         % subpath)
-                if isinstance(data, StringType):
+                if isinstance(data, type('')):
                     raise FSWriteError(
                         'Data for a directory must be a list or tuple at %s'
                         % subpath)
                 for item in data:
-                    if not self._isLegalFilename(item):
+                    if not self.afs.isLegalFilename(item):
                         raise FSWriteError(
                             'Not a legal object name: %s' % repr(item))
             else:
@@ -501,20 +302,20 @@
                     'Node type must be "d" or "f" at %s' % subpath)
 
 
-    def _queue(self, subpath, section_name, data, force=0):
+    def _queue(self, subpath, name, data, force=0):
         """Queues data to be written at commit time"""
         m = self._pending
-        sections = m.get(subpath)
-        if sections is None:
-            sections = {}
-            m[subpath] = sections
-        if sections.has_key(section_name) and not force:
-            if sections[section_name] != data:
+        anns = m.get(subpath)
+        if anns is None:
+            anns = {}
+            m[subpath] = anns
+        if anns.has_key(name) and not force:
+            if anns[name] != data:
                 raise FSWriteError(
                     'Conflicting data storage at %s (%s)' %
-                    (subpath, section_name))
+                    (subpath, name))
         else:
-            sections[section_name] = data
+            anns[name] = data
 
 
     #
@@ -532,7 +333,7 @@
             self.ops.makedirs(self.basepath)
 
     def begin(self):
-        self._clearCache()
+        self.afs.clearCache()
 
     def vote(self):
         """Do some early verification
@@ -547,7 +348,7 @@
     def reset(self):
         self._final = 0
         self._pending.clear()
-        self._clearCache()
+        self.afs.clearCache()
 
     def abort(self):
         self.reset()
@@ -557,8 +358,8 @@
             try:
                 items = self._pending.items()
                 items.sort()  # Ensure that base directories come first.
-                for subpath, sections in items:
-                    self._writeFinal(subpath, sections)
+                for subpath, anns in items:
+                    self._writeFinal(subpath, anns)
             finally:
                 self.reset()
 
@@ -575,14 +376,12 @@
                 t.append(None)
         return t
 
-
     def getPollSources(self, subpath):
-        p = self._expandPath(subpath)
-        props, remainder = self._getPropertyPaths(p)
-        paths = (p, props, remainder)
+        path = self.getPath(subpath)
+        extra = self.afs.getAnnotationPaths(path)
+        paths = (path,) + tuple(extra)
         t = self._get_paths_mtime(paths)
         return {(self, paths): t}
-
 
     def poll(self, sources):
         """ISourceRepository implementation.


=== Products/Ape/lib/apelib/fs/interfaces.py 1.2.2.1 => 1.2.2.2 ===
--- Products/Ape/lib/apelib/fs/interfaces.py:1.2.2.1	Sat Dec 20 23:24:04 2003
+++ Products/Ape/lib/apelib/fs/interfaces.py	Wed Jan 21 00:21:12 2004
@@ -19,63 +19,80 @@
 from Interface import Interface
 
 
+class FSWriteError (Exception):
+    """Unable to write data"""
+
+
 class IFSConnection (Interface):
-    """Simple filesystem connection (with textual annotations).
+    """Simple filesystem connection with annotations.
     """
 
-    def writeSection(subpath, section_name, data):
-        """Writes a text-based metadata section for a filesystem node."""
-
-    def writeNodeType(subpath, data):
-        """Writes the node type for a filesystem node.
+    def getPath(subpath):
+        """Returns the filesystem path for a subpath.
 
-        'd' (directory) and 'f' (file) are supported.
+        May automatically append an extension if the file already
+        exists.
         """
 
-    def writeData(subpath, data, as_text=0):
-        """Writes data to a filesystem node.
+    def readNodeType(subpath):
+        """Reads the node type of a filesystem node.
+        """
 
-        In the case of directories, expects a tuple containing the names
-        of the files that should be in the directory.  In the case of
-        files, expects a string.
+    def readData(subpath, allow_missing=0, as_text=0):
+        """Reads the main data stream from a file.
 
-        If as_text is true, the file is written in text mode.  The
-        as_text flag is ignored for directories.
+        If the allow_missing flag is specified, this method returns
+        None if no such file is found.  If as_text is true, the file
+        is read in text mode.
         """
 
-    def suggestExtension(subpath, ext):
-        """Suggests a filename extension for a filesystem node.
+    def readDirectory(subpath, allow_missing=0):
+        """Reads the contents of a directory.
 
-        The IFSConnection may use this information to store the file
-        with an automatically appended filename extension.
+        Returns a list of object names.  If the allow_missing flag is
+        specified, this method returns None if no such directory is
+        found.
         """
 
-    def readSection(subpath, section_name, default=None):
-        """Reads a text-based metadata section.
+    def readAnnotation(subpath, name, default=None):
+        """Reads a text-based annotation for a file.
         """
 
-    def readNodeType(subpath):
-        """Reads the node type of a filesystem node.
+    def writeNodeType(subpath, data):
+        """Writes the node type for a filesystem node.
+
+        'd' (directory) and 'f' (file) are supported.
         """
 
-    def readData(subpath, allow_missing=0, as_text=0):
-        """Reads the data from a filesystem node.
+    def writeData(subpath, data, as_text=0):
+        """Writes string data to a filesystem node.
 
-        For files, this reads the main data stream.  For directories,
-        this returns a list of names.  If the allow_missing flag is
-        specified, this method returns None if no filesystem node is
-        found.
+        If 'as_text' is true, the file is written in text mode.
+        """
+
+    def writeDirectory(subpath, names):
+        """Writes data to a directory.
 
-        If as_text is true, the file is read in text mode.  The
-        as_text flag is ignored for directories.
+        'names' is a sequence of object names used for determining filenames..
+        """
+
+    def writeAnnotation(subpath, name, data):
+        """Writes a text-based annotation for a filesystem node.
         """
 
     def getExtension(subpath):
-        """Returns the filename extension used for a filesystem node.
+        """Returns the filename extension for a subpath.
+        """
+
+    def suggestExtension(subpath, ext):
+        """Suggests a filename extension for a filesystem node.
+
+        The IFSConnection may use this information to store the file
+        with an automatically appended filename extension.
         """
 
     def getModTime(subpath, default=0):
-        """Returns the modification time of a file.
+        """Returns the last-modified time of a file.
         """
 
     def getPollSources(subpath):


=== Products/Ape/lib/apelib/fs/properties.py 1.3.4.1 => 1.3.4.2 ===
--- Products/Ape/lib/apelib/fs/properties.py:1.3.4.1	Sat Dec 20 23:24:04 2003
+++ Products/Ape/lib/apelib/fs/properties.py	Wed Jan 21 00:21:12 2004
@@ -58,7 +58,8 @@
 
 
 class FSProperties (FSGatewayBase):
-    """Simple properties to filesystem property section gateway."""
+    """Simple properties to filesystem properties annotation gateway.
+    """
 
     __implements__ = IGateway
 
@@ -67,14 +68,14 @@
     schema.addField('type', 'string')
     schema.addField('data', 'string')
 
-    def __init__(self, section='properties', conn_name='fs'):
-        self.section = str(section)
+    def __init__(self, annotation='properties', conn_name='fs'):
+        self.annotation = str(annotation)
         FSGatewayBase.__init__(self, conn_name)
 
     def load(self, event):
         p = event.oid
         fs_conn = self.getConnection(event)
-        text = fs_conn.readSection(p, self.section, '')
+        text = fs_conn.readAnnotation(p, self.annotation, '')
         res = []
         if text:
             lines = text.split('\n')
@@ -97,27 +98,27 @@
         text = '\n'.join(lines)
         p = event.oid
         fs_conn = self.getConnection(event)
-        fs_conn.writeSection(p, self.section, text)
+        fs_conn.writeAnnotation(p, self.annotation, text)
         state = list(state)
         state.sort()
         return tuple(state)
 
 
-class FSSectionData (FSGatewayBase):
-    """Text to filesystem property section gateway."""
+class FSAnnotationData (FSGatewayBase):
+    """Text to filesystem property annotation gateway."""
 
     __implements__ = IGateway
 
     schema = FieldSchema('data', 'string')
 
-    def __init__(self, section, conn_name='fs'):
-        self.section = str(section)
+    def __init__(self, annotation, conn_name='fs'):
+        self.annotation = str(annotation)
         FSGatewayBase.__init__(self, conn_name)
 
     def load(self, event):
         fs_conn = self.getConnection(event)
         p = event.oid
-        state = fs_conn.readSection(p, self.section, '').strip()
+        state = fs_conn.readAnnotation(p, self.annotation, '').strip()
         return state, state
 
     def store(self, event, state):
@@ -127,6 +128,6 @@
         if state:
             p = event.oid
             fs_conn = self.getConnection(event)
-            fs_conn.writeSection(p, self.section, state)
+            fs_conn.writeAnnotation(p, self.annotation, state)
         return state
 


=== Products/Ape/lib/apelib/fs/security.py 1.2.6.2 => 1.2.6.3 ===
--- Products/Ape/lib/apelib/fs/security.py:1.2.6.2	Sat Dec 20 23:24:04 2003
+++ Products/Ape/lib/apelib/fs/security.py	Wed Jan 21 00:21:12 2004
@@ -34,13 +34,13 @@
     schema.addField('permission', 'string')
     schema.addField('username', 'string')
 
-    def __init__(self, section='security', conn_name='fs'):
-        self.section = section
+    def __init__(self, annotation='security', conn_name='fs'):
+        self.annotation = annotation
         FSGatewayBase.__init__(self, conn_name)
 
     def load(self, event):
         fs_conn = self.getConnection(event)
-        text = fs_conn.readSection(event.oid, self.section, '')
+        text = fs_conn.readAnnotation(event.oid, self.annotation, '')
         res = []
         if text:
             lines = text.split('\n')
@@ -88,7 +88,7 @@
             lines.sort()
             text = '\n'.join(lines)
             fs_conn = self.getConnection(event)
-            fs_conn.writeSection(event.oid, self.section, text)
+            fs_conn.writeAnnotation(event.oid, self.annotation, text)
         state = list(state)
         state.sort()
         return tuple(state)


=== Products/Ape/lib/apelib/fs/structure.py 1.4.2.2 => 1.4.2.3 ===
--- Products/Ape/lib/apelib/fs/structure.py:1.4.2.2	Sat Dec 20 23:24:04 2003
+++ Products/Ape/lib/apelib/fs/structure.py	Wed Jan 21 00:21:12 2004
@@ -65,12 +65,12 @@
     schema = FieldSchema('id', 'string')
 
     def getIdFrom(self, event):
-        path = event.oid
-        pos = path.rfind('/')
+        p = event.oid
+        pos = p.rfind('/')
         if pos >= 0:
-            return path[pos + 1:]
+            return p[pos + 1:]
         else:
-            return path
+            return p
 
     def load(self, event):
         id = self.getIdFrom(event)
@@ -79,7 +79,8 @@
     def store(self, event, state):
         id = self.getIdFrom(event)
         if state != id:
-            raise ValueError('Mismatched file ID')
+            raise ValueError('Mismatched object name: %s != %s' %
+                             (state, id))
         return id
 
     def getPollSources(self, event):
@@ -100,7 +101,7 @@
         p = event.oid
         c = self.getConnection(event)
         assert c.readNodeType(p) == 'd'
-        names = c.readData(p)
+        names = c.readDirectory(p)
         names.sort()
         res = []
         for name in names:
@@ -122,7 +123,7 @@
                     "Child of %s named %s must use OID %s, but used %s" %
                     (event.oid, name, expect, oid))
         names = [row[0] for row in state]
-        c.writeData(p, names)
+        c.writeDirectory(p, names)
         return tuple(state)
 
 
@@ -170,7 +171,7 @@
             names = []
         else:
             assert t == 'd', 'The root object must be a directory'
-            names = c.readData(p)
+            names = c.readDirectory(p)
             names.sort()
         res = [('Application', '/')]
         for name in names:
@@ -196,7 +197,5 @@
             assert expect == oid, (
                 "Child of %s named %s must use OID %s, but used %s" %
                 (event.oid, name, expect, oid))
-        c.writeData(p, names)
+        c.writeDirectory(p, names)
         return tuple(state)
-
-

=== Removed File Products/Ape/lib/apelib/fs/cache.py ===

=== Removed File Products/Ape/lib/apelib/fs/exceptions.py ===




More information about the Zope-CVS mailing list