[Checkins] SVN: zope.app.fssync/trunk/ First buildout of the fssync refactoring.

Uwe Oestermeier u.oestermeier at iwm-kmrc.de
Thu Jun 14 03:05:38 EDT 2007


Log message for revision 76675:
  First buildout of the fssync refactoring.

Changed:
  A   zope.app.fssync/trunk/bootstrap.py
  A   zope.app.fssync/trunk/buildout.cfg
  U   zope.app.fssync/trunk/setup.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/browser/__init__.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/browser/configure.zcml
  A   zope.app.fssync/trunk/src/zope/app/fssync/command.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/committer.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/configure.zcml
  U   zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/adapter.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/configure.zcml
  U   zope.app.fssync/trunk/src/zope/app/fssync/dublincore/adapter.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/dublincore/configure.zcml
  U   zope.app.fssync/trunk/src/zope/app/fssync/file/adapter.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/file/configure.zcml
  U   zope.app.fssync/trunk/src/zope/app/fssync/file/tests.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/folder/adapter.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/folder/configure.zcml
  U   zope.app.fssync/trunk/src/zope/app/fssync/folder/tests.py
  A   zope.app.fssync/trunk/src/zope/app/fssync/fsmerge.txt
  U   zope.app.fssync/trunk/src/zope/app/fssync/fspickle.txt
  A   zope.app.fssync/trunk/src/zope/app/fssync/fssite.txt
  A   zope.app.fssync/trunk/src/zope/app/fssync/fssync.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/fssync.txt
  U   zope.app.fssync/trunk/src/zope/app/fssync/ftesting.zcml
  U   zope.app.fssync/trunk/src/zope/app/fssync/ftests.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/interfaces.py
  A   zope.app.fssync/trunk/src/zope/app/fssync/main.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/module/adapter.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/module/configure.zcml
  A   zope.app.fssync/trunk/src/zope/app/fssync/passwd.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/registration.txt
  U   zope.app.fssync/trunk/src/zope/app/fssync/security.txt
  U   zope.app.fssync/trunk/src/zope/app/fssync/syncer.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/testing.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/tests/sampleclass.py
  A   zope.app.fssync/trunk/src/zope/app/fssync/tests/test_command.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/tests/test_committer.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/tests/test_fspickle.py
  A   zope.app.fssync/trunk/src/zope/app/fssync/tests/test_network.py
  A   zope.app.fssync/trunk/src/zope/app/fssync/tests/test_passwd.py
  D   zope.app.fssync/trunk/src/zope/app/fssync/tests/test_registration.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/zptpage/adapter.py
  U   zope.app.fssync/trunk/src/zope/app/fssync/zptpage/configure.zcml

-=-
Added: zope.app.fssync/trunk/bootstrap.py
===================================================================
--- zope.app.fssync/trunk/bootstrap.py	                        (rev 0)
+++ zope.app.fssync/trunk/bootstrap.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,52 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id: bootstrap.py 73102 2007-03-09 08:53:20Z baijum $
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                     ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+    cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+    os.P_WAIT, sys.executable, sys.executable,
+    '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
+    dict(os.environ,
+         PYTHONPATH=
+         ws.find(pkg_resources.Requirement.parse('setuptools')).location
+         ),
+    ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)

Added: zope.app.fssync/trunk/buildout.cfg
===================================================================
--- zope.app.fssync/trunk/buildout.cfg	                        (rev 0)
+++ zope.app.fssync/trunk/buildout.cfg	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,10 @@
+[buildout]
+develop = .
+parts = test
+
+find-links = http://download.zope.org/distribution/
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = zope.app.fssync
+defaults = ['--tests-pattern', '^f?tests$', '-v']

Modified: zope.app.fssync/trunk/setup.py
===================================================================
--- zope.app.fssync/trunk/setup.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/setup.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -6,13 +6,34 @@
       version = '3.4.0b1',
       url='http://svn.zope.org/zope.app.fssync',
       license='ZPL 2.1',
-      description='Zope container',
+      description='Zope app fssync',
       author='Zope Corporation and Contributors',
-      
-	  packages=find_packages('src'),
+      author_email='zope3-dev at zope.org',
+      long_description="Filesystem synchronization for Zope 3 Applications.",
+
+      packages=find_packages('src'),
       package_dir = {'': 'src'},
 
-      namespace_packages=['zope', 'zope.app'],
+      namespace_packages=['zope',],
+      tests_require = ['zope.testing'],
+      install_requires=['setuptools',
+                        'zope.dublincore',
+                        'zope.fssync',
+                        'zope.interface',
+                        'zope.proxy',
+                        'zope.testbrowser',
+                        'zope.traversing',
+                        'zope.xmlpickle',
+                        'zope.app.catalog',
+                        'zope.app.component',
+                        'zope.app.dtmlpage',
+                        'zope.app.file',
+                        'zope.app.folder',
+                        'zope.app.module',
+                        'zope.app.securitypolicy',
+                        'zope.app.zcmlfiles',
+                        'zope.app.zptpage'
+                        ],
       include_package_data = True,
 
       zip_safe = False,

Modified: zope.app.fssync/trunk/src/zope/app/fssync/browser/__init__.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/browser/__init__.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/browser/__init__.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -28,11 +28,15 @@
 
 from zope.fssync.snarf import Snarfer, Unsnarfer
 from zope.fssync.metadata import Metadata
-
 from zope.app.fssync import syncer
-from zope.app.fssync.committer import Committer, Checker
 from zope.app.i18n import ZopeMessageFactory as _
 
+from zope.fssync import task
+from zope.fssync import repository
+
+from zope.app.fssync.syncer import getSynchronizer
+
+
 def snarf_dir(response, dirname):
     """Helper to snarf a directory to the response."""
     
@@ -54,25 +58,23 @@
 
     def show(self):
         """Return the snarfed response."""
-        dirname = tempfile.mktemp()
-        try:
-            os.mkdir(dirname)
-            syncer.toFS(self.context,
-                        getName(self.context) or "root",
-                        dirname)
-            return snarf_dir(self.request.response, dirname)
-        finally:
-            if os.path.isdir(dirname):
-                shutil.rmtree(dirname)
 
-class NewMetadata(Metadata):
-    """Subclass of Metadata that sets the ``added`` flag in all entries."""
+        temp = syncer.toSNARF(self.context, getName(self.context) or "root")
+        temp.seek(0)
+        response = self.request.response
+        response.setStatus(200)
+        response.setHeader('Content-type', 'application/x-snarf')
+        return temp
 
-    def getentry(self, file):
-        entry = Metadata.getentry(self, file)
-        if entry:
-            entry["flag"] = "added"
-        return entry
+# uo: remove
+# class NewMetadata(Metadata):
+#     """Subclass of Metadata that sets the ``added`` flag in all entries."""
+# 
+#     def getentry(self, file):
+#         entry = Metadata.getentry(self, file)
+#         if entry:
+#             entry["flag"] = "added"
+#         return entry
 
 
 class SnarfSubmission(BrowserView):
@@ -86,8 +88,8 @@
         try:
             self.make_tempdir()
             self.set_arguments()
-            self.make_metadata()
-            self.unsnarf_body()
+            #self.make_metadata()
+            #uo: self.unsnarf_body()
             return self.run_submission()
         finally:
             self.remove_tempdir()
@@ -136,12 +138,7 @@
         uns = Unsnarfer(stream)
         uns.unsnarf(self.tempdir)
 
-    def call_committer(self):
-        c = Committer(syncer.getSerializer, self.metadata,
-                      getAnnotations=syncer.getAnnotations)
-        c.synch(self.container, self.name, self.fspath)
 
-
 class SnarfCheckin(SnarfSubmission):
     """View for checking a new sub-tree into Zope.
 
@@ -150,9 +147,10 @@
     """
 
     def run_submission(self):
-        # TODO need to make sure the top-level name doesn't already
-        # exist, or existing site data can get screwed
-        self.call_committer()
+        stream = self.request.bodyStream.getCacheStream()
+        snarf = repository.SnarfRepository(stream)
+        checkin = task.Checkin(getSynchronizer, snarf)
+        checkin.perform(self.container, self.name, self.fspath)
         return ""
 
     def set_arguments(self):
@@ -165,12 +163,9 @@
             src = name
         self.container = self.context
         self.name = name
-        self.fspath = os.path.join(self.tempdir, src)
+        self.fspath = src # os.path.join(self.tempdir, src)
 
-    def make_metadata(self):
-        self.metadata = NewMetadata()
 
