[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