Skip to content

Commit fa8d0ae

Browse files
committed
replace PR #155 to avoid unneed reading files into memory during import/upload
1 parent 98efd90 commit fa8d0ae

File tree

3 files changed

+69
-6
lines changed

3 files changed

+69
-6
lines changed

plone/namedfile/field.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from plone.namedfile.interfaces import INamedFileField
1111
from plone.namedfile.interfaces import INamedImage
1212
from plone.namedfile.interfaces import INamedImageField
13+
from plone.namedfile.interfaces import INamedTyped
1314
from plone.namedfile.interfaces import IPluggableFileFieldValidation
1415
from plone.namedfile.interfaces import IPluggableImageFieldValidation
1516
from plone.namedfile.utils import get_contenttype
@@ -138,7 +139,17 @@ class NamedBlobFile(NamedField):
138139
accept = ()
139140

140141
def validate(self, value):
141-
super().validate(value, IPluggableFileFieldValidation)
142+
# Bit of a hack but we avoid loading the .data into memory
143+
# because schema validation checks the property exists
144+
# which loads the entire file into memory for no reaon.
145+
# This can slow down imports and uploads a lot.
146+
# TODO: better fix might be get DX to check for a decorator first
147+
# - https://stackoverflow.com/questions/16169948/check-if-something-is-an-attribute-or-decorator-in-python
148+
self.schema = INamedTyped
149+
try:
150+
super().validate(value, IPluggableFileFieldValidation)
151+
finally:
152+
self.schema = INamedBlobFile
142153

143154

144155
@implementer(INamedBlobImageField)
@@ -150,4 +161,9 @@ class NamedBlobImage(NamedField):
150161
accept = ("image/*",)
151162

152163
def validate(self, value):
153-
super().validate(value, IPluggableImageFieldValidation)
164+
# see NamedBlobFile comment
165+
self.schema = INamedTyped
166+
try:
167+
super().validate(value, IPluggableImageFieldValidation)
168+
finally:
169+
self.schema = INamedBlobImage

plone/namedfile/interfaces.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010

1111
HAVE_BLOBS = True
1212

13-
14-
class IFile(Interface):
13+
class ITyped(Interface):
1514

1615
contentType = schema.NativeStringLine(
1716
title="Content Type",
@@ -21,6 +20,8 @@ class IFile(Interface):
2120
missing_value="",
2221
)
2322

23+
class IFile(Interface):
24+
2425
data = schema.Bytes(
2526
title="Data",
2627
description="The actual content of the object.",
@@ -83,12 +84,15 @@ class INamed(Interface):
8384

8485
filename = schema.TextLine(title="Filename", required=False, default=None)
8586

87+
class INamedTyped(INamed, ITyped):
88+
"""An item with a filename and contentType"""
89+
8690

87-
class INamedFile(INamed, IFile):
91+
class INamedFile(INamedTyped, IFile):
8892
"""A non-BLOB file with a filename"""
8993

9094

91-
class INamedImage(INamed, IImage):
95+
class INamedImage(INamedTyped, IImage):
9296
"""A non-BLOB image with a filename"""
9397

9498

plone/namedfile/tests/test_storable.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from OFS.Image import Pdata
1919
from plone.namedfile.file import FileChunk
2020
from plone.namedfile.file import NamedBlobImage
21+
from plone.namedfile.file import NamedBlobFile
22+
from plone.namedfile import field
2123
from plone.namedfile.testing import PLONE_NAMEDFILE_FUNCTIONAL_TESTING
2224
from plone.namedfile.tests import getFile
2325

@@ -56,3 +58,44 @@ def test_opened_file_storable(self):
5658
if os.path.exists(path):
5759
os.remove(path)
5860
self.assertEqual(303, fi.getSize())
61+
62+
def test_upload_no_read(self):
63+
# ensure we don't read the whole file into memory
64+
65+
import ZODB.blob
66+
67+
old_open = ZODB.blob.Blob.open
68+
blob_read = 0
69+
blob_write = 0
70+
71+
def count_open(self, mode="r"):
72+
nonlocal blob_read, blob_write
73+
blob_read += 1 if "r" in mode else 0
74+
blob_write += 1 if "w" in mode else 0
75+
return old_open(self, mode)
76+
77+
with unittest.mock.patch.object(ZODB.blob.Blob, 'open', count_open):
78+
data = getFile("image.gif")
79+
f = tempfile.NamedTemporaryFile(delete=False)
80+
try:
81+
path = f.name
82+
f.write(data)
83+
f.close()
84+
with open(path, "rb") as f:
85+
fi = NamedBlobFile(f, filename="image.gif")
86+
finally:
87+
if os.path.exists(path):
88+
os.remove(path)
89+
self.assertEqual(303, fi.getSize())
90+
self.assertEqual(blob_read, 1, "blob should have only been opened to get size")
91+
self.assertEqual(
92+
blob_write,
93+
1,
94+
"Slow write to blob instead of os rename. Should be only 1 on init",
95+
)
96+
blob_read = 0
97+
98+
blob_field = field.NamedBlobFile()
99+
blob_field.validate(fi)
100+
101+
self.assertEqual(blob_read, 0, "Validation is reading the whole blob in memory")

0 commit comments

Comments
 (0)