Zope 3的文件系统同步工具。
项目描述
zope.fssync软件包
此软件包为Zope 3提供文件系统同步工具。它由zope.app.fssync软件包使用。
文件系统同步
此软件包提供了一个API,用于将Python对象与序列化文件系统表示进行同步。此API不涉及安全问题。(有关受保护的基于Web的API,请参阅zope.app.fssync)。此API与Zope和ZODB无关。
主要用例包括
数据导出/导入(例如,将数据从一个地方移动到另一个地方)
内容管理(例如,管理wiki或其他文档集合的离线操作)
目标表示取决于您的用例。在数据导出/导入用例中,例如,所有数据尽可能完全导出是至关重要的。由于大多数情况下数据不需要由人类读取,因此pickle格式可能是最完整且易于使用的。在内容管理用例中,可能更重要的是所有元数据都可供人类阅读。在这种情况下,另一种格式,例如RDFa,可能更合适。
主要组件
同步器将内容对象序列化,并将序列化数据以特定于应用程序的格式存储在应用程序的存储库中。它使用反序列化器将对象读取回内容空间。序列化格式必须足够丰富,以保留应在反序列化时重建的各种形式的引用。
所有这些组件都应可替换。应用程序可能使用不同的序列化格式,针对不同的目的(例如备份与内容管理)和不同的目标系统(例如zip存档与svn仓库)有不同的引用。
主要组件包括
ISyncTasks,如Checkout、Check和Commit,这些任务同步内容空间与仓库。这些任务使用序列化器以应用程序特定的格式生成仓库的序列化数据。它们使用反序列化器读取数据。默认实现使用xmlpickle用于Python对象,数据流用于文件内容,特殊目录用于额外数据和元数据。替代实现可能使用标准pickle,可读性高的人读格式如RDFa,或应用程序特定的格式。
ISynchronizer:同步器生成Python对象(同步器的ISerializer部分)的序列化片段,并消费序列化数据以(重新)创建Python对象(同步器的IDeserializer部分)。
IPickler:一个确定pickle格式的适配器。
IRepository:表示一个可用来读取和写入序列化数据的目标系统。
让我们来看一些示例
>>> from StringIO import StringIO >>> from zope import interface >>> from zope import component >>> from zope.fssync import interfaces >>> from zope.fssync import task >>> from zope.fssync import synchronizer >>> from zope.fssync import repository >>> from zope.fssync import pickle>>> class A(object): ... data = 'data of a' >>> class B(A): ... pass >>> a = A() >>> b = B() >>> b.data = 'data of b' >>> b.extra = 'extra of b' >>> root = dict(a=a, b=b)
持久引用
许多应用程序使用多个持久引用系统。例如,Zope使用p_oids、int ids、键引用、遍历路径、点名称、命名实用程序等。
其他系统可能使用通用的引用系统,如全局唯一ID或主键,以及与特定领域相关的引用,如电子邮件、URI、邮寄地址、代码号等。只要它们可以在导入或重新导入时解析,所有这些引用都是可导出引用的候选者。
在我们的示例中,我们使用简单的整数ID
>>> class GlobalIds(object): ... ids = dict() ... count = 0 ... def getId(self, obj): ... for k, v in self.ids.iteritems(): ... if obj == v: ... return k ... def register(self, obj): ... uid = self.getId(obj) ... if uid is not None: ... return uid ... self.count += 1 ... self.ids[self.count] = obj ... return self.count ... def resolve(self, uid): ... return self.ids.get(int(uid), None)>>> globalIds = GlobalIds() >>> globalIds.register(a) 1 >>> globalIds.register(b) 2 >>> globalIds.register(root) 3
在我们的示例中,我们使用整数ID作为默认路径引用的替代,这是Zope中最常见的引用。
在我们的示例中,我们使用一个SnarfRepository,它很容易进行检查
>>> snarf = repository.SnarfRepository(StringIO()) >>> checkout = task.Checkout(synchronizer.getSynchronizer, snarf)
Snarf是Zope3特定的存档格式,主要用于简单软件。该格式非常简单:每个文件由以下字符串表示
‘<size> <pathname>n’
后面跟着确切的<size>字节。目录没有显式表示。
条目ID
持久ID也用于fssync的元数据文件中。这些引用由IEntryId适配器生成,该适配器必须有一个字符串表示形式,以便可以将其保存在文本文件中。通常,这些对象ID对应于持久的pickle ID,但这并非必然。
由于我们没有路径,我们使用我们的整数ID
>>> @component.adapter(interface.Interface) ... @interface.implementer(interfaces.IEntryId) ... def entryId(obj): ... global globalIds ... return globalIds.getId(obj) >>> component.provideAdapter(entryId)
同步器
在数据导出/导入的使用场景中,fssync能够序列化“所有”对象数据至关重要。请注意,数据是否属于对象本身并不总是显而易见的。因此,我们必须提供特殊的序列化/反序列化工具,这些工具负责写入和读取“所有”数据。
一个明显的解决方案是使用继承的同步适配器。但这种方案存在风险。如果有人创建了一个子类而忘记创建适配器,那么他们的数据可能会被部分序列化。以一个例子来说明:如果有人为Person类创建了一个序列化适配器,该适配器序列化Person实例的每个方面,后来又定义了一个子类Employee(Person),那么如果Employee类有一些额外的方面(例如额外的属性,如保险ID、工资等),那么如果没有为Employee创建特殊的序列化适配器来处理这些额外的方面,这些额外的方面将永远不会被序列化。如果通过点分类名(即最具体的类)而不是类或接口(这可能会导致为超类编写的适配器)来查找适配器,那么行为会有所不同。如果没有特定的适配器,则可以使用默认序列化器(例如xmlpickler)完全序列化对象。所以即使你忘记为所有类提供特殊的序列化器,你也可以确保你的数据是完整的。
由于组件架构不支持仅适用于单个类(而不是其子类)的适配器,我们将适配器类注册为命名ISynchronizerFactory工具,并使用类的点分名称作为查找键。默认同步器注册为无名的ISynchronizerFactory工具。这个同步器确保所有数据都被序列化到目标存储库。
>>> component.provideUtility(synchronizer.DefaultSynchronizer, ... provides=interfaces.ISynchronizerFactory)
所有特殊同步器都是为特定内容类而不是抽象接口注册的。类由工厂注册中的点分类名表示
>>> class AFileSynchronizer(synchronizer.Synchronizer): ... interface.implements(interfaces.IFileSynchronizer) ... def dump(self, writeable): ... writeable.write(self.context.data) ... def load(self, readable): ... self.context.data = readable.read()>>> component.provideUtility(AFileSynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname(A))
通过点分类名查找工具由getSynchronizer函数处理,该函数首先尝试查找命名工具。IDefaultSynchronizer工具用作后备
>>> synchronizer.getSynchronizer(a) <zope.fssync.doctest.AFileSynchronizer object at ...>
如果没有注册命名适配器,则返回已注册的无名默认适配器(只要权限允许这样做)
>>> synchronizer.getSynchronizer(b) <zope.fssync.synchronizer.DefaultSynchronizer object at ...>
这个默认序列化器通常使用pickle格式,该格式由IPickler适配器确定。在这里,我们使用Zope的xmlpickle。
>>> component.provideAdapter(pickle.XMLPickler) >>> component.provideAdapter(pickle.XMLUnpickler)
对于类似于容器的对象,我们必须提供一个适配器,将容器映射到一个目录。在我们的例子中,我们使用内置的dict类
>>> component.provideUtility(synchronizer.DirectorySynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname(dict))
现在我们可以将对象导出到snarf存档
>>> checkout.perform(root, 'test') >>> print snarf.stream.getvalue() 00000213 @@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="__builtin__.dict" factory="__builtin__.dict" id="3" /> </entries> 00000339 test/@@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="a" keytype="__builtin__.str" type="zope.fssync.doctest.A" factory="zope.fssync.doctest.A" id="1" /> <entry name="b" keytype="__builtin__.str" type="zope.fssync.doctest.B" id="2" /> </entries> 00000009 test/a data of a00000370 test/b <?xml version="1.0" encoding="utf-8" ?> <pickle> <object> <klass> <global name="B" module="zope.fssync.doctest"/> </klass> <attributes> <attribute name="data"> <string>data of b</string> </attribute> <attribute name="extra"> <string>extra of b</string> </attribute> </attributes> </object> </pickle> <BLANKLINE>
在注册必要的生成器后,我们可以从存储库重新导入序列化的数据
>>> component.provideUtility(synchronizer.FileGenerator(), ... provides=interfaces.IFileGenerator)>>> target = {} >>> commit = task.Commit(synchronizer.getSynchronizer, snarf) >>> commit.perform(target, 'root', 'test') >>> sorted(target.keys()) ['root'] >>> sorted(target['root'].keys()) ['a', 'b']>>> target['root']['a'].data 'data of a'>>> target['root']['b'].extra 'extra of b'
如果我们想将数据回滚到原始位置,我们必须检查存储库是否仍然与原始内容一致。我们修改对象以查看会发生什么
>>> check = task.Check(synchronizer.getSynchronizer, snarf) >>> check.check(root, '', 'test') >>> check.errors() []>>> root['a'].data = 'overwritten' >>> root['b'].extra = 'overwritten'>>> check = task.Check(synchronizer.getSynchronizer, snarf) >>> check.check(root, '', 'test') >>> check.errors() ['test/a', 'test/b']>>> commit.perform(root, '', 'test') >>> sorted(root.keys()) ['a', 'b'] >>> root['a'].data 'data of a' >>> root['b'].extra 'extra of b'>>> del root['a'] >>> commit.perform(root, '', 'test') >>> sorted(root.keys()) ['a', 'b']>>> del root['b'] >>> commit.perform(root, '', 'test') >>> sorted(root.keys()) ['a', 'b']>>> del root['a'] >>> del root['b'] >>> commit.perform(root, '', 'test') >>> sorted(root.keys()) ['a', 'b']
序列化
在许多数据结构中,大型、复杂对象由较小的对象组成。这些对象通常以以下两种方式之一存储
较小的对象存储在较大的对象内部。
较小的对象分配在它们自己的位置,而较大的对象存储对它们的引用。
在情况1中,对象是自包含的,可以完全序列化。这是fssync序列化器的默认行为
>>> pickler = interfaces.IPickler([42]) >>> pickler <zope.fssync.pickle.XMLPickler object at ...> >>> print pickler.dumps() <?xml version="1.0" encoding="utf-8" ?> <pickle> <list> <int>42</int> </list> </pickle> <BLANKLINE>
情况2更复杂,因为序列化器必须考虑持久引用。
>>> class Complex(object): ... def __init__(self, part1, part2): ... self.part1 = part1 ... self.part2 = part2
这里的一切都取决于我们定义的内在引用的定义。在上面的例子中,我们简单地考虑了所有对象都是内在的。
>>> from zope.fssync import pickle >>> c = root['c'] = Complex(a, b) >>> stream = StringIO() >>> print interfaces.IPickler(c).dumps() <?xml version="1.0" encoding="utf-8" ?> <pickle> <initialized_object> <klass> <global id="o0" name="_reconstructor" module="copy_reg"/> </klass> <arguments> <tuple> <global name="Complex" module="zope.fssync.doctest"/> <global id="o1" name="object" module="__builtin__"/> <none/> </tuple> </arguments> <state> <dictionary> <item key="part1"> <object> <klass> <global name="A" module="zope.fssync.doctest"/> </klass> <attributes> <attribute name="data"> <string>data of a</string> </attribute> </attributes> </object> </item> <item key="part2"> <object> <klass> <global name="B" module="zope.fssync.doctest"/> </klass> <attributes> <attribute name="data"> <string>data of b</string> </attribute> <attribute name="extra"> <string>overwritten</string> </attribute> </attributes> </object> </item> </dictionary> </state> </initialized_object> </pickle> <BLANKLINE>
为了使用持久引用,我们必须为我们的序列化器定义一个PersistentIdGenerator,它确定对象是否应该完全序列化或仅通过引用序列化
>>> class PersistentIdGenerator(object): ... interface.implements(interfaces.IPersistentIdGenerator) ... component.adapts(interfaces.IPickler) ... def __init__(self, pickler): ... self.pickler = pickler ... def id(self, obj): ... if isinstance(obj, Complex): ... return None ... return globalIds.getId(obj)>>> component.provideAdapter(PersistentIdGenerator)>>> globalIds.register(a) 1 >>> globalIds.register(b) 2 >>> globalIds.register(root) 3>>> xml = interfaces.IPickler(c).dumps() >>> print xml <?xml version="1.0" encoding="utf-8" ?> <pickle> <object> <klass> <global name="Complex" module="zope.fssync.doctest"/> </klass> <attributes> <attribute name="part1"> <persistent> <string>1</string> </persistent> </attribute> <attribute name="part2"> <persistent> <string>2</string> </persistent> </attribute> </attributes> </object> </pickle> <BLANKLINE>
如果定义并注册了IPersistentIdLoader适配器,则可以加载持久ID
>>> class PersistentIdLoader(object): ... interface.implements(interfaces.IPersistentIdLoader) ... component.adapts(interfaces.IUnpickler) ... def __init__(self, unpickler): ... self.unpickler = unpickler ... def load(self, id): ... global globalIds ... return globalIds.resolve(id)>>> component.provideAdapter(PersistentIdLoader) >>> c2 = interfaces.IUnpickler(None).loads(xml) >>> c2.part1 == a True
注释、额外信息和元数据
复杂对象通常以各种方式组合元数据和内容数据。fssync包允许区分文件内容、额外信息、注释和fssync特定的元数据
文件内容或主体直接存储在相应的文件中。
额外信息是对象的属性,它们是对象的一部分,但不属于文件内容。它们通常存储在额外文件中。
注释是与内容相关的元数据,可以存储为属性注释或对象之外。通常,它们在每个注释命名空间中存储在单独的pickle文件中。
与fssync直接相关的元数据存储在Entries.xml文件中。
这些方面的具体存储位置由同步格式定义。默认格式使用@@Zope目录,其中包含对象额外内容和注释的子目录。这些@@Zope目录还包含一个Entries.xml元数据文件,它定义了以下属性
id:对象的系统ID,在Zope中通常是遍历路径
name:序列化对象的文件名
factory:对象的工厂,通常是类的点分名称
type:没有工厂的pickle对象的类型标识符
provides:对象直接提供的接口
- key:在内容空间中使用的原始名称
在无法明确存储此键的情况下
binary:防止合并二进制数据的标志
flag:包含值“添加”或“删除”的状态标志
部分元数据必须由同步器提供。例如,基本同步器将对象的直接提供接口作为其元数据的一部分返回
>>> class IMarkerInterface(interface.Interface): ... pass >>> interface.directlyProvides(a, IMarkerInterface) >>> pprint(synchronizer.Synchronizer(a).metadata()) {'factory': 'zope.fssync.doctest.A', 'provides': 'zope.fssync.doctest.IMarkerInterface'}
可以使用setmetadata方法将元数据写回对象。哪些元数据被消费取决于同步器
>>> metadata = {'provides': 'zope.fssync.doctest.IMarkerInterface'} >>> synchronizer.Synchronizer(b).setmetadata(metadata) >>> [x for x in interface.directlyProvidedBy(b)] [<InterfaceClass zope.fssync.doctest.IMarkerInterface>]
为了序列化注释,我们首先必须提供一个ISynchronizableAnnotations适配器
>>> snarf = repository.SnarfRepository(StringIO()) >>> checkout = task.Checkout(synchronizer.getSynchronizer, snarf)>>> from zope import annotation >>> from zope.annotation.attribute import AttributeAnnotations >>> component.provideAdapter(AttributeAnnotations) >>> class IAnnotatableSample(interface.Interface): ... pass >>> class AnnotatableSample(object): ... interface.implements(IAnnotatableSample, ... annotation.interfaces.IAttributeAnnotatable) ... data = 'Main file content' ... extra = None >>> sample = AnnotatableSample()>>> class ITestAnnotations(interface.Interface): ... a = interface.Attribute('A') ... b = interface.Attribute('B') >>> import persistent >>> class TestAnnotations(persistent.Persistent): ... interface.implements(ITestAnnotations, ... annotation.interfaces.IAnnotations) ... component.adapts(IAnnotatableSample) ... def __init__(self): ... self.a = None ... self.b = None>>> component.provideAdapter(synchronizer.SynchronizableAnnotations)>>> from zope.annotation.factory import factory >>> component.provideAdapter(factory(TestAnnotations)) >>> ITestAnnotations(sample).a = 'annotation a' >>> ITestAnnotations(sample).a 'annotation a' >>> sample.extra = 'extra'
如果没有特殊的序列化器,由于注释存储在__annotions__属性中,注释将被pickle
>>> root = dict() >>> root['test'] = sample >>> checkout.perform(root, 'test') >>> print snarf.stream.getvalue() 00000197 @@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="__builtin__.dict" factory="__builtin__.dict" /> </entries> 00000182 test/@@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="zope.fssync.doctest.AnnotatableSample" /> </entries> 00001929 test/test <?xml version="1.0" encoding="utf-8" ?> <pickle> <object> <klass> <global name="AnnotatableSample" module="zope.fssync.doctest"/> </klass> ... </attributes> </object> </pickle> <BLANKLINE>
如果我们为注释和额外内容提供一个目录序列化器,我们为每个额外属性和注释命名空间得到一个文件
>>> component.provideUtility( ... synchronizer.DirectorySynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname(synchronizer.Extras))>>> component.provideUtility( ... synchronizer.DirectorySynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname( ... synchronizer.SynchronizableAnnotations))
由于注释已被Synchronizer基类处理,我们只需要在这里指定额外属性
>>> class SampleFileSynchronizer(synchronizer.Synchronizer): ... interface.implements(interfaces.IFileSynchronizer) ... def dump(self, writeable): ... writeable.write(self.context.data) ... def extras(self): ... return synchronizer.Extras(extra=self.context.extra) ... def load(self, readable): ... self.context.data = readable.read() >>> component.provideUtility(SampleFileSynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname(AnnotatableSample))>>> interface.directlyProvides(sample, IMarkerInterface) >>> root['test'] = sample >>> checkout.perform(root, 'test') >>> print snarf.stream.getvalue() 00000197 @@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="__builtin__.dict" factory="__builtin__.dict" /> </entries> 00000182 test/@@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="zope.fssync.doctest.AnnotatableSample" /> </entries> 00001929 test/test <?xml version="1.0" encoding="utf-8" ?> <pickle> <object> <klass> <global name="AnnotatableSample" module="zope.fssync.doctest"/> </klass> <attributes> <attribute name="__annotations__"> ... </attribute> <attribute name="extra"> <string>extra</string> </attribute> </attributes> </object> </pickle> 00000197 @@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="__builtin__.dict" factory="__builtin__.dict" /> </entries> 00000296 test/@@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="test" keytype="__builtin__.str" type="zope.fssync.doctest.AnnotatableSample" factory="zope.fssync.doctest.AnnotatableSample" provides="zope.fssync.doctest.IMarkerInterface" /> </entries> 00000211 test/@@Zope/Annotations/test/@@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="zope.fssync.doctest.TestAnnotations" keytype="__builtin__.str" type="zope.fssync.doctest.TestAnnotations" /> </entries> 00000617 test/@@Zope/Annotations/test/zope.fssync.doctest.TestAnnotations <?xml version="1.0" encoding="utf-8" ?> <pickle> ... </pickle> 00000161 test/@@Zope/Extra/test/@@Zope/Entries.xml <?xml version='1.0' encoding='utf-8'?> <entries> <entry name="extra" keytype="__builtin__.str" type="__builtin__.str" /> </entries> 00000082 test/@@Zope/Extra/test/extra <?xml version="1.0" encoding="utf-8" ?> <pickle> <string>extra</string> </pickle> 00000017 test/test Main file content
当然,注释和额外内容也可以反序列化。默认的反序列化器处理这两种情况
>>> target = {} >>> commit = task.Commit(synchronizer.getSynchronizer, snarf) >>> commit.perform(target, 'root', 'test') >>> result = target['root']['test'] >>> result.extra 'extra' >>> ITestAnnotations(result).a 'annotation a'
由于我们使用IDirectorySynchronizer,每个额外属性和注释命名空间都得到自己的文件
>>> for path in sorted(snarf.iterPaths()): ... print path @@Zope/Entries.xml test/@@Zope/Annotations/test/@@Zope/Entries.xml test/@@Zope/Annotations/test/zope.fssync.doctest.TestAnnotations test/@@Zope/Entries.xml test/@@Zope/Extra/test/@@Zope/Entries.xml test/@@Zope/Extra/test/extra test/test
如果我们提供一个默认同步器,它使用单个文件存储所有注释和所有额外内容,则可以减少文件数量
>>> component.provideUtility( ... synchronizer.DefaultSynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname(synchronizer.Extras))>>> component.provideUtility( ... synchronizer.DefaultSynchronizer, ... interfaces.ISynchronizerFactory, ... name=synchronizer.dottedname( ... synchronizer.SynchronizableAnnotations))>>> root['test'] = sample >>> snarf = repository.SnarfRepository(StringIO()) >>> checkout.repository = snarf >>> checkout.perform(root, 'test') >>> for path in sorted(snarf.iterPaths()): ... print path @@Zope/Entries.xml test/@@Zope/Annotations/test test/@@Zope/Entries.xml test/@@Zope/Extra/test test/test
当然,注释和额外内容也可以反序列化。默认的反序列化器处理这两种情况
>>> target = {} >>> commit = task.Commit(synchronizer.getSynchronizer, snarf) >>> commit.perform(target, 'root', 'test') >>> result = target['root']['test'] >>> result.extra 'extra' >>> ITestAnnotations(result).a 'annotation a' >>> [x for x in interface.directlyProvidedBy(result)] [<InterfaceClass zope.fssync.doctest.IMarkerInterface>]
如果在提交时遇到错误或多个错误,我们将在跟踪中看到它们。
>>> def bad_sync(container, key, fspath, add_callback): ... raise ValueError('1','2','3')>>> target = {} >>> commit = task.Commit(synchronizer.getSynchronizer, snarf) >>> old_sync_new = commit.synchNew >>> commit.synchNew = bad_sync >>> commit.perform(target, 'root', 'test') Traceback (most recent call last): ... Exception: test: '1', '2', '3'
请注意,如果遇到多个异常,我们将在最后打印出所有异常。
>>> old_sync_old = commit.synchOld >>> commit.synchOld = bad_sync >>> commit.perform(target, 'root', 'test') Traceback (most recent call last): ... Exceptions: test: '1', '2', '3' test: '1', '2', '3'>>> commit.synchNew = old_sync_new >>> commit.synchOld = old_sync_old
更改
3.6.1 (2013-05-02)
修复了在检查时解pickle错误时引发的异常。
改进了解pickle错误的报告。
3.6.0 (2012-03-15)
提交任务将收集错误并将它们全部发送回去,而不是在遇到第一个错误时停止。
3.5.2 (2010-10-18)
修复测试;zope.location不再导出TLocation。
在zope.fssync.synchronizer中配置的同步器不存在时,引发正确的错误。
更新依赖信息。
小的代码清理。
3.5.1 (2009-07-24)
正确设置测试,以便它们在发布时也能工作。
删除了缩写。
3.5 (????)
添加了对snarf格式中空目录的支持。现在目录可以由snarf显式描述。
同步器现在可以从load方法返回回调。这允许稍后运行修复操作。当添加多个相互依赖的对象时,这很有用。回调可以返回回调。
添加对FSMerger的支持,允许本地修改的文件被服务器返回的文件覆盖。这样做的目的是避免在提交时文件格式不同导致的冲突。
3.4.0b1 (????)
将zope.fssync和zope.app.fssync重构为两个清晰分离的包
zope.fssync 现在包含了一个 Python API,它不依赖于 Zope、ZODB 和安全机制。
zope.app.fssync 包含一个受保护的基于 Web 的 API 和针对 zope.app 内容类型的特殊同步器。
其他主要更改包括
同步器(即序列化/反序列化适配器)由命名实用工具创建,这些实用工具使用点分隔的类名作为查找键。
添加了 doctests
支持大文件
拾取器、反拾取器和处理持久化拾取器 ID 的适配器
二进制文件不再合并
不区分大小写的文件系统和存储库在导出时使用区分歧义的名称,在导入时使用原始名称
直接提供的接口的导出和导入
直接导出到存档/直接从存档导入
解决了 Mac OSX 上的编码问题
项目详情
zope.fssync-3.6.1.zip 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | a23b56644a894405a270a1195076e7c79de68bbc4066ec61ef8c17781bc9208b |
|
MD5 | f62812c50707178c95dc6c4c22b8572b |
|
BLAKE2b-256 | 2c9c7c738647ac8d8bf25de9ce2f52854fc9006a6dcca7cabf45bd62b606b184 |