[Zope3-checkins] CVS: Zope3/src/zope/app/fssync - committer.py:1.1
Guido van Rossum
guido@python.org
Tue, 27 May 2003 15:41:44 -0400
Update of /cvs-repository/Zope3/src/zope/app/fssync
In directory cvs.zope.org:/tmp/cvs-serv1071
Added Files:
committer.py
Log Message:
A new committer, replacing fromFS(). With unit tests!
=== Added File Zope3/src/zope/app/fssync/committer.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Commit changes from the filesystem.
$Id: committer.py,v 1.1 2003/05/27 19:41:44 gvanrossum Exp $
"""
import os
import shutil
from zope.component import queryAdapter, getService
from zope.xmlpickle import dumps, loads
from zope.configuration.name import resolve
from zope.proxy.introspection import removeAllProxies
from zope.fssync.metadata import Metadata
from zope.fssync import fsutil
from zope.app.interfaces.fssync \
import IObjectEntry, IObjectDirectory, IObjectFile
from zope.app.interfaces.annotation import IAnnotations
from zope.app.interfaces.container import IContainer
from zope.app.fssync.classes import Default
from zope.app.traversing import getPath
from zope.app.interfaces.file import IFileFactory
class SynchronizationError(Exception):
pass
class Committer(object):
"""Commit changes from the filesystem to the object database.
The filesystem's originals should be consistent with the object database.
"""
def __init__(self, metadata=None):
"""Constructor. Optionally pass a metadata database."""
if metadata is None:
metadata = Metadata()
self.metadata = metadata
self.conflicts = []
def report_conflict(self, fspath):
"""Helper to report a conflict.
Conflicts can be retrieved by calling get_errors().
"""
if fspath not in self.conflicts:
self.conflicts.append(fspath)
def get_errors(self):
"""Get 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 synch(self, container, name, fspath):
"""Synchronize an object or object tree from the filesystem.
If the originals on the filesystem is not uptodate, errors are
reported by calling report_conflict(), but no exception is
raised unless something unexpected is wrong.
SynchronizationError is raised for errors that can't be
corrected by a update operation.
"""
if (os.sep in name or
(os.altsep and os.altsep in name) or
name == os.curdir or
name == os.pardir):
# This name can't be mapped safely to the filesystem :-(
raise SynchronizationError("invalid separator in name %r" % name)
if not name:
self.synch_dir(container, fspath)
else:
if name not in container:
self.synch_new(container, name, fspath)
else:
self.synch_old(container, name, fspath)
# Now update extra and annotations
if name in container:
obj = container[name]
adapter = self.get_adapter(obj)
extra = adapter.extra()
extrapath = fsutil.getextra(fspath)
if extra is not None and os.path.exists(extrapath):
self.synch_dir(extra, extrapath)
ann = queryAdapter(obj, IAnnotations)
annpath = fsutil.getannotations(fspath)
if ann is not None and os.path.exists(annpath):
self.synch_dir(ann, annpath)
def synch_dir(self, container, fspath):
"""Helper to synchronize a directory."""
adapter = self.get_adapter(container)
nameset = {}
if IObjectDirectory.isImplementedBy(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.synch(container, name, os.path.join(fspath, name))
def synch_new(self, container, name, fspath):
"""Helper to synchronize a new object."""
entry = self.metadata.getentry(fspath)
if entry:
if entry.get("flag") != "added":
self.report_conflict(fspath)
else:
del entry["flag"]
if not os.path.exists(fspath):
self.report_conflict(fspath)
return
self.create_object(container, name, entry, fspath)
obj = container[name]
adapter = self.get_adapter(obj)
if IObjectDirectory.isImplementedBy(adapter):
self.synch_dir(obj, fspath)
def synch_old(self, container, name, fspath):
"""Helper to synchronize an existing object."""
entry = self.metadata.getentry(fspath)
obj = container[name]
adapter = self.get_adapter(obj)
if IObjectDirectory.isImplementedBy(adapter):
self.synch_dir(obj, fspath)
if entry.get("flag") == "removed":
del container[name]
entry.clear()
self.remove_all(fspath)
else:
if entry.get("flag") == "added":
self.report_conflict(fspath)
del entry["flag"]
oldfspath = fsutil.getoriginal(fspath)
if not os.path.exists(oldfspath):
self.report_conflict(fspath)
olddata = None
else:
olddata = self.read_file(oldfspath)
curdata = adapter.getBody()
if curdata != olddata:
self.report_conflict(fspath)
if entry.get("flag") == "removed":
if os.path.exists(fspath):
self.report_conflict(fspath)
del container[name]
entry.clear()
self.remove_all(fspath)
else:
if adapter.typeIdentifier() != entry.get("type"):
self.create_object(container, name, entry, fspath,
replace=True)
else:
newdata = self.read_file(fspath)
if newdata != olddata:
adapter.setBody(newdata)
newdata = adapter.getBody() # Normalize
self.write_file_and_original(newdata, fspath)
def create_object(self, container, name, entry, fspath, replace=False):
"""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()
else:
# No factory; try using the IFileFactory feature
as = getService(container, "Adapters")
isuffix = name.rfind(".")
if isuffix >= 0:
suffix = name[isuffix:]
else:
suffix = "."
factory = as.queryNamedAdapter(container, IFileFactory, suffix)
if factory is None:
factory = as.queryAdapter(container, IFileFactory)
if factory:
data = self.read_file(fspath)
obj = factory(name, None, data)
obj = removeAllProxies(obj)
else:
# Oh well, assume the file is an xml pickle
obj = self.load_file(fspath)
self.set_item(container, name, obj, replace)
adapter = self.get_adapter(obj)
entry["type"] = adapter.typeIdentifier()
entry["factory"] = adapter.factory()
if "flag" in entry:
del entry["flag"]
if IObjectFile.isImplementedBy(adapter):
newdata = adapter.getBody()
self.write_file_and_original(newdata, fspath)
def set_item(self, container, name, obj, replace=False):
"""Helper to set an item in a container or mapping."""
if IContainer.isImplementedBy(container):
if replace:
del container[name]
newname = container.setObject(name, obj)
if newname != name:
raise SynchronizationError(
"Container generated new name for %s (new name %s)" %
(name, newname))
else:
# Not a container, must be a mapping
# (This is used for extras and annotations)
container[name] = obj
def load_file(self, fspath):
"""Helper to load an xml pickle from a file."""
return loads(self.read_file(fspath, "r"))
def read_file(self, fspath, mode="rb"):
"""Helper to read the data from a file."""
assert mode in ("r", "rb")
f = open(fspath, mode)
try:
data = f.read()
finally:
f.close()
return data
def write_file_and_original(self, data, fspath, mode="wb"):
"""Helper to write data to a file *and* to its original."""
self.write_file(data, fspath, mode)
self.write_file(data, fsutil.getoriginal(fspath), mode)
def write_file(self, data, fspath, mode="wb"):
"""Helper to write data to a file."""
assert mode in ("w", "wb")
head, tail = os.path.split(fspath)
if not os.path.exists(head):
os.makedirs(head)
f = open(fspath, mode)
try:
f.write(data)
finally:
f.close()
def remove_all(self, fspath):
"""Helper to remove a path and the corresponding original."""
self.remove(fspath)
self.remove(fsutil.getoriginal(fspath))
self.remove(fsutil.getextra(fspath))
self.remove(fsutil.getannotations(fspath))
def remove(self, fspath):
"""Helper to remove a file or directory tree if it exists."""
if os.path.isdir(fspath):
shutil.rmtree(fspath)
elif os.path.exists(fspath):
os.remove(fspath)
def get_adapter(self, obj):
"""Helper to get the special fssync adapter."""
syncService = getService(obj, 'FSRegistryService')
return syncService.getSynchronizer(obj)