[Zope-CVS] CVS: Products/Ape/lib/apelib/fs - __init__.py:1.1 cache.py:1.1 classification.py:1.1 connection.py:1.1 exceptions.py:1.1 interfaces.py:1.1 params.py:1.1 properties.py:1.1 security.py:1.1 structure.py:1.1

Shane Hathaway shane@zope.com
Wed, 9 Apr 2003 23:09:57 -0400


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

Added Files:
	__init__.py cache.py classification.py connection.py 
	exceptions.py interfaces.py params.py properties.py 
	security.py structure.py 
Log Message:
Moved apelib into a "lib" subdirectory.  This simplified the
Python hacking required to make apelib a top-level package.  Sorry
about the flood of checkins, but CVS makes a move like this quite painful.


=== Added File Products/Ape/lib/apelib/fs/__init__.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.
#
##############################################################################
"""Filesystem gateway package.

$Id: __init__.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""



=== Added File Products/Ape/lib/apelib/fs/cache.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.
#
##############################################################################
"""Simple short-lived object cache.

$Id: cache.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

from time import time


class ShortLivedCache:

    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()



=== Added File Products/Ape/lib/apelib/fs/classification.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.
#
##############################################################################
"""Filesystem classification section.

$Id: classification.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

from apelib.core.interfaces import IGateway
from apelib.core.schemas import FieldSchema


class FSClassificationSection:
    """Gateway for storing classification data."""

    __implements__ = IGateway

    schema = FieldSchema('classification', 'classification')

    def __init__(self, fs_conn):
        self.fs_conn = fs_conn

    def getSchema(self):
        return self.schema

    def load(self, event):
        c = self.fs_conn
        p = event.getKeychain()[-1]
        classification = {'node_type': c.readNodeType(p)}
        text = c.readSection(p, 'classification', '')
        if text:
            lines = text.split('\n')
            for line in lines:
                if '=' in line:
                    k, v = line.split('=', 1)
                    classification[k.strip()] = v.strip()
        classification['extension'] = c.getExtension(p)
        return classification, text.strip()

    def store(self, event, state):
        # state is a classification
        p = event.getKeychain()[-1]
        items = state.items()
        items.sort()
        text = []
        for k, v in items:
            if k == 'extension':
                self.fs_conn.suggestExtension(p, v)
            else:
                text.append('%s=%s' % (k, v))
        text = '\n'.join(text)
        self.fs_conn.writeSection(p, 'classification', text)
        return text.strip()


=== Added File Products/Ape/lib/apelib/fs/connection.py === (486/586 lines abridged)
##############################################################################
#
# 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.
#
##############################################################################
"""Full-featured Filesystem connection class.

$Id: connection.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

import os
import re
from shutil import rmtree
from types import StringType

from apelib.core.interfaces import ITPCConnection
from apelib.core.exceptions import NoStateFoundError
from interfaces import IFSConnection
from exceptions import FSWriteError
from cache import ShortLivedCache


# 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'.

[-=- -=- -=- 486 lines omitted -=- -=- -=-]

    #
    # ITPCConnection implementation
    #

    def sortKey(self):
        return self.basepath

    def getName(self):
        return self.basepath

    def connect(self):
        if not os.path.exists(self.basepath):
            os.makedirs(self.basepath)

    def begin(self):
        self._props_cache.clear()
        self._dir_cache.clear()

    def vote(self):
        """Do some early verification

        This is done while the transaction can still be vetoed safely.
        """
        items = self._pending.items()
        items.sort()  # Ensure that base directories come first.
        self._beforeWrite(items)
        self._final = 1

    def reset(self):
        self._final = 0
        self._pending.clear()
        self._props_cache.clear()
        self._dir_cache.clear()

    def abort(self):
        self.reset()

    def finish(self):
        if self._final:
            try:
                items = self._pending.items()
                items.sort()  # Ensure that base directories come first.
                for subpath, sections in items:
                    self._writeFinal(subpath, sections)
            finally:
                self.reset()

    def close(self):
        self.reset()



=== Added File Products/Ape/lib/apelib/fs/exceptions.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.
#
##############################################################################
"""fs_gateway exception types

$Id: exceptions.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

class FSWriteError (Exception):
    """Unable to write data"""



=== Added File Products/Ape/lib/apelib/fs/interfaces.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.
#
##############################################################################
"""Filesystem-specific interfaces

$Id: interfaces.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

from Interface import Interface


