--- 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()