Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Am/paste async button #614

Merged
merged 8 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions castle/cms/blobmissing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from pkg_resources import resource_filename
from shutil import copyfile
from ZEO import ClientStorage
from ZODB.blob import BlobFile
from ZODB.POSException import POSKeyError
from ZODB.POSException import Unsupported

import logging
import os


logger = logging.getLogger(__name__)


def patched_blob_init(self, name, mode, blob):
allow_experimental = os.getenv("CASTLE_ALLOW_EXPERIMENTAL_BLOB_REPLACEMENT", None)
if not os.path.exists(name) and allow_experimental is not None:
create_empty_blob(name)
super(BlobFile, self).__init__(name, mode + 'b')
self.blob = blob


def patched_loadBlob(self, oid, serial):
# Load a blob. If it isn't present and we have a shared blob
# directory, then assume that it doesn't exist on the server
# and return None.

blob_filename = self.fshelper.getBlobFilename(oid, serial)
allow_experimental = os.getenv("CASTLE_ALLOW_EXPERIMENTAL_BLOB_REPLACEMENT", None)

if allow_experimental is None:
# Exhibit original behavior if blob replacement is not enabled
if not os.path.exists(blob_filename):
logger.error("No blob file at path: %s", blob_filename)
raise POSKeyError("No blob file", oid, serial)
return blob_filename

if self.shared_blob_dir:
if os.path.exists(blob_filename):
return blob_filename
else:
# create empty file
create_empty_blob(blob_filename)
if os.path.exists(blob_filename):
return blob_filename
else:
# We're using a server shared cache. If the file isn't
# here, it's not anywhere.
raise POSKeyError("No blob file", oid, serial)

if os.path.exists(blob_filename):
return ClientStorage._accessed(blob_filename)
else:
# create empty file
create_empty_blob(blob_filename)

if os.path.exists(blob_filename):
return ClientStorage._accessed(blob_filename)

# First, we'll create the directory for this oid, if it doesn't exist.
self.fshelper.createPathForOID(oid)

# OK, it's not here and we (or someone) needs to get it. We
# want to avoid getting it multiple times. We want to avoid
# getting it multiple times even accross separate client
# processes on the same machine. We'll use file locking.

lock = ClientStorage._lock_blob(blob_filename)
try:
# We got the lock, so it's our job to download it. First,
# we'll double check that someone didn't download it while we
# were getting the lock:

if os.path.exists(blob_filename):
return ClientStorage._accessed(blob_filename)

# Ask the server to send it to us. When this function
# returns, it will have been sent. (The recieving will
# have been handled by the asyncore thread.)

self._server.sendBlob(oid, serial)

if os.path.exists(blob_filename):
return ClientStorage._accessed(blob_filename)

raise POSKeyError("No blob file", oid, serial)

finally:
lock.close()


def create_empty_blob(filename):
dirname = os.path.split(filename)[0]
if not os.path.isdir(dirname):
os.makedirs(dirname, 0o700)

logger.info("Broken blob reference detected for: %s", filename)

source = resource_filename(
'castle.cms',
'static/images/placeholder.png',
)

copyfile(source, filename)
logger.info("Placeholder blob created for: %s", filename)
29 changes: 29 additions & 0 deletions castle/cms/blobmissing.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:monkey="http://namespaces.plone.org/monkey"
i18n_domain="castle.cms">

<include package="collective.monkeypatcher" />
<include package="collective.monkeypatcher" file="meta.zcml" />

<monkey:patch
description="Create the blob folder path and create (touch) an empty file for each blob file if it's missing."
class="ZODB.blob.BlobFile"
original="__init__"
replacement=".blobmissing.patched_blob_init"
docstringWarning="true"
/>

<monkey:patch
description="Create the blob-file if it's missing."
class="ZEO.ClientStorage.ClientStorage"
original="loadBlob"
replacement=".blobmissing.patched_loadBlob"
docstringWarning="true"
/>


