Added blobs references manager
authorThierry Florac <tflorac@ulthar.net>
Tue, 05 Feb 2019 09:37:43 +0100
changeset 173 a68e640a4089
parent 172 992148384f22
child 174 d49bcf382187
Added blobs references manager
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()