# HG changeset patch # User Thierry Florac # Date 1549355863 -3600 # Node ID a68e640a408929d2fdfb321e21fbd4a6e929d791 # Parent 992148384f22b43f35265ebe698b24360ea9cb17 Added blobs references manager diff -r 992148384f22 -r a68e640a4089 src/pyams_file/file.py --- a/src/pyams_file/file.py Fri Feb 01 18:35:42 2019 +0100 +++ b/src/pyams_file/file.py Tue Feb 05 09:37:43 2019 +0100 @@ -18,20 +18,25 @@ magic = None import os -import shutil from io import BytesIO +from BTrees.OOBTree import OOBTree from PIL import Image from ZODB.blob import Blob +from ZODB.utils import oid_repr from persistent import Persistent +from pyramid.events import subscriber from zope.container.contained import Contained from zope.copy.interfaces import ICopyHook, ResumeCopy from zope.interface import implementer +from zope.lifecycleevent import IObjectAddedEvent, IObjectRemovedEvent from zope.location.interfaces import IContained from zope.schema.fieldproperty import FieldProperty -from pyams_file.interfaces import FileModifiedEvent, IAudio, IFile, IFileInfo, IImage, ISVGImage, IVideo +from pyams_file.interfaces import FileModifiedEvent, IAudio, IBlobReferenceManager, IFile, IFileInfo, IImage, ISVGImage, \ + IVideo from pyams_utils.adapter import ContextAdapter, adapter_config +from pyams_utils.registry import get_utility from pyams_utils.request import check_request @@ -107,6 +112,49 @@ } +# +# Blobs references manager utility +# + +@implementer(IBlobReferenceManager) +class BlobReferencesManager(Persistent, Contained): + """Global blobs references manager utility + + The utility is used to keep all references of persistent files objects to + their blobs. + References management is done automatically when using file-related properties, + like :ref:`pyams_file.property.FileProperty` or :ref:`pyams_i18n.property.I18nFileProperty`. + """ + + def __init__(self): + self.refs = OOBTree() + + def add_reference(self, blob, reference): + oid = getattr(blob, '_p_oid') + if not oid: + getattr(reference, '_p_jar').add(blob) + oid = getattr(blob, '_p_oid') + oid = oid_repr(oid) + refs = self.refs.get(oid) or set() + refs.add(reference) + self.refs[oid] = refs + + def drop_reference(self, blob, reference): + oid = oid_repr(getattr(blob, '_p_oid')) + refs = self.refs.get(oid) or set() + if reference in refs: + refs.remove(reference) + if refs: + self.refs[oid] = refs + else: + del self.refs[oid] + del blob + + +# +# Persistent file class +# + @implementer(IFile, IFileInfo, IContained) class File(Persistent, Contained): """Generic file persistent object""" @@ -129,6 +177,27 @@ finally: f.close() + def init_blob(self): + """Initialize internal blob and add reference to it""" + self.remove_blob_reference() + self._blob = Blob() + + def add_blob_reference(self, reference=None): + """Add reference to internal blob""" + if self._blob is not None: + references = get_utility(IBlobReferenceManager) + references.add_reference(self._blob, reference if reference is not None else self) + + def remove_blob_reference(self): + """Remove reference to internal blob + + Blob is deleted if there is no more reference to it. + """ + if self._blob is not None: + references = get_utility(IBlobReferenceManager) + references.drop_reference(self._blob, self) + self._blob = None + def get_blob(self, mode='r'): if self._blob is None: return None @@ -150,8 +219,7 @@ f.close() def _set_data(self, data): - if self._blob is None: - self._blob = Blob() + self.init_blob() if isinstance(data, str): data = data.encode('utf-8') elif hasattr(data, 'seek'): @@ -199,6 +267,40 @@ return self._size > 0 +@subscriber(IObjectAddedEvent, context_selector=IFile) +def handle_added_file(event): + """Add blob reference when file is added""" + event.object.add_blob_reference() + + +@subscriber(IObjectRemovedEvent, context_selector=IFile) +def handle_removed_file(event): + """Remove blob associated with file when removed""" + event.object.remove_blob_reference() + + +@adapter_config(context=IFile, provides=ICopyHook) +class BlobFileCopyHook(ContextAdapter): + """Blob file copy hook + + Inspired by z3c.blobfile package + """ + + def __call__(self, toplevel, register): + register(self._copy_blob) + raise ResumeCopy + + def _copy_blob(self, translate): + # Just add a reference to blob when copying file + target = translate(self.context) + setattr(target, '_blob', getattr(self.context, '_blob')) + target.add_blob_reference(target) + + +# +# Persistent images +# + @implementer(IImage) class ImageFile(File): """Image file persistent object""" @@ -274,6 +376,10 @@ """Audio file persistent object""" +# +# Generic files utilities +# + def get_magic_content_type(input): """Get content-type based on magic library as *bytes* @@ -312,26 +418,3 @@ else: factory = File return factory(data, content_type) - - -@adapter_config(context=IFile, provides=ICopyHook) -class BlobFileCopyHook(ContextAdapter): - """Blob file copy hook - - Copied from z3c.blobfile package - """ - - def __call__(self, toplevel, register): - register(self._copy_blob) - raise ResumeCopy - - def _copy_blob(self, translate): - blob = self.context._blob - if blob is not None: - target = translate(self.context) - target._blob = Blob() - fsrc = blob.open('r') - fdst = target._blob.open('w') - shutil.copyfileobj(fsrc, fdst) - fdst.close() - fsrc.close()