-
 class SnarfCommit(SnarfSubmission):
     """View for committing changes to an existing tree.
 
@@ -184,8 +179,10 @@
         if self.errors:
             return self.send_errors()
         else:
-            self.call_committer()
-            self.write_to_filesystem()
+            stream = self.request.bodyStream.getCacheStream()
+            snarf = repository.SnarfRepository(stream)
+            c = task.Commit(getSynchronizer, snarf)
+            c.perform(self.container, self.name, self.fspath)
             return self.send_archive()
 
     def set_arguments(self):
@@ -195,18 +192,20 @@
         if self.container is None and self.name == "":
             # Hack to get loading the root to work
             self.container = getRoot(self.context)
-            self.fspath = os.path.join(self.tempdir, "root")
+            self.fspath = 'root'
         else:
-            self.fspath = os.path.join(self.tempdir, self.name)
+            self.fspath = self.name
 
-    def make_metadata(self):
-        self.metadata = Metadata()
-
     def get_checker(self, raise_on_conflicts=False):
-        return Checker(syncer.getSerializer,
-                       self.metadata,
-                       raise_on_conflicts,
-                       getAnnotations=syncer.getAnnotations)
+    
+        from zope.app.fssync import syncer
+        from zope.fssync import repository
+        
+        stream = self.request.bodyStream.getCacheStream()
+        stream.seek(0)
+        snarf = repository.SnarfRepository(stream)
+        return task.Check(getSynchronizer, snarf,
+                        raise_on_conflicts=raise_on_conflicts)
 
     def call_checker(self):
         if self.get_arg("raise"):
@@ -226,12 +225,10 @@
         self.request.response.setHeader("Content-Type", "text/plain")
         return "\n".join(lines)
 
-    def write_to_filesystem(self):
-        shutil.rmtree(self.tempdir) # Start with clean slate
-        os.mkdir(self.tempdir)
-        syncer.toFS(self.context,
-                    getName(self.context) or "root",
-                    self.tempdir)
-
     def send_archive(self):
-        return snarf_dir(self.request.response, self.tempdir)
+        temp = syncer.toSNARF(self.context, getName(self.context) or "root")
+        temp.seek(0)
+        response = self.request.response
+        response.setStatus(200)
+        response.setHeader('Content-type', 'application/x-snarf')
+        return temp

Modified: zope.app.fssync/trunk/src/zope/app/fssync/browser/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/browser/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/browser/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -8,7 +8,7 @@
   <page
       for="zope.interface.Interface"
       name="toFS.snarf"
-      permission="zope.ManageContent"
+      permission="zope.View"
       class=".SnarfFile"
       attribute="show"
       />

Added: zope.app.fssync/trunk/src/zope/app/fssync/command.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/command.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/command.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,149 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Table-based program command dispatcher.
+
+This dispatcher supports a 'named command' dispatch similar to that
+found in the standard CVS and Subversion client applications.
+
+$Id: command.py 72418 2007-02-07 11:48:58Z oestermeier $
+"""
+import getopt
+import os.path
+import sys
+
+
+from zope.fssync.fsutil import Error
+
+
+class Usage(Error):
+    """Subclass for usage error (command-line syntax).
+
+    You should return an exit status of 2 rather than 1 when catching this.
+    """
+
+
+class Command(object):
+
+    def __init__(self, name=None, usage=None):
+        if name is None:
+            name = os.path.basename(sys.argv[0])
+        self.program = name
+        if usage is None:
+            import __main__
+            usage = __main__.__doc__
+        self.helptext = usage
+        self.command_table = {}
+        self.global_options = []
+        self.local_options = []
+        self.command = None
+
+    def addCommand(self, name, function, short="", long="", aliases=""):
+        names = [name] + aliases.split()
+        cmdinfo = short, long.split(), function
+        for n in names:
+            assert n not in self.command_table
+            self.command_table[n] = cmdinfo
+
+    def main(self, args=None):
+        try:
+            self.realize()
+            self.run()
+
+        except Usage, msg:
+            self.usage(sys.stderr, msg)
+            self.usage(sys.stderr, 'for help use "%(program)s help"')
+            return 2
+
+        except Error, msg:
+            self.usage(sys.stderr, msg)
+            return 1
+
+        except SystemExit:
+            raise
+
+        else:
+            return None
+
+    def realize(self, args=None):
+        if "help" not in self.command_table:
+            self.addCommand("help", self.help)
+        short, long, func = self.command_table["help"]
+        for alias in ("h", "?"):
+            if alias not in self.command_table:
+                self.addCommand(alias, func, short, " ".join(long))
+        if args is None:
+            args = sys.argv[1:]
+        self.global_options, args = self.getopt("global",
+                                                args, "h", ["help"],
+                                                self.helptext)
+        if not args:
+            raise Usage("missing command argument")
+        self.command = args.pop(0)
+        if self.command not in self.command_table:
+            raise Usage("unrecognized command")
+        cmdinfo = self.command_table[self.command]
+        short, long, func = cmdinfo
+        short = "h" + short
+        long = ["help"] + list(long)
+        self.local_options, self.args = self.getopt(self.command,
+                                                    args, short, long,
+                                                    func.__doc__)
+
+    def getopt(self, cmd, args, short, long, helptext):
+        try:
+            opts, args = getopt.getopt(args, short, long)
+        except getopt.error, e:
+            raise Usage("%s option error: %s", cmd, e)
+        for opt, arg in opts:
+            if opt in ("-h", "--help"):
+                self.usage(sys.stdout, helptext)
+                sys.exit()
+        return opts, args
+
+    def run(self):
+        _, _, func = self.command_table[self.command]
+        func(self.local_options, self.args)
+
+    def usage(self, file, text):
+        text = str(text)
+        try:
+            text = text % {"program": self.program}
+        except:
+            pass
+        print >>file, text
+
+    def help(self, opts, args):
+        """%(program)s help [COMMAND ...]
+
+        Display help text.  If COMMAND is specified, help text about
+        each named command is displayed, otherwise general help about
+        using %(program)s is shown.
+        """
+        if not args:
+            self.usage(sys.stdout, self.helptext)
+        else:
+            for cmd in args:
+                if cmd not in self.command_table:
+                    print >>sys.stderr, "unknown command:", cmd
+            first = True
+            for cmd in args:
+                cmdinfo = self.command_table.get(cmd)
+                if cmdinfo is None:
+                    continue
+                _, _, func = cmdinfo
+                if first:
+                    first = False
+                else:
+                    print
+                self.usage(sys.stdout, func.__doc__)

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/committer.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/committer.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/committer.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,445 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2003 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (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.
-#
-##############################################################################
-"""Commit changes from the filesystem.
-
-$Id$
-"""
-__docformat__ = 'restructuredtext'
-
-import os
-
-import zope.component
-from zope.event import notify
-from zope.configuration.name import resolve
-from zope.fssync import fsutil
-from zope.fssync.metadata import Metadata
-from zope.fssync.server.interfaces import IObjectDirectory, IObjectFile
-from zope.xmlpickle import fromxml
-from zope.traversing.api import traverseName, getName
-from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
-from zope.filerepresentation.interfaces import IFileFactory
-from zope.filerepresentation.interfaces import IDirectoryFactory
-
-from zope.app.fssync import fspickle
-from zope.app.container.interfaces import IContainer
-from zope.app.container.contained import contained
-
-class SynchronizationError(Exception):
-    pass
-
-class Checker(object):
-    """Check that the filesystem is consistent with the object database.
-
-    The public API consists of `__init__()`, `check()` and `errors()` only.
-    """
-
-    def __init__(self,
-                 getSerializer,
-                 metadata=None,
-                 raise_on_conflicts=False,
-                 getAnnotations=lambda obj: None,
-                 ):
-        """Constructor.  Optionally pass a metadata database."""
-        if metadata is None:
-            metadata = Metadata()
-        self.metadata = metadata
-        self.raise_on_conflicts = raise_on_conflicts
-        self.conflicts = []
-        self.getSerializer = getSerializer
-        self.getAnnotations = getAnnotations
-
-    def errors(self):
-        """Return a list of errors (conflicts).
-
-        The return value is a list of filesystem pathnames for which
-        a conflict exists.  A conflict usually refers to a file that
-        was modified on the filesystem while the corresponding object
-        was also modified in the database.  Other forms of conflicts
-        are possible, e.g. a file added while an object was added in
-        the corresponding place, or inconsistent labeling of the
-        filesystem objects (e.g. an existing file marked as removed,
-        or a non-existing file marked as added).
-        """
-        return self.conflicts
-
-    def conflict(self, fspath):
-        """Helper to report a conflict.
-
-        Conflicts can be retrieved by calling `errors()`.
-        """
-        if self.raise_on_conflicts:
-            raise SynchronizationError(fspath)
-        if fspath not in self.conflicts:
-            self.conflicts.append(fspath)
-
-    def check(self, container, name, fspath):
-        """Compare an object or object tree from the filesystem.
-
-        If the originals on the filesystem are not uptodate, errors
-        are reported by calling `conflict()`.
-
-        Invalid object names are reported by raising
-        ``SynchronizationError``.
-        """
-        if (os.sep in name or
-            (os.altsep and os.altsep in name) or
-            name == os.curdir or
-            name == os.pardir or
-            name == "." or
-            name == ".." or
-            "/" in name):
-            # This name can't be mapped safely to the filesystem
-            # or it is a magic value for traverseName (".", "..", "/")
-            raise SynchronizationError("invalid separator in name %r" % name)
-
-        if not name:
-            self.check_dir(container, fspath)
-        else:
-            try:
-                traverseName(container, name)
-            except:
-                self.check_new(fspath)
-            else:
-                self.check_old(container, name, fspath)
-
-            # Now check extra and annotations
-            try:
-                obj = traverseName(container, name)
-            except:
-                pass
-            else:
-                adapter = self.getSerializer(obj)
-                extra = adapter.extra()
-                extrapath = fsutil.getextra(fspath)
-                if extra is not None and os.path.exists(extrapath):
-                    self.check_dir(extra, extrapath)
-                ann = self.getAnnotations(obj)
-                annpath = fsutil.getannotations(fspath)
-                if ann is not None and os.path.exists(annpath):
-                    self.check_dir(ann, annpath)
-
-    def check_dir(self, container, fspath):
-        """Helper to check a directory."""
-        adapter = self.getSerializer(container)
-        nameset = {}
-        if IObjectDirectory.providedBy(adapter):
-            for name, obj in adapter.contents():
-                nameset[name] = 1
-        else:
-            for name in container:
-                nameset[name] = 1
-        for name in self.metadata.getnames(fspath):
-            nameset[name] = 1
-        # Sort the list of keys for repeatability
-        names = nameset.keys()
-        names.sort()
-        for name in names:
-            self.check(container, name, os.path.join(fspath, name))
-
-    def check_new(self, fspath):
-        """Helper to check a new object."""
-        entry = self.metadata.getentry(fspath)
-        if entry:
-            if entry.get("flag") != "added":
-                self.conflict(fspath)
-            else:
-                if not os.path.exists(fspath):
-                    self.conflict(fspath)
-            if os.path.isdir(fspath):
-                # Recursively check registered contents
-                for name in self.metadata.getnames(fspath):
-                    self.check_new(os.path.join(fspath, name))
-
-    def check_old(self, container, name, fspath):
-        """Helper to check an existing object."""
-        entry = self.metadata.getentry(fspath)
-        if not entry:
-            self.conflict(fspath)
-        if "conflict" in entry:
-            self.conflict(fspath)
-        flag = entry.get("flag")
-        if flag == "removed":
-            if os.path.exists(fspath):
-                self.conflict(fspath)
-        else:
-            if not os.path.exists(fspath):
-                self.conflict(fspath)
-        obj = traverseName(container, name)
-        adapter = self.getSerializer(obj)
-        if IObjectDirectory.providedBy(adapter):
-            if flag != "removed" or os.path.exists(fspath):
-                self.check_dir(obj, fspath)
-        else:
-            if flag == "added":
-                self.conflict(fspath)
-            oldfspath = fsutil.getoriginal(fspath)
-            if not os.path.exists(oldfspath):
-                self.conflict(fspath)
-            else:
-                olddata = read_file(oldfspath)
-                curdata = adapter.getBody()
-                if curdata != olddata:
-                    self.conflict(fspath)
-
-class Committer(object):
-    """Commit changes from the filesystem to the object database.
-
-    The filesystem's originals must consistent with the object
-    database; this should be checked beforehand by a `Checker` instance
-    with the same arguments.
-
-    The public API consists of `__init__()` and `synch()` only.
-    """
-
-    def __init__(self, getSerializer, metadata=None,
-                 getAnnotations=lambda obj: None):
-        """Constructor.  Optionally pass a metadata database."""
-        self.getSerializer = getSerializer
-        self.getAnnotations = getAnnotations
-        if metadata is None:
-            metadata = Metadata()
-        self.metadata = metadata
-
-    # The Extra and Annotations directories of a zsync need to be
-    # treated specially to allow create_object() to use the the
-    # innermost object 'o' from which the root is reachable via
-    # __parent__ references to perform location-sensitive operations.
-    # This is the only way these objects can perform adapter looks or
-    # resolve persistent references to objects in the tree.  They will
-    # (still) not be able to resolve "parent" references because the
-    # extra and annotation values for object 'o' have no parent.
-    #
-    # This passes around a context object once traversal has entered
-    # the extra and annotation areas of the data area.  This object is
-    # used to provide location if not None; it is only checked by
-    # create_object().
-
-    def synch(self, container, name, fspath, context=None):
-        """Synchronize an object or object tree from the filesystem.
-
-        ``SynchronizationError`` is raised for errors that can't be
-        corrected by a update operation, including invalid object
-        names.
-        """
-        if (os.sep in name or
-            (os.altsep and os.altsep in name) or
-            name == os.curdir or
-            name == os.pardir or
-            name == "." or
-            name == ".." or
-            "/" in name):
-            # This name can't be mapped safely to the filesystem
-            # or it is a magic value for traverseName (".", "..", "/")
-            raise SynchronizationError("invalid separator in name %r" % name)
-        
-        if not name:
-            self.synch_dir(container, fspath, context)
-        else:
-            try:
-                traverseName(container, name)
-            except:
-                self.synch_new(container, name, fspath, context)
-            else:
-                self.synch_old(container, name, fspath, context)
-
-            # Now update extra and annotations
-            try:
-                obj = traverseName(container, name)
-            except:
-                pass
-            else:
-                adapter = self.getSerializer(obj)
-                extra = adapter.extra()
-                extrapath = fsutil.getextra(fspath)
-                if extra is not None and os.path.exists(extrapath):
-                    self.synch_dir(extra, extrapath, obj)
-                ann = self.getAnnotations(obj)
-                annpath = fsutil.getannotations(fspath)
-                if ann is not None and os.path.exists(annpath):
-                    self.synch_dir(ann, annpath, obj)
-
-    def synch_dir(self, container, fspath, context=None):
-        """Helper to synchronize a directory."""
-        adapter = self.getSerializer(container)
-        nameset = {} # name --> absolute path
-        if IObjectDirectory.providedBy(adapter):
-            for name, obj in adapter.contents():
-                nameset[name] = os.path.join(fspath, name)
-        else:
-            # Annotations, Extra
-            for name in container:
-                nameset[name] = os.path.join(fspath, name)
-        for name in self.metadata.getnames(fspath):
-            nameset[name] = os.path.join(fspath, name)
-        # Sort the list of keys for repeatability
-        names_paths = nameset.items()
-        names_paths.sort()
-        subdirs = []
-        # Do the non-directories first.
-        # This ensures that the objects are created before dealing
-        # with Annotations/Extra for those objects.
-        for name, path in names_paths:
-            if os.path.isdir(path):
-                subdirs.append((name, path))
-            else:
-                self.synch(container, name, path, context)
-        # Now do the directories
-        for name, path in subdirs:
-            self.synch(container, name, path, context)
-
-    def synch_new(self, container, name, fspath, context=None):
-        """Helper to synchronize a new object."""
-        entry = self.metadata.getentry(fspath)
-        if entry:
-            self.create_object(container, name, entry, fspath,
-                               context=context)
-            obj = traverseName(container, name)
-            adapter = self.getSerializer(obj)
-            if IObjectDirectory.providedBy(adapter):
-                self.synch_dir(obj, fspath, context)
-
-    def synch_old(self, container, name, fspath, context=None):
-        """Helper to synchronize an existing object."""
-        entry = self.metadata.getentry(fspath)
-        if entry.get("flag") == "removed":
-            delete_item(container, name)
-            return
-        if not entry:
-            # This object was not included on the filesystem; skip it
-            return
-        obj = traverseName(container, name)
-        adapter = self.getSerializer(obj)
-        if IObjectDirectory.providedBy(adapter):
-            self.synch_dir(obj, fspath, context)
-        else:
-            if adapter.typeIdentifier() != entry.get("type"):
-                self.create_object(container, name, entry, fspath,
-                                   replace=True, context=context)
-            else:
-                original_fn = fsutil.getoriginal(fspath)
-                if os.path.exists(original_fn):
-                    olddata = read_file(original_fn)
-                else:
-                    # value appears to exist in the object tree, but
-                    # may have been created as a side effect of an
-                    # addition in the parent; this can easily happen
-                    # in the extra or annotation data for an object
-                    # copied from another using "zsync copy" (for
-                    # example)
-                    olddata = None
-                newdata = read_file(fspath)
-                if newdata != olddata:
-                    if not entry.get("factory"):
-                        # If there's no factory, we can't call setBody()
-                        self.create_object(container, name, entry, fspath,
-                                           True, context=context)
-                        obj = traverseName(container, name)
-                    else:
-                        adapter.setBody(newdata)
-                            
-                    # Now publish an event, but not for annotations or
-                    # extras.  To know which case we have, see if
-                    # getName() works.  *** This is a hack. ***
-                    try:
-                        getName(obj)
-                    except:
-                        pass
-                    else:
-                        notify(ObjectModifiedEvent(obj))
-
-    def create_object(self, container, name, entry, fspath, replace=False,
-                      context=None):
-        """Helper to create an item in a container or mapping."""
-        factory_name = entry.get("factory")
-        if factory_name:
-            # A given factory overrides everything
-            factory = resolve(factory_name)
-            obj = factory()
-            obj = contained(obj, container, name=name)
-            adapter = self.getSerializer(obj)
-            if IObjectFile.providedBy(adapter):
-                data = read_file(fspath)
-                adapter.setBody(data)
-        else:
-            if context is None:
-                location = container
-                parent = container
-            else:
-                location = context
-                parent = None
-
-            # No factory; try using IFileFactory or IDirectoryFactory
-            isuffix = name.rfind(".")
-            if isuffix >= 0:
-                suffix = name[isuffix:]
-            else:
-                suffix = "."
-
-            if os.path.isdir(fspath):
-                iface = IDirectoryFactory
-            else:
-                iface = IFileFactory
-
-            factory = zope.component.queryAdapter(location, iface, suffix)
-            if factory is None:
-                factory = iface(location, None)
-
-            if iface is IDirectoryFactory:
-                if factory:
-                    obj = factory(name)
-                    #obj = removeAllProxies(obj)
-                else:
-                    raise SynchronizationError(
-                        "don't know how to create a directory",
-                        container,
-                        name)
-            else:
-                if factory:
-                    data = read_file(fspath)
-                    obj = factory(name, None, data)
-                   # obj = removeAllProxies(obj)
-                else:
-                    # The file must contain an xml pickle, or we can't load it:
-                    s = read_file(fspath)
-                    s = fromxml(s)
-                    obj = fspickle.loads(s, location, parent)
-
-        set_item(container, name, obj, replace)
-
-# Functions below this point are all helpers and not part of the
-# API offered by this module.  They can be functions because they
-# don't use the metadata database or add to the list of conflicts.
-
-def set_item(container, name, obj, replace=False):
-    """Helper to set an item in a container or mapping."""
-    if IContainer.providedBy(container):
-        if not replace:
-            notify(ObjectCreatedEvent(obj))
-        if replace:
-            del container[name]
-
-    container[name] = obj
-
-def delete_item(container, name):
-    """Helper to delete an item from a container or mapping."""
-    del container[name]
-
-def read_file(fspath):
-    """Helper to read the data from a file."""
-    f = open(fspath, "rb")
-    try:
-        data = f.read()
-    finally:
-        f.close()
-    return data

Modified: zope.app.fssync/trunk/src/zope/app/fssync/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -2,52 +2,115 @@
     xmlns="http://namespaces.zope.org/zope"
     xmlns:fssync="http://namespaces.zope.org/fssync"
     >
-    
-  <adapter
-      for="zope.annotation.interfaces.IAttributeAnnotatable"
-      provides="zope.app.fssync.interfaces.IFSSyncAnnotations"
-      factory="zope.app.fssync.syncer.FSSyncAnnotations"
-      trusted="true"
-      />
- 
-  <class class="zope.app.fssync.syncer.FSSyncAnnotations">
-    
-     <require
-         permission="zope.ManageContent"
-         interface="zope.app.fssync.interfaces.IFSSyncAnnotations" 
-         />
-  
-  </class>
 
-  <!-- The zope.fssync DefaultFileAdapter uses zope.xmlpickle which 
-       is not location aware. Therefore we must register a location aware 
-       zope.app.fssync.fspickle serializer here. -->
+   <!-- We must be carefull with pickled data and annotations since 
+        we cannot rule out a priori that these data contain security 
+        related informations like passwords, security setting etc. 
+        Therefore we use zope.ManageSite for the unnamed default
+        ISynchronizerFactory. -->
 
    <utility
-      component="zope.app.fssync.syncer.LocationAwareDefaultFileAdapter"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
+      component="zope.fssync.synchronizer.DefaultSynchronizer"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       permission="zope.ManageSite"
       />
+
+   <utility
+      component="zope.fssync.synchronizer.DirectorySynchronizer"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
+      name="zope.fssync.synchronizer.Extras"
+      permission="zope.ManageContent"
+      />
+
+   <utility
+      component="zope.fssync.synchronizer.DirectorySynchronizer"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
+      name="zope.fssync.synchronizer.SynchronizableAnnotations"
+      permission="zope.ManageContent"
+      />
+
    
+   <adapter
+      factory="zope.fssync.synchronizer.SynchronizableAnnotations"
+      trusted="true" />
+   
+   <adapter
+      factory="zope.fssync.task.EntryId" />
+
+   <adapter
+      factory="zope.fssync.pickle.XMLPickler" />
+
+   <adapter
+      factory="zope.fssync.pickle.XMLUnpickler" />
+
+   <adapter
+      factory="zope.fssync.pickle.PathPersistentIdGenerator" />
+
+   <adapter
+      factory="zope.fssync.pickle.PathPersistentLoader" />
+
    <class
-      class="zope.app.fssync.syncer.LocationAwareDefaultFileAdapter">
-      
+      class="zope.fssync.synchronizer.Extras">
+
       <require
           permission="zope.ManageContent"
-          interface="zope.fssync.server.interfaces.IObjectFile" />
+          interface="zope.interface.common.mapping.IMapping" />
+
+      <require
+          permission="zope.View"
+          attributes="iteritems" />
+
+   </class>
+
+   <class
+      class="zope.fssync.synchronizer.SynchronizableAnnotations">
+
+      <require
+          permission="zope.ManageContent"
+          interface="zope.fssync.interfaces.ISynchronizableAnnotations" />
           
-   </class>   
-     
+      <require
+          permission="zope.View"
+          attributes="iteritems" />
+
+   </class>
+
+
    <class
-      class="zope.fssync.server.entryadapter.AttrMapping">
-      
+      class="zope.fssync.synchronizer.DefaultSynchronizer">
+
       <require
+          permission="zope.ManageSite"
+          interface="zope.fssync.interfaces.IDefaultSynchronizer" />
+
+   </class>
+
+   <class
+      class="zope.fssync.synchronizer.DirectorySynchronizer">
+
+      <require
+          permission="zope.View"
+          interface="zope.fssync.interfaces.IDirectorySerializer" />
+
+      <require
           permission="zope.ManageContent"
-          interface="zope.fssync.server.interfaces.IAttrMapping" />
+          interface="zope.fssync.interfaces.IDirectoryDeserializer" />
 
    </class>
 
+   <utility
+      factory="zope.fssync.synchronizer.FileGenerator"
+      provides="zope.fssync.interfaces.IFileGenerator"
+      permission="zope.ManageContent"
+      />
 
+   <utility
+      factory="zope.fssync.synchronizer.DirectoryGenerator"
+      provides="zope.fssync.interfaces.IDirectoryGenerator"
+      permission="zope.ManageContent"
+      />
+
+
   <!-- Include browser package -->
 
   <include package=".browser" />

Modified: zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/adapter.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/adapter.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/adapter.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -18,14 +18,14 @@
 __docformat__ = 'restructuredtext'
 
 from zope.interface import implements
-from zope.fssync.server.entryadapter import ObjectEntryAdapter
-from zope.fssync.server.interfaces import IObjectFile
+from zope.fssync import synchronizer
+from zope.fssync import interfaces
 
-class DTMLPageAdapter(ObjectEntryAdapter):
-    implements(IObjectFile)
+class DTMLPageAdapter(synchronizer.FileSynchronizer):
+    implements(interfaces.IFileSynchronizer)
 
-    def getBody(self):
-        return self.context.getSource()
+    def dump(self, writeable):
+        writeable.write(self.context.getSource())
 
-    def setBody(self, data):
-        self.context.setSource(data)
+    def load(self, readable):
+        self.context.setSource(readable.read())

Modified: zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/dtmlpage/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -5,7 +5,7 @@
 
   <utility
       component=".adapter.DTMLPageAdapter"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       name="zope.app.dtmlpage.dtmlpage.DTMLPage"
       permission="zope.ManageContent"
       />
@@ -15,7 +15,7 @@
       
       <require
           permission="zope.ManageCode"
-          interface="zope.fssync.server.interfaces.IObjectFile" />
+          interface="zope.fssync.interfaces.IFileSynchronizer" />
           
    </class>
 

Modified: zope.app.fssync/trunk/src/zope/app/fssync/dublincore/adapter.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/dublincore/adapter.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/dublincore/adapter.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -17,20 +17,13 @@
 """
 __docformat__ = 'restructuredtext'
 
-from zope.interface import implements
-from zope.fssync.server.entryadapter import ObjectEntryAdapter
-from zope.fssync.server.interfaces import IObjectFile
 
-from zope.xmlpickle import dumps
+from zope import interface
+from zope import component
+from zope.fssync import synchronizer
 
-class ZDCAnnotationDataAdapter(ObjectEntryAdapter):
+class ZDCAnnotationDataSynchronizer(synchronizer.DefaultSynchronizer):
+    """A default serializer which can be registered with less strict
+    permissions, since DC metadata are rarely security related.
+    """
 
-    implements(IObjectFile)
-
-    def getBody(self):
-        return dumps(self.context.data)
-
-    def setBody(self, data):
-        data = loads(data)
-        self.context.clear()
-        self.context.update(data)

Modified: zope.app.fssync/trunk/src/zope/app/fssync/dublincore/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/dublincore/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/dublincore/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,21 +1,20 @@
 <configure
    xmlns="http://namespaces.zope.org/zope">
-      
+
    <utility
-      component=".adapter.ZDCAnnotationDataAdapter"
+      component="zope.app.fssync.dublincore.adapter.ZDCAnnotationDataSynchronizer"
       name="zope.dublincore.annotatableadapter.ZDCAnnotationData"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       permission="zope.ManageContent"
       />
 
-  <class
-      class=".adapter.ZDCAnnotationDataAdapter">
-      
+   <class
+      class="zope.app.fssync.dublincore.adapter.ZDCAnnotationDataSynchronizer">
+
       <require
           permission="zope.ManageContent"
-          interface="zope.fssync.server.interfaces.IObjectFile" />
-          
+          interface="zope.fssync.interfaces.IDefaultSynchronizer" />
+
    </class>
 
-   
 </configure>

Modified: zope.app.fssync/trunk/src/zope/app/fssync/file/adapter.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/file/adapter.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/file/adapter.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -17,20 +17,91 @@
 """
 __docformat__ = 'restructuredtext'
 
-from zope.interface import implements
-from zope.fssync.server.entryadapter import ObjectEntryAdapter, AttrMapping
-from zope.fssync.server.interfaces import IObjectFile
 
-class FileAdapter(ObjectEntryAdapter):
-    """ObjectFile adapter for file objects.
+from zope import interface
+from zope import component
+from zope.fssync import synchronizer
+from zope.fssync import interfaces
+
+from zope.app.file import file
+
+
+class FileSynchronizer(synchronizer.Synchronizer):
+    """Adapter to provide a fssync serialization of a file.
+
+    >>> sample = file.File('some data', 'text/plain')
+    >>> sample.getSize()
+    9
+    
+    >>> class Writeable(object):
+    ...     def write(self, data):
+    ...         print data
+
+    >>> synchronizer = FileSynchronizer(sample)
+    >>> synchronizer.dump(Writeable())
+    some data
+    >>> sample.data = 'other data'
+    >>> synchronizer.dump(Writeable())
+    other data
+    
+    >>> sorted(synchronizer.extras().items())
+    [('contentType', 'text/plain')]
+    
+    If we deserialize the file we must use chunking
+    in for large files:
+    
+    >>> class Readable(object):
+    ...     size = file.MAXCHUNKSIZE + 1
+    ...     data = size * 'x'
+    ...     def read(self, bytes=None):
+    ...         result = Readable.data[:bytes]
+    ...         Readable.data = Readable.data[bytes:]
+    ...         return result
+    
+    >>> synchronizer.load(Readable())
+    >>> len(sample.data) == file.MAXCHUNKSIZE + 1
+    True
+    >>> sample.getSize() == file.MAXCHUNKSIZE + 1
+    True
+
     """
-    implements(IObjectFile)
 
-    def getBody(self):
-        return self.context.data
+    interface.implements(interfaces.IFileSynchronizer)
 
-    def setBody(self, data):
-        self.context.data = data
+    def metadata(self):
+        md = super(FileSynchronizer, self).metadata()
+        if not self.context.contentType or \
+            not self.context.contentType.startswith('text'):
+            md['binary'] = 'true'
+        return md
+        
+    def dump(self, writeable):
+        data = self.context._data
+        if isinstance(data, file.FileChunk):
+            chunk = data
+            while chunk:
+                writeable.write(chunk._data)
+                chunk = chunk.next
+        else:
+            writeable.write(self.context.data)
 
-    def extra(self):
-        return AttrMapping(self.context, ('contentType',))
+    def extras(self):
+        return synchronizer.Extras(contentType=self.context.contentType)
+
+    def load(self, readable):
+        chunk = None
+        size = 0
+        data = readable.read(file.MAXCHUNKSIZE)
+        while data:
+            size += len(data)
+            next = file.FileChunk(data)
+            if chunk is None:
+                self.context._data = next
+            else:
+                chunk.next = next
+            chunk = next
+            data = readable.read(file.MAXCHUNKSIZE)
+        self.context._size = size
+        if size < file.MAXCHUNKSIZE:
+            self.context.data = self.context.data
+

Modified: zope.app.fssync/trunk/src/zope/app/fssync/file/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/file/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/file/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -3,28 +3,31 @@
     i18n_domain='zope'
     >
 
-  <utility
-      component=".adapter.FileAdapter"
+   <utility
+      component=".adapter.FileSynchronizer"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       name="zope.app.file.file.File"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
-      permission="zope.ManageContent"
+      permission="zope.View"
       />
 
-  <utility
-      component=".adapter.FileAdapter"
+   <utility
+      component=".adapter.FileSynchronizer"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       name="zope.app.file.image.Image"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
-      permission="zope.ManageContent"
+      permission="zope.View"
       />
       
-  <class
-      class=".adapter.FileAdapter">
+   <class
+      class=".adapter.FileSynchronizer">
       
       <require
+          permission="zope.View"
+          interface="zope.fssync.interfaces.IFileSerializer" />
+
+      <require
           permission="zope.ManageContent"
-          interface="zope.fssync.server.interfaces.IObjectFile" />
+          interface="zope.fssync.interfaces.IFileDeserializer" />
           
    </class>
-
-
+      
 </configure>

Modified: zope.app.fssync/trunk/src/zope/app/fssync/file/tests.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/file/tests.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/file/tests.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -17,42 +17,13 @@
 __docformat__ = "reStructuredText"
 
 import unittest
+from zope.testing import doctest
+from zope.testing import doctestunit
 
-import zope.app.fssync.file.adapter
-
-
-class FauxFile:
-
-    def __init__(self, data, contentType=None):
-        self.data = data
-        self.contentType = contentType
-
-
-class FileAdapterTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.ob = FauxFile("test data", "text/plain")
-        self.adapter = zope.app.fssync.file.adapter.FileAdapter(self.ob)
-
-    def test_extra(self):
-        extra = self.adapter.extra()
-        self.assertEqual(extra["contentType"], "text/plain")
-        extra["contentType"] = "text/x-foo"
-        self.assertEqual(extra["contentType"], "text/x-foo")
-        self.assertEqual(self.ob.contentType, "text/x-foo")
-        self.ob.contentType = "text/x-bar"
-        self.assertEqual(extra["contentType"], "text/x-bar")
-
-    def test_getBody(self):
-        self.assertEqual(self.adapter.getBody(), "test data")
-        self.ob.data = "other data"
-        self.assertEqual(self.adapter.getBody(), "other data")
-
-    def test_setBody(self):
-        self.adapter.setBody("more text")
-        self.assertEqual(self.ob.data, "more text")
-        self.assertEqual(self.adapter.getBody(), "more text")
-
-
 def test_suite():
-    return unittest.makeSuite(FileAdapterTestCase)
+    flags = doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS
+    return doctestunit.DocTestSuite('zope.app.fssync.file.adapter', 
+                                        optionflags=flags)
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')

Modified: zope.app.fssync/trunk/src/zope/app/fssync/folder/adapter.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/folder/adapter.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/folder/adapter.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -17,15 +17,23 @@
 """
 __docformat__ = 'restructuredtext'
 
-from zope.fssync.server.entryadapter import DirectoryAdapter
+
+from zope import interface
+from zope import component
+from zope.fssync import synchronizer
+from zope.fssync import interfaces
+
 from zope.app.component.interfaces import ISite
+from zope.app.component.site import LocalSiteManager
 
 
-class FolderAdapter(DirectoryAdapter):
-    """Adapter to provide an fssync interpretation of folders
+class FolderSynchronizer(synchronizer.DirectorySynchronizer):
+    """Adapter to provide an fssync serialization of folders
     """
-
-    def contents(self):
+ 
+    interface.implements(interfaces.IDirectorySynchronizer)
+    
+    def iteritems(self):
         """Compute a folder listing.
 
         A folder listing is a list of the items in the folder.  It is
@@ -33,31 +41,76 @@
         a folder is a site.
 
         The adapter will take any mapping:
+        
+        >>> adapter = FolderSynchronizer({'x': 1, 'y': 2})
+        >>> len(list(adapter.iteritems()))
+        2
+        
+        If a folder is a site, then we'll get ++etc++site included.
 
-        >>> adapter = FolderAdapter({'x': 1, 'y': 2})
-        >>> contents = adapter.contents()
-        >>> contents.sort()
-        >>> contents
-        [('x', 1), ('y', 2)]
-
-        If a folder is a site, then we'll get ++etc++site included:
-
         >>> import zope.interface
+        >>> class SiteManager(dict):
+        ...     pass
         >>> class Site(dict):
         ...     zope.interface.implements(ISite)
         ...
         ...     def getSiteManager(self):
-        ...         return 'site goes here :)'
+        ...         return SiteManager()
         
-        >>> adapter = FolderAdapter(Site({'x': 1, 'y': 2}))
-        >>> contents = adapter.contents()
-        >>> contents.sort()
-        >>> contents
-        [('++etc++site', 'site goes here :)'), ('x', 1), ('y', 2)]
+        >>> adapter = FolderSynchronizer(Site({'x': 1, 'y': 2}))
+        >>> len(list(adapter.iteritems()))
+        3
 
+
         """
-        result = super(FolderAdapter, self).contents()
+        for key, value in self.context.items():
+            yield (key, value)
+     
         if ISite.providedBy(self.context):
             sm = self.context.getSiteManager()
-            result.append(('++etc++site', sm))
-        return result
+            yield ('++etc++site', sm)
+
+    def __setitem__(self, key, value):
+        """Sets a folder item.
+        
+        Note that the ++etc++site key can also be handled 
+        by the LocalSiteManagerGenerator below.
+
+        >>> from zope.app.folder import Folder
+        >>> folder = Folder()
+        >>> adapter = FolderSynchronizer(folder)
+        >>> adapter[u'test'] = 42
+        >>> folder[u'test']
+        42
+        
+        Since non-unicode names must be 7bit-ascii we try
+        to convert them to unicode first:
+        
+        >>> adapter['t\xc3\xa4st'] = 43
+        >>> adapter[u't\xe4st']
+        43
+        
+        """
+        if key == '++etc++site':
+            self.context.setSiteManager(value)
+        else:
+            if not isinstance(key, unicode):
+                key = unicode(key, encoding='utf-8')
+            self.context[key] = value
+
+
+class LocalSiteManagerGenerator(object):
+    """A generator for a LocalSiteManager.
+    
+    A LocalSiteManager has a special __init__ method.
+    Therefore we must provide a create method.
+    """
+    interface.implements(interfaces.IObjectGenerator)
+    
+    def create(self, context, name):
+        """Creates a sitemanager in the given context."""
+        sm = LocalSiteManager(context)
+        context.setSiteManager(sm)
+        return sm
+
+

Modified: zope.app.fssync/trunk/src/zope/app/fssync/folder/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/folder/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/folder/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -2,21 +2,32 @@
     xmlns="http://namespaces.zope.org/zope"
     i18n_domain="zope"
     >
-
-  <utility
-      component=".adapter.FolderAdapter"
+  
+   <utility
+      component=".adapter.FolderSynchronizer"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       name="zope.app.folder.folder.Folder"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
-      permission="zope.ManageContent"
+      permission="zope.View"
       />
       
-  <class
-      class=".adapter.FolderAdapter">
+   <utility
+      factory=".adapter.LocalSiteManagerGenerator"
+      provides="zope.fssync.interfaces.IObjectGenerator"
+      name="zope.app.component.site.LocalSiteManager"
+      permission="zope.ManageSite"
+      />
+
+   <class
+      class=".adapter.FolderSynchronizer">
       
       <require
+          permission="zope.View"
+          interface="zope.fssync.interfaces.IDirectorySerializer" />
+ 
+      <require
           permission="zope.ManageContent"
-          interface="zope.fssync.server.interfaces.IContentDirectory" />
-          
+          interface="zope.fssync.interfaces.IDirectoryDeserializer" />
+
    </class>
 
 </configure>

Modified: zope.app.fssync/trunk/src/zope/app/fssync/folder/tests.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/folder/tests.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/folder/tests.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -17,12 +17,14 @@
 """
 
 import unittest
+from zope.testing import doctest
+from zope.testing import doctestunit
 
-from zope.testing.doctestunit import DocTestSuite
 
-
 def test_suite():
-    return DocTestSuite('zope.app.fssync.folder.adapter')
+    flags = doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS
+    return doctestunit.DocTestSuite('zope.app.fssync.folder.adapter',
+                                        optionflags=flags)
 
 if __name__=='__main__':
     unittest.main(defaultTest='test_suite')

Added: zope.app.fssync/trunk/src/zope/app/fssync/fsmerge.txt
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/fsmerge.txt	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/fsmerge.txt	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,138 @@
+Merge support
+=============
+
+Note: This is a platform specific test that doesn't run on Windows since
+the merge utility uses the diff3 command.
+
+Let's start with some basic infrastructure on the server side. We assume 
+that a folder with some content already exists:
+
+    >>> root = getRootFolder()
+    >>> from zope.app.folder import Folder
+    >>> from zope.lifecycleevent import ObjectCreatedEvent
+    >>> serverfolder = root[u'test'] = Folder()
+    >>> from zope.app.file import File
+    >>> serverfile1 = File('A\nB\nC', 'text/plain')
+    >>> zope.event.notify(ObjectCreatedEvent(serverfile1))
+    >>> serverfolder[u'file1.txt'] = serverfile1
+
+On the client side we need a directory for the initial checkout:
+
+    >>> os.path.exists(checkoutdir)
+    True
+
+We perform an initial checkout:
+
+    >>> from zope.app.fssync.fssync import FSSync
+    >>> rooturl = 'http://globalmgr:globalmgrpw@localhost/test'
+    >>> network = TestNetwork(handle_errors=True)
+    >>> zsync = FSSync(network=network, rooturl=rooturl)
+
+Now we can call the checkout method:
+
+    >>> zsync.checkout(checkoutdir)
+    N .../test/
+    U .../test/file1.txt
+    N .../test/@@Zope/Extra/file1.txt/
+    U .../test/@@Zope/Extra/file1.txt/contentType
+    N .../test/@@Zope/Annotations/file1.txt/
+    U .../test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
+    N .../@@Zope/Annotations/test/
+    U .../@@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
+    All done.
+
+
+    >>> localdir = os.path.join(checkoutdir, 'test')
+    >>> localfile1 = os.path.join(localdir, 'file1.txt')
+
+
+Merging
+-------
+
+Changes on the server are merged (via diff3) into the local files.
+
+    >>> fp = open(localfile1, 'w')
+    >>> fp.write('A\nB\nC\nD')
+    >>> fp.close()
+    
+    >>> serverfile1.data = 'A\nX\nB\nC\nD'
+    >>> zsync.update(localdir)
+    M .../test/file1.txt
+    All done.
+    
+    >>> print file(localfile1).read()
+    A
+    X
+    B
+    C
+    D
+
+Binary data are handled differently. It's up to the serializer
+to correctly classify the data as binary. Let's start with a 
+incorrect assignment of the content type:
+
+    >>> serverfile2 = File('01\n01\n01', 'text/plain')
+    >>> zope.event.notify(ObjectCreatedEvent(serverfile2))
+    >>> serverfolder[u'file2.binary'] = serverfile2
+    >>> zsync.update(localdir)
+    U .../test/file2.binary
+    N .../test/@@Zope/Extra/file2.binary/
+    U .../test/@@Zope/Extra/file2.binary/contentType
+    N .../test/@@Zope/Annotations/file2.binary/
+    U .../test/@@Zope/Annotations/file2.binary/zope.app.dublincore.ZopeDublinCore
+    U .../@@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
+    All done.
+    
+    >>> localfile2 = os.path.join(localdir, 'file2.binary')
+    >>> fp = open(localfile2, 'wb')
+    >>> fp.write('01\n01\n01\n01')
+    >>> fp.close()
+    >>> zsync.status(localdir)
+    / .../test/
+    ...
+    M .../test/file2.binary
+    
+    >>> serverfile2.data = '11\n01\n01'
+    >>> zsync.update(localdir)
+    M .../test/file2.binary
+    All done.
+    
+Since the serializer declared these data as non-binary, the changes
+are merged:
+
+    >>> print file(localfile2).read()
+    11
+    01
+    01
+    01
+    
+We commit the merged data to keep server and local file again in sync:
+
+    >>> zsync.commit(localdir)
+    U .../test/file2.binary
+    U .../test/@@Zope/Annotations/file2.binary/zope.app.dublincore.ZopeDublinCore
+    All done.
+
+Now we correct the mistaken classification of the binary file:
+    
+    >>> serverfile2.contentType = 'binary/unknown'
+    >>> zsync.update(localdir)
+    U .../test/@@Zope/Extra/file2.binary/contentType
+    All done.
+
+Again we modify the local file:
+
+    >>> serverfile2.data = '01\n01\n01'
+    >>> fp = open(localfile2, 'wb')
+    >>> fp.write('01\n01\n01\n01\n01')
+    >>> fp.close()
+    >>> zsync.status(localdir)
+    / .../test/
+    ...
+    M .../test/file2.binary
+    
+Now we get a conflict, since binaries cannot be merged:
+
+    >>> zsync.update(localdir)
+    C .../test/file2.binary
+    All done.

Modified: zope.app.fssync/trunk/src/zope/app/fssync/fspickle.txt
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/fspickle.txt	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/fspickle.txt	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,11 +1,11 @@
 Pickle Issues
 =============
 
-Pickling large and complex objects can be tricky, since the boundaries of arbitrary objects
-are often complex and difficult to define.
+Pickling large and complex objects can be tricky, since the boundaries 
+of arbitrary objects are often complex and difficult to define.
 
-To illustrate possible problems it is sufficient to look at a single folder with a 
-local site manager:
+To illustrate possible problems it is sufficient to look at a 
+single folder with a local site manager:
 
 >>> root = getRootFolder()
 >>> from zope.app.folder import Folder
@@ -32,15 +32,17 @@
 >>> before < after
 True
 
-Bang! The pickle size increased. That means that the pickle of folder1 contains
-additional data probably from folder2 or at least it's parent. How complex and unpredictable
+Bang! The pickle size increased. That means that the pickle of 
+folder1 contains additional data probably from folder2 or at 
+least it's parent. How complex and unpredictable
 the situation is shows the following relation:
 
 >>> len(dumps(folder1)) > len(dumps(root))
 True
 
-Let's try the same with a location aware pickler. The location aware pickler saves persistent
-references to locatable objects and thus stops pickling when a pointer leads to an object outside
+Let's try the same with a location aware pickler. The location
+aware pickler saves persistent references to locatable objects and 
+thus stops pickling when a pointer leads to an object outside
 the tree of sublocations:
 
 >>> from zope.app.fssync.fspickle import dumps

Added: zope.app.fssync/trunk/src/zope/app/fssync/fssite.txt
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/fssite.txt	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/fssite.txt	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,169 @@
+Synchronize a Site
+==================
+
+Pickling large and complex objects can be tricky, since the boundaries 
+of arbitrary objects are often complex and difficult to define.
+
+To illustrate possible problems it is sufficient to look at a site, i.e.
+single folder with a local site manager:
+
+    >>> from zope import component
+    >>> root = getRootFolder()
+    >>> from zope.app.folder import Folder
+    >>> from zope.app.component.site import LocalSiteManager
+    >>> folder1 = root[u'folder1'] = Folder()
+    >>> sm1 = LocalSiteManager(folder1)
+    >>> folder1.setSiteManager(sm1)
+
+    >>> from zope.xmlpickle import dumps
+    >>> before = len(dumps(folder1))
+
+Now we add a second folder:
+
+    >>> folder2 = root[u'folder2'] = Folder()
+    >>> sm2 = LocalSiteManager(folder2)
+    >>> folder2.setSiteManager(sm2)
+
+Let's look at the pickle of our first folder again:
+
+    >>> after = len(dumps(folder1))
+
+Then Let's compare.
+
+    >>> before < after
+    True
+
+Bang! The pickle size increased. That means that the pickle of folder1 
+contains additional data probably from folder2 or at least it's parent.
+How complex and unpredictable the situation can be shows the 
+following relation:
+
+    >>> len(dumps(folder1)) > len(dumps(root))
+    True
+
+Let's try the same with a location aware pickler. A location aware 
+pickler saves persistent references to locatable objects and thus 
+stops pickling when a pointer leads to an object outside the tree:
+
+    >>> from zope.fssync.pickle import XMLPickler
+    >>> before = len(XMLPickler(folder1).dumps())
+
+    >>> folder3 = root[u'folder3'] = Folder()
+    >>> sm = LocalSiteManager(folder3)
+    >>> folder3.setSiteManager(sm)
+
+    >>> after = len(XMLPickler(folder1).dumps())
+    >>> after == before
+    True
+
+Now the relation of the overall sizes looks plausible:
+
+    >>> len(XMLPickler(root).dumps()) > len(XMLPickler(folder1).dumps())
+    True
+
+A site can be a rather complex structure which contains many special
+objects like catalogs and caches besides the content objects. 
+Here we build a small sample:
+
+    >>> root = getRootFolder()
+    >>> from zope.lifecycleevent import ObjectCreatedEvent
+    >>> serverfolder = root[u'test'] = Folder()
+    >>> zope.event.notify(ObjectCreatedEvent(serverfolder))
+    >>> sm = LocalSiteManager(serverfolder)
+    >>> zope.event.notify(ObjectCreatedEvent(sm))
+    >>> serverfolder.setSiteManager(sm)
+    
+    >>> from zope.app.component.interfaces import ISite
+    >>> ISite.providedBy(serverfolder)
+    True
+    
+    >>> from zope.app.file import File
+    >>> serverfile1 = File('A text file', 'text/plain')
+    >>> serverfile2 = File('Another text file', 'text/plain')
+    >>> zope.event.notify(ObjectCreatedEvent(serverfile1))
+    >>> zope.event.notify(ObjectCreatedEvent(serverfile2))
+    >>> serverfolder[u'file1.txt'] = serverfile1
+    >>> serverfolder[u'file2.txt'] = serverfile2
+
+On the client side we need a directory for the initial checkout:
+
+    >>> os.path.exists(checkoutdir)
+    True
+    
+    >>> from zope.app.fssync.fssync import FSSync
+    >>> rooturl = 'http://globalmgr:globalmgrpw@localhost/test'
+    >>> zsync = FSSync(network=TestNetwork(), rooturl=rooturl)
+
+Now we can call the checkout method:
+
+    >>> zsync.checkout(checkoutdir)
+    N .../test/
+    U .../test/++etc++site
+    U .../test/file1.txt
+    N .../test/@@Zope/Extra/file1.txt/
+    U .../test/@@Zope/Extra/file1.txt/contentType
+    N .../test/@@Zope/Annotations/file1.txt/
+    U .../test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
+    U .../test/file2.txt
+    N .../test/@@Zope/Extra/file2.txt/
+    U .../test/@@Zope/Extra/file2.txt/contentType
+    N .../test/@@Zope/Annotations/file2.txt/
+    U .../test/@@Zope/Annotations/file2.txt/zope.app.dublincore.ZopeDublinCore
+    N .../@@Zope/Annotations/test/
+    U .../@@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
+    All done.
+
+
+If we want to import (or reimport) the data into a content space 
+we can use the checkin command:
+
+    >>> localdir = os.path.join(checkoutdir, 'test')
+    >>> del root[u'test']
+    >>> zsync.checkin(localdir)
+    >>> serverfolder = root[u'test']
+    >>> sorted(serverfolder.keys())
+    [u'file1.txt', u'file2.txt']
+    >>> serverfolder[u'file1.txt'].data
+    'A text file'
+    >>> serverfolder.getSiteManager()
+    <LocalSiteManager ++etc++site>
+
+In the example above the whole site manager is pickled into a single
+file. If we want to break the site manager up into parts we can provide
+a directory serializer for the site manager:
+
+    >>> from zope.fssync import synchronizer
+    >>> from zope.fssync import interfaces
+    >>> component.provideUtility(
+    ...     synchronizer.DirectorySynchronizer, 
+    ...     interfaces.ISynchronizerFactory,
+    ...     name='zope.app.component.site.LocalSiteManager')
+    
+    >>> zsync.checkout(checkoutdir)
+    D .../test/++etc++site
+    N .../test/++etc++site/
+    U .../test/++etc++site/default
+    ...
+    
+Note that the merger removed the old ++etc++site file and replaced it 
+with a directory. Again we try to reimport the site manager, now from 
+a directory:
+
+    >>> del root[u'test']
+    >>> zsync.checkin(localdir)
+
+    >>> serverfolder = root[u'test']
+    >>> ISite.providedBy(serverfolder)
+    True
+    >>> sorted(serverfolder.keys())
+    [u'file1.txt', u'file2.txt']
+    >>> serverfolder[u'file1.txt'].data
+    'A text file'
+    >>> sm = serverfolder.getSiteManager()
+    >>> sm
+    <LocalSiteManager ++etc++site>
+    >>> sorted(sm.keys())
+    [u'default']
+
+
+

Added: zope.app.fssync/trunk/src/zope/app/fssync/fssync.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/fssync.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/fssync.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,759 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+# 
+##############################################################################
+"""Highest-level classes to support filesystem synchronization:
+
+class Network -- handle network connection
+class FSSync  -- implement various commands (checkout, commit etc.)
+
+$Id: fssync.py 73012 2007-03-06 17:16:44Z oestermeier $
+"""
+
+import os
+import sys
+import shutil
+import urllib
+import filecmp
+import htmllib
+import httplib
+import tempfile
+import urlparse
+import formatter
+
+from StringIO import StringIO
+
+from os.path import exists, isfile, isdir
+from os.path import dirname, basename, split, join
+from os.path import realpath, normcase, normpath
+
+from zope.fssync.metadata import Metadata, dump_entries
+from zope.fssync.fsmerger import FSMerger
+from zope.fssync.fsutil import Error
+from zope.fssync import fsutil
+from zope.fssync.snarf import Snarfer, Unsnarfer
+from zope.app.fssync.passwd import PasswordManager
+
+if sys.platform[:3].lower() == "win":
+    DEV_NULL = r".\nul"
+else:
+    DEV_NULL = "/dev/null"
+
+
+class Network(PasswordManager):
+
+    """Handle network communication.
+
+    This class has various methods for managing the root url (which is
+    stored in a file @@Zope/Root) and has a method to send an HTTP(S)
+    request to the root URL, expecting a snarf file back (that's all the
+    application needs).
+
+    Public instance variables:
+
+    rooturl     -- full root url, e.g. 'http://user:passwd@host:port/path'
+    roottype    -- 'http' or 'https'
+    user_passwd -- 'user:passwd'
+    host_port   -- 'host:port'
+    rootpath    -- '/path'
+    """
+
+    def __init__(self, rooturl=None):
+        """Constructor.  Optionally pass the root url."""
+        super(Network, self).__init__()
+        self.setrooturl(rooturl)
+
+    def loadrooturl(self, target):
+        """Load the root url for the given target.
+
+        This calls findrooturl() to find the root url for the target,
+        and then calls setrooturl() to set it.  If self.findrooturl()
+        can't find a root url, Error() is raised.
+        """
+        rooturl = self.findrooturl(target)
+        if not rooturl:
+            raise Error("can't find root url for target", target)
+        self.setrooturl(rooturl)
+
+    def saverooturl(self, target):
+        """Save the root url in the target's @@Zope directory.
+
+        This writes the file <target>/@@Zope/Root; the directory
+        <target>/@@Zope must already exist.
+        """
+        if self.rooturl:
+            dir = join(target, "@@Zope")
+            if not exists(dir):
+                os.mkdir(dir)
+            fn = join(dir, "Root")
+            self.writefile(self.rooturl + "\n",
+                           fn)
+
+    def findrooturl(self, target):
+        """Find the root url for the given target.
+
+        This looks in <target>/@@Zope/Root, and then in the
+        corresponding place for target's parent, and then further
+        ancestors, until the filesystem root is reached.
+
+        If no root url is found, return None.
+        """
+        dir = realpath(target)
+        while dir:
+            rootfile = join(dir, "@@Zope", "Root")
+            try:
+                data = self.readfile(rootfile)
+            except IOError:
+                pass
+            else:
+                data = data.strip()
+                if data:
+                    return data
+            head, tail = split(dir)
+            if tail in fsutil.unwanted:
+                break
+            dir = head
+        return None
+
+    def setrooturl(self, rooturl):
+        """Set the root url.
+
+        If the argument is None or empty, self.rooturl and all derived
+        instance variables are set to None.  Otherwise, self.rooturl
+        is set to the argument the broken-down root url is stored in
+        the other instance variables.
+        """
+        if not rooturl:
+            rooturl = roottype = rootpath = user_passwd = host_port = None
+        else:
+            roottype, rest = urllib.splittype(rooturl)
+            if roottype not in ("http", "https"):
+                raise Error("root url must be 'http' or 'https'", rooturl)
+            if roottype == "https" and not hasattr(httplib, "HTTPS"):
+                raise Error("https not supported by this Python build")
+            netloc, rootpath = urllib.splithost(rest)
+            if not rootpath:
+                rootpath = "/"
+            user_passwd, host_port = urllib.splituser(netloc)
+
+        self.rooturl = rooturl
+        self.roottype = roottype
+        self.rootpath = rootpath
+        self.user_passwd = user_passwd
+        self.host_port = host_port
+
+    def readfile(self, file, mode="r"):
+        # Internal helper to read a file
+        f = open(file, mode)
+        try:
+            return f.read()
+        finally:
+            f.close()
+
+    def writefile(self, data, file, mode="w"):
+        # Internal helper to write a file
+        f = open(file, mode)
+        try:
+            f.write(data)
+        finally:
+            f.close()
+
+    def httpreq(self, path, view, datasource=None,
+                content_type="application/x-snarf",
+                expected_type="application/x-snarf"):
+        """Issue an HTTP or HTTPS request.
+
+        The request parameters are taken from the root url, except
+        that the requested path is constructed by concatenating the
+        path and view arguments.
+
+        If the optional 'datasource' argument is not None, it should
+        be a callable with a stream argument which, when called,
+        writes data to the stream.  In this case, a POST request is
+        issued, and the content-type header is set to the
+        'content_type' argument, defaulting to 'application/x-snarf'.
+        Otherwise (if datasource is None), a GET request is issued and
+        no input document is sent.
+
+        If the request succeeds and returns a document whose
+        content-type is 'application/x-snarf', the return value is a tuple
+        (fp, headers) where fp is a non-seekable stream from which the
+        return document can be read, and headers is a case-insensitive
+        mapping giving the response headers.
+
+        If the request returns an HTTP error, the Error exception is
+        raised.  If it returns success (error code 200) but the
+        content-type of the result is not 'application/x-snarf', the Error
+        exception is also raised.  In these error cases, if the result
+        document's content-type is a text type (anything starting with
+        'text/'), the text of the result document is included in the
+        Error exception object; in the specific case that the type is
+        text/html, HTML formatting is removed using a primitive
+        formatter.
+
+        TODO: This doesn't support proxies or redirect responses.
+        """
+        # TODO: Don't change the case of the header names; httplib might
+        # not treat them in a properly case-insensitive manner.
+        assert self.rooturl
+        if not path.endswith("/"):
+            path += "/"
+        path = urllib.quote(path)  
+        path += view
+        if self.roottype == "https":
+            conn = httplib.HTTPSConnection(self.host_port)
+        else:
+            conn = httplib.HTTPConnection(self.host_port)
+         
+        if datasource is None:
+            conn.putrequest("GET", path)
+        else:
+            conn.putrequest("POST", path)
+            conn.putheader("Content-type", content_type)
+            #conn.putheader("Transfer-encoding", "chunked")
+            #XXX Chunking works only with the zserver. Twisted responds with
+            #    HTTP error 400 (Bad Request); error document:
+            #      Excess 4 bytes sent in chunk transfer mode
+            #We use a buffer as workaround and compute the Content-Length in
+            #advance
+            tmp = tempfile.TemporaryFile('w+b')
+            datasource(tmp)
+            conn.putheader("Content-Length", str(tmp.tell()))
+            
+        if self.user_passwd:
+            if ":" not in self.user_passwd:
+                auth = self.getToken(self.roottype,
+                                     self.host_port,
+                                     self.user_passwd)
+            else:
+                auth = self.createToken(self.user_passwd)
+            conn.putheader('Authorization', 'Basic %s' % auth)
+        conn.putheader("Host", self.host_port)
+        conn.putheader("Connection", "close")
+        conn.endheaders()
+        if datasource is not None:
+            #XXX If chunking works again, replace the following lines with 
+            # datasource(PretendStream(conn))
+            # conn.send("0\r\n\r\n")
+            tmp.seek(0)
+            data = tmp.read(1<<16)
+            while data:
+                conn.send(data)
+                data = tmp.read(1<<16)
+            tmp.close()   
+                
+        response = conn.getresponse()
+        if response.status != 200:
+            raise Error("HTTP error %s (%s); error document:\n%s",
+                        response.status, response.reason,
+                        self.slurptext(response.fp, response.msg))
+        elif expected_type and response.msg["Content-type"] != expected_type:
+            raise Error(self.slurptext(response.fp, response.msg))
+        else:
+            return response.fp, response.msg
+
+    def slurptext(self, fp, headers):
+        """Helper to read the result document.
+
+        This removes the formatting from a text/html document; returns
+        other text documents as-is; and for non-text documents,
+        returns just a string giving the content-type.
+        """
+        # Too often, we just get HTTP response code 200 (OK), with an
+        # HTML document that explains what went wrong.
+        data = fp.read()
+        ctype = headers.get("Content-type", 'unknown')
+        if ctype == "text/html":
+            s = StringIO()
+            f = formatter.AbstractFormatter(formatter.DumbWriter(s))
+            p = htmllib.HTMLParser(f)
+            p.feed(data)
+            p.close()
+            return s.getvalue().strip()
+        if ctype.startswith("text/"):
+            return data.strip()
+        return "Content-type: %s" % ctype
+
+class PretendStream(object):
+
+    """Helper class to turn writes into chunked sends."""
+
+    def __init__(self, conn):
+        self.conn = conn
+
+    def write(self, s):
+        self.conn.send("%x\r\n" % len(s))
+        self.conn.send(s)
+
+class DataSource(object):
+
+    """Helper class to provide a data source for httpreq."""
+
+    def __init__(self, head, tail):
+        self.head = head
+        self.tail = tail
+
+    def __call__(self, f):
+        snf = Snarfer(f)
+        snf.add(join(self.head, self.tail), self.tail)
+        snf.addtree(join(self.head, "@@Zope"), "@@Zope/")
+
+class FSSync(object):
+
+    def __init__(self, metadata=None, network=None, rooturl=None):
+        if metadata is None:
+            metadata = Metadata()
+        if network is None:
+            network = Network()
+        self.metadata = metadata
+        self.network = network
+        self.network.setrooturl(rooturl)
+        self.fsmerger = FSMerger(self.metadata, self.reporter)
+
+    def login(self, url=None, user=None):
+        scheme, host_port, user = self.get_login_info(url, user)
+        token = self.network.getToken(scheme, host_port, user)
+        self.network.addToken(scheme, host_port, user, token)
+
+    def logout(self, url=None, user=None):
+        scheme, host_port, user = self.get_login_info(url, user)
+        if scheme:
+            ok = self.network.removeToken(scheme, host_port, user)
+        else:
+            # remove both, if present
+            ok1 = self.network.removeToken("http", host_port, user)
+            ok2 = self.network.removeToken("https", host_port, user)
+            ok = ok1 or ok2
+        if not ok:
+            raise Error("matching login info not found")
+
+    def get_login_info(self, url, user):
+        if url:
+            parts = urlparse.urlsplit(url)
+            scheme = parts[0]
+            host_port = parts[1]
+            if not (scheme and host_port):
+                raise Error(
+                    "URLs must include both protocol (http or https)"
+                    " and host information")
+            if "@" in host_port:
+                user_passwd, host_port = host_port.split("@", 1)
+                if not user:
+                    if ":" in user_passwd:
+                        user = user_passwd.split(":", 1)[0]
+                    else:
+                        user = user_passwd
+        else:
+            self.network.loadrooturl(os.curdir)
+            scheme = self.network.roottype
+            host_port = self.network.host_port
+            if not user:
+                upw = self.network.user_passwd
+                if ":" in upw:
+                    user = upw.split(":", 1)[0]
+                else:
+                    user = upw
+        if not user:
+            user = raw_input("Username: ").strip()
+            if not user:
+                raise Error("username cannot be empty")
+        return scheme, host_port, user
+
+    def checkout(self, target):
+        rootpath = self.network.rootpath
+        if not rootpath:
+            raise Error("root url not set")
+        if self.metadata.getentry(target):
+            raise Error("target already registered", target)
+        if exists(target) and not isdir(target):
+            raise Error("target should be a directory", target)
+        fsutil.ensuredir(target)
+        i = rootpath.rfind("/")
+        tail = rootpath[i+1:]
+        tail = tail or "root"
+        fp, headers = self.network.httpreq(rootpath, "@@toFS.snarf")
+        try:
+            self.merge_snarffile(fp, target, tail)
+        finally:
+            fp.close()
+        self.network.saverooturl(target)
+
+    def multiple(self, args, method, *more):
+        if not args:
+            args = [os.curdir]
+        for target in args:
+            if self.metadata.getentry(target):
+                method(target, *more)
+            else:
+                names = self.metadata.getnames(target)
+                if not names:
+                    # just raise Error directly?
+                    method(target, *more) # Will raise an exception
+                else:
+                    for name in names:
+                        method(join(target, name), *more)
+
+    def commit(self, target, note="fssync_commit", raise_on_conflicts=False):
+        entry = self.metadata.getentry(target)
+        if not entry:
+            raise Error("nothing known about", target)
+        self.network.loadrooturl(target)
+        path = entry["id"]
+        view = "@@fromFS.snarf?note=%s" % urllib.quote(note)
+        if raise_on_conflicts:
+            view += "&raise=1"
+        head, tail = split(realpath(target))
+        data = DataSource(head, tail)
+        fp, headers = self.network.httpreq(path, view, data)
+        try:
+            self.merge_snarffile(fp, head, tail)
+        finally:
+            fp.close()
+
+    def checkin(self, target, note="fssync_checkin"):
+        rootpath = self.network.rootpath
+        if not rootpath:
+            raise Error("root url not set")
+        if rootpath == "/":
+            raise Error("root url should name an inferior object")
+        i = rootpath.rfind("/")
+        path, name = rootpath[:i], rootpath[i+1:]
+        if not path:
+            path = "/"
+        if not name:
+            raise Error("root url should not end in '/'")
+        entry = self.metadata.getentry(target)
+        if not entry:
+            raise Error("nothing known about", target)
+        qnote = urllib.quote(note)
+        qname = urllib.quote(name)
+        head, tail = split(realpath(target))
+        qsrc = urllib.quote(tail)
+        view = "@@checkin.snarf?note=%s&name=%s&src=%s" % (qnote, qname, qsrc)
+        data = DataSource(head, tail)
+        fp, headers = self.network.httpreq(path, view, data,
+                                           expected_type=None)
+        message = self.network.slurptext(fp, headers)
+        if message:
+            print message
+
+    def update(self, target):
+        entry = self.metadata.getentry(target)
+        if not entry:
+            raise Error("nothing known about", target)
+        self.network.loadrooturl(target)
+        head, tail = fsutil.split(target)
+        path = entry["id"]
+        fp, headers = self.network.httpreq(path, "@@toFS.snarf")
+        try:
+            self.merge_snarffile(fp, head, tail)
+        finally:
+            fp.close()
+
+    def merge_snarffile(self, fp, localdir, tail):
+        uns = Unsnarfer(fp)
+        tmpdir = tempfile.mktemp()
+        try:
+            os.mkdir(tmpdir)
+            uns.unsnarf(tmpdir)
+            self.fsmerger.merge(join(localdir, tail), join(tmpdir, tail))
+            self.metadata.flush()
+            print "All done."
+        finally:
+            if isdir(tmpdir):
+                shutil.rmtree(tmpdir)
+
+    def resolve(self, target):
+        entry = self.metadata.getentry(target)
+        if "conflict" in entry:
+            del entry["conflict"]
+            self.metadata.flush()
+        elif isdir(target):
+            self.dirresolve(target)
+
+    def dirresolve(self, target):
+        assert isdir(target)
+        names = self.metadata.getnames(target)
+        for name in names:
+            t = join(target, name)
+            e = self.metadata.getentry(t)
+            if e:
+                self.resolve(t)
+
+    def revert(self, target):
+        entry = self.metadata.getentry(target)
+        if not entry:
+            raise Error("nothing known about", target)
+        flag = entry.get("flag")
+        orig = fsutil.getoriginal(target)
+        if flag == "added":
+            entry.clear()
+        elif flag == "removed":
+            if exists(orig):
+                shutil.copyfile(orig, target)
+            del entry["flag"]
+        elif "conflict" in entry:
+            if exists(orig):
+                shutil.copyfile(orig, target)
+            del entry["conflict"]
+        elif isfile(orig):
+            if filecmp.cmp(target, orig, shallow=False):
+                return
+            shutil.copyfile(orig, target)
+        elif isdir(target):
+            # TODO: how to recurse?
+            self.dirrevert(target)
+        self.metadata.flush()
+        if os.path.isdir(target):
+            target = join(target, "")
+        self.reporter("Reverted " + target)
+
+    def dirrevert(self, target):
+        assert isdir(target)
+        names = self.metadata.getnames(target)
+        for name in names:
+            t = join(target, name)
+            e = self.metadata.getentry(t)
+            if e:
+                self.revert(t)
+
+    def reporter(self, msg):
+        if msg[0] not in "/*":
+            print msg.encode('utf-8') # uo: is encode needed here?
+    
+    def diff(self, target, mode=1, diffopts="", need_original=True):
+        assert mode == 1, "modes 2 and 3 are not yet supported"
+        entry = self.metadata.getentry(target)
+        if not entry:
+            raise Error("diff target '%s' doesn't exist", target)
+        if "flag" in entry and need_original:
+            raise Error("diff target '%s' is added or deleted", target)
+        if isdir(target):
+            self.dirdiff(target, mode, diffopts, need_original)
+            return
+        orig = fsutil.getoriginal(target)
+        if not isfile(target):
+            if entry.get("flag") == "removed":
+                target = DEV_NULL
+            else:
+                raise Error("diff target '%s' is file nor directory", target)
+        have_original = True
+        if not isfile(orig):
+            if entry.get("flag") != "added":
+                raise Error("can't find original for diff target '%s'", target)
+            have_original = False
+            orig = DEV_NULL
+        if have_original and filecmp.cmp(target, orig, shallow=False):
+            return
+        print "Index:", target
+        sys.stdout.flush()
+        cmd = ("diff %s %s %s" % (diffopts, quote(orig), quote(target)))
+        os.system(cmd)
+
+    def dirdiff(self, target, mode=1, diffopts="", need_original=True):
+        assert isdir(target)
+        names = self.metadata.getnames(target)
+        for name in names:
+            t = join(target, name)
+            e = self.metadata.getentry(t)
+            if e and (("flag" not in e) or not need_original):
+                self.diff(t, mode, diffopts, need_original)
+
+    def add(self, path, type=None, factory=None):
+        entry = self.basicadd(path, type, factory)
+        head, tail = fsutil.split(path)
+        pentry = self.metadata.getentry(head)
+        if not pentry:
+            raise Error("can't add '%s': its parent is not registered", path)
+        if "id" not in pentry:
+            raise Error("can't add '%s': its parent has no 'id' key", path)
+        zpath = fsutil.encode(pentry["id"])
+        if not zpath.endswith("/"):
+            zpath += "/"
+        zpath += tail
+        entry["id"] = zpath
+        self.metadata.flush()
+        if isdir(path):
+            # Force Entries.xml to exist, even if it wouldn't normally
+            zopedir = join(path, "@@Zope")
+            efile = join(zopedir, "Entries.xml")
+            if not exists(efile):
+                if not exists(zopedir):
+                    os.makedirs(zopedir)
+                    self.network.writefile(dump_entries({}), efile)
+            print "A", join(path, "")
+        else:
+            print "A", path
+
+    def basicadd(self, path, type=None, factory=None):
+        if not exists(path):
+            raise Error("nothing known about '%s'", path)
+        entry = self.metadata.getentry(path)
+        if entry:
+            raise Error("object '%s' is already registered", path)
+        entry["flag"] = "added"
+        if type:
+            entry["type"] = type
+        if factory:
+            entry["factory"] = factory
+        return entry
+
+    def copy(self, src, dst=None, children=True):
+        if not exists(src):
+            raise Error("%s does not exist" % src)
+        dst = dst or ''
+        if (not dst) or isdir(dst):
+            target_dir = dst
+            target_name = basename(os.path.abspath(src))
+        else:
+            target_dir, target_name = os.path.split(dst)
+            if target_dir:
+                if not exists(target_dir):
+                    raise Error("destination directory does not exist: %r"
+                                % target_dir)
+                if not isdir(target_dir):
+                    import errno
+                    err = IOError(errno.ENOTDIR, "Not a directory", target_dir)
+                    raise Error(str(err))
+        if not self.metadata.getentry(target_dir):
+            raise Error("nothing known about '%s'" % target_dir)
+        srcentry = self.metadata.getentry(src)
+        from zope.fssync import copier
+        if srcentry:
+            # already known to fssync; we need to deal with metadata,
+            # Extra, and Annotations
+            copier = copier.ObjectCopier(self)
+        else:
+            copier = copier.FileCopier(self)
+        copier.copy(src, join(target_dir, target_name), children)
+
+    def mkdir(self, path):
+        dir, name = split(path)
+        if dir:
+            if not exists(dir):
+                raise Error("directory %r does not exist" % dir)
+            if not isdir(dir):
+                raise Error("%r is not a directory" % dir)
+        else:
+            dir = os.curdir
+        entry = self.metadata.getentry(dir)
+        if not entry:
+            raise Error("know nothing about container for %r" % path)
+        if exists(path):
+            raise Error("%r already exists" % path)
+        os.mkdir(path)
+        self.add(path)
+
+    def remove(self, path):
+        if exists(path):
+            raise Error("'%s' still exists", path)
+        entry = self.metadata.getentry(path)
+        if not entry:
+            raise Error("nothing known about '%s'", path)
+        zpath = entry.get("id")
+        if not zpath:
+            raise Error("can't remote '%s': its zope path is unknown", path)
+        if entry.get("flag") == "added":
+            entry.clear()
+        else:
+            entry["flag"] = "removed"
+        self.metadata.flush()
+        print "R", path
+
+    def status(self, target, descend_only=False):
+        entry = self.metadata.getentry(target)
+        flag = entry.get("flag")
+        if isfile(target):
+            if not entry:
+                if not self.fsmerger.ignore(target):
+                    print "?", target
+            elif flag == "added":
+                print "A", target
+            elif flag == "removed":
+                print "R(reborn)", target
+            else:
+                original = fsutil.getoriginal(target)
+                if isfile(original):
+                    if filecmp.cmp(target, original):
+                        print "=", target
+                    else:
+                        print "M", target
+                else:
+                    print "M(lost-original)", target
+        elif isdir(target):
+            pname = join(target, "")
+            if not entry:
+                if not descend_only and not self.fsmerger.ignore(target):
+                    print "?", pname
+            elif flag == "added":
+                print "A", pname
+            elif flag == "removed":
+                print "R(reborn)", pname
+            else:
+                print "/", pname
+            if entry:
+                # Recurse down the directory
+                namesdir = {}
+                for name in os.listdir(target):
+                    ncname = normcase(name)
+                    if ncname != fsutil.nczope:
+                        namesdir[ncname] = name
+                for name in self.metadata.getnames(target):
+                    ncname = normcase(name)
+                    namesdir[ncname] = name
+                ncnames = namesdir.keys()
+                ncnames.sort()
+                for ncname in ncnames:
+                    self.status(join(target, namesdir[ncname]))
+        elif exists(target):
+            if not entry:
+                if not self.fsmerger.ignore(target):
+                    print "?", target
+            elif flag:
+                print flag[0].upper() + "(unrecognized)", target
+            else:
+                print "M(unrecognized)", target
+        else:
+            if not entry:
+                print "nonexistent", target
+            elif flag == "removed":
+                print "R", target
+            elif flag == "added":
+                print "A(lost)", target
+            else:
+                print "lost", target
+        annotations = fsutil.getannotations(target)
+        if isdir(annotations):
+            self.status(annotations, True)
+        extra = fsutil.getextra(target)
+        if isdir(extra):
+            self.status(extra, True)
+
+def quote(s):
+    """Helper to put quotes around arguments passed to shell if necessary."""
+    if os.name == "posix":
+        meta = "\\\"'*?[&|()<>`#$; \t\n"
+    else:
+        meta = " "
+    needquotes = False
+    for c in meta:
+        if c in s:
+            needquotes = True
+            break
+    if needquotes:
+        if os.name == "posix":
+            # use ' to quote, replace ' by '"'"'
+            s = "'" + s.replace("'", "'\"'\"'") + "'"
+        else:
+            # (Windows) use " to quote, replace " by ""
+            s = '"' + s.replace('"', '""') + '"'
+    return s

Modified: zope.app.fssync/trunk/src/zope/app/fssync/fssync.txt
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/fssync.txt	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/fssync.txt	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,101 +1,184 @@
 Using FSSync
 ============
 
-The fssync package allows users to download objects from a Zope3 server to the local disk, edit the
-objects offline and synchronize the modifications with the server later on.
+The fssync package allows users to download objects from a Zope3 server
+to the local disk, edit the objects offline and synchronize the 
+modifications with the server later on.
 
-Let's start with some basic infrastructure on the server side. We assume that a folder
-with some content already exists:
+Let's start with some basic infrastructure on the server side. We 
+assume that a folder with some content already exists:
 
->>> root = getRootFolder()
->>> from zope.app.folder import Folder
->>> serverfolder = root[u'test'] = Folder()
->>> from zope.app.file import File
->>> serverfile1 = serverfolder[u'file1.txt'] = File('A text file', 'plain/text')
->>> serverfile2 = serverfolder[u'file2.txt'] = File('Another text file', 'plain/text')
+    >>> root = getRootFolder()
+    >>> from zope.app.folder import Folder
+    >>> from zope.lifecycleevent import ObjectCreatedEvent
+    >>> from zope.lifecycleevent import ObjectModifiedEvent
+    >>> serverfolder = root[u'test'] = Folder()
+    >>> from zope.app.file import File
+    >>> serverfile1 = File('A text file', 'text/plain')
+    >>> serverfile2 = File('Another text file', 'text/plain')
+    >>> zope.event.notify(ObjectCreatedEvent(serverfile1))
+    >>> zope.event.notify(ObjectCreatedEvent(serverfile2))
+    >>> serverfolder[u'file1.txt'] = serverfile1
+    >>> serverfolder[u'file2.txt'] = serverfile2
 
 On the client side we need a directory for the initial checkout:
 
->>> os.path.exists(checkoutdir)
-True
+    >>> os.path.exists(checkoutdir)
+    True
 
 
 Serialization format
 --------------------
 
-On the server side everything must be registered in a manner that we are allowed to access the 
-serialized data (see registration.txt for details). The serialized content is delivered in 
-a Zope3 specific SNARF archive.
-SNARF (Simple New ARchival Format) is a very simple format that basically puts one file 
-after another. Here we download it by calling the @@toFS.snarf view to give an impression of the
-internal structure of this format:
+On the server side everything must be registered in a manner that we 
+are allowed to access the serialized data (see registration.txt for 
+details). The serialized content is delivered in a Zope3 specific 
+SNARF archive.
+SNARF (Simple New ARchival Format) is a very simple format that 
+basically puts one file after another. Here we download it by calling 
+the @@toFS.snarf view to give an impression of the internal structure 
+of this format:
 
->>> headers = {'Authorization': 'Basic globalmgr:globalmgrpw'}
->>> conn = PublisherConnection('localhost')
->>> conn.request('GET', 'test/@@toFS.snarf', headers=headers)
->>> print conn.getresponse().read(-1)
-264 @@Zope/Annotations/test/@@Zope/Entries.xml
-<?xml version='1.0' encoding='utf-8'?>
-<entries>
-  <entry name="zope.app.dublincore.ZopeDublinCore"
-         type="zope.dublincore.annotatableadapter.ZDCAnnotationData"
-         factory="zope.dublincore.annotatableadapter.ZDCAnnotationData"
-         />
-</entries>
-...
-87 test/@@Zope/Extra/file1.txt/contentType
-<?xml version="1.0" encoding="utf-8" ?>
-<pickle> <string>plain/text</string> </pickle>
-132 test/@@Zope/Extra/file2.txt/@@Zope/Entries.xml
-<?xml version='1.0' encoding='utf-8'?>
-<entries>
-  <entry name="contentType"
-         type="__builtin__.str"
-         />
-</entries>
-87 test/@@Zope/Extra/file2.txt/contentType
-<?xml version="1.0" encoding="utf-8" ?>
-<pickle> <string>plain/text</string> </pickle>
-11 test/file1.txt
-A text file17 test/file2.txt
-Another text file
+    >>> headers = {'Authorization':'Basic globalmgr:globalmgrpw'}
+    >>> conn = PublisherConnection('localhost')
+    >>> conn.request('GET', 'test/@@toFS.snarf', headers=headers)
+    >>> print conn.getresponse().read(-1)
+    00000227 @@Zope/Annotations/test/@@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="zope.app.dublincore.ZopeDublinCore"
+             keytype="__builtin__.str"
+             type="zope.dublincore.annotatableadapter.ZDCAnnotationData"
+             />
+    </entries>
+    00000768 @@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle>
+    ...
+    </pickle>
+    00000247 @@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="test"
+             keytype="__builtin__.unicode"
+             type="zope.app.folder.folder.Folder"
+             factory="zope.app.folder.folder.Folder"
+             id="/test"
+             />
+    </entries>
+    00000440 test/@@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="file1.txt"
+             keytype="__builtin__.unicode"
+             type="zope.app.file.file.File"
+             factory="zope.app.file.file.File"
+             id="/test/file1.txt"
+             />
+      <entry name="file2.txt"
+             keytype="__builtin__.unicode"
+             type="zope.app.file.file.File"
+             factory="zope.app.file.file.File"
+             id="/test/file2.txt"
+             />
+    </entries>
+    00000227 test/@@Zope/Annotations/file1.txt/@@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="zope.app.dublincore.ZopeDublinCore"
+             keytype="__builtin__.str"
+             type="zope.dublincore.annotatableadapter.ZDCAnnotationData"
+             />
+    </entries>
+    00001046 test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle>
+    ...
+    </pickle>
+    00000227 test/@@Zope/Annotations/file2.txt/@@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="zope.app.dublincore.ZopeDublinCore"
+             keytype="__builtin__.str"
+             type="zope.dublincore.annotatableadapter.ZDCAnnotationData"
+             />
+    </entries>
+    00001046 test/@@Zope/Annotations/file2.txt/zope.app.dublincore.ZopeDublinCore
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle>
+    ...
+    </pickle>
+    00000167 test/@@Zope/Extra/file1.txt/@@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="contentType"
+             keytype="__builtin__.str"
+             type="__builtin__.str"
+             />
+    </entries>
+    00000087 test/@@Zope/Extra/file1.txt/contentType
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle> <string>text/plain</string> </pickle>
+    00000167 test/@@Zope/Extra/file2.txt/@@Zope/Entries.xml
+    <?xml version='1.0' encoding='utf-8'?>
+    <entries>
+      <entry name="contentType"
+             keytype="__builtin__.str"
+             type="__builtin__.str"
+             />
+    </entries>
+    00000087 test/@@Zope/Extra/file2.txt/contentType
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle> <string>text/plain</string> </pickle>
+    00000011 test/file1.txt
+    A text file00000017 test/file2.txt
+    Another text file
 
-Note that the main content is directly serialized whereas extra attributes and metadata are
-pickled in an XML format. These various aspects are saved on the local disk in numerous files.
 
+Note that the main content is directly serialized whereas extra 
+attributes and metadata are pickled in an XML format.
 
+
 Initial Checkout
 ----------------
 
-We perform an initial checkout to see what happens. We mimic the command line syntax 
+We perform an initial checkout to see what happens. We mimic the 
+command line syntax 
 
     zsync checkout http://user:password@host:port/path targetdir
 
-by using the corresponding FSSync command object. (The zsync script can be found in Zope3's
-topmost bin directory. Type ``zsync help`` for a list of available commands). 
-The FSSync object must be initialised with all relevant
-connection data and for the sake of this doctest with a special network instance.
+by using the corresponding FSSync command object. (The zsync script 
+can be found in this directory. Type ``zsync help`` for a list of 
+available commands). 
+The FSSync object must be initialized with all relevant connection data.
+For the sake of this doctest we need also a special network instance:
 
->>> from zope.fssync.fssync import FSSync
->>> rooturl = 'http://globalmgr:globalmgrpw@localhost/test'
->>> zsync = FSSync(network=TestNetwork(), rooturl=rooturl)
+    >>> from zope.app.fssync.fssync import FSSync
+    >>> rooturl = 'http://globalmgr:globalmgrpw@localhost/test'
+    >>> network = TestNetwork(handle_errors=False)
+    >>> zsync = FSSync(network=network, rooturl=rooturl)
 
 Now we can call the checkout method:
 
->>> zsync.checkout(checkoutdir)
-N .../test/
-U .../test/file1.txt
-N .../test/@@Zope/Extra/file1.txt/
-U .../test/@@Zope/Extra/file1.txt/contentType
-U .../test/file2.txt
-N .../test/@@Zope/Extra/file2.txt/
-U .../test/@@Zope/Extra/file2.txt/contentType
-N .../@@Zope/Annotations/test/
-U .../@@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
-All done.
+    >>> zsync.checkout(checkoutdir)
+    N .../test/
+    U .../test/file1.txt
+    N .../test/@@Zope/Extra/file1.txt/
+    U .../test/@@Zope/Extra/file1.txt/contentType
+    N .../test/@@Zope/Annotations/file1.txt/
+    U .../test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
+    U .../test/file2.txt
+    N .../test/@@Zope/Extra/file2.txt/
+    U .../test/@@Zope/Extra/file2.txt/contentType
+    N .../test/@@Zope/Annotations/file2.txt/
+    U .../test/@@Zope/Annotations/file2.txt/zope.app.dublincore.ZopeDublinCore
+    N .../@@Zope/Annotations/test/
+    U .../@@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
+    All done.
 
-The printout shows all new directories and updated files. As you can see, the file content is 
-directly mapped onto the filesystem whereas extra data and metadata are stored in special @@Zope 
+The printout shows all new directories and updated files. As you can see, 
+the file content is directly mapped onto the filesystem whereas extra data 
+and metadata are stored in special @@Zope 
 directories.
 
 Local Modifications
@@ -103,97 +186,184 @@
 
 Now we can edit the content and metadata on the local filesystem.
 
->>> localdir = os.path.join(checkoutdir, 'test')
->>> localfile1 = os.path.join(localdir, 'file1.txt')
->>> fp = open(localfile1, 'w')
->>> fp.write('A modified text file')
->>> fp.close()
+    >>> localdir = os.path.join(checkoutdir, 'test')
+    >>> localfile1 = os.path.join(localdir, 'file1.txt')
+    >>> fp = open(localfile1, 'w')
+    >>> fp.write('A modified text file')
+    >>> fp.close()
 
 The status command lists all local modifications:
 
->>> zsync.status(localdir)
-/ .../test/
-M .../test/file1.txt
-= .../test/file2.txt
+    >>> zsync.status(localdir)
+    / .../test/
+    M .../test/file1.txt
+    = .../test/file2.txt
 
-If we want to add a file to the repository we must update the local list of entries by calling the
-add command explicitely:
+If we want to add a file to the repository we must update the local 
+list of entries by calling the add command explicitely:
 
->>> newlocalfile = os.path.join(localdir, 'file3.txt')
->>> fp = open(newlocalfile, 'w')
->>> fp.write('A new local text file')
->>> fp.close()
+    >>> newlocalfile = os.path.join(localdir, 'file3.txt')
+    >>> fp = open(newlocalfile, 'w')
+    >>> fp.write('A new local text file')
+    >>> fp.close()
+    
+    >>> zsync.add(newlocalfile)
+    A .../test/file3.txt
+    
+    >>> zsync.status(localdir)
+    / .../test/
+    M .../test/file1.txt
+    = .../test/file2.txt
+    A .../test/file3.txt
 
->>> zsync.add(newlocalfile)
-A .../test/file3.txt
 
->>> zsync.status(localdir)
-/ .../test/
-M .../test/file1.txt
-= .../test/file2.txt
-A .../test/file3.txt
-
-
 Commiting Modifications
 -----------------------
 
-Before we commit our local modifications we should check whether our local repository is still
-up to date. Let's say that by a coincidence someone else edited the same file on the server:
+Before we commit our local modifications we should check whether our
+local repository is still up to date. Let's say that by a coincidence 
+someone else edited the same file on the server:
 
->>> serverfile1.data = 'Ooops'
->>> zsync.commit(localdir)
-Traceback (most recent call last):
-...
-Error: Up-to-date check failed:
-test/file1.txt
+    >>> serverfile1.data = 'Ooops'
+    >>> zope.event.notify(ObjectModifiedEvent(serverfile1))
+    
+    >>> zsync.commit(localdir)
+    Traceback (most recent call last):
+    ...
+    Error: Up-to-date check failed:
+    test/file1.txt
+    test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
 
-We must update the local files and resolve all conflicts before we can proceed:
+We must update the local files and resolve all conflicts before 
+we can proceed:
 
->>> zsync.update(localdir)
-C .../test/file1.txt
-A .../test/file3.txt
-All done.
+    >>> zsync.update(localdir)
+    C .../test/file1.txt
+    U .../test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
+    A .../test/file3.txt
+    All done.
 
 The conflicts are marked in a diff3 manner:
 
->>> print open(localfile1).read()
-<<<<<<< .../test/file1.txt
-A modified text file=======
-Ooops>>>>>>> .../test/file1.txt
-<BLANKLINE>
+    >>> print open(localfile1).read()
+    <<<<<<< .../test/file1.txt
+    A modified text file=======
+    Ooops>>>>>>> .../test/file1.txt
+    <BLANKLINE>
 
-Resolving the conflict is easy:
+We need to resolve the conflict:
 
->>> fp = open(localfile1, 'w')
->>> fp.write('Oops, a modified text file.')
->>> fp.close()
->>> zsync.resolve(localfile1)
+    >>> fp = open(localfile1, 'w')
+    >>> fp.write('A resolved conflict')
+    >>> fp.close()
+    >>> zsync.resolve(localfile1)
 
-Now we can commit our work:
+Now we can commit our work and have a look at the resulting events:
 
->>> zsync.commit(localdir)
-U .../test/file1.txt
-N .../test/@@Zope/Annotations/file1.txt/
-U .../test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
-U .../test/file3.txt
-N .../test/@@Zope/Extra/file3.txt/
-U .../test/@@Zope/Extra/file3.txt/contentType
-N .../test/@@Zope/Annotations/file3.txt/
-U .../test/@@Zope/Annotations/file3.txt/zope.app.dublincore.ZopeDublinCore
-U .../@@Zope/Annotations/test/zope.app.dublincore.ZopeDublinCore
-All done.
+    >>> def traceEvent(event):
+    ...     print event.__class__.__name__, 
+    ...     print getattr(event.object, '__name__', ''),
+    ...     descriptions = getattr(event, 'descriptions', None)
+    ...     if descriptions is not None:
+    ...         for desc in descriptions:
+    ...             print desc.__class__.__name__,
+    ...     print ''
+    
+    >>> zope.event.subscribers.append(traceEvent)
+    >>> import time
+    >>> time.sleep(0.1)
+    >>> zsync.commit(localdir)
+    BeforeTraverseEvent None 
+    BeforeTraverseEvent test 
+    BeforeTraverseEvent fromFS.snarf 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent file1.txt ObjectSynchronized 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectCreatedEvent  
+    ObjectAddedEvent file3.txt 
+    ContainerModifiedEvent test 
+    ObjectModifiedEvent  ObjectSynchronized 
+    EndRequestEvent run 
+    U .../test/file1.txt
+    U .../test/@@Zope/Annotations/file1.txt/zope.app.dublincore.ZopeDublinCore
+    U .../test/file3.txt
+    N .../test/@@Zope/Extra/file3.txt/
+    U .../test/@@Zope/Extra/file3.txt/contentType
+    N .../test/@@Zope/Annotations/file3.txt/
+    U .../test/@@Zope/Annotations/file3.txt/zope.app.dublincore.ZopeDublinCore
+    All done.
 
+    
 Let's check whether the server objects have been updated accordingly:
 
->>> serverfile1.data
-'Oops, a modified text file.'
->>> u'file3.txt' in serverfolder.keys()
-True
+    >>> serverfile1.data
+    'A resolved conflict'
+    >>> serverfile1.getSize() == len(serverfile1.data)
+    True
+    >>> u'file3.txt' in serverfolder.keys()
+    True
 
 
+Checkin
+-------
 
+If we want to import (or reimport) the data into a content space 
+we can use the checkin command:
 
+    >>> del root[u'test']
+    ObjectRemovedEvent test
+    ContainerModifiedEvent None
 
+    >>> zsync.checkin(localdir)
+    BeforeTraverseEvent None 
+    BeforeTraverseEvent checkin.snarf 
+    ObjectCreatedEvent None 
+    ObjectAddedEvent test 
+    ContainerModifiedEvent None 
+    ObjectCreatedEvent  
+    ObjectAddedEvent file1.txt 
+    ContainerModifiedEvent test 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent file1.txt Attributes 
+    ObjectCreatedEvent  
+    ObjectAddedEvent file2.txt 
+    ContainerModifiedEvent test 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent file2.txt Attributes 
+    ObjectCreatedEvent  
+    ObjectAddedEvent file3.txt 
+    ContainerModifiedEvent test 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent  ObjectSynchronized 
+    ObjectModifiedEvent file3.txt Attributes 
+    ObjectModifiedEvent  ObjectSynchronized 
+    EndRequestEvent run 
 
+    >>> serverfolder = root[u'test']
+    >>> sorted(serverfolder.keys())
+    [u'file1.txt', u'file2.txt', u'file3.txt']
+    >>> serverfile1 = serverfolder[u'file1.txt']
+    >>> print serverfile1.data
+    A resolved conflict
+    >>> serverfile1.getSize() == len(serverfile1.data)
+    True
+    
+We need to make sure that the top-level name doesn't already exist, 
+or existing data can get screwed:
 
+    >>> zsync.checkin(localdir)
+    Traceback (most recent call last):
+    ...
+    SynchronizationError: object already exists 'test'
 
+
+
+Clean up
+--------
+
+    >>> zope.event.subscribers.remove(traceEvent)
+

Modified: zope.app.fssync/trunk/src/zope/app/fssync/ftesting.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/ftesting.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/ftesting.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -30,18 +30,18 @@
 
   <!-- Permissions for the readonly example in security.txt -->
   
-  <permission id="zope.fssync.Read" title="Read access" />
-  <permission id="zope.fssync.Write" title="Write access" />
-    
-  <class class="zope.app.fssync.file.adapter.FileAdapter">
+  <permission id="zope.fssync.Serialize" title="Access to serializers" />
+  <permission id="zope.fssync.Deserialize" title="Access to deserializers" />
+      
+  <class class="zope.app.fssync.file.adapter.FileSynchronizer">
        
      <require
-       permission="zope.fssync.Write"
-       interface="zope.fssync.server.interfaces.IWriteObjectFile" />
+       permission="zope.fssync.Deserialize"
+       interface="zope.fssync.interfaces.IFileDeserializer" />
        
   </class>
-
-  
+ 
+   
   <!-- Replace the following directive if you don't want public access -->
   <grant permission="zope.View"
                   role="zope.Anonymous" />
@@ -90,8 +90,6 @@
   <grant role="zope.Manager" principal="zope.globalmgr" />
   <grant role="zope.Member" principal="zope.cm" />
   <grant permission="zope.ManageContent" principal="zope.cm" />
-  <grant permission="zope.ManageContent" principal="zope.rom" />
-  <grant permission="zope.fssync.Read" principal="zope.rom" />
-
+  <grant permission="zope.View" principal="zope.rom" />
   
 </configure>

Modified: zope.app.fssync/trunk/src/zope/app/fssync/ftests.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/ftests.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/ftests.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -13,110 +13,45 @@
 ##############################################################################
 """Functional fssync tests
 
-$Id: test_fssync.py 40495 2005-12-02 17:51:22Z efge $
+$Id: ftests.py 40495 2005-12-02 17:51:22Z efge $
 """
-
 import re
 import unittest
 import os
 import shutil
 import time
 import tempfile
+import sys
 import zope
-from cStringIO import StringIO
 from zope.testing import renormalizing, doctest, module, doctestunit
 from zope.app.testing import functional
 from zope.testbrowser.testing import PublisherConnection
 
-from zope.fssync import fssync
-from zope.fssync import fsutil
-
 from zope.app.fssync.testing import AppFSSyncLayer
+from zope.app.fssync.testing import TestNetwork
 
-
 checkoutdir = tempfile.mkdtemp(prefix='checkoutdir')
 
+checker = renormalizing.RENormalizing([
+    (re.compile(r"\\"), r"/"),
+    ])
 
-class TestNetwork(fssync.Network):
-    """A specialization which uses a PublisherConnection suitable
-       for functional doctests.
-    """
-
-    def httpreq(self, path, view, datasource=None,
-                content_type="application/x-snarf",
-                expected_type="application/x-snarf"):
-        """Issue an request. This is a overwritten version of the original
-        Network.httpreq method that uses a TestConnection as a replacement for
-        httplib connections.
-        """
-        assert self.rooturl
-        if not path.endswith("/"):
-            path += "/"
-        path += view
-        conn = PublisherConnection(self.host_port)
-        headers = {}
-        if datasource is None:
-            method = 'GET'
-        else:
-            method = 'POST'
-            headers["Content-type"] = content_type
-            stream = StringIO()
-            datasource(stream)
-            headers["Content-Length"] = str(stream.tell())
-
-        if self.user_passwd:
-            if ":" not in self.user_passwd:
-                auth = self.getToken(self.roottype,
-                                     self.host_port,
-                                     self.user_passwd)
-            else:
-                auth = self.createToken(self.user_passwd)
-            headers['Authorization'] = 'Basic %s' % auth
-        headers['Host'] = self.host_port
-        headers['Connection'] = 'close'
-
-        data = None
-        if datasource is not None:
-            data = stream.getvalue()
-
-        conn.request(method, path, body=data, headers=headers)
-        response = conn.getresponse()
-
-        if response.status != 200:
-            raise fsutil.Error("HTTP error %s (%s); error document:\n%s",
-                        response.status, response.reason,
-                        self.slurptext(response.content_as_file, response.msg))
-        elif expected_type and response.msg["Content-type"] != expected_type:
-            raise fsutil.Error(
-                self.slurptext(response.content_as_file, response.msg))
-        else:
-            return response.content_as_file, response.msg
-
-
 def setUp(test):
     module.setUp(test, 'zope.app.fssync.fssync_txt')
     if not os.path.exists(checkoutdir):
         os.mkdir(checkoutdir)
 
-
 def tearDown(test):
     module.tearDown(test, 'zope.app.fssync.fssync_txt')
     shutil.rmtree(checkoutdir)
 
-
 def cleanUpTree(dir):
     if os.path.exists(dir):
         shutil.rmtree(dir)
     os.mkdir(dir)
 
-
-checker = renormalizing.RENormalizing([
-    (re.compile(r"\\"), r"/"),
-    ])
-
-
 def test_suite():
-
+    
     globs = {'os': os,
             'zope':zope,
             'pprint': doctestunit.pprint,
@@ -128,15 +63,22 @@
 
     suite = unittest.TestSuite()
 
-    for file in 'fspickle.txt', 'fssync.txt', 'security.txt':
+    for file in 'fssync.txt', 'security.txt', 'fssite.txt':
         test = functional.FunctionalDocFileSuite(file,
-             setUp=setUp, tearDown=tearDown, globs=globs, checker=checker,
-             optionflags=doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS)
+                    setUp=setUp, tearDown=tearDown,
+                    globs=globs, checker=checker,
+                    optionflags=doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS)
         test.layer = AppFSSyncLayer
         suite.addTest(test)
+        
+    if sys.platform != 'win32':
+        test = functional.FunctionalDocFileSuite('fsmerge.txt',
+                    setUp=setUp, tearDown=tearDown,
+                    globs=globs, checker=checker,
+                    optionflags=doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS)
+        test.layer = AppFSSyncLayer
+        suite.addTest(test)
 
     return suite
 
-
-if __name__ == '__main__':
-    unittest.main()
+if __name__ == '__main__': unittest.main()

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/interfaces.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/interfaces.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/interfaces.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,52 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (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.
-#
-##############################################################################
-"""Interfaces for filesystem synchronization.
-
-$Id$
-"""
-__docformat__ = 'restructuredtext'
-
-from zope import interface
-from zope import component
-from zope import annotation
-
-class IFSSyncAnnotations(annotation.interfaces.IAnnotations):
-    """Access to synchronizable annotations."""
-    
-    def __iter__():
-        """Iterates over the package-unique keys."""
-
-class IFSSyncFactory(component.interfaces.IFactory):
-    """A factory for file-system representation adapters.
-    
-    This factory should be registered as a named utility with the dotted name of 
-    the adapted class as the lookup key. 
-    
-    The default factory should be registered without a name.
-    
-    The call of the factory should return
-    
-    - an `IDirectoryEntry` adapter for the object if the
-      object is represented as a directory on the file system.
-
-    - an `IFileEntry` adapter for the object if the
-      object is represented as a file on the file system.
-
-    or
-
-    - default, if no special synchronizser has been registered.
-    
-    See registration.txt for further explanations.
-    """
-    

Added: zope.app.fssync/trunk/src/zope/app/fssync/main.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/main.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/main.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,396 @@
+#! /usr/bin/env python
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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 synchronization utility for Zope 3.
+
+Command line syntax summary:
+
+%(program)s add [options] PATH ...
+%(program)s checkin [options] URL [TARGETDIR]
+%(program)s checkout [options] URL [TARGETDIR]
+%(program)s commit [options] [TARGET ...]
+%(program)s copy [options] SOURCE [TARGET]
+%(program)s diff [options] [TARGET ...]
+%(program)s login [options] URL
+%(program)s logout [options] URL
+%(program)s mkdir PATH ...
+%(program)s remove [options] TARGET ...
+%(program)s resolve PATH ...
+%(program)s revert PATH ...
+%(program)s status [TARGET ...]
+%(program)s update [TARGET ...]
+
+``%(program)s help'' prints the global help (this message)
+``%(program)s help command'' prints the local help for the command
+"""
+"""
+$Id: main.py 73012 2007-03-06 17:16:44Z oestermeier $
+"""
+
+import os
+import urlparse
+
+from zope.app.fssync.command import Command, Usage
+from zope.app.fssync.fssync import FSSync
+from zope.fssync import fsutil
+
+def main():
+    """Main program.
+
+    The return value is the suggested sys.exit() status code:
+    0 or None for success
+    2 for command line syntax errors
+    1 or other for later errors
+    """
+    cmd = Command(usage=__doc__)
+    for func, aliases, short, long in command_table:
+        cmd.addCommand(func.__name__, func, short, long, aliases)
+
+    return cmd.main()
+
+def checkout(opts, args):
+    """%(program)s checkout [-u user] URL [TARGETDIR]
+
+    URL should be of the form ``http://user:password@host:port/path''.
+    Only http and https are supported (and https only where Python has
+    been built to support SSL).  This should identify a Zope 3 server;
+    user:password should have management privileges; /path should be
+    the traversal path to an existing object, not including views or
+    skins.  The user may be specified using the -u option instead of
+    encoding it in the URL, since the URL syntax for username and
+    password isn't so well known.  The password may be omitted; if so,
+    an authentication token stored using '%(program)s login' will be
+    used if available; otherwise you will be propted for the password.
+
+    TARGETDIR should be a directory; if it doesn't exist, it will be
+    created.  The object tree rooted at /path is copied to a
+    subdirectory of TARGETDIR whose name is the last component of
+    /path.  TARGETDIR defaults to the current directory.  A metadata
+    directory named @@Zope is also created in TARGETDIR.
+    """
+    if not args:
+        raise Usage("checkout requires a URL argument")
+    rooturl = args[0]
+    if len(args) > 1:
+        target = args[1]
+        if len(args) > 2:
+            raise Usage("checkout requires at most one TARGETDIR argument")
+    else:
+        target = os.curdir
+    user = _getuseroption(opts)
+    if user:
+        parts = list(urlparse.urlsplit(rooturl))
+        netloc = parts[1]
+        if "@" in netloc:
+            user_passwd, host_port = netloc.split("@", 1)
+            if ":" in user_passwd:
+                u, p = user_passwd.split(":", 1)
+            else:
+                u = user_passwd
+            # only scream if the -u option and the URL disagree:
+            if u != user:
+                raise Usage("-u/--user option and URL disagree on user name")
+        else:
+            # no username in URL; insert
+            parts[1] = "%s@%s" % (user, netloc)
+        rooturl = urlparse.urlunsplit(tuple(parts))
+    fs = FSSync(rooturl=rooturl)
+    fs.checkout(target)
+
+def commit(opts, args):
+    """%(program)s commit [-m message] [-r] [TARGET ...]
+
+    Commit the TARGET files or directories to the Zope 3 server
+    identified by the checkout command.  TARGET defaults to the
+    current directory.  Each TARGET is committed separately.  Each
+    TARGET should be up-to-date with respect to the state of the Zope
+    3 server; if not, a detailed error message will be printed, and
+    you should use the update command to bring your working directory
+    in sync with the server.
+
+    The -m option specifies a message to label the transaction.
+    The default message is 'fssync_commit'.
+    """
+    message, opts = extract_message(opts, "commit")
+    raise_on_conflicts = False
+    for o, a in opts:
+        if o in ("-r", "--raise-on-conflicts"):
+            raise_on_conflicts = True
+    fs = FSSync()
+    fs.multiple(args, fs.commit, message, raise_on_conflicts)
+
+def update(opts, args):
+    """%(program)s update [TARGET ...]
+
+    Bring the TARGET files or directories in sync with the
+    corresponding objects on the Zope 3 server identified by the
+    checkout command.  TARGET defaults to the current directory.  Each
+    TARGET is updated independently.  This command will merge your
+    changes with changes made on the server; merge conflicts will be
+    indicated by diff3 markings in the file and noted by a 'C' in the
+    update output.
+    """
+    fs = FSSync()
+    fs.multiple(args, fs.update)
+
+def add(opts, args):
+    """%(program)s add [-t TYPE] [-f FACTORY] TARGET ...
+
+    Add the TARGET files or directories to the set of registered
+    objects.  Each TARGET must exist.  The next commit will add them
+    to the Zope 3 server.
+
+    The options -t and -f can be used to set the type and factory of
+    the newly created object; these should be dotted names of Python
+    objects.  Usually only the factory needs to be specified.
+
+    If no factory is specified, the type will be guessed when the
+    object is inserted into the Zope 3 server based on the filename
+    extension and the contents of the data.  For example, some common
+    image types are recognized by their contents, and the extensions
+    .pt and .dtml are used to create page templates and DTML
+    templates, respectively.
+    """
+    type = None
+    factory = None
+    for o, a in opts:
+        if o in ("-t", "--type"):
+            type = a
+        elif o in ("-f", "--factory"):
+            factory = a
+    if not args:
+        raise Usage("add requires at least one TARGET argument")
+    fs = FSSync()
+    for a in args:
+        fs.add(a, type, factory)
+
+def copy(opts, args):
+    """%(program)s copy [-l | -R] SOURCE [TARGET]
+
+    """
+    recursive = None
+    for o, a in opts:
+        if o in ("-l", "--local"):
+            if recursive:
+                raise Usage("%r conflicts with %r" % (o, recursive))
+            recursive = False
+        elif o in ("-R", "--recursive"):
+            if recursive is False:
+                raise Usage("%r conflicts with -l" % o)
+            recursive = o
+    if not args:
+        raise Usage("copy requires at least one argument")
+    if len(args) > 2:
+        raise Usage("copy allows at most two arguments")
+    source = args[0]
+    if len(args) == 2:
+        target = args[1]
+    else:
+        target = None
+    if recursive is None:
+        recursive = True
+    else:
+        recursive = bool(recursive)
+    fs = FSSync()
+    fs.copy(source, target, children=recursive)
+
+def remove(opts, args):
+    """%(program)s remove TARGET ...
+
+    Remove the TARGET files or directories from the set of registered
+    objects.  No TARGET must exist.  The next commit will remove them
+    from the Zope 3 server.
+    """
+    if not args:
+        raise Usage("remove requires at least one TARGET argument")
+    fs = FSSync()
+    for a in args:
+        fs.remove(a)
+
+diffflags = ["-b", "-B", "--brief", "-c", "-C", "--context",
+             "-i", "-u", "-U", "--unified"]
+def diff(opts, args):
+    """%(program)s diff [diff_options] [TARGET ...]
+
+    Write a diff listing for the TARGET files or directories to
+    standard output.  This shows the differences between the working
+    version and the version seen on the server by the last update.
+    Nothing is printed for files that are unchanged from that version.
+    For directories, a recursive diff is used.
+
+    Various GNU diff options can be used, in particular -c, -C NUMBER,
+    -u, -U NUMBER, -b, -B, --brief, and -i.
+    """
+    diffopts = []
+    mode = 1
+    need_original = True
+    for o, a in opts:
+        if o == '-1':
+            mode = 1
+        elif o == '-2':
+            mode = 2
+        elif o == '-3':
+            mode = 3
+        elif o == '-N':
+            need_original = False
+        elif o in diffflags:
+            if a:
+                diffopts.append(o + " " + a)
+            else:
+                diffopts.append(o)
+    diffopts = " ".join(diffopts)
+    fs = FSSync()
+    fs.multiple(args, fs.diff, mode, diffopts, need_original)
+
+def status(opts, args):
+    """%(program)s status [TARGET ...]
+
+    Print brief (local) status for each target, without changing any
+    files or contacting the Zope server.
+    """
+    fs = FSSync()
+    fs.multiple(args, fs.status)
+
+def checkin(opts, args):
+    """%(program)s checkin [-m message] URL [TARGETDIR]
+
+    URL should be of the form ``http://user:password@host:port/path''.
+    Only http and https are supported (and https only where Python has
+    been built to support SSL).  This should identify a Zope 3 server;
+    user:password should have management privileges; /path should be
+    the traversal path to a non-existing object, not including views
+    or skins.
+
+    TARGETDIR should be a directory; it defaults to the current
+    directory.  The object tree rooted at TARGETDIR is copied to
+    /path.  subdirectory of TARGETDIR whose name is the last component
+    of /path.
+    """
+    message, opts = extract_message(opts, "checkin")
+    if not args:
+        raise Usage("checkin requires a URL argument")
+    rooturl = args[0]
+    if len(args) > 1:
+        target = args[1]
+        if len(args) > 2:
+            raise Usage("checkin requires at most one TARGETDIR argument")
+    else:
+        target = os.curdir
+    fs = FSSync(rooturl=rooturl)
+    fs.checkin(target, message)
+
+def login(opts, args):
+    """%(program)s login [-u user] [URL]
+
+    Save a basic authentication token for a URL that doesn't include a
+    password component.
+    """
+    _loginout(opts, args, "login", FSSync().login)
+
+def logout(opts, args):
+    """%(program)s logout [-u user] [URL]
+
+    Remove a saved basic authentication token for a URL.
+    """
+    _loginout(opts, args, "logout", FSSync().logout)
+
+def _loginout(opts, args, cmdname, cmdfunc):
+    url = user = None
+    if args:
+        if len(args) > 1:
+            raise Usage("%s allows at most one argument" % cmdname)
+        url = args[0]
+    user = _getuseroption(opts)
+    cmdfunc(url, user)
+
+def _getuseroption(opts):
+    user = None
+    for o, a in opts:
+        if o in ("-u", "--user"):
+            if user:
+                raise Usage("-u/--user may only be specified once")
+            user = a
+    return user
+
+def mkdir(opts, args):
+    """%(program)s mkdir PATH ...
+
+    Create new directories in directories that are already known to
+    %(program)s and schedule the new directories for addition.
+    """
+    fs = FSSync()
+    fs.multiple(args, fs.mkdir)
+
+def resolve(opts, args):
+    """%(program)s resolve [PATH ...]
+
+    Clear any conflict markers associated with PATH.  This would allow
+    commits to proceed for the relevant files.
+    """
+    fs = FSSync()
+    fs.multiple(args, fs.resolve)
+
+def revert(opts, args):
+    """%(program)s revert [TARGET ...]
+
+    Revert changes to targets.  Modified files are overwritten by the
+    unmodified copy cached in @@Zope/Original/ and scheduled additions
+    and deletions are de-scheduled.  Additions that are de-scheduled
+    do not cause the working copy of the file to be removed.
+    """
+    fs = FSSync()
+    fs.multiple(args, fs.revert)
+
+def extract_message(opts, cmd):
+    L = []
+    message = None
+    msgfile = None
+    for o, a in opts:
+        if o in ("-m", "--message"):
+            if message:
+                raise Usage(cmd + " accepts at most one -m/--message option")
+            message = a
+        elif o in ("-F", "--file"):
+            if msgfile:
+                raise Usage(cmd + " accepts at most one -F/--file option")
+            msgfile = a
+        else:
+            L.append((o, a))
+    if not message:
+        if msgfile:
+            message = open(msgfile).read()
+        else:
+            message = "fssync_" + cmd
+    elif msgfile:
+        raise Usage(cmd + " requires at most one of -F/--file or -m/--message")
+    return message, L
+
+command_table = [
+    # name is taken from the function name
+    # function, aliases,  short opts,   long opts
+    (add,      "",        "f:t:",       "factory= type="),
+    (checkin,  "",        "F:m:",       "file= message="),
+    (checkout, "co",      "u:",         "user="),
+    (commit,   "ci",      "F:m:r",      "file= message= raise-on-conflicts"),
+    (copy,     "cp",      "lR",         "local recursive"),
+    (diff,     "di",      "bBcC:iNuU:", "brief context= unified="),
+    (login,    "",        "u:",         "user="),
+    (logout,   "",        "u:",         "user="),
+    (mkdir,    "",        "",           ""),
+    (remove,   "del delete rm", "",     ""),
+    (resolve,  "",        "",           ""),
+    (revert,   "",        "",           ""),
+    (status,   "stat st", "",           ""),
+    (update,   "up",      "",           ""),
+    ]


Property changes on: zope.app.fssync/trunk/src/zope/app/fssync/main.py
___________________________________________________________________
Name: svn:executable
   + *

Modified: zope.app.fssync/trunk/src/zope/app/fssync/module/adapter.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/module/adapter.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/module/adapter.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -20,19 +20,19 @@
 
 from zope.interface import implements
 
-from zope.fssync.server.entryadapter import ObjectEntryAdapter, AttrMapping
-from zope.fssync.server.interfaces import IObjectFile
+from zope.fssync import synchronizer
+from zope.fssync import interfaces
 
 
-class ModuleAdapter(ObjectEntryAdapter):
+class ModuleAdapter(synchronizer.FileSynchronizer):
 
-    implements(IObjectFile)
+    implements(interfaces.IFileSynchronizer)
 
-    def getBody(self):
-        return self.context.source
+    def dump(self, writeable):
+        writeable.write(self.context.source)
 
-    def setBody(self, source):
-        self.context.update(source)
+    def load(self, readable):
+        self.context.update(readable.read())
 
-    def extra(self):
-        return AttrMapping(self.context, ("name",))
+    def extras(self):
+        return synchronizer.Extras(name=self.context.name)

Modified: zope.app.fssync/trunk/src/zope/app/fssync/module/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/module/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/module/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -5,7 +5,7 @@
   <utility
       component=".adapter.ModuleAdapter"
       name="zope.app.module.manager.ModuleManager"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
+      provides="zope.fssync.interfaces.ISynchronizerFactory"
       permission="zope.ManageCode"
       />
 
@@ -14,7 +14,7 @@
       
       <require
           permission="zope.ManageCode"
-          interface="zope.fssync.server.interfaces.IObjectFile" />
+          interface="zope.fssync.interfaces.ISynchronizer" />
           
    </class>
 

Added: zope.app.fssync/trunk/src/zope/app/fssync/passwd.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/passwd.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/passwd.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,196 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Password manager for fssync clients.
+
+$Id: passwd.py 25177 2004-06-02 13:17:31Z jim $
+"""
+
+import base64
+import httplib
+import os
+
+from cStringIO import StringIO
+
+
+DEFAULT_FILENAME = os.path.expanduser(os.path.join("~", ".zsyncpass"))
+
+
+class PasswordManager(object):
+    """Manager for a cache of basic authentication tokens for zsync.
+
+    This stores tokens in a file, and allows them to be retrieved by
+    the zsync application.  The tokens are stored in their 'cooked'
+    form, so while someone could easily decode them or use them to
+    make requests, the casual reader won't be able to use them easily.
+
+    The cache file is created with restricted permissions, so other
+    users should not be able to read it unless the permissions are
+    modified.
+    """
+
+    def __init__(self, filename=None):
+        if not filename:
+            filename = DEFAULT_FILENAME
+        self.authfile = filename
+
+    def getPassword(self, user, host_port):
+        """Read a password from the user."""
+        import getpass
+        prompt = "Password for %s at %s: " % (user, host_port)
+        return getpass.getpass(prompt)
+
+    def createToken(self, user_passwd):
+        """Generate a basic authentication token from 'user:password'."""
+        return base64.encodestring(user_passwd).strip()
+
+    def getToken(self, scheme, host_port, user):
+        """Get an authentication token for the user for a specific server.
+
+        If a corresponding token exists in the cache, that is retured,
+        otherwise the user is prompted for their password and a new
+        token is generated.  A new token is not automatically stored
+        in the cache.
+        """
+        host_port = _normalize_host(scheme, host_port)
+        prefix = [scheme, host_port, user]
+
+        if os.path.exists(self.authfile):
+            f = open(self.authfile, "r")
+            try:
+                for line in f:
+                    line = line.strip()
+                    if line[:1] in ("#", ""):
+                        continue
+                    parts = line.split()
+                    if parts[:3] == prefix:
+                        return parts[3]
+            finally:
+                f.close()
+
+        # not in ~/.zsyncpass
+        pw = self.getPassword(user, host_port)
+        user_passwd = "%s:%s" % (user, pw)
+        return self.createToken(user_passwd)
+
+    def addToken(self, scheme, host_port, user, token):
+        """Add a token to the persistent cache.
+
+        If a corresponding token already exists in the cache, it is
+        replaced.
+        """
+        host_port = _normalize_host(scheme, host_port)
+        record = "%s %s %s %s\n" % (scheme, host_port, user, token)
+
+        if os.path.exists(self.authfile):
+            prefix = [scheme, host_port, user]
+            f = open(self.authfile)
+            sio = StringIO()
+            found = False
+            for line in f:
+                parts = line.split()
+                if parts[:3] == prefix:
+                    sio.write(record)
+                    found = True
+                else:
+                    sio.write(line)
+            f.close()
+            if not found:
+                sio.write(record)
+            text = sio.getvalue()
+        else:
+            text = record
+        f = self.createAuthFile()
+        f.write(text)
+        f.close()
+
+    def removeToken(self, scheme, host_port, user):
+        """Remove a token from the authentication database.
+
+        Returns True if a token was found and removed, or False if no
+        matching token was found.
+
+        If the resulting cache file contains only blank lines, it is
+        removed.
+        """
+        if not os.path.exists(self.authfile):
+            return False
+        host_port = _normalize_host(scheme, host_port)
+        prefix = [scheme, host_port, user]
+        found = False
+        sio = StringIO()
+        f = open(self.authfile)
+        nonblank = False
+        for line in f:
+            parts = line.split()
+            if parts[:3] == prefix:
+                found = True
+            else:
+                if line.strip():
+                    nonblank = True
+                sio.write(line)
+        f.close()
+        if found:
+            if nonblank:
+                text = sio.getvalue()
+                f = self.createAuthFile()
+                f.write(text)
+                f.close()
+            else:
+                # nothing left in the file but blank lines; remove it
+                os.unlink(self.authfile)
+        return found
+
+    def createAuthFile(self):
+        """Create the token cache file with the right permissions."""
+        new = not os.path.exists(self.authfile)
+        if os.name == "posix":
+            old_umask = os.umask(0077)
+            try:
+                f = open(self.authfile, "w", 0600)
+            finally:
+                os.umask(old_umask)
+        else:
+            f = open(self.authfile, "w")
+        if new:
+            f.write(_NEW_FILE_HEADER)
+        return f
+
+_NEW_FILE_HEADER = """\
+#
+# Stored authentication tokens for zsync.
+# Manipulate this data using the 'zsync login' and 'zsync logout';
+# read the zsync documentation for more information.
+#
+"""
+
+def _normalize_host(scheme, host_port):
+    if scheme == "http":
+        return _normalize_port(host_port, httplib.HTTP_PORT)
+    elif scheme == "https":
+        return _normalize_port(host_port, httplib.HTTPS_PORT)
+    else:
+        raise fsutil.Error("unsupported URL scheme: %r" % scheme)
+
+def _normalize_port(host_port, default_port):
+    if ":" in host_port:
+        host, port = host_port.split(":", 1)
+        try:
+            port = int(port)
+        except ValueError:
+            raise fsutil.Error("invalid port specification: %r" % port)
+        if port <= 0:
+            raise fsutil.Error("invalid port: %d" % port)
+        if port == default_port:
+            host_port = host
+    return host_port.lower()

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/registration.txt
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/registration.txt	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/registration.txt	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,65 +0,0 @@
-Registration of Synchronizers
-=============================
-
-The fssync package exchanges serialized objects between Zope and the
-local filesystem. This is usefull in at least two cases:
-
-- data export / import (e.g. moving objects from one server to another)
-- content management (e.g. managing a wiki or other collections of
-documents offline)
-
-In the first case it is crucial that fssync is able to serialize "all"
-object data.  Note that it isn't always obvious what data is intrinsic
-to  an object. Therefore we must provide special serialization /
-de-serialization adapters which take care of writing and reading "all"
-data. An obvious solution would be to use inheriting synchronization
-adapters. But this solution bears a risk. If someone created a subclass
-and forgot to create an adapter, then their data would be serialized
-incompletely.
-
-A better solution is to provide class based adapters for special object
-types and a default serializer which tries to capture
-the forgotten serialization specifications of subclasses.
-
->>> from zope import component
->>> from interfaces import IFSSyncFactory
->>> from zope.app.fssync.tests import sampleclass
->>> a = sampleclass.C1()
->>> b = sampleclass.C2()
-
-The default adapter factory is registered as an unnamed utility:
-
->>> component.provideUtility(sampleclass.CDefaultAdapter, 
-...                                 provides=IFSSyncFactory) 
-
-
-All special serializers are registered for a specific class which is
-represented by the dotted class name in the factory registration:
-
->>> dottedname1 = 'zope.app.fssync.tests.sampleclass.C1'
->>> dottedname2 = 'zope.app.fssync.tests.sampleclass.C2'
->>> component.provideUtility(sampleclass.CFileAdapter,
-...             provides=IFSSyncFactory, name=dottedname1)
-
-
->>> sa = component.getUtility(IFSSyncFactory, dottedname1)(a)
->>> sa
-<zope.app.fssync.tests.sampleclass.CFileAdapter object at ...>
-
->>> sb = component.getUtility(IFSSyncFactory, dottedname2)(b)
-Traceback (most recent call last):
-...
-ComponentLookupError: ...
-
-
-The syncer.getSerializer method ensures that we get the class based
-serializer or the default serializer:
-
->>> from zope.app.fssync import syncer
->>> syncer.getSerializer(a)
-<zope.app.fssync.tests.sampleclass.CFileAdapter object at ...>
-
->>> syncer.getSerializer(b)
-<zope.app.fssync.tests.sampleclass.CDefaultAdapter object at ...>
-
-

Modified: zope.app.fssync/trunk/src/zope/app/fssync/security.txt
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/security.txt	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/security.txt	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,124 +1,132 @@
 Security Issues
 ===============
 
-The Python API of zope.fssync and zope.app.fssync does not care about security.
-There are neither security checks nor their natural enemies (removeSecurityProxy calls).
-
-The web-based API however has to deal with security issues which mainly depends on the permissions 
-to access serialization adapters and their read and write methods.
+The Python API of zope.fssync does not care about security. The 
+web-based API however has to deal with security issues which mainly 
+depends on the permissions to access serialization and 
+deserialization adapters.
     
 By default the permissions are set as follows:
 
-    Read and write access to content and annotations is generally governed by the 
-    zope.ManageContent permission;
+    read access to content and annotations requires zope.View;
     
-    access to zope.app.module.manager.ModuleManager by zope.ManageCode;
+    write access requires zope.ManageContent;
     
-    and access to everything else by zope.ManageSite.   
+    access to page templates and zope.app.module.manager 
+    requires zope.ManageCode;
     
-If you need more fine grained distinctions, e.g. differential read and write access, you must
-specify the appropriate permissions for the read and write methods of the registered serialization 
-adapters.
+    and access to everything else zope.ManageSite.   
+    
+If you need more fine grained distinctions, e.g. differential read 
+and write access, you must specify the appropriate permissions for 
+registered serialization and deserialization adapters.
 
 Let's start with some objects on the server side:
 
->>> root = getRootFolder()
->>> from zope.app.file import File
->>> serverfile1 = root[u'file1.txt'] = File('A text file', 'plain/text')
->>> serverfile2 = root[u'file2.txt'] = File('Another text file', 'plain/text')
+    >>> root = getRootFolder()
+    >>> from zope.app.file import File
+    >>> serverfile1 = root[u'file1.txt'] = File('A text file', 'text/plain')
+    >>> serverfile2 = root[u'file2.txt'] = File('Another file', 'text/plain')
 
 The root folder has a SiteManager which should only be accessible to
-site managers. This access is governed by the serialization adapters which are
-registered as named utilities. FSSync first looks whether a class based adapter is registered
-as a named utility:
+site managers. This access is governed by the serialization adapters. The 
+factories of these adapters are registered as named utilities. 
+FSSync first looks whether a class based adapter is registered:
 
->>> from zope.app.fssync.interfaces import IFSSyncFactory
->>> zope.component.getUtility(IFSSyncFactory, 'zope.app.component.site.LocalSiteManager')
-Traceback (most recent call last):
-...
-ComponentLookupError: (<...IFSSyncFactory>, 'zope.app.component.site.LocalSiteManager')
+    >>> from zope.fssync.interfaces import ISynchronizer, ISynchronizerFactory
+    >>> sm = root.getSiteManager()
+    >>> zope.component.getUtility(ISynchronizerFactory, 
+    ...                     name='zope.app.component.site.LocalSiteManager')
+    Traceback (most recent call last):
+    ...
+    ComponentLookupError: (..., '...LocalSiteManager')
 
-Since the LocalSiteManager class has no special serializer FSSync uses the unnamed DefaultAdapter
-which is protected by the ManageSite permission:
+Since no special serializer is registered we must check the security 
+settings of the default serializer which should be registered as a unnamed
+ISynchronizerFactory:
 
->>> factory = zope.component.getUtility(IFSSyncFactory)
->>> factory.__Security_checker__.get_permissions['__call__']
-'zope.ManageSite'
+    >>> factory = zope.component.getUtility(ISynchronizerFactory) 
+    >>> checker = getattr(factory, '__Security_checker__', None)
+    >>> checker
+    <zope.security.checker.Checker object at ...>
+    >>> sorted(checker.get_permissions.items())
+    [('__call__', 'zope.ManageSite'), ...]
 
-
-
 Reading with Different Permissions
 ----------------------------------
 
-The global manager has all permissions and thus is able to check out all objects:
+The global manager has all permissions and thus is able to check out all 
+objects:
 
->>> from zope.fssync.fssync import FSSync
->>> rooturl = 'http://globalmgr:globalmgrpw@localhost'
->>> admin = FSSync(network=TestNetwork(), rooturl=rooturl)
+    >>> from zope.app.fssync.fssync import FSSync
+    >>> rooturl = 'http://globalmgr:globalmgrpw@localhost'
+    >>> admin = FSSync(network=TestNetwork(), rooturl=rooturl)
 
 Note that the ++etc++site subtree is part of the checkout:
 
->>> admin.checkout(checkoutdir)
-N .../root/
-U .../root/++etc++site
-N .../root/@@Zope/Annotations/++etc++site/
-U .../root/@@Zope/Annotations/++etc++site/zope.app.dublincore.ZopeDublinCore
-U .../root/file1.txt
-N .../root/@@Zope/Extra/file1.txt/
-U .../root/@@Zope/Extra/file1.txt/contentType
-U .../root/file2.txt
-N .../root/@@Zope/Extra/file2.txt/
-U .../root/@@Zope/Extra/file2.txt/contentType
-N .../@@Zope/Annotations/root/
-U .../@@Zope/Annotations/root/zope.app.dublincore.ZopeDublinCore
-U .../@@Zope/Annotations/root/zope.app.security.AnnotationPrincipalRoleManager
-All done.
+    >>> admin.checkout(checkoutdir)
+    N .../root/
+    U .../root/++etc++site
+    U .../root/file1.txt
+    N .../root/@@Zope/Extra/file1.txt/
+    U .../root/@@Zope/Extra/file1.txt/contentType
+    U .../root/file2.txt
+    N .../root/@@Zope/Extra/file2.txt/
+    U .../root/@@Zope/Extra/file2.txt/contentType
+    N .../@@Zope/Annotations/root/
+    U .../@@Zope/Annotations/root/zope.app.dublincore.ZopeDublinCore
+    U .../@@Zope/Annotations/root/zope.app.security.AnnotationPrincipalRoleManager
+    All done.
+    
+    >>> cleanUpTree(checkoutdir)
 
->>> cleanUpTree(checkoutdir)
+Now we perform the same checkout with the limited permission of a content 
+manager which has no access to the site manager:
 
-Now we perform the same checkout with the limited permission of a content manager:
+    >>> rooturl = 'http://cm:cmpw@localhost'
+    >>> contentmanager = FSSync(network=TestNetwork(), rooturl=rooturl)
+    >>> contentmanager.checkout(checkoutdir)
+    N .../root/
+    U .../root/file1.txt
+    U .../root/file2.txt
+    N .../@@Zope/Annotations/root/
+    U .../@@Zope/Annotations/root/zope.app.dublincore.ZopeDublinCore
+    All done.
+    
+    >>> cleanUpTree(checkoutdir)
 
->>> rooturl = 'http://cm:cmpw@localhost'
->>> contentmanager = FSSync(network=TestNetwork(), rooturl=rooturl)
->>> contentmanager.checkout(checkoutdir)
-N .../root/
-U .../root/file1.txt
-U .../root/file2.txt
-N .../@@Zope/Annotations/root/
-U .../@@Zope/Annotations/root/zope.app.dublincore.ZopeDublinCore
-All done.
 
->>> cleanUpTree(checkoutdir)
-
-
 Limited Write Permissions
 -------------------------
 
-What if we want to restrict the write access to specific users? One possibility is to introduce
-new permissions, e.g. zope.app.fssync.Read and zope.app.fssync.Write which are used as 
-specific permissions to call read and write methods (see ftesting.zcml).
+What if we want to restrict the write access to specific users? 
+This is easy since deserialize permissions are separated from 
+the serialize permissions:
 
+    >>> rooturl = 'http://rom:rompw@localhost'
+    >>> readonly = FSSync(network=TestNetwork(handle_errors=False),
+    ...                   rooturl=rooturl)
+    >>> readonly.checkout(checkoutdir)
+    N .../root/
+    U .../root/file1.txt
+    U .../root/file2.txt
+    All done.
 
->>> rooturl = 'http://rom:rompw@localhost'
->>> readonly = FSSync(network=TestNetwork(), rooturl=rooturl)
->>> readonly.checkout(checkoutdir)
-N .../root/
-U .../root/file1.txt
-U .../root/file2.txt
-N .../@@Zope/Annotations/root/
-U .../@@Zope/Annotations/root/zope.app.dublincore.ZopeDublinCore
-All done.
+    >>> localfile1 = os.path.join(checkoutdir, 'root', 'file1.txt')
+    >>> fp = open(localfile1, 'w')
+    >>> fp.write('A modified text file')
+    >>> fp.close()
 
+One security level is realized by the commit view which requires 
+the ManageContent permission:
 
->>> localfile1 = os.path.join(checkoutdir, 'root', 'file1.txt')
->>> fp = open(localfile1, 'w')
->>> fp.write('A modified text file')
->>> fp.close()
+    >>> readonly.commit(localfile1)
+    Traceback (most recent call last):
+    ...
+    Unauthorized: (<zope.app.publisher.browser.viewmeta.SnarfCommit ...>, ...
 
->>> readonly.commit(localfile1)
-Traceback (most recent call last):
-...
-Error: HTTP error 403 (Forbidden); error document:
-...
 
+TODO:
+-----
 
+Write a test for unprotected views.

Modified: zope.app.fssync/trunk/src/zope/app/fssync/syncer.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/syncer.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/syncer.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -17,82 +17,66 @@
 """
 __docformat__ = 'restructuredtext'
 
+import tempfile
+
 from zope import interface
 from zope import component
-from zope import xmlpickle
-from zope.traversing.api import getPath
-from zope.annotation.interfaces import IAnnotations
-from zope.annotation.attribute import AttributeAnnotations
-from zope.proxy import removeAllProxies
+from zope import proxy
 from zope.security.management import checkPermission
 from zope.security.checker import ProxyFactory
 
-from zope.fssync.server.syncer import Syncer
-from zope.fssync.server import entryadapter
-from zope.app.fssync.interfaces import IFSSyncFactory
+from zope.fssync import synchronizer
+from zope.fssync import task
+from zope.fssync import repository
+from zope.fssync import interfaces
+from zope.fssync import pickle
 
-import interfaces
-import fspickle
 
-def dottedname(klass):
-    return "%s.%s" % (klass.__module__, klass.__name__)
+def getSynchronizer(obj, raise_error=True):
+    """Returns a synchronizer.
 
+    Looks for a named factory first and returns a default serializer
+    if the dotted class name is not known.
 
-class LocationAwareDefaultFileAdapter(entryadapter.DefaultFileAdapter):
-    """A specialization of the DefaultFileAdapter which uses a location aware
-    pickle.
-    """
-    
-    def getBody(self):
-        return xmlpickle.toxml(fspickle.dumps(self.context))
-    
-    
-class FSSyncAnnotations(AttributeAnnotations):
-    """Default adapter for access to attribute annotations.
-       Should be registered as trusted adapter.
-    """
-    interface.implements(interfaces.IFSSyncAnnotations)
+    Checks also for the permission to call the factory in the context 
+    of the given object.
 
-
-def provideSynchronizer(klass, factory):
-    if klass is not None:
-        name = dottedname(klass)
-    else:
-        name = ''
-    component.provideUtility(factory, provides=interfaces.IFSSyncFactory, name=name)
-    
-
-def getObjectId(obj):
-    return getPath(obj)
-
-def getSerializer(obj):
-    """Returns a synchronizer.
-    
-    Looks for a named factory first and returns a default adapter
-    if the dotted class name is not known.
-    
-    Checks also for the permission to call the factory in the context of the given object.
-    Removes the security proxy if a call is allowed.
+    Removes the security proxy for the adapted object 
+    if a call is allowed and adds a security proxy to the
+    synchronizer instead.
     """
-    name = dottedname(obj.__class__)
-    factory = component.queryUtility(interfaces.IFSSyncFactory, name=name)
+    dn = synchronizer.dottedname(obj.__class__)    
+
+    factory = component.queryUtility(interfaces.ISynchronizerFactory, name=dn)
     if factory is None:
-        factory = component.queryUtility(interfaces.IFSSyncFactory)
+        factory = component.queryUtility(interfaces.ISynchronizerFactory)
+    if factory is None:
+        if raise_error:
+            raise synchronizer.MissingSerializer(dn)
+        return None    
 
     checker = getattr(factory, '__Security_checker__', None)
     if checker is None:
         return factory(obj)
-        
     permission = checker.get_permissions['__call__']
     if checkPermission(permission, obj):
-        return ProxyFactory(factory(removeAllProxies(obj)))
-        
-    return None
+        return ProxyFactory(factory(proxy.removeAllProxies(obj)))
 
-def getAnnotations(obj):
-    return interfaces.IFSSyncAnnotations(obj, None)
 
-
 def toFS(obj, name, location):
-    syncer = Syncer(getObjectId, getSerializer, getAnnotations)
-    return syncer.toFS(obj, name, location)
+    filesystem = repository.FileSystemRepository()
+    checkout = task.Checkout(getSynchronizer, filesystem)
+    return checkout.perform(obj, name, location)
+
+def toSNARF(obj, name):
+    temp = tempfile.TemporaryFile()
+    # TODO: Since we do not know anything about the target system here,
+    # we try to be on the save side. Case-sensivity and NFD should be 
+    # determined from the request.
+    
+    snarf = repository.SnarfRepository(temp, case_insensitive=True, enforce_nfd=True)
+    checkout = task.Checkout(getSynchronizer, snarf)
+    checkout.perform(obj, name)
+    return temp
+
+        
\ No newline at end of file

Modified: zope.app.fssync/trunk/src/zope/app/fssync/testing.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/testing.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/testing.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -19,8 +19,78 @@
 __docformat__ = "reStructuredText"
 
 import os
+from cStringIO import StringIO
 from zope.app.testing.functional import ZCMLLayer
+from zope.testbrowser.testing import PublisherConnection
+from zope.app.fssync import fssync
+from zope.fssync import fsutil
 
+
 AppFSSyncLayer = ZCMLLayer(
     os.path.join(os.path.split(__file__)[0], 'ftesting.zcml'),
     __name__, 'AppFSSyncLayer', allow_teardown=True)
+
+
+class TestNetwork(fssync.Network):
+    """A specialization which uses a PublisherConnection suitable 
+    for functional doctests.
+    """
+
+    def __init__(self, handle_errors=True):
+        super(TestNetwork, self).__init__()
+        self.handle_errors = handle_errors
+
+    def httpreq(self, path, view, datasource=None,
+                content_type="application/x-snarf",
+                expected_type="application/x-snarf",
+                ):
+        """Issue an request.
+        
+        This is a overwritten version of the original Network.httpreq
+        method that uses a TestConnection as a replacement for httplib 
+        connections.
+        """
+        assert self.rooturl
+        if not path.endswith("/"):
+            path += "/"
+        path += view
+        conn = PublisherConnection(self.host_port)
+        headers = {}
+        if datasource is None:
+            method = 'GET'
+        else:
+            method = 'POST'
+            headers["Content-type"] = content_type
+            stream = StringIO()
+            datasource(stream)
+            headers["Content-Length"] = str(stream.tell())
+            
+        if self.user_passwd:
+            if ":" not in self.user_passwd:
+                auth = self.getToken(self.roottype,
+                                     self.host_port,
+                                     self.user_passwd)
+            else:
+                auth = self.createToken(self.user_passwd)
+            headers['Authorization'] = 'Basic %s' % auth
+        headers['Host'] = self.host_port
+        headers['Connection'] = 'close'
+        headers['X-zope-handle-errors'] = self.handle_errors
+
+        data = None
+        if datasource is not None:
+            data = stream.getvalue()
+
+        conn.request(method, path, body=data, headers=headers)
+        response = conn.getresponse()
+
+        if response.status != 200:
+            raise fsutil.Error("HTTP error %s (%s); error document:\n%s",
+                        response.status, response.reason,
+                        self.slurptext(response.content_as_file, response.msg))
+        elif expected_type and response.msg["Content-type"] != expected_type:
+            raise fsutil.Error(
+                        self.slurptext(response.content_as_file, response.msg))
+        else:
+            return response.content_as_file, response.msg
+

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/tests/sampleclass.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/sampleclass.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/sampleclass.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,92 +0,0 @@
-##############################################################################
-#
-# Copyright) 2001, 2002 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (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.
-#
-##############################################################################
-"""Test SampleClass for testing File-system synchronization utilities
-
-$Id$
-"""
-
-from zope.fssync.server.interfaces import IObjectDirectory, IObjectFile
-from zope.interface import implements
-
-class C1(object): "C1 Doc"
-class C2(object): "C2 Doc"
-
-
-class CDefaultAdapter(object):
-    """Default File-system representation for object
-    """
-    implements(IObjectFile)
-
-    def __init__(self, object):
-         self.context = object
-
-    def extra(self):
-         pass
-
-    def typeIdentifier(self):
-         return "Default"
-
-    def factory(self):
-         return "Default Factory"
-
-    def getBody(self):
-         return self.context.__doc__
-
-    def setBody(self):
-         pass
-
-class CDirAdapter(object):
-    """Directory Adapter
-    """
-
-    implements(IObjectDirectory)
-
-    def __init__(self, object):
-        self.context = object
-
-    def extra(self):
-        pass
-
-    def typeIdentifier(self):
-        return "Folder"
-
-    def factory(self):
-        return "Folder Factory"
-
-    def contents(self):
-        return []
-
-class CFileAdapter(object):
-    """File Adapter
-    """
-
-    implements(IObjectFile)
-
-    def __init__(self, object):
-        self.context = object
-
-    def extra(self):
-        pass
-
-    def typeIdentifier(self):
-        return "File"
-
-    def factory(self):
-        return "File Factory"
-
-    def getBody(self):
-        return self.context.__doc__
-
-    def setBody(self):
-        pass

Added: zope.app.fssync/trunk/src/zope/app/fssync/tests/test_command.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/test_command.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/test_command.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,102 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Tests for yet another command-line handler.
+
+$Id: test_command.py 25177 2004-06-02 13:17:31Z jim $
+"""
+
+import sys
+import unittest
+
+from cStringIO import StringIO
+
+from zope.app.fssync import command
+
+
+class CommandTests(unittest.TestCase):
+
+    def setUp(self):
+        self.called = False
+        self.old_stdout = sys.stdout
+        self.old_stderr = sys.stderr
+        self.new_stdout = StringIO()
+        self.new_stderr = StringIO()
+        sys.stdout = self.new_stdout
+        sys.stderr = self.new_stderr
+        self.cmd = command.Command("testcmd", "%(program)s msg")
+
+    def tearDown(self):
+        sys.stdout = self.old_stdout
+        sys.stderr = self.old_stderr
+
+    def test_no_command(self):
+        self.assertRaises(command.Usage, self.cmd.realize, [])
+
+    def test_unknown_command(self):
+        self.assertRaises(command.Usage, self.cmd.realize, ["throb"])
+
+    def test_global_help_short(self):
+        self.assertRaises(SystemExit, self.cmd.realize, ["-h"])
+        self.assert_(self.new_stdout.getvalue())
+
+    def test_global_help_long(self):
+        self.assertRaises(SystemExit, self.cmd.realize, ["--help"])
+        self.assert_(self.new_stdout.getvalue())
+
+    def test_calling_command(self):
+        self.cmd.addCommand("throb", self.mycmd)
+        self.cmd.realize(["throb"])
+        self.cmd.run()
+        self.assertEqual(self.opts, [])
+        self.assertEqual(self.args, [])
+
+    def mycmd(self, opts, args):
+        """dummy help text"""
+        self.called = True
+        self.opts = opts
+        self.args = args
+
+    def test_calling_command_via_alias(self):
+        self.cmd.addCommand("throb", self.mycmd, "x:y", "prev next",
+                            aliases="chunk thunk")
+        self.cmd.realize(["thunk", "-yx", "42", "--", "-more", "args"])
+        self.cmd.run()
+        self.assertEqual(self.opts, [("-y", ""), ("-x", "42")])
+        self.assertEqual(self.args, ["-more", "args"])
+
+    def test_calling_command_with_args(self):
+        self.cmd.addCommand("throb", self.mycmd, "x:", "spew")
+        self.cmd.realize(["throb", "-x", "42", "--spew", "more", "args"])
+        self.cmd.run()
+        self.assertEqual(self.opts, [("-x", "42"), ("--spew", "")])
+        self.assertEqual(self.args, ["more", "args"])
+
+    def test_local_help_short(self):
+        self.cmd.addCommand("throb", self.mycmd)
+        self.assertRaises(SystemExit, self.cmd.realize, ["throb", "-h"])
+        self.assert_(self.new_stdout.getvalue())
+        self.assert_(not self.called)
+
+    def test_local_help_long(self):
+        self.cmd.addCommand("throb", self.mycmd)
+        self.assertRaises(SystemExit, self.cmd.realize, ["throb", "--help"])
+        self.assert_(self.new_stdout.getvalue())
+        self.assert_(not self.called)
+
+
+def test_suite():
+    return unittest.makeSuite(CommandTests)
+
+if __name__ == "__main__":
+    unittest.main(defaultTest="test_suite")

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/tests/test_committer.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/test_committer.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/test_committer.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,618 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2003 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (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.
-#
-##############################################################################
-"""Tests for the Committer class.
-
-$Id$
-"""
-import os
-import shutil
-import tempfile
-import unittest
-
-import zope.component
-import zope.interface
-from zope.traversing.interfaces import TraversalError, IContainmentRoot
-from zope.traversing.interfaces import ITraversable, ITraverser
-from zope.xmlpickle import loads, dumps
-from zope.location import Location
-from zope.filerepresentation.interfaces import IFileFactory
-from zope.filerepresentation.interfaces import IDirectoryFactory
-
-from zope.fssync import fsutil
-from zope.fssync.server.entryadapter import DefaultFileAdapter
-from zope.fssync.tests.mockmetadata import MockMetadata
-from zope.fssync.tests.tempfiles import TempFiles
-from zope.fssync.server.entryadapter import DirectoryAdapter
-
-from zope.app.testing import ztapi
-from zope.app.container.interfaces import IContainer
-from zope.app.testing.placelesssetup import PlacelessSetup
-
-from zope.app.fssync import committer, syncer # The module
-from zope.app.fssync.committer import Checker, Committer, SynchronizationError
-
-class Sample(object):
-    pass
-
-
-class PretendContainer(Location):
-    zope.interface.implements(IContainer, ITraversable, ITraverser)
-
-    def __init__(self):
-        self.holding = {}
-
-    def __setitem__(self, name, value):
-        name = name.lower()
-        if name in self.holding:
-            raise KeyError
-        self.holding[name] = value
-        return name
-
-    def __delitem__(self, name):
-        name = name.lower()
-        del self.holding[name]
-
-    def __getitem__(self, name):
-        name = name.lower()
-        return self.holding[name]
-
-    def get(self, name):
-        name = name.lower()
-        return self.holding.get(name)
-
-    def __contains__(self, name):
-        name = name.lower()
-        return name in self.holding
-
-    def keys(self):
-        return self.holding.keys()
-
-    def items(self):
-        return self.holding.items()
-
-    def traverse(self, name, furtherPath):
-        try:
-            return self[name]
-        except KeyError:
-            raise TraversalError
-
-PCname = PretendContainer.__module__ + "." + PretendContainer.__name__
-
-class PretendRootContainer(PretendContainer):
-    zope.interface.implements(IContainmentRoot)
-
-
-class DictAdapter(DefaultFileAdapter):
-
-    def setBody(self, body):
-        old = self.context
-        if old.__class__ is not dict:
-            raise AssertionError("old.__class__ is not dict")
-        new = loads(body)
-        if type(new) is not dict:
-            raise AssertionError("type(new) is not dict")
-        old.update(new)
-        for key in old.keys():
-            if key not in new:
-                del old[key]
-
-
-class TestBase(PlacelessSetup, TempFiles):
-
-    # Base class for test classes
-
-    def setUp(self):
-        super(TestBase, self).setUp()
-
-        # Set up synchronizer factory
-        syncer.provideSynchronizer(None, DefaultFileAdapter)
-
-        # Set up temporary name administration
-        TempFiles.setUp(self)
-
-    def tearDown(self):
-        # Clean up temporary files and directories
-        TempFiles.tearDown(self)
-
-        PlacelessSetup.tearDown(self)
-
-
- at zope.component.adapter(IContainer)
- at zope.interface.implementer(IFileFactory)
-def file_factory_maker(container):
-    def file_factory(name, content_type, data):
-        return loads(data)
-    return file_factory
-
- at zope.component.adapter(IContainer)
- at zope.interface.implementer(IDirectoryFactory)
-def directory_factory_maker(container):
-    def directory_factory(name):
-        return PretendContainer()
-    return directory_factory
-
-def sort(lst):
-    lst.sort()
-    return lst
-
-
-class TestSyncerModule(TestBase):
-
-    def setUp(self):
-        super(TestSyncerModule, self).setUp()
-        self.location = tempfile.mktemp()
-        os.mkdir(self.location)
-
-    def tearDown(self):
-        super(TestSyncerModule, self).tearDown()
-        shutil.rmtree(self.location)
-
-    def test_toFS(self):
-        obj = Sample()
-        syncer.toFS(obj, "foo", self.location)
-
-    def test_getSerializer(self):
-        obj = Sample()
-        adapter = syncer.getSerializer(obj)
-        self.assertEqual(adapter.__class__, DefaultFileAdapter)
-
-class TestCommitterModule(TestBase):
-
-    def test_read_file(self):
-        data = "12345\rabcde\n12345\r\nabcde"
-        tfn = self.tempfile(data, "wb")
-        x = committer.read_file(tfn)
-        self.assertEqual(x, data)
-
-    def test_set_item_non_icontainer(self):
-        container = {}
-        committer.set_item(container, "foo", 42)
-        self.assertEqual(container, {"foo": 42})
-
-    def test_set_item_icontainer_new(self):
-        container = PretendContainer()
-        committer.set_item(container, "foo", 42)
-        self.assertEqual(container.holding, {"foo": 42})
-
-    def test_set_item_icontainer_replace(self):
-        container = PretendContainer()
-        committer.set_item(container, "foo", 42)
-        committer.set_item(container, "foo", 24, replace=True)
-        self.assertEqual(container.holding, {"foo": 24})
-
-    def test_set_item_icontainer_error_nonexisting(self):
-        container = PretendContainer()
-        self.assertRaises(KeyError, committer.set_item,
-                          container, "foo", 42, replace=True)
-
-    def create_object(self, *args, **kw):
-        # Helper for the create_object() tests.
-        c = Committer(syncer.getSerializer,
-                      getAnnotations=syncer.getAnnotations)
-        c.create_object(*args, **kw)
-
-    def test_create_object_extra(self):
-        class TestContainer(object):
-            # simulate AttrMapping
-            def __setitem__(self, name, value):
-                self.name = name
-                self.value = value
-        class TestRoot(object):
-            zope.interface.implements(IContainmentRoot, ITraverser)
-            def traverse(self, *args):
-                pass
-        fspath = tempfile.mktemp()
-        f = open(fspath, 'w')
-        f.write('<pickle> <string>text/plain</string> </pickle>')
-        f.close()
-        container = TestContainer()
-        name = "contentType"
-        root = TestRoot()
-        try:
-            self.create_object(container, name, {}, fspath, context=root)
-        finally:
-            os.remove(fspath)
-        self.assertEqual(container.name, name)
-        self.assertEqual(container.value, "text/plain")
-
-    def test_create_object_factory_file(self):
-        syncer.provideSynchronizer(dict, DictAdapter)
-        container = {}
-        entry = {"flag": "added", "factory": "__builtin__.dict"}
-        tfn = os.path.join(self.tempdir(), "foo")
-        data = {"hello": "world"}
-        self.writefile(dumps(data), tfn)
-        self.create_object(container, "foo", entry, tfn)
-        self.assertEqual(container, {"foo": data})
-
-    def test_create_object_factory_directory(self):
-        syncer.provideSynchronizer(PretendContainer, DirectoryAdapter)
-        container = {}
-        entry = {"flag": "added", "factory": PCname}
-        tfn = os.path.join(self.tempdir(), "foo")
-        os.mkdir(tfn)
-        self.create_object(container, "foo", entry, tfn)
-        self.assertEqual(container.keys(), ["foo"])
-        self.assertEqual(container["foo"].__class__, PretendContainer)
-
-    def test_create_object_default(self):
-        container = PretendRootContainer()
-        entry = {"flag": "added"}
-        data = ["hello", "world"]
-        tfn = os.path.join(self.tempdir(), "foo")
-        self.writefile(dumps(data), tfn, "wb")
-        self.create_object(container, "foo", entry, tfn)
-        self.assertEqual(container.items(), [("foo", ["hello", "world"])])
-
-    def test_create_object_ifilefactory(self):
-        zope.component.provideAdapter(file_factory_maker)
-        container = PretendContainer()
-        entry = {"flag": "added"}
-        data = ["hello", "world"]
-        tfn = os.path.join(self.tempdir(), "foo")
-        self.writefile(dumps(data), tfn, "wb")
-        self.create_object(container, "foo", entry, tfn)
-        self.assertEqual(container.holding, {"foo": ["hello", "world"]})
-
-    def test_create_object_idirectoryfactory(self):
-        zope.component.provideAdapter(directory_factory_maker)
-        container = PretendContainer()
-        entry = {"flag": "added"}
-        tfn = os.path.join(self.tempdir(), "foo")
-        os.mkdir(tfn)
-        self.create_object(container, "foo", entry, tfn)
-        self.assertEqual(container.holding["foo"].__class__, PretendContainer)
-
-
-class TestCheckerClass(TestBase):
-
-    def setUp(self):
-        # Set up base class (PlacelessSetup and TempNames)
-        TestBase.setUp(self)
-
-        # Set up environment
-        syncer.provideSynchronizer(PretendContainer, DirectoryAdapter)
-        syncer.provideSynchronizer(dict, DictAdapter)
-        zope.component.provideAdapter(file_factory_maker)
-        zope.component.provideAdapter(directory_factory_maker)
-
-        # Set up fixed part of object tree
-        self.parent = PretendContainer()
-        self.child = PretendContainer()
-        self.grandchild = PretendContainer()
-        self.parent["child"] = self.child
-        self.child["grandchild"] = self.grandchild
-        self.foo = ["hello", "world"]
-        self.child["foo"] = self.foo
-
-        # Set up fixed part of filesystem tree
-        self.parentdir = self.tempdir()
-        self.childdir = os.path.join(self.parentdir, "child")
-        os.mkdir(self.childdir)
-        self.foofile = os.path.join(self.childdir, "foo")
-        self.writefile(dumps(self.foo), self.foofile, "wb")
-        self.originalfoofile = fsutil.getoriginal(self.foofile)
-        self.writefile(dumps(self.foo), self.originalfoofile, "wb")
-        self.grandchilddir = os.path.join(self.childdir, "grandchild")
-        os.mkdir(self.grandchilddir)
-
-        # Set up metadata
-        self.metadata = MockMetadata()
-        self.getentry = self.metadata.getentry
-
-        # Set up fixed part of entries
-        self.parententry = self.getentry(self.parentdir)
-        self.parententry["path"] = "/parent"
-        self.childentry = self.getentry(self.childdir)
-        self.childentry["path"] = "/parent/child"
-        self.grandchildentry = self.getentry(self.grandchilddir)
-        self.grandchildentry["path"] = "/parent/child/grandchild"
-        self.fooentry = self.getentry(self.foofile)
-        self.fooentry["path"] = "/parent/child/foo"
-
-        # Set up checker
-        self.checker = Checker(syncer.getSerializer, self.metadata,
-                               getAnnotations=syncer.getAnnotations)
-
-    def check_errors(self, expected_errors):
-        # Helper to call the checker and assert a given set of errors
-        self.checker.check(self.parent, "", self.parentdir)
-        self.assertEqual(sort(self.checker.errors()), sort(expected_errors))
-
-    def check_no_errors(self):
-        # Helper to call the checker and assert there are no errors
-        self.check_errors([])
-
-    def test_vanilla(self):
-        # The vanilla situation should not be an error
-        self.check_no_errors()
-
-    def test_file_changed(self):
-        # Changing a file is okay
-        self.newfoo = self.foo + ["news"]
-        self.writefile(dumps(self.newfoo), self.foofile, "wb")
-        self.check_no_errors()
-
-    def test_file_type_changed(self):
-        # Changing a file's type is okay
-        self.newfoo = ("one", "two")
-        self.fooentry["type"] = "__builtin__.tuple"
-        self.writefile(dumps(self.newfoo), self.foofile, "wb")
-        self.check_no_errors()
-
-    def test_file_conflict(self):
-        # A real conflict is an error
-        newfoo = self.foo + ["news"]
-        self.writefile(dumps(newfoo), self.foofile, "wb")
-        self.foo.append("something else")
-        self.check_errors([self.foofile])
-
-    def test_file_sticky_conflict(self):
-        # A sticky conflict is an error
-        self.fooentry["conflict"] = 1
-        self.check_errors([self.foofile])
-
-    def test_file_added(self):
-        # Adding a file properly is okay
-        self.bar = ["this", "is", "bar"]
-        barfile = os.path.join(self.childdir, "bar")
-        self.writefile(dumps(self.bar), barfile, "wb")
-        barentry = self.getentry(barfile)
-        barentry["flag"] = "added"
-        self.check_no_errors()
-
-    def test_file_added_no_file(self):
-        # Flagging a non-existing file as added is an error
-        barfile = os.path.join(self.childdir, "bar")
-        barentry = self.getentry(barfile)
-        barentry["flag"] = "added"
-        self.check_errors([barfile])
-
-    def test_file_spurious(self):
-        # A spurious file (empty entry) is okay
-        bar = ["this", "is", "bar"]
-        barfile = os.path.join(self.childdir, "bar")
-        self.writefile(dumps(bar), barfile, "wb")
-        self.check_no_errors()
-
-    def test_file_added_no_flag(self):
-        # Adding a file without setting the "added" flag is an error
-        bar = ["this", "is", "bar"]
-        barfile = os.path.join(self.childdir, "bar")
-        self.writefile(dumps(bar), barfile, "wb")
-        barentry = self.getentry(barfile)
-        barentry["path"] = "/parent/child/bar"
-        self.check_errors([barfile])
-
-    def test_file_added_twice(self):
-        # Adding a file in both places is an error
-        bar = ["this", "is", "bar"]
-        self.child["bar"] = bar
-        barfile = os.path.join(self.childdir, "bar")
-        self.writefile(dumps(bar), barfile, "wb")
-        barentry = self.getentry(barfile)
-        barentry["path"] = "/parent/child/bar"
-        self.check_errors([barfile])
-
-    def test_file_lost(self):
-        # Losing a file is an error
-        os.remove(self.foofile)
-        self.check_errors([self.foofile])
-
-    def test_file_lost_originial(self):
-        # Losing the original file is an error
-        os.remove(self.originalfoofile)
-        self.check_errors([self.foofile])
-
-    def test_file_removed(self):
-        # Removing a file properly is okay
-        os.remove(self.foofile)
-        self.fooentry["flag"] = "removed"
-        self.check_no_errors()
-
-    def test_file_removed_conflict(self):
-        # Removing a file that was changed in the database is an error
-        os.remove(self.foofile)
-        self.fooentry["flag"] = "removed"
-        self.foo.append("news")
-        self.check_errors([self.foofile])
-
-    def test_file_removed_twice(self):
-        # Removing a file in both places is an error
-        os.remove(self.foofile)
-        self.fooentry["flag"] = "removed"
-        del self.child["foo"]
-        self.check_errors([self.foofile])
-
-    def test_file_removed_object(self):
-        # Removing the object should cause a conflict
-        del self.child["foo"]
-        self.check_errors([self.foofile])
-
-    def test_file_entry_cleared(self):
-        # Clearing out a file's entry is an error
-        self.fooentry.clear()
-        self.check_errors([self.foofile])
-
-    def test_dir_added(self):
-        # Adding a directory is okay
-        bardir = os.path.join(self.childdir, "bar")
-        os.mkdir(bardir)
-        barentry = self.getentry(bardir)
-        barentry["flag"] = "added"
-        self.check_no_errors()
-
-    def test_dir_spurious(self):
-        # A spurious directory is okay
-        bardir = os.path.join(self.childdir, "bar")
-        os.mkdir(bardir)
-        self.check_no_errors()
-
-    def test_dir_added_no_flag(self):
-        # Adding a directory without setting the "added" flag is an error
-        bardir = os.path.join(self.childdir, "bar")
-        os.mkdir(bardir)
-        barentry = self.getentry(bardir)
-        barentry["path"] = "/parent/child/bar"
-        self.check_errors([bardir])
-
-    def test_dir_lost(self):
-        # Losing a directory is an error
-        shutil.rmtree(self.grandchilddir)
-        self.check_errors([self.grandchilddir])
-
-    def test_dir_removed(self):
-        # Removing a directory properly is okay
-        shutil.rmtree(self.grandchilddir)
-        self.grandchildentry["flag"] = "removed"
-        self.check_no_errors()
-
-    def test_dir_entry_cleared(self):
-        # Clearing ot a directory's entry is an error
-        self.grandchildentry.clear()
-        self.check_errors([self.grandchilddir])
-
-    def test_tree_added(self):
-        # Adding a subtree is okay
-        bardir = os.path.join(self.childdir, "bar")
-        os.mkdir(bardir)
-        barentry = self.getentry(bardir)
-        barentry["path"] = "/parent/child/bar"
-        barentry["flag"] = "added"
-        bazfile = os.path.join(bardir, "baz")
-        self.baz = ["baz"]
-        self.writefile(dumps(self.baz), bazfile, "wb")
-        bazentry = self.getentry(bazfile)
-        bazentry["flag"] = "added"
-        burpdir = os.path.join(bardir, "burp")
-        os.mkdir(burpdir)
-        burpentry = self.getentry(burpdir)
-        burpentry["flag"] = "added"
-        self.check_no_errors()
-
-    def test_tree_added_no_flag(self):
-        # Adding a subtree without flagging everything as added is an error
-        bardir = os.path.join(self.childdir, "bar")
-        os.mkdir(bardir)
-        barentry = self.getentry(bardir)
-        barentry["path"] = "/parent/child/bar"
-        barentry["flag"] = "added"
-        bazfile = os.path.join(bardir, "baz")
-        baz = ["baz"]
-        self.writefile(dumps(baz), bazfile, "wb")
-        bazentry = self.getentry(bazfile)
-        bazentry["path"] = "/parent/child/bar/baz"
-        burpdir = os.path.join(bardir, "burp")
-        os.mkdir(burpdir)
-        burpentry = self.getentry(burpdir)
-        burpentry["path"] = "/parent/child/bar/burp"
-        self.check_errors([bazfile, burpdir])
-
-    def test_tree_removed(self):
-        # Removing a subtree is okay
-        shutil.rmtree(self.childdir)
-        self.childentry["flag"] = "removed"
-        self.grandchildentry.clear()
-        self.fooentry.clear()
-        self.check_no_errors()
-
-    # TODO Extra and Annotations is not tested directly
-
-    # TODO Changing directories into files or vice versa is not tested
-
-
-
-class TestCommitterClass(TestCheckerClass):
-
-    # This class extends all tests from TestCheckerClass that call
-    # self.check_no_errors() to carry out the change and check on it.
-    # Yes, this means that all the tests that call check_errors() are
-    # repeated.  Big deal. :-)
-
-    def __init__(self, name):
-        TestCheckerClass.__init__(self, name)
-        self.name = name
-
-    def setUp(self):
-        TestCheckerClass.setUp(self)
-        self.committer = Committer(syncer.getSerializer, self.metadata,
-                                   getAnnotations=syncer.getAnnotations)
-
-    def check_no_errors(self):
-        TestCheckerClass.check_no_errors(self)
-        self.committer.synch(self.parent, "", self.parentdir)
-        name = "verify" + self.name[4:]
-        method = getattr(self, name, None)
-        if method:
-            method()
-        else:
-            print "?", name
-
-    def verify_vanilla(self):
-        self.assertEqual(self.parent.keys(), ["child"])
-        self.assertEqual(self.parent["child"], self.child)
-        self.assertEqual(sort(self.child.keys()), ["foo", "grandchild"])
-        self.assertEqual(self.child["foo"], self.foo)
-        self.assertEqual(self.child["grandchild"], self.grandchild)
-        self.assertEqual(self.grandchild.keys(), [])
-
-    def verify_file_added(self):
-        self.assertEqual(self.child["bar"], self.bar)
-        self.assertEqual(sort(self.child.keys()), ["bar", "foo", "grandchild"])
-
-    def verify_file_changed(self):
-        self.assertEqual(self.child["foo"], self.newfoo)
-    
-    def verify_file_removed(self):
-        self.assertEqual(self.child.keys(), ["grandchild"])
-
-    def verify_file_spurious(self):
-        self.verify_vanilla()
-
-    def verify_file_type_changed(self):
-        self.assertEqual(self.child["foo"], self.newfoo)
-
-    def verify_dir_removed(self):
-        self.assertEqual(self.child.keys(), ["foo"])
-
-    def verify_dir_added(self):
-        self.assertEqual(sort(self.child.keys()), ["bar", "foo", "grandchild"])
-
-    def verify_dir_spurious(self):
-        self.verify_vanilla()
-
-    def verify_tree_added(self):
-        self.assertEqual(sort(self.child.keys()), ["bar", "foo", "grandchild"])
-        bar = self.child["bar"]
-        self.assertEqual(bar.__class__, PretendContainer)
-        baz = bar["baz"]
-        self.assertEqual(self.baz, baz)
-
-    def verify_tree_removed(self):
-        self.assertEqual(self.parent.keys(), [])
-
-
-def test_suite():
-    s = unittest.TestSuite()
-    s.addTest(unittest.makeSuite(TestSyncerModule))
-    s.addTest(unittest.makeSuite(TestCommitterModule))
-    s.addTest(unittest.makeSuite(TestCheckerClass))
-    s.addTest(unittest.makeSuite(TestCommitterClass))
-    return s
-
-def test_main():
-    unittest.TextTestRunner().run(test_suite())
-
-if __name__=='__main__':
-    test_main()

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/tests/test_fspickle.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/test_fspickle.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/test_fspickle.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,60 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2003 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (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.
-#
-##############################################################################
-"""Location support tests
-
-$Id$
-"""
-import unittest
-from zope.testing.doctestunit import DocTestSuite
-from zope.interface import directlyProvides
-from zope.traversing.interfaces import IContainmentRoot
-from zope.location.tests import TLocation
-
-from zope.app.fssync import fspickle
-
-class PersistentLoaderTestCase(unittest.TestCase):
-
-    def setUp(self):
-        root = TLocation()
-        directlyProvides(root, IContainmentRoot)
-        o1 = TLocation(); o1.__parent__ = root; o1.__name__ = 'o1'
-        o2 = TLocation(); o2.__parent__ = root; o2.__name__ = 'o2'
-        o3 = TLocation(); o3.__parent__ = o1; o3.__name__ = 'o3'
-        root.o1 = o1
-        root.o2 = o2
-        o1.foo = o2
-        o1.o3 = o3
-        self.root = root
-        self.o1 = o1
-        self.o2 = o2
-
-    def testPersistentLoader(self):
-        loader = fspickle.PersistentLoader(self.o1)
-        self.assert_(loader.load('/') is self.root)
-        self.assert_(loader.load('/o2') is self.o2)
-
-    def testParentPersistentLoader(self):
-        loader = fspickle.ParentPersistentLoader(self.o1, self.o1)
-        self.assert_(loader.load(fspickle.PARENT_MARKER) is self.o1)
-        self.assert_(loader.load('/') is self.root)
-        self.assert_(loader.load('/o2') is self.o2)
-
-
-def test_suite():
-    suite = unittest.makeSuite(PersistentLoaderTestCase)
-    suite.addTest(DocTestSuite('zope.app.fssync.fspickle'))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main()

Added: zope.app.fssync/trunk/src/zope/app/fssync/tests/test_network.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/test_network.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/test_network.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,194 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Tests for the Network class.
+
+$Id: test_network.py 26878 2004-08-03 16:25:34Z jim $
+"""
+
+import os
+import select
+import socket
+import unittest
+import threading
+
+from StringIO import StringIO
+
+from os.path import join
+
+from zope.app.fssync.fssync import Network, Error
+from zope.fssync.tests.tempfiles import TempFiles
+
+sample_rooturl = "http://user:passwd@host:8080/path"
+
+HOST = "127.0.0.1"     # localhost
+PORT = 60841           # random number
+RESPONSE = """HTTP/1.0 404 Not found\r
+Content-type: text/plain\r
+Content-length: 0\r
+\r
+"""
+
+class DummyServer(threading.Thread):
+    """A server that can handle one HTTP request (returning a 404 error)."""
+
+    def __init__(self, ready):
+        self.ready = ready     # Event signaling we're listening
+        self.stopping = False
+        threading.Thread.__init__(self)
+
+    def run(self):
+        svr = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        svr.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        svr.bind((HOST, PORT))
+        svr.listen(1)
+        self.ready.set()
+        conn = None
+        sent_response = False
+        while not self.stopping:
+            if conn is None:
+                r = [svr]
+            else:
+                r = [conn]
+            r, w, x = select.select(r, [], [], 0.01)
+            if not r:
+                continue
+            s = r[0]
+            if s is svr:
+                conn, addr = svr.accept()
+                ##print "connect from", `addr`
+            else:
+                if s is not conn:
+                    raise AssertionError("s is not conn")
+                data = conn.recv(1000)
+                ##print "received", `data`
+                if not data:
+                    break
+                if not sent_response:
+                    conn.send(RESPONSE)
+                    conn.close()
+                    conn = None
+                    sent_response = True
+        if conn is not None:
+            conn.close()
+        svr.close()
+        ##print "stopped"
+        
+    def stop(self):
+        ##print "stopping"
+        self.stopping = True
+
+class TestNetwork(TempFiles):
+
+    def setUp(self):
+        TempFiles.setUp(self)
+        self.network = Network()
+
+    def test_initial_state(self):
+        self.assertEqual(self.network.rooturl, None)
+        self.assertEqual(self.network.roottype, None)
+        self.assertEqual(self.network.rootpath, None)
+        self.assertEqual(self.network.user_passwd, None)
+        self.assertEqual(self.network.host_port, None)
+
+    def test_setrooturl(self):
+        self.network.setrooturl(sample_rooturl)
+        self.assertEqual(self.network.rooturl, sample_rooturl)
+        self.assertEqual(self.network.roottype, "http")
+        self.assertEqual(self.network.rootpath, "/path")
+        self.assertEqual(self.network.user_passwd, "user:passwd")
+        self.assertEqual(self.network.host_port, "host:8080")
+
+    def test_setrooturl_nopath(self):
+        rooturl = "http://user:passwd@host:8080"
+        self.network.setrooturl(rooturl)
+        self.assertEqual(self.network.rooturl, rooturl)
+        self.assertEqual(self.network.roottype, "http")
+        self.assertEqual(self.network.rootpath, "/")
+        self.assertEqual(self.network.user_passwd, "user:passwd")
+        self.assertEqual(self.network.host_port, "host:8080")
+
+    def test_findrooturl_notfound(self):
+        # TODO: This test will fail if a file /tmp/@@Zope/Root exists :-(
+        target = self.tempdir()
+        self.assertEqual(self.network.findrooturl(target), None)
+
+    def test_findrooturl_found(self):
+        target = self.tempdir()
+        zdir = join(target, "@@Zope")
+        os.mkdir(zdir)
+        rootfile = join(zdir, "Root")
+        f = open(rootfile, "w")
+        f.write(sample_rooturl + "\n")
+        f.close()
+        self.assertEqual(self.network.findrooturl(target), sample_rooturl)
+
+    def test_saverooturl(self):
+        self.network.setrooturl(sample_rooturl)
+        target = self.tempdir()
+        zdir = join(target, "@@Zope")
+        os.mkdir(zdir)
+        rootfile = join(zdir, "Root")
+        self.network.saverooturl(target)
+        f = open(rootfile, "r")
+        data = f.read()
+        f.close()
+        self.assertEqual(data.strip(), sample_rooturl)
+
+    def test_loadrooturl(self):
+        target = self.tempdir()
+        self.assertRaises(Error, self.network.loadrooturl, target)
+        zdir = join(target, "@@Zope")
+        os.mkdir(zdir)
+        self.network.setrooturl(sample_rooturl)
+        self.network.saverooturl(target)
+        new = Network()
+        new.loadrooturl(target)
+        self.assertEqual(new.rooturl, sample_rooturl)
+
+    def test_httpreq(self):
+        ready = threading.Event()
+        svr = DummyServer(ready)
+        svr.start()
+        ready.wait()
+        try:
+            self.network.setrooturl("http://%s:%s" % (HOST, PORT))
+            self.assertRaises(Error, self.network.httpreq, "/xyzzy", "@@view")
+        finally:
+            svr.stop()
+            svr.join()
+
+    def test_slurptext_html(self):
+        fp = StringIO("<p>This is some\n\ntext.</p>\n")
+        result = self.network.slurptext(fp, {"Content-type": "text/html"})
+        self.assertEqual(result, "This is some text.")
+
+    def test_slurptext_plain(self):
+        fp = StringIO("<p>This is some\n\ntext.</p>\n")
+        result = self.network.slurptext(fp, {"Content-type": "text/plain"})
+        self.assertEqual(result, "<p>This is some\n\ntext.</p>")
+
+    def test_slurptext_nontext(self):
+        fp = StringIO("<p>This is some\n\ntext.</p>\n")
+        result = self.network.slurptext(fp, {"Content-type": "foo/bar"})
+        self.assertEqual(result, "Content-type: foo/bar")
+
+def test_suite():
+    loader = unittest.TestLoader()
+    return loader.loadTestsFromTestCase(TestNetwork)
+
+def test_main():
+    unittest.TextTestRunner().run(test_suite())
+
+if __name__=='__main__':
+    test_main()

Added: zope.app.fssync/trunk/src/zope/app/fssync/tests/test_passwd.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/test_passwd.py	                        (rev 0)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/test_passwd.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -0,0 +1,184 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (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.
+#
+##############################################################################
+"""Test the authentication token manager.
+
+$Id: test_passwd.py 26559 2004-07-15 21:22:32Z srichter $
+"""
+import os
+import tempfile
+import unittest
+
+from zope.app.fssync import passwd
+
+
+class PasswordGetter(object):
+    """PasswordManager.getPassword() replacement to use in the tests."""
+
+    def __call__(self, user, host_port):
+        self.user = user
+        self.host_port = host_port
+        return "mypassword"
+
+
+class TestPasswordManager(unittest.TestCase):
+
+    def setUp(self):
+        self.filename = tempfile.mktemp()
+        self.pwmgr = passwd.PasswordManager(filename=self.filename)
+        self.getter = PasswordGetter()
+        self.pwmgr.getPassword = self.getter
+
+    def tearDown(self):
+        if os.path.exists(self.filename):
+            os.unlink(self.filename)
+
+    def create_file(self, include_comment=True):
+        """Create the file with a single record."""
+        f = open(self.filename, "w")
+        if include_comment:
+            print >>f, "# this is a comment"
+        print >>f
+        print >>f, "http", "example.com", "testuser", "faketoken"
+        f.close()
+
+    def read_file(self):
+        """Return a list of non-blank, non-comment lines from the file."""
+        f = open(self.filename)
+        lines = f.readlines()
+        f.close()
+        return [line.split()
+                for line in lines
+                if line.strip()[:1] not in ("#", "")]
+
+    # getToken()
+
+    def test_hostport_normalization(self):
+        token1 = self.pwmgr.getToken("http", "example.com", "testuser")
+        token2 = self.pwmgr.getToken("http", "example.com:80", "testuser")
+        self.assertEqual(token1, token2)
+        self.assertEqual(self.getter.host_port, "example.com")
+
+    def test_load_token_from_file(self):
+        self.create_file()
+        token = self.pwmgr.getToken("http", "example.com:80", "testuser")
+        self.assertEqual(token, "faketoken")
+        self.failIf(hasattr(self.getter, "user"))
+        self.failIf(hasattr(self.getter, "host_post"))
+
+    def test_load_token_missing_from_file(self):
+        self.create_file()
+        token = self.pwmgr.getToken("http", "example.com:80", "otheruser")
+        self.assertNotEqual(token, "faketoken")
+        self.assertEqual(self.getter.user, "otheruser")
+        self.assertEqual(self.getter.host_port, "example.com")
+
+    def test_diff_in_scheme(self):
+        self.create_file()
+        token = self.pwmgr.getToken("https", "example.com", "testuser")
+        self.assertNotEqual(token, "faketoken")
+
+    def test_diff_in_host(self):
+        self.check_difference("http", "example.net", "testuser")
+
+    def test_diff_in_port(self):
+        self.check_difference("http", "example.com:9000", "testuser")
+
+    def test_diff_in_username(self):
+        self.check_difference("http", "example.com", "otheruser")
+
+    def check_difference(self, scheme, host_port, username):
+        self.create_file()
+        token = self.pwmgr.getToken(scheme, host_port, username)
+        self.assertNotEqual(token, "faketoken")
+        self.assertEqual(self.getter.user, username)
+        self.assertEqual(self.getter.host_port, host_port)
+
+    # addToken()
+
+    def test_add_token_to_new_file(self):
+        self.pwmgr.addToken("http", "example.com:80", "testuser", "faketoken")
+        records = self.read_file()
+        self.assertEqual(len(records), 1)
+        self.assertEqual(records[0],
+                         ["http", "example.com", "testuser", "faketoken"])
+
+    def test_add_token_to_file(self):
+        self.create_file()
+        self.pwmgr.addToken("http", "example.com", "otheruser", "mytoken")
+        records = self.read_file()
+        records.sort()
+        self.assertEqual(len(records), 2)
+        self.assertEqual(records,
+                         [["http", "example.com", "otheruser", "mytoken"],
+                          ["http", "example.com", "testuser",  "faketoken"],
+                          ])
+
+    def test_replace_token_from_file(self):
+        self.create_file()
+        self.pwmgr.addToken("http", "example.com", "testuser", "newtoken")
+        records = self.read_file()
+        self.assertEqual(len(records), 1)
+        self.assertEqual(records[0],
+                         ["http", "example.com", "testuser", "newtoken"])
+
+    # removeToken()
+
+    def test_remove_without_file(self):
+        found = self.pwmgr.removeToken("http", "example.com", "someuser")
+        self.assert_(not found)
+
+    def test_remove_not_in_file(self):
+        self.create_file()
+        found = self.pwmgr.removeToken("http", "example.com", "someuser")
+        self.assert_(not found)
+        # file should not have been modified
+        records = self.read_file()
+        self.assertEqual(len(records), 1)
+        self.assertEqual(records[0],
+                         ["http", "example.com", "testuser", "faketoken"])
+
+    def test_remove_last_in_file_with_comment(self):
+        self.create_file()
+        found = self.pwmgr.removeToken("http", "example.com", "testuser")
+        self.assert_(found)
+        records = self.read_file()
+        self.assertEqual(len(records), 0)
+        # the file included a comment, so must not have been removed:
+        self.assert_(os.path.exists(self.filename))
+
+    def test_remove_last_in_file_without_comment(self):
+        self.create_file(include_comment=False)
+        found = self.pwmgr.removeToken("http", "example.com", "testuser")
+        self.assert_(found)
+        # the result should only include a blank line, so should be removed:
+        self.assert_(not os.path.exists(self.filename))
+
+    def test_remove_one_of_two(self):
+        f = open(self.filename, "w")
+        print >>f, "http", "example.com", "testuser",  "faketoken"
+        print >>f, "http", "example.com", "otheruser", "othertoken"
+        f.close()
+        found = self.pwmgr.removeToken("http", "example.com", "testuser")
+        self.assert_(found)
+        records = self.read_file()
+        self.assertEqual(len(records), 1)
+        self.assertEqual(records[0],
+                         ["http", "example.com", "otheruser", "othertoken"])
+
+
+def test_suite():
+    return unittest.makeSuite(TestPasswordManager)
+
+if __name__ == "__main__":
+    unittest.main(defaultTest="test_suite")

Deleted: zope.app.fssync/trunk/src/zope/app/fssync/tests/test_registration.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/tests/test_registration.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/tests/test_registration.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -1,34 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2003 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (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.
-#
-##############################################################################
-"""Registration tests
-
-$Id: test_registration.py 40495 2005-12-02 17:51:22Z efge $
-"""
-import unittest
-from zope.testing import doctest, module
-
-def setUp(test):
-    module.setUp(test, 'zope.app.fssync.registration_txt')
-
-def tearDown(test):
-    module.tearDown(test, 'zope.app.fssync.registration_txt')
-
-def test_suite():
-    return unittest.TestSuite((
-        doctest.DocFileSuite('../registration.txt',
-                             setUp=setUp, tearDown=tearDown,
-                             optionflags=doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS),
-        ))
-
-if __name__ == '__main__': unittest.main()

Modified: zope.app.fssync/trunk/src/zope/app/fssync/zptpage/adapter.py
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/zptpage/adapter.py	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/zptpage/adapter.py	2007-06-14 07:05:37 UTC (rev 76675)
@@ -16,21 +16,22 @@
 $Id$
 """
 from zope.interface import implements
-from zope.fssync.server.entryadapter import ObjectEntryAdapter
-from zope.fssync.server.interfaces import IObjectFile
+from zope.fssync import synchronizer
+from zope.fssync import interfaces
 
-class ZPTPageAdapter(ObjectEntryAdapter):
-    """ObjectFile adapter for ZPT page objects.
+class ZPTPageAdapter(synchronizer.FileSynchronizer):
+    """Synchronizer adapter for ZPT page objects.
     """
-    implements(IObjectFile)
+    implements(interfaces.IFileSynchronizer)
 
-    def getBody(self):
-        return self.context.getSource()
+    def dump(self, writeable):
+        writeable.write(self.context.getSource())
 
-    def setBody(self, data):
+    def load(self, readable):
         # Convert the data to Unicode, since that's what ZPTPage wants;
         # it's normally read from a file so it'll be bytes.
 
         # Sometimes we cannot communicate an encoding. Zope's default is UTF-8,
         # so use it.
+        data = readable.read()
         self.context.setSource(data.decode('UTF-8'))

Modified: zope.app.fssync/trunk/src/zope/app/fssync/zptpage/configure.zcml
===================================================================
--- zope.app.fssync/trunk/src/zope/app/fssync/zptpage/configure.zcml	2007-06-14 05:09:41 UTC (rev 76674)
+++ zope.app.fssync/trunk/src/zope/app/fssync/zptpage/configure.zcml	2007-06-14 07:05:37 UTC (rev 76675)
@@ -6,7 +6,7 @@
   <utility
       component=".adapter.ZPTPageAdapter"
       name="zope.app.zptpage.zptpage.ZPTPage"
-      provides="zope.app.fssync.interfaces.IFSSyncFactory"
+      provides="zope.fssync.interfaces.ISynchronizer"
       permission="zope.ManageContent"
       />
 
@@ -15,7 +15,7 @@
       
       <require
           permission="zope.ManageCode"
-          interface="zope.fssync.server.interfaces.IObjectFile" />
+          interface="zope.fssync.interfaces.ISynchronizer" />
           
    </class>
 



More information about the Checkins mailing list