</configure>
39 changes: 1 addition & 38 deletions castle/cms/browser/content/fc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,6 @@
from zope.interface import implementer
from zope.lifecycleevent import ObjectModifiedEvent
from zope.component.hooks import getSite
from plone.app.contenttypes.interfaces import IDocument
from zope.annotation.interfaces import IAnnotations
from persistent.mapping import PersistentMapping
from plone.namedfile.file import NamedBlobImage
from ZODB.blob import FilesystemHelper
from pkg_resources import resource_filename
from shutil import copyfile
import os
from re import sub

from plone.app.content.browser.vocabulary import (
Expand Down Expand Up @@ -227,39 +219,10 @@ def __call__(self):
except Exception as e:
logger.error("Error retrieving object: {}".format(e))
return

# Set up FilesystemHelper to handle broken blob references, if any
conn = self.context._p_jar
storage = conn._storage
fshelper = storage.fshelper
base_dir = fshelper.base_dir
zeofshelper = FilesystemHelper(base_dir)
broken_items = []

if IDocument.providedBy(obj):
annotations = IAnnotations(obj)
for key in annotations.keys():
data = annotations[key]

# PersistentMapping tiles contain the blobs we need
if isinstance(data, PersistentMapping):
for item in data.values():
if isinstance(item, dict) and isinstance(item.get('data'), NamedBlobImage):
blobfile = item.get('data')
blob = blobfile._blob
try:
# Check blob
blob._p_activate()
except Exception as e:
# Broken blob reference
broken_items.append('/'.join(obj.getPhysicalPath()))
filename = zeofshelper.getBlobFilename(blob._p_oid, blob._p_serial)
if not os.path.exists(filename):
self.create_empty_blob(filename)

tasks.paste_items.delay(
self.request.form['folder'], paste_data['op'],
paste_data['mdatas'], broken_items)
paste_data['mdatas'], fix_blobs=True)

return self.do_redirect(
self.canonical_object_url,
Expand Down
1 change: 1 addition & 0 deletions castle/cms/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
<include file="static.zcml" />
<include file="theming.zcml" />
<include file="meta.zcml" />
<include file="blobmissing.zcml" />

<include zcml:condition="installed collective.easyform" package=".easyform" />

Expand Down
13 changes: 10 additions & 3 deletions castle/cms/tasks/content.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

from AccessControl import Unauthorized
from Acquisition import aq_parent
from castle.cms import cache
Expand All @@ -12,6 +13,7 @@

import logging
import transaction
import os


logger = logging.getLogger('castle.cms')
Expand Down Expand Up @@ -39,11 +41,14 @@ def paste_error_handle(where, op, mdatas):


@retriable(on_retry_exhausted=paste_error_handle)
def _paste_items(where, op, mdatas, broken_items):
def _paste_items(where, op, mdatas, fix_blobs):
logger.info('Copying a bunch of items')
portal = api.portal.get()
dest = portal.restrictedTraverse(str(where.lstrip('/')))

if fix_blobs:
os.environ["CASTLE_ALLOW_EXPERIMENTAL_BLOB_REPLACEMENT"] = 'True'

if not getCelery().conf.task_always_eager:
portal._p_jar.sync()

Expand Down Expand Up @@ -102,11 +107,13 @@ def _paste_items(where, op, mdatas, broken_items):

except Exception:
logger.warn('Could not send status email ', exc_info=True)

os.environ.pop("CASTLE_ALLOW_EXPERIMENTAL_BLOB_REPLACEMENT", None)


@task()
def paste_items(where, op, mdatas, broken_items=[]):
_paste_items(where, op, mdatas, broken_items)
def paste_items(where, op, mdatas, fix_blobs=None):
_paste_items(where, op, mdatas, fix_blobs)


def delete_error_handle(where, op, mdatas):
Expand Down
Loading