class IFSConnection (Interface):
    """Simple filesystem connection (with textual 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.

        'd' (directory) and 'f' (file) are supported.
        """

    def writeData(subpath, data, as_text=0):
        """Writes data to 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.

        If as_text is true, the file is written in text mode.  The
        as_text flag is ignored for directories.
        """

    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 readSection(subpath, section_name, default=None):
        """Reads a text-based metadata section.
        """

    def readNodeType(subpath):
        """Reads the node type of a filesystem node.
        """

    def readData(subpath, allow_missing=0, as_text=0):
        """Reads the data from 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 read in text mode.  The
        as_text flag is ignored for directories.
        """

    def getExtension(subpath):
        """Returns the filename extension used for a filesystem node.
        """

    def getModTime(subpath, default=0):
        """Returns the modification time of a file.
        """



=== Added File Products/Ape/lib/apelib/fs/params.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.
#
##############################################################################
"""Functions for encoding/decoding parameter strings.

$Id: params.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

import re

token_re = re.compile(r'([" \t]|\\["\\trn])')
token_replacements = {
    '\\"': '"',
    '\\\\': '\\',
    '\\t': '\t',
    '\\r': '\r',
    '\\n': '\n',
    }

key_re = re.compile(r'[A-Za-z_-][A-Za-z0-9_-]*$')


def splitParams(s):
    tokens = re.split(token_re, s)
    params = []
    param = []
    quoting = 0
    for tok in tokens:
        if tok:
            v = token_replacements.get(tok)
            if v:
                param.append(v)
            elif not quoting and (tok == ' ' or tok == '\t'):
                if param:
                    params.append(''.join(param))
                    param = []
            else:
                if tok == '"':
                    quoting = not quoting
                    if not quoting:
                        params.append(''.join(param))
                        param = []
                else:
                    param.append(tok)
    leftover = ''.join(param).strip()
    if leftover:
        params.append(leftover)
    return params


def escapeParam(s):
    return s.replace('\\', '\\\\').replace('"', '\\"').replace(
        '\r', '\\r').replace('\n', '\\n').replace('\t', '\\t')


def stringToParams(s):
    """Decodes a string of the format 'a="..." b="..."'.

    Returns a list of (key, value) pairs.
    """
    params = splitParams(s)
    res = []
    for param in params:
        p = param.split('=', 1)
        if len(p) == 1:
            k = p[0]
            v = ''
        else:
            k, v = p
        res.append((k, v))
    return res


def paramsToString(params):
    """Encodes a list of (key, value) pairs as a string."""
    parts = []
    for k, v in params:
        if not key_re.match(k):
            raise ValueError, 'Bad parameter name: %s' % repr(k)
        if v:
            parts.append('%s="%s"' % (k, escapeParam(v)))
        else:
            parts.append(k)
    return ' '.join(parts)



=== Added File Products/Ape/lib/apelib/fs/properties.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.
#
##############################################################################
"""Filesystem property gateways.

$Id: properties.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

from types import StringType

from apelib.core.interfaces import IGateway
from apelib.core.schemas import FieldSchema, RowSequenceSchema


token_replacements = {
    '\\\\': '\\',
    '\\t': '\t',
    '\\r': '\r',
    '\\n': '\n',
    }

def escape_string(s):
    return s.replace('\\', '\\\\').replace('\n', '\\n').replace(
        '\r', '\\r').replace('\t', '\\t')

def unescape_string(s):
    res = []
    pos = 0
    while 1:
        p = s.find('\\', pos)
        if p >= 0:
            res.append(s[pos:p])
            token = s[p:p+2]
            c = token_replacements.get(token)
            if c is not None:
                # known escape sequence
                res.append(c)
            else:
                # unknown sequence
                res.append(token)
            pos = p + 2
        else:
            res.append(s[pos:])
            break
    return ''.join(res)


class FSProperties:
    """Simple properties to filesystem property section gateway."""

    __implements__ = IGateway

    schema = RowSequenceSchema()
    schema.addField('id', 'string', 1)
    schema.addField('type', 'string')
    schema.addField('data', 'string')

    def __init__(self, fs_conn, section='properties'):
        self.fs_conn = fs_conn
        self.section = section

    def getSchema(self):
        return self.schema

    def load(self, event):
        p = event.getKeychain()[-1]
        text = self.fs_conn.readSection(p, self.section, '')
        res = []
        if text:
            lines = text.split('\n')
            for line in lines:
                if '=' in line:
                    k, v = line.split('=', 1)
                    if ':' in k:
                        k, t = k.split(':', 1)
                    else:
                        t = 'string'
                    res.append((k, t, unescape_string(v)))
        res.sort()
        return res, tuple(res)

    def store(self, event, state):
        lines = []
        for k, t, v in state:
            lines.append('%s:%s=%s' % (k, t, escape_string(v)))
        lines.sort()
        text = '\n'.join(lines)
        p = event.getKeychain()[-1]
        self.fs_conn.writeSection(p, self.section, text)
        state = list(state)
        state.sort()
        return tuple(state)


