collective.xsendfile 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- collective/xsendfile/__init__.py +0 -0
- collective/xsendfile/configure.zcml +97 -0
- collective/xsendfile/controlpanel.py +30 -0
- collective/xsendfile/interfaces.py +51 -0
- collective/xsendfile/monkeypatch.py +117 -0
- collective/xsendfile/profiles/default/controlpanel.xml +20 -0
- collective/xsendfile/profiles/default/metadata.xml +8 -0
- collective/xsendfile/profiles/default/registry.xml +4 -0
- collective/xsendfile/testing.py +60 -0
- collective/xsendfile/tests/__init__.py +0 -0
- collective/xsendfile/tests/image.gif +0 -0
- collective/xsendfile/tests/test_blobs.py +139 -0
- collective/xsendfile/utils.py +252 -0
- collective/xsendfile/zope2.py +3 -0
- collective_xsendfile-2.0.0.dist-info/METADATA +347 -0
- collective_xsendfile-2.0.0.dist-info/RECORD +20 -0
- collective_xsendfile-2.0.0.dist-info/WHEEL +5 -0
- collective_xsendfile-2.0.0.dist-info/entry_points.txt +6 -0
- collective_xsendfile-2.0.0.dist-info/licenses/LICENSE +338 -0
- collective_xsendfile-2.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<configure
|
|
2
|
+
xmlns="http://namespaces.zope.org/zope"
|
|
3
|
+
xmlns:monkey="http://namespaces.plone.org/monkey"
|
|
4
|
+
xmlns:five="http://namespaces.zope.org/five"
|
|
5
|
+
xmlns:browser="http://namespaces.zope.org/browser"
|
|
6
|
+
xmlns:zcml="http://namespaces.zope.org/zcml"
|
|
7
|
+
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
|
|
8
|
+
i18n_domain="collective.xsendfile"
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
<five:registerPackage package="." initialize=".zope2.initialize"/>
|
|
12
|
+
|
|
13
|
+
<!-- Control panel -->
|
|
14
|
+
<include package="plone.registry"/>
|
|
15
|
+
<include package="plone.app.registry"/>
|
|
16
|
+
<browser:page
|
|
17
|
+
name="xsendfile-settings"
|
|
18
|
+
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
|
|
19
|
+
class=".controlpanel.xsendfileSettingsControlPanel"
|
|
20
|
+
permission="cmf.ManagePortal"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<!-- Monkey patcher -->
|
|
24
|
+
<include package="collective.monkeypatcher"/>
|
|
25
|
+
<configure zcml:condition="installed plone.app.blob">
|
|
26
|
+
<monkey:patch
|
|
27
|
+
description="Add support for XSendfile"
|
|
28
|
+
class="plone.app.blob.field.BlobWrapper"
|
|
29
|
+
original="index_html"
|
|
30
|
+
replacement=".utils.plone_app_blob_field_BlobWrapper_index_html"
|
|
31
|
+
preserveOriginal="true"
|
|
32
|
+
/>
|
|
33
|
+
<monkey:patch
|
|
34
|
+
description="Add support for XSendfile"
|
|
35
|
+
class="plone.app.blob.field.BlobWrapper"
|
|
36
|
+
original="getIterator"
|
|
37
|
+
replacement=".utils.plone_app_blob_field_BlobWrapper_getIterator"
|
|
38
|
+
preserveOriginal="true"
|
|
39
|
+
/>
|
|
40
|
+
</configure>
|
|
41
|
+
<monkey:patch
|
|
42
|
+
description="Create blobstorage and internal folders/files with proper permissions for groups"
|
|
43
|
+
class="ZODB.blob.FilesystemHelper"
|
|
44
|
+
original="create"
|
|
45
|
+
replacement=".monkeypatch.FilesystemHelper_create"
|
|
46
|
+
preserveOriginal="true"
|
|
47
|
+
preconditions="ZODB-=5.2.1"
|
|
48
|
+
/>
|
|
49
|
+
<monkey:patch
|
|
50
|
+
description="Create blobstorage and internal folders/files with proper permissions for groups"
|
|
51
|
+
class="ZODB.blob.FilesystemHelper"
|
|
52
|
+
original="isSecure"
|
|
53
|
+
replacement=".monkeypatch.FilesystemHelper_isSecure"
|
|
54
|
+
preserveOriginal="true"
|
|
55
|
+
preconditions="ZODB-=5.2.1"
|
|
56
|
+
/>
|
|
57
|
+
<monkey:patch
|
|
58
|
+
description="Create blobstorage and internal folders/files with proper permissions for groups"
|
|
59
|
+
class="ZODB.blob.FilesystemHelper"
|
|
60
|
+
original="getPathForOID"
|
|
61
|
+
replacement=".monkeypatch.FilesystemHelper_getPathForOID"
|
|
62
|
+
preserveOriginal="true"
|
|
63
|
+
preconditions="ZODB-=5.2.1"
|
|
64
|
+
/>
|
|
65
|
+
<configure zcml:condition="installed plone.namedfile">
|
|
66
|
+
<monkey:patch
|
|
67
|
+
description="Add support for XSendfile"
|
|
68
|
+
class="plone.namedfile.browser.Download"
|
|
69
|
+
original="__call__"
|
|
70
|
+
replacement=".utils.monkeypatch_plone_namedfile_browser_Download__call__"
|
|
71
|
+
preserveOriginal="true"
|
|
72
|
+
/>
|
|
73
|
+
<monkey:patch
|
|
74
|
+
description="Add support for XSendfile"
|
|
75
|
+
class="plone.formwidget.namedfile.widget.Download"
|
|
76
|
+
original="__call__"
|
|
77
|
+
replacement=".utils.monkeypatch_plone_formwidget_namedfile_widget_download__call__"
|
|
78
|
+
preserveOriginal="true"
|
|
79
|
+
/>
|
|
80
|
+
<monkey:patch
|
|
81
|
+
description="Add support for XSendfile for image scales"
|
|
82
|
+
class="plone.namedfile.scaling.ImageScale"
|
|
83
|
+
original="index_html"
|
|
84
|
+
replacement=".utils.plone_namedfile_scaling_ImageScale_index_html"
|
|
85
|
+
preserveOriginal="true"
|
|
86
|
+
/>
|
|
87
|
+
</configure>
|
|
88
|
+
|
|
89
|
+
<genericsetup:registerProfile
|
|
90
|
+
name="default"
|
|
91
|
+
title="XSendFile support"
|
|
92
|
+
directory="profiles/default"
|
|
93
|
+
description="Installs the collective.xsendfile package"
|
|
94
|
+
provides="Products.GenericSetup.interfaces.EXTENSION"
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
</configure>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from collective.xsendfile.interfaces import IxsendfileSettings
|
|
3
|
+
from collective.xsendfile.interfaces import _
|
|
4
|
+
from plone.app.registry.browser import controlpanel
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
if 'XSENDFILE_RESPONSEHEADER' in os.environ:
|
|
9
|
+
DESC = _(
|
|
10
|
+
'WARNING: xsendfile settings configured by environment. These settings '
|
|
11
|
+
'will be ignored.')
|
|
12
|
+
else:
|
|
13
|
+
DESC = _(u'')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class xsendfileSettingsEditForm(controlpanel.RegistryEditForm):
|
|
17
|
+
schema = IxsendfileSettings
|
|
18
|
+
label = _(u'xsendfile settings')
|
|
19
|
+
|
|
20
|
+
description = DESC
|
|
21
|
+
|
|
22
|
+
def updateFields(self):
|
|
23
|
+
super(xsendfileSettingsEditForm, self).updateFields()
|
|
24
|
+
|
|
25
|
+
def updateWidgets(self):
|
|
26
|
+
super(xsendfileSettingsEditForm, self).updateWidgets()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class xsendfileSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
|
|
30
|
+
form = xsendfileSettingsEditForm
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from zope import schema
|
|
3
|
+
from zope.i18nmessageid import MessageFactory
|
|
4
|
+
from zope.interface import Interface
|
|
5
|
+
from zope.schema.vocabulary import SimpleTerm
|
|
6
|
+
from zope.schema.vocabulary import SimpleVocabulary
|
|
7
|
+
|
|
8
|
+
_ = MessageFactory('collective.xsendfile')
|
|
9
|
+
http_response_headers = SimpleVocabulary(
|
|
10
|
+
[
|
|
11
|
+
SimpleTerm(value=u'X-Sendfile', title=_(u'X-Sendfile (Apache, LHTTPd)')),
|
|
12
|
+
SimpleTerm(value=u'X-Accel-Redirect',
|
|
13
|
+
title=_(u'X-Accel-Redirect (nginx, needs path rewriting)')),
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IxsendfileSettings(Interface):
|
|
19
|
+
"""Global xsendfile settings. This describes records stored in the
|
|
20
|
+
configuration registry and obtainable via plone.registry.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
xsendfile_responseheader = schema.Choice(
|
|
24
|
+
title=_(u'HTTP Response Header'),
|
|
25
|
+
vocabulary=http_response_headers,
|
|
26
|
+
required=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
xsendfile_enable_fallback = schema.Bool(
|
|
30
|
+
title=_(u'Enable fallback based on HTTP_X_FORWARDED_FOR proxy header'),
|
|
31
|
+
description=_(
|
|
32
|
+
u'Check if REQUEST header contains HTTP_X_FORWARDED_FOR and re-enable fallback (zope) file delivery if no proxy is detected.'),
|
|
33
|
+
default=True,
|
|
34
|
+
required=False,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
xsendfile_pathregex_search = schema.TextLine(
|
|
38
|
+
title=_(u'Blob-path rewriting regex'),
|
|
39
|
+
description=_(
|
|
40
|
+
u'This regex will be used to modify the files path, in case you are using a different mountpoint on the xsendfile Server. If you are using nginx you have to prepend the id of the location you configured.'),
|
|
41
|
+
default=u'(.*)',
|
|
42
|
+
required=False,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
xsendfile_pathregex_substitute = schema.TextLine(
|
|
46
|
+
title=_(u'Blob-path rewriting substitution'),
|
|
47
|
+
description=_(
|
|
48
|
+
u'This regex will be used to modify the files path, in case you are using a different mountpoint on the xsendfile Server. If you are using nginx you have to prepend the id of the location you configured.'),
|
|
49
|
+
default=u'\\1',
|
|
50
|
+
required=False,
|
|
51
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
Monkey-patch ZODB to support blob storage FS permissios
|
|
5
|
+
which allows reading them by Apache.
|
|
6
|
+
http://stackoverflow.com/questions/6168566/collective-xsendfile-zodb-blobs-and-unix-file-permissions/6169177#6169177
|
|
7
|
+
|
|
8
|
+
ZODB was changed in 5.2.2 to no longer set permissions. As such, these patches aren't needed on later versions.
|
|
9
|
+
https://github.com/zopefoundation/ZODB/commit/12eaf5cbc379257ed4b1ed7eece386329796b560
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
from ZODB import utils
|
|
13
|
+
from ZODB import blob
|
|
14
|
+
from ZODB.blob import LAYOUT_MARKER
|
|
15
|
+
from ZODB.blob import log
|
|
16
|
+
from importlib.metadata import version as distribution_version
|
|
17
|
+
|
|
18
|
+
import itertools
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import stat
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def FilesystemHelper_create(self):
|
|
27
|
+
if not os.path.exists(self.base_dir):
|
|
28
|
+
os.makedirs(self.base_dir, 0o750)
|
|
29
|
+
log('Blob directory \'%s\' does not exist. '
|
|
30
|
+
'Created new directory.' % self.base_dir)
|
|
31
|
+
if not os.path.exists(self.temp_dir):
|
|
32
|
+
os.makedirs(self.temp_dir, 0o750)
|
|
33
|
+
log('Blob temporary directory \'%s\' does not exist. '
|
|
34
|
+
'Created new directory.' % self.temp_dir)
|
|
35
|
+
|
|
36
|
+
layout_marker_path = os.path.join(self.base_dir, LAYOUT_MARKER)
|
|
37
|
+
if not os.path.exists(layout_marker_path):
|
|
38
|
+
with open(layout_marker_path, 'w') as layout_marker:
|
|
39
|
+
layout_marker.write(self.layout_name)
|
|
40
|
+
else:
|
|
41
|
+
with open(layout_marker_path, 'r') as layout_marker:
|
|
42
|
+
layout = layout_marker.read().strip()
|
|
43
|
+
if layout != self.layout_name:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"Directory layout `%s` selected for blob directory %s, but "
|
|
46
|
+
"marker found for layout `%s`" %
|
|
47
|
+
(self.layout_name, self.base_dir, layout))
|
|
48
|
+
|
|
49
|
+
def FilesystemHelper_isSecure(self, path):
|
|
50
|
+
"""Ensure that (POSIX) path mode bits are 0750."""
|
|
51
|
+
return (os.stat(path).st_mode & 0o27) == 0
|
|
52
|
+
|
|
53
|
+
def FilesystemHelper_getPathForOID(self, oid, create=False):
|
|
54
|
+
"""Given an OID, return the path on the filesystem where
|
|
55
|
+
the blob data relating to that OID is stored.
|
|
56
|
+
|
|
57
|
+
If the create flag is given, the path is also created if it didn't
|
|
58
|
+
exist already.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
# OIDs are numbers and sometimes passed around as integers. For our
|
|
62
|
+
# computations we rely on the 64-bit packed string representation.
|
|
63
|
+
if isinstance(oid, int):
|
|
64
|
+
oid = utils.p64(oid)
|
|
65
|
+
|
|
66
|
+
path = self.layout.oid_to_path(oid)
|
|
67
|
+
path = os.path.join(self.base_dir, path)
|
|
68
|
+
|
|
69
|
+
if create and not os.path.exists(path):
|
|
70
|
+
try:
|
|
71
|
+
os.makedirs(path, 0o750)
|
|
72
|
+
except OSError:
|
|
73
|
+
# We might have lost a race. If so, the directory
|
|
74
|
+
# must exist now
|
|
75
|
+
logger.exception('Error creating blob directory.')
|
|
76
|
+
assert os.path.exists(path)
|
|
77
|
+
return path
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def rename_or_copy_blob(f1, f2, chmod=True):
|
|
81
|
+
"""Try to rename f1 to f2, fallback to copy.
|
|
82
|
+
|
|
83
|
+
Under certain conditions a rename might not work, e.g. because the target
|
|
84
|
+
directory is on a different partition. In this case we try to copy the
|
|
85
|
+
data and remove the old file afterwards.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
os.rename(f1, f2)
|
|
90
|
+
except OSError:
|
|
91
|
+
logger.debug("Copied blob file %r to %r.", f1, f2)
|
|
92
|
+
file1 = open(f1, 'rb')
|
|
93
|
+
file2 = open(f2, 'wb')
|
|
94
|
+
try:
|
|
95
|
+
utils.cp(file1, file2)
|
|
96
|
+
finally:
|
|
97
|
+
file1.close()
|
|
98
|
+
file2.close()
|
|
99
|
+
blob.remove_committed(f1)
|
|
100
|
+
|
|
101
|
+
os.chmod(f2, stat.S_IREAD | stat.S_IRGRP)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _version_tuple(version):
|
|
105
|
+
parts = []
|
|
106
|
+
for part in version.split('.'):
|
|
107
|
+
digits = ''.join(itertools.takewhile(str.isdigit, part))
|
|
108
|
+
if not digits:
|
|
109
|
+
break
|
|
110
|
+
parts.append(int(digits))
|
|
111
|
+
return tuple(parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if _version_tuple(distribution_version('ZODB')) < (5, 2, 2):
|
|
115
|
+
blob.rename_or_copy_blob = rename_or_copy_blob
|
|
116
|
+
else:
|
|
117
|
+
logger.info('Not patching rename_or_copy_blob as ZODB >= 5.2.2')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<object
|
|
3
|
+
name="portal_controlpanel"
|
|
4
|
+
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
|
|
5
|
+
i18n:domain="plone"
|
|
6
|
+
purge="False">
|
|
7
|
+
|
|
8
|
+
<configlet
|
|
9
|
+
title="XSendFile Settings"
|
|
10
|
+
action_id="plone.app.event"
|
|
11
|
+
appId="plone.app.event"
|
|
12
|
+
category="Plone"
|
|
13
|
+
condition_expr=""
|
|
14
|
+
url_expr="string:${portal_url}/@@xsendfile-settings"
|
|
15
|
+
visible="True"
|
|
16
|
+
i18n:attributes="title">
|
|
17
|
+
<permission>Manage portal</permission>
|
|
18
|
+
</configlet>
|
|
19
|
+
|
|
20
|
+
</object>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from plone.app.testing import FunctionalTesting
|
|
2
|
+
from plone.app.testing import IntegrationTesting
|
|
3
|
+
from plone.app.testing import PLONE_FIXTURE
|
|
4
|
+
from plone.app.testing import PloneSandboxLayer
|
|
5
|
+
from plone.app.testing import TEST_USER_ID
|
|
6
|
+
from plone.app.testing import TEST_USER_NAME
|
|
7
|
+
from plone.app.testing import login
|
|
8
|
+
from plone.app.testing import setRoles
|
|
9
|
+
from plone.namedfile.file import NamedBlobFile
|
|
10
|
+
from plone.namedfile.file import NamedBlobImage
|
|
11
|
+
|
|
12
|
+
import collective.xsendfile
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def getData(filename):
|
|
17
|
+
"""Return contents of a file from the tests directory."""
|
|
18
|
+
path = os.path.join(os.path.dirname(__file__), 'tests', filename)
|
|
19
|
+
with open(path, 'rb') as f:
|
|
20
|
+
return f.read()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestLayer(PloneSandboxLayer):
|
|
24
|
+
defaultBases = (PLONE_FIXTURE,)
|
|
25
|
+
|
|
26
|
+
def setUpZope(self, app, configurationContext):
|
|
27
|
+
self.loadZCML(package=collective.xsendfile)
|
|
28
|
+
import plone.app.registry
|
|
29
|
+
self.loadZCML(package=plone.app.registry)
|
|
30
|
+
|
|
31
|
+
def setUpPloneSite(self, portal):
|
|
32
|
+
setRoles(portal, TEST_USER_ID, ['Manager'])
|
|
33
|
+
login(portal, TEST_USER_NAME)
|
|
34
|
+
|
|
35
|
+
data = getData('image.gif')
|
|
36
|
+
portal.invokeFactory(
|
|
37
|
+
'Image', id='image',
|
|
38
|
+
image=NamedBlobImage(data=data, filename='image.gif'),
|
|
39
|
+
)
|
|
40
|
+
portal.invokeFactory(
|
|
41
|
+
'File', id='file',
|
|
42
|
+
file=NamedBlobFile(data=data, filename='image.gif'),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Pre-generate the default image scale so its blob is committed by
|
|
46
|
+
# the layer fixture. Otherwise the xsendfile patch on ImageScale
|
|
47
|
+
# bails out for uncommitted blobs and never sets X-SENDFILE.
|
|
48
|
+
portal['image'].unrestrictedTraverse('@@images').scale(
|
|
49
|
+
fieldname='image',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
FIXTURE = TestLayer()
|
|
54
|
+
|
|
55
|
+
INTEGRATION_TESTING = IntegrationTesting(
|
|
56
|
+
bases=(FIXTURE,),
|
|
57
|
+
name='collective.xsendfile:Integration')
|
|
58
|
+
FUNCTIONAL_TESTING = FunctionalTesting(
|
|
59
|
+
bases=(FIXTURE,),
|
|
60
|
+
name='collective.xsendfile:Functional')
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from collective.xsendfile.interfaces import IxsendfileSettings
|
|
5
|
+
from collective.xsendfile.testing import INTEGRATION_TESTING
|
|
6
|
+
from plone.app.testing import applyProfile
|
|
7
|
+
from plone.registry.interfaces import IRegistry
|
|
8
|
+
from zope.component import getUtility
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def clean_env():
|
|
12
|
+
for e in ['XSENDFILE_RESPONSEHEADER',
|
|
13
|
+
'XSENDFILE_PATHREGEX_SEARCH',
|
|
14
|
+
'XSENDFILE_PATHREGEX_SUBSTITUTE',
|
|
15
|
+
'XSENDFILE_ENABLE_FALLBACK']:
|
|
16
|
+
if e in os.environ:
|
|
17
|
+
del os.environ[e]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseTestCase(unittest.TestCase):
|
|
21
|
+
layer = INTEGRATION_TESTING
|
|
22
|
+
|
|
23
|
+
def setUp(self):
|
|
24
|
+
self.portal = self.layer['portal']
|
|
25
|
+
self.request = self.layer['request']
|
|
26
|
+
|
|
27
|
+
def tearDown(self):
|
|
28
|
+
clean_env()
|
|
29
|
+
|
|
30
|
+
def _download(self, content_id='file', field='file'):
|
|
31
|
+
view = self.portal[content_id].unrestrictedTraverse('@@download')
|
|
32
|
+
field_view = view.publishTraverse(self.request, field)
|
|
33
|
+
field_view()
|
|
34
|
+
return self.request.RESPONSE
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DownloadTestCase(BaseTestCase):
|
|
38
|
+
|
|
39
|
+
def test_download(self):
|
|
40
|
+
os.environ['XSENDFILE_RESPONSEHEADER'] = 'X-SENDFILE'
|
|
41
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
42
|
+
|
|
43
|
+
response = self._download()
|
|
44
|
+
xsendfile = response.getHeader('X-SENDFILE')
|
|
45
|
+
self.assertIsNotNone(xsendfile)
|
|
46
|
+
self.assertTrue(os.path.isfile(xsendfile))
|
|
47
|
+
|
|
48
|
+
def test_download_with_filename(self):
|
|
49
|
+
os.environ['XSENDFILE_RESPONSEHEADER'] = 'X-SENDFILE'
|
|
50
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
51
|
+
|
|
52
|
+
view = self.portal['file'].unrestrictedTraverse('@@download')
|
|
53
|
+
field_view = view.publishTraverse(self.request, 'file')
|
|
54
|
+
filename = field_view.publishTraverse(self.request, 'image.gif')
|
|
55
|
+
filename()
|
|
56
|
+
|
|
57
|
+
xsendfile = self.request.RESPONSE.getHeader('X-SENDFILE')
|
|
58
|
+
self.assertIsNotNone(xsendfile)
|
|
59
|
+
self.assertTrue(os.path.isfile(xsendfile))
|
|
60
|
+
|
|
61
|
+
def test_substitute(self):
|
|
62
|
+
os.environ['XSENDFILE_RESPONSEHEADER'] = 'X-SENDFILE'
|
|
63
|
+
os.environ['XSENDFILE_PATHREGEX_SEARCH'] = r'(.*)'
|
|
64
|
+
os.environ['XSENDFILE_PATHREGEX_SUBSTITUTE'] = r'/xsendfile/\1'
|
|
65
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
66
|
+
|
|
67
|
+
response = self._download()
|
|
68
|
+
xsendfile = response.getHeader('X-SENDFILE')
|
|
69
|
+
self.assertIsNotNone(xsendfile)
|
|
70
|
+
self.assertIn('/xsendfile/', xsendfile)
|
|
71
|
+
|
|
72
|
+
def test_fallback(self):
|
|
73
|
+
os.environ['XSENDFILE_RESPONSEHEADER'] = 'X-SENDFILE'
|
|
74
|
+
os.environ['XSENDFILE_ENABLE_FALLBACK'] = 'True'
|
|
75
|
+
|
|
76
|
+
response = self._download()
|
|
77
|
+
self.assertIsNone(response.getHeader('X-SENDFILE'))
|
|
78
|
+
|
|
79
|
+
def test_not_configured(self):
|
|
80
|
+
response = self._download()
|
|
81
|
+
self.assertIsNone(response.getHeader('X-SENDFILE'))
|
|
82
|
+
|
|
83
|
+
def _image_scale(self):
|
|
84
|
+
scales = self.portal['image'].unrestrictedTraverse('@@images')
|
|
85
|
+
scale = scales.publishTraverse(self.request, 'image')
|
|
86
|
+
scale.index_html()
|
|
87
|
+
return self.request.RESPONSE
|
|
88
|
+
|
|
89
|
+
def test_image_scale(self):
|
|
90
|
+
os.environ['XSENDFILE_RESPONSEHEADER'] = 'X-SENDFILE'
|
|
91
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
92
|
+
|
|
93
|
+
response = self._image_scale()
|
|
94
|
+
xsendfile = response.getHeader('X-SENDFILE')
|
|
95
|
+
self.assertIsNotNone(xsendfile)
|
|
96
|
+
self.assertTrue(os.path.isfile(xsendfile))
|
|
97
|
+
|
|
98
|
+
def test_image_scale_not_configured(self):
|
|
99
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
100
|
+
|
|
101
|
+
response = self._image_scale()
|
|
102
|
+
self.assertIsNone(response.getHeader('X-SENDFILE'))
|
|
103
|
+
|
|
104
|
+
def _generate_image_scale(self, scalename):
|
|
105
|
+
# Traverse @@images/image/<scalename>. Unlike _image_scale (which
|
|
106
|
+
# serves the pre-generated default field scale), <scalename> has not
|
|
107
|
+
# been generated by the layer, so this both generates and serves it
|
|
108
|
+
# within the same, still-uncommitted transaction -- reproducing the
|
|
109
|
+
# on-the-fly generation an acceptance/prod first-access request hits.
|
|
110
|
+
scales = self.portal['image'].unrestrictedTraverse('@@images')
|
|
111
|
+
self.request['TraversalRequestNameStack'] = [scalename]
|
|
112
|
+
scale = scales.publishTraverse(self.request, 'image')
|
|
113
|
+
scale.index_html()
|
|
114
|
+
return self.request.RESPONSE
|
|
115
|
+
|
|
116
|
+
def test_image_scale_on_the_fly_generation(self):
|
|
117
|
+
# A scale generated on-the-fly is uncommitted while the generating
|
|
118
|
+
# request is still running, so Blob.committed() raises BlobError. The
|
|
119
|
+
# xsendfile ImageScale patch must detect that and fall back to
|
|
120
|
+
# streaming rather than propagating a 500. Before the fix this call
|
|
121
|
+
# raises ZODB.interfaces.BlobError: Uncommitted changes.
|
|
122
|
+
os.environ['XSENDFILE_RESPONSEHEADER'] = 'X-SENDFILE'
|
|
123
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
124
|
+
|
|
125
|
+
response = self._generate_image_scale('preview')
|
|
126
|
+
# Uncommitted blob -> no delegation, streamed by Zope instead.
|
|
127
|
+
self.assertIsNone(response.getHeader('X-SENDFILE'))
|
|
128
|
+
|
|
129
|
+
def test_registry(self):
|
|
130
|
+
self.request.set('HTTP_X_FORWARDED_FOR', '0.0.0.0')
|
|
131
|
+
|
|
132
|
+
applyProfile(self.portal, 'plone.app.registry:default')
|
|
133
|
+
applyProfile(self.portal, 'collective.xsendfile:default')
|
|
134
|
+
registry = getUtility(IRegistry)
|
|
135
|
+
settings = registry.forInterface(IxsendfileSettings)
|
|
136
|
+
settings.xsendfile_responseheader = 'X-Sendfile'
|
|
137
|
+
|
|
138
|
+
response = self._download()
|
|
139
|
+
self.assertIsNotNone(response.getHeader('X-Sendfile'))
|