class FSSectionData:
    """Text to filesystem property section gateway."""

    __implements__ = IGateway

    schema = FieldSchema('data', 'string')

    def __init__(self, fs_conn, section):
        self.fs_conn = fs_conn
        self.section = section

    def getSchema(self):
        return self.schema

    def load(self, event):
        p = event.getKey()
        state = self.fs_conn.readSection(p, self.section, '').strip()
        return state, state

    def store(self, event, state):
        if not isinstance(state, StringType):
            raise RuntimeError('Not a string: %s' % repr(state))
        state = state.strip()
        if state:
            p = event.getKey()
            self.fs_conn.writeSection(p, self.section, state)
        return state



=== Added File Products/Ape/lib/apelib/fs/security.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.
#
##############################################################################
"""Gateways for storing security information.

$Id: security.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

from apelib.core.interfaces import IGateway
from apelib.core.schemas import RowSequenceSchema
from apelib.core.exceptions import MappingError
from params import stringToParams, paramsToString


class FSSecurityAttributes:
    """Gateway for storing security attributes."""

    __implements__ = IGateway

    schema = RowSequenceSchema()
    schema.addField('declaration_type', 'string')
    schema.addField('role', 'string')
    schema.addField('permission', 'string')
    schema.addField('username', 'string')

    def __init__(self, fs_conn, section='security'):
        self.fs_conn = fs_conn
        self.section = section

    def getSchema(self):
        return self.schema

    def load(self, event):
        key = event.getKey()
        text = self.fs_conn.readSection(key, self.section, '')
        res = []
        if text:
            lines = text.split('\n')
            for line in lines:
                line = line.strip()
                if not line or line.startswith('#'):
                    continue
                params = stringToParams(line)
                if params:
                    decl_type = params[0][0]
                    row = [decl_type, '', '', '']
                    for k, v in params[1:]:
                        k = k.lower()
                        if '_' in k:
                            # temporary backward compatibility
                            k = k.split('_', 1)[0]
                        if k == 'role':
                            row[1] = v
                        elif k == 'permission':
                            row[2] = v
                        elif k == 'username':
                            row[3] = v
                        else:
                            raise ValueError(
                                "Could not read security declaration "
                                "%s for %s" % (repr(line), repr(key)))
                    res.append(tuple(row))
        res.sort()
        return res, tuple(res)


    def store(self, event, state):
        lines = []
        for d, r, p, u in state:
            params = [(d, '')]
            if r:
                params.append(('role', r))
            if p:
                params.append(('permission', p))
            if u:
                params.append(('username', u))
            s = paramsToString(params)
            lines.append(s)
        if lines:
            lines.sort()
            text = '\n'.join(lines)
            self.fs_conn.writeSection(event.getKey(), self.section, text)
        state = list(state)
        state.sort()
        return tuple(state)



class FSUserList:
    """User list gateway, where the user list is stored in a flat file."""

    __implements__ = IGateway

    schema = RowSequenceSchema()
    schema.addField('id', 'string', 1)
    schema.addField('password', 'string')
    schema.addField('roles', 'string:list')
    schema.addField('domains', 'string:list')

    def __init__(self, fs_conn):
        self.fs_conn = fs_conn

    def getSchema(self):
        return self.schema

    def load(self, event):
        c = self.fs_conn
        p = event.getKeychain()[-1]
        assert c.readNodeType(p) == 'f'
        text = c.readData(p)
        res = []
        for line in text.split('\n'):
            L = line.strip()
            if not L.startswith('#') and ':' in L:
                id, password, rolelist, domainlist = L.split(':', 3)
                roles = self._splitList(rolelist)
                domains = self._splitList(domainlist)
                res.append((id, password, roles, domains))
        res.sort()
        return res, text


    def _splitList(self, s):
        return tuple([item.strip() for item in s.split(',') if item])


    def _joinList(self, items):
        for item in items:
            if item.strip() != item:
                raise MappingError(
                    "Leading and trailing whitespace are not allowed "
                    "in roles and domains")
            item = item.strip()
            if not item:
                raise MappingError("Empty role or domain not allowed")
            if ',' in item or ':' in item or '\n' in item:
                raise MappingError(
                    "Commas, colons, and newlines are not allowed "
                    "in roles and domains")
        return ','.join(items)


    def store(self, event, state):
        replace_lines = {}
        for id, password, roles, domains in state:
            if ':' in id or '\n' in id:
                raise MappingError('User IDs cannot have colons or newlines')
            if id.startswith('#'):
                raise MappingError('User IDs cannot start with #')
            if ':' in password or '\n' in password:
                raise MappingError('Passwords cannot have colons or newlines')
            rolelist = self._joinList(roles)
            domainlist = self._joinList(domains)
            to_write = '%s:%s:%s:%s' % (id, password, rolelist, domainlist)
            replace_lines[id] = to_write
        p = event.getKeychain()[-1]
        self.fs_conn.writeNodeType(p, 'f')
        text = self.fs_conn.readData(p, allow_missing=1)
        if text is None:
            text = ''
        new_lines = []
        # Replace / remove users
        for line in text.split('\n'):
            L = line.strip()
            if not L.startswith('#'):
                if ':' in L:
                    name, stuff = L.split(':', 1)
                    replace = replace_lines.get(name, '')
                    if replace and replace != L:
                        new_lines.append(replace)
                        del replace_lines[name]
                # else remove the line
            else:
                new_lines.append(line)
        # Append new users
        for line in replace_lines.values():
            new_lines.append(line)
        # Write it
        text = '\n'.join(new_lines)
        self.fs_conn.writeData(p, text)
        serial = list(state)
        serial.sort()
        return text



=== Added File Products/Ape/lib/apelib/fs/structure.py ===
##############################################################################
#
# Copyright (c) 2002 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.
#
##############################################################################
"""Basic filesystem gateways.

$Id: structure.py,v 1.1 2003/04/10 03:09:55 shane Exp $
"""

from types import StringType

from apelib.core.interfaces import IGateway
from apelib.core.schemas import FieldSchema, RowSequenceSchema


class FSFileData:
    """File data gateway, where data is a string.
    """

    __implements__ = IGateway

    schema = FieldSchema('data', 'string')

    def __init__(self, fs_conn, text=0):
        self.fs_conn = fs_conn
        self.text = text

    def getSchema(self):
        return self.schema

    def load(self, event):
        c = self.fs_conn
        p = event.getKeychain()[-1]
        assert c.readNodeType(p) == 'f'
        state = c.readData(p, as_text=self.text)
        return state, state

    def store(self, event, state):
        if not isinstance(state, StringType):
            raise RuntimeError('Not a string: %s' % repr(state))
        c = self.fs_conn
        p = event.getKeychain()[-1]
        c.writeNodeType(p, 'f')
        c.writeData(p, state, as_text=self.text)
        return state



class FSAutoId:
    """Automatic ID gateway based on the keychain of the item."""

    __implements__ = IGateway

    schema = FieldSchema('id', 'string')

    def getSchema(self):
        return self.schema

    def getIdFrom(self, event):
        path = event.getKeychain()[-1]
        pos = path.rfind('/')
        if pos >= 0:
            return path[pos + 1:]
        else:
            return path

    def load(self, event):
        id = self.getIdFrom(event)
        return id, id

    def store(self, event, state):
        id = self.getIdFrom(event)
        if state != id:
            raise ValueError('Mismatched file ID')
        return id



class FSDirectoryItems:
    """Read/write objects in a filesystem directory."""

    __implements__ = IGateway

    schema = RowSequenceSchema()
    schema.addField('id', 'string', 1)

    def __init__(self, fs_conn):
        self.fs_conn = fs_conn

    def getSchema(self):
        return self.schema

    def load(self, event):
        p = event.getKeychain()[-1]
        c = self.fs_conn
        assert c.readNodeType(p) == 'd'
        names = c.readData(p)
        names.sort()
        res = tuple([(name,) for name in names])
        return res, res

    def store(self, event, state):
        p = event.getKeychain()[-1]
        c = self.fs_conn
        c.writeNodeType(p, 'd')
        state = list(state)
        state.sort()
        names = [row[0] for row in state]
        c.writeData(p, names)
        return tuple(state)


class FSModTime:
    """Reads the modification time of a file."""

    __implements__ = IGateway

    schema = FieldSchema('mtime', 'int')

    def __init__(self, fs_conn):
        self.fs_conn = fs_conn

    def getSchema(self):
        return self.schema

    def load(self, event):
        p = event.getKey()
        state = long(self.fs_conn.getModTime(p))
        return state, None  # Use None as the hash (see store())

    def store(self, event, state):
        # Under normal circumstances, there is no need to change the mod
        # time of a file.  Ignore by returning None as the hash.
        return None