0001-add-alternative-storage-system-39517.patch
tests/test_upload_storage.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | ||
3 |
import os |
|
4 |
import mock |
|
5 |
import pytest |
|
6 | ||
7 |
from webtest import Upload |
|
8 | ||
9 |
from wcs.qommon.ident.password_accounts import PasswordAccount |
|
10 |
from wcs.formdef import FormDef |
|
11 |
from wcs.categories import Category |
|
12 |
from wcs import fields |
|
13 | ||
14 |
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub |
|
15 | ||
16 | ||
17 |
def pytest_generate_tests(metafunc): |
|
18 |
if 'pub' in metafunc.fixturenames: |
|
19 |
metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates', 'pickle-lazy'], indirect=True) |
|
20 | ||
21 | ||
22 |
@pytest.fixture |
|
23 |
def pub(request, emails): |
|
24 |
pub = create_temporary_pub( |
|
25 |
sql_mode=bool('sql' in request.param), |
|
26 |
templates_mode=bool('templates' in request.param), |
|
27 |
lazy_mode=bool('lazy' in request.param), |
|
28 |
) |
|
29 |
pub.cfg['identification'] = {'methods': ['password']} |
|
30 |
pub.cfg['language'] = {'language': 'en'} |
|
31 |
pub.write_cfg() |
|
32 |
open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w').write(''' |
|
33 |
[storage-remote] |
|
34 |
label = test out storage |
|
35 |
class = wcs.qommon.upload_storage.RemoteOpaqueUploadStorage |
|
36 |
ws = https://crypto.example.net/ws/ |
|
37 | ||
38 |
[api-secrets] |
|
39 |
crypto.example.net = 1234 |
|
40 | ||
41 |
[wscall-secrets] |
|
42 |
crypto.example.net = 1234 |
|
43 |
''') |
|
44 |
return pub |
|
45 | ||
46 | ||
47 |
def teardown_module(module): |
|
48 |
clean_temporary_pub() |
|
49 | ||
50 | ||
51 |
def create_formdef(): |
|
52 |
FormDef.wipe() |
|
53 |
formdef = FormDef() |
|
54 |
formdef.name = 'test' |
|
55 |
formdef.fields = [ |
|
56 |
fields.FileField(id='0', label='file', varname='file'), |
|
57 |
fields.FileField(id='1', label='remote file', varname='remote_file') |
|
58 |
] |
|
59 |
formdef.store() |
|
60 |
return formdef |
|
61 | ||
62 | ||
63 |
def create_user_and_admin(pub): |
|
64 |
pub.user_class.wipe() |
|
65 |
PasswordAccount.wipe() |
|
66 | ||
67 |
user = pub.user_class() |
|
68 |
user.email = 'foo@localhost' |
|
69 |
user.store() |
|
70 |
account = PasswordAccount(id='foo') |
|
71 |
account.set_password('foo') |
|
72 |
account.user_id = user.id |
|
73 |
account.store() |
|
74 | ||
75 |
admin = pub.user_class() |
|
76 |
admin.email = 'admin@localhost' |
|
77 |
admin.is_admin = True |
|
78 |
admin.store() |
|
79 |
account = PasswordAccount(id='admin') |
|
80 |
account.set_password('admin') |
|
81 |
account.user_id = admin.id |
|
82 |
account.store() |
|
83 |
return user, admin |
|
84 | ||
85 | ||
86 |
@mock.patch('wcs.wscalls.call_webservice') |
|
87 |
def test_form_file_field_upload_storage(wscall, pub): |
|
88 |
create_user_and_admin(pub) |
|
89 |
formdef = create_formdef() |
|
90 |
formdef.data_class().wipe() |
|
91 | ||
92 |
assert formdef.fields[0].storage == formdef.fields[1].storage == 'default' |
|
93 | ||
94 |
assert 'remote' in pub.get_site_storages() |
|
95 |
formdef.fields[1].storage = 'remote' |
|
96 |
formdef.store() |
|
97 |
assert formdef.fields[0].storage == 'default' |
|
98 |
assert formdef.fields[1].storage == 'remote' |
|
99 | ||
100 |
wscall.return_value = None, 200, '{"err": 0, "data": {"redirect_url": "https://crypto.example.net/"}}' |
|
101 | ||
102 |
image_content = open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read() |
|
103 | ||
104 |
upload_0 = Upload('file.jpg', image_content, 'image/jpeg') |
|
105 |
upload_1 = Upload('remote.jpg', image_content, 'image/jpeg') |
|
106 |
resp = get_app(pub).get('/test/') |
|
107 |
resp.forms[0]['f0$file'] = upload_0 |
|
108 |
resp.forms[0]['f1$file'] = upload_1 |
|
109 |
resp = resp.forms[0].submit('submit') |
|
110 |
assert 'Check values then click submit.' in resp.text |
|
111 |
resp = resp.forms[0].submit('submit') |
|
112 |
assert resp.status_int == 302 |
|
113 |
resp = resp.follow() |
|
114 |
assert 'The form has been recorded' in resp.text |
|
115 | ||
116 |
assert resp.text.count('thumbnail=1') == 1 # thumbnail only for first file |
|
117 | ||
118 |
resp = resp.click('remote.jpg') |
|
119 |
assert resp.location.startswith('https://crypto.example.net/') |
|
120 |
assert '&signature=' in resp.location |
|
121 | ||
122 |
admin_app = login(get_app(pub), username='admin', password='admin') |
|
123 |
resp = admin_app.get('/api/forms/test/1/', status=200) |
|
124 |
assert resp.json['fields']['file']['content'].startswith('/9j/4AAQSkZJRg') |
|
125 |
assert resp.json['fields']['remote_file']['content'] == '' |
|
126 |
assert resp.json['fields']['remote_file']['storage_attrs']['redirect_url'] == 'https://crypto.example.net/' |
wcs/fields.py | ||
---|---|---|
943 | 943 |
max_file_size = None |
944 | 944 |
automatic_image_resize = False |
945 | 945 |
allow_portfolio_picking = False |
946 |
storage = 'default' |
|
946 | 947 | |
947 | 948 |
widget_class = FileWithPreviewWidget |
948 | 949 |
extra_attributes = [ |
... | ... | |
950 | 951 |
'max_file_size', |
951 | 952 |
'allow_portfolio_picking', |
952 | 953 |
'automatic_image_resize', |
954 |
'storage', |
|
953 | 955 |
] |
954 | 956 | |
955 | 957 |
def __init__(self, *args, **kwargs): |
... | ... | |
988 | 990 |
title=_('Allow user to pick a file from a portfolio'), |
989 | 991 |
value=self.allow_portfolio_picking, |
990 | 992 |
advanced=(self.allow_portfolio_picking is FileField.allow_portfolio_picking)) |
993 |
storages = get_publisher().get_site_storages() |
|
994 |
if storages: |
|
995 |
storage_options = [('default', '---', {})] |
|
996 |
storage_options += [(key, value['label'], key) for key, value in storages.items()] |
|
997 |
form.add(SingleSelectWidget, 'storage', title=_('File storage system'), |
|
998 |
value=self.storage, options=storage_options, |
|
999 |
advanced=bool(not self.storage or self.storage == 'default')) |
|
991 | 1000 | |
992 | 1001 |
def get_admin_attributes(self): |
993 | 1002 |
return WidgetField.get_admin_attributes(self) + [ |
994 | 1003 |
'document_type', 'max_file_size', 'allow_portfolio_picking', |
995 |
'automatic_image_resize'] |
|
1004 |
'automatic_image_resize', 'storage']
|
|
996 | 1005 | |
997 | 1006 |
@classmethod |
998 | 1007 |
def convert_value_from_anything(cls, value): |
... | ... | |
1032 | 1041 |
t = TemplateIO(html=True) |
1033 | 1042 |
t += htmltext('<div class="file-field">') |
1034 | 1043 |
t += htmltext('<a download="%s" href="[download]?f=%s">') % (value.base_filename, self.id) |
1035 |
if include_image_thumbnail and can_thumbnail(value.content_type): |
|
1044 |
if include_image_thumbnail and can_thumbnail(value.content_type) and not hasattr(value, 'storage_attrs'):
|
|
1036 | 1045 |
t += htmltext('<img alt="" src="[download]?f=%s&thumbnail=1"/>') % self.id |
1037 | 1046 |
t += htmltext('<span>%s</span>') % value |
1038 | 1047 |
t += htmltext('</a></div>') |
... | ... | |
1042 | 1051 |
return [str(value) if value else ''] |
1043 | 1052 | |
1044 | 1053 |
def get_json_value(self, value): |
1045 |
return {
|
|
1054 |
out = {
|
|
1046 | 1055 |
'field_id': self.id, |
1047 | 1056 |
'filename': value.base_filename, |
1048 | 1057 |
'content_type': value.content_type or 'application/octet-stream', |
1049 | 1058 |
'content': force_text(base64.b64encode(value.get_content())), |
1050 | 1059 |
} |
1060 |
if hasattr(value, 'storage_attrs'): |
|
1061 |
out['storage_attrs'] = value.storage_attrs |
|
1062 |
return out |
|
1051 | 1063 | |
1052 | 1064 |
def from_json_value(self, value): |
1053 | 1065 |
if value and 'filename' in value and 'content' in value: |
wcs/forms/common.py | ||
---|---|---|
22 | 22 |
from quixote.util import randbytes |
23 | 23 | |
24 | 24 |
from wcs import data_sources |
25 |
from wcs.api_utils import get_user_from_api_query_string, is_url_signed |
|
25 |
from wcs.api_utils import get_user_from_api_query_string, is_url_signed, sign_url_auto_orig
|
|
26 | 26 |
from wcs.fields import WidgetField, FileField |
27 | 27 |
from wcs.workflows import EditableWorkflowStatusItem |
28 | 28 | |
... | ... | |
67 | 67 |
if component and component != file.base_filename: |
68 | 68 |
raise errors.TraversalError() |
69 | 69 | |
70 |
if hasattr(file, 'storage_attrs'): # remote storage |
|
71 |
if self.thumbnails: |
|
72 |
raise errors.TraversalError() |
|
73 |
if not file.storage_attrs.get('redirect_url'): |
|
74 |
raise errors.TraversalError() |
|
75 |
redirect_url = sign_url_auto_orig(file.storage_attrs['redirect_url']) |
|
76 |
return redirect(redirect_url) |
|
77 | ||
70 | 78 |
response = get_response() |
71 | 79 |
if file.content_type: |
72 | 80 |
response.set_content_type(file.content_type) |
... | ... | |
289 | 297 |
continue |
290 | 298 |
if field.type == 'file': |
291 | 299 |
# add back file to session |
292 |
tempfile = session.add_tempfile(form_data[field.id]) |
|
300 |
tempfile = session.add_tempfile(form_data[field.id], storage=field.storage)
|
|
293 | 301 |
form_data[field.id].token = tempfile['token'] |
294 | 302 |
form_data['is_recalled_draft'] = True |
295 | 303 |
form_data['draft_formdata_id'] = filled.id |
... | ... | |
673 | 681 |
if not hasattr(file, 'content_type'): |
674 | 682 |
raise errors.TraversalError() |
675 | 683 | |
684 |
if hasattr(file, 'storage_attrs'): # remote storage |
|
685 |
if get_request().form.get('thumbnail') == '1': |
|
686 |
raise errors.TraversalError() |
|
687 |
if not file.storage_attrs.get('redirect_url'): |
|
688 |
raise errors.TraversalError() |
|
689 |
redirect_url = sign_url_auto_orig(file.storage_attrs['redirect_url']) |
|
690 |
return redirect(redirect_url) |
|
691 | ||
676 | 692 |
file_url = 'files/%s/' % fn |
677 | 693 |
if get_request().form.get('thumbnail') == '1': |
678 | 694 |
file_url += 'thumbnail/' |
wcs/qommon/form.py | ||
---|---|---|
58 | 58 |
import quixote.form.widget |
59 | 59 | |
60 | 60 |
from quixote import get_publisher, get_request, get_response, get_session |
61 |
from quixote.http_request import Upload |
|
62 | 61 |
from quixote.form import * |
63 | 62 |
from quixote.html import htmltext, htmltag, htmlescape, TemplateIO |
63 |
from quixote.http_request import Upload |
|
64 | 64 |
from quixote.util import randbytes |
65 | 65 | |
66 | 66 |
from django.utils.encoding import force_bytes, force_text |
67 | 67 |
from django.utils import six |
68 | 68 |
from django.utils.six.moves.html_parser import HTMLParser |
69 |
from django.utils.six import StringIO |
|
70 | 69 | |
71 | 70 |
from django.conf import settings |
72 | 71 |
from django.utils.safestring import mark_safe |
... | ... | |
81 | 80 |
from .misc import strftime, C_, HAS_PDFTOPPM, json_loads |
82 | 81 |
from .publisher import get_cfg |
83 | 82 |
from .template_utils import render_block_to_string |
83 |
from .upload_storage import PicklableUpload |
|
84 | ||
84 | 85 | |
85 | 86 |
QuixoteForm = Form |
86 | 87 | |
... | ... | |
666 | 667 |
max_file_size_bytes = None # will be filled automatically |
667 | 668 | |
668 | 669 |
def __init__(self, name, value=None, **kwargs): |
670 |
self.storage = kwargs.pop('storage', None) |
|
669 | 671 |
CompositeWidget.__init__(self, name, value, **kwargs) |
670 | 672 |
self.value = value |
671 | 673 |
self.readonly = kwargs.get('readonly') |
... | ... | |
677 | 679 |
self.add(HiddenWidget, 'token') |
678 | 680 |
if not self.readonly: |
679 | 681 |
attrs = {'data-url': get_publisher().get_root_url() + 'tmp-upload'} |
682 |
if self.storage: |
|
683 |
attrs['data-url'] += '?storage=%s' % self.storage |
|
680 | 684 |
self.file_type = kwargs.pop('file_type', None) |
681 | 685 |
if self.file_type: |
682 | 686 |
attrs['accept'] = ','.join(self.file_type) |
... | ... | |
700 | 704 |
# oops, it has a token but it's not in the session; this is |
701 | 705 |
# probably because it was restored from a draft file created |
702 | 706 |
# from an expired session. |
703 |
self.value.token = get_session().add_tempfile(self.value).get('token') |
|
707 |
self.value.token = get_session().add_tempfile(self.value, storage=self.storage).get('token')
|
|
704 | 708 |
self.get_widget('token').set_value(self.value.token) |
705 | 709 | |
706 | 710 |
def add_media(self): |
... | ... | |
742 | 746 |
if self.get('token'): |
743 | 747 |
token = self.get('token') |
744 | 748 |
elif self.get('file'): |
745 |
token = get_session().add_tempfile(self.get('file'))['token'] |
|
749 |
token = get_session().add_tempfile(self.get('file'), storage=self.storage)['token']
|
|
746 | 750 |
request.form[self.get_widget('token').get_name()] = token |
747 | 751 |
else: |
748 | 752 |
token = None |
... | ... | |
755 | 759 |
# there's no file, the other checks are irrelevant. |
756 | 760 |
return |
757 | 761 | |
762 |
if self.storage and self.storage != self.storage: |
|
763 |
self.error = _('unknown storage system (system error)') |
|
764 | ||
758 | 765 |
# Don't trust the browser supplied MIME type, update the Upload object |
759 | 766 |
# with a MIME type created with magic (or based on the extension if the |
760 | 767 |
# module is missing). |
... | ... | |
762 | 769 |
# This also helps people uploading PDF files that were downloaded from |
763 | 770 |
# sites setting a wrong MIME type (like application/force-download) for |
764 | 771 |
# various reasons. |
765 |
if magic: |
|
772 |
if magic and self.value.fp:
|
|
766 | 773 |
if hasattr(magic, 'MagicException'): |
767 | 774 |
mime = magic.Magic(mime=True) |
768 | 775 |
filetype = mime.from_file(self.value.fp.name) |
... | ... | |
772 | 779 |
filetype = magic_object.file(self.value.fp.name).split(';')[0] |
773 | 780 |
magic_object.close() |
774 | 781 |
else: |
775 |
filetype, encoding = mimetypes.guess_type(self.value.base_filename) |
|
782 |
filetype = getattr(self.value, 'storage_attrs', {}).get('content_type') |
|
783 |
if not filetype: |
|
784 |
filetype, encoding = mimetypes.guess_type(self.value.base_filename) |
|
776 | 785 | |
777 | 786 |
if not filetype: |
778 | 787 |
filetype = 'application/octet-stream' |
... | ... | |
812 | 821 |
self.error = _('forbidden file type') |
813 | 822 | |
814 | 823 | |
815 |
class PicklableUpload(Upload): |
|
816 |
def __getstate__(self): |
|
817 |
odict = self.__dict__.copy() |
|
818 |
if 'fp' in odict: |
|
819 |
del odict['fp'] |
|
820 | ||
821 |
basedir = os.path.join(get_publisher().app_dir, 'uploads') |
|
822 |
if not os.path.exists(basedir): |
|
823 |
os.mkdir(basedir) |
|
824 |
if 'qfilename' in odict: |
|
825 |
filepath = os.path.join(basedir, self.qfilename) |
|
826 |
else: |
|
827 |
self.qfilename = misc.file_digest(self.fp) |
|
828 |
filepath = os.path.join(basedir, self.qfilename) |
|
829 | ||
830 |
if 'fp' in self.__dict__ and not self.fp.closed: |
|
831 |
self.fp.seek(0) |
|
832 |
atomic_write(filepath, self.fp, async_op=False) |
|
833 | ||
834 |
odict['qfilename'] = self.qfilename |
|
835 |
return odict |
|
836 | ||
837 |
def get_file_pointer(self): |
|
838 |
if 'fp' in self.__dict__ and self.__dict__.get('fp') is not None: |
|
839 |
return self.__dict__.get('fp') |
|
840 |
elif hasattr(self, 'qfilename'): |
|
841 |
basedir = os.path.join(get_publisher().app_dir, 'uploads') |
|
842 |
self.fp = open(os.path.join(basedir, self.qfilename), 'rb') |
|
843 |
return self.fp |
|
844 |
return None |
|
845 | ||
846 |
def __setstate__(self, dict): |
|
847 |
self.__dict__.update(dict) |
|
848 |
if hasattr(self, 'data'): |
|
849 |
# backward compatibility with older w.c.s. version |
|
850 |
self.fp = StringIO(self.data) |
|
851 |
del self.data |
|
852 | ||
853 |
def get_file(self): |
|
854 |
# quack like UploadedFile |
|
855 |
return self.get_file_pointer() |
|
856 | ||
857 |
def get_filename(self): |
|
858 |
if not hasattr(self, 'qfilename'): |
|
859 |
raise AttributeError('filename') |
|
860 |
basedir = os.path.join(get_publisher().app_dir, 'uploads') |
|
861 |
return os.path.join(basedir, self.qfilename) |
|
862 | ||
863 |
def get_content(self): |
|
864 |
if hasattr(self, 'qfilename'): |
|
865 |
filename = os.path.join(get_publisher().app_dir, 'uploads', self.qfilename) |
|
866 |
return open(filename, 'rb').read() |
|
867 |
return None |
|
868 | ||
869 |
def get_base64_content(self): |
|
870 |
content = self.get_content() |
|
871 |
if content: |
|
872 |
return base64.encodestring(content) |
|
873 |
return None |
|
874 | ||
875 | ||
876 | 824 |
class EmailWidget(StringWidget): |
877 | 825 |
HTML_TYPE = 'email' |
878 | 826 |
wcs/qommon/misc.py | ||
---|---|---|
540 | 540 |
return obj.decode('ascii') |
541 | 541 | |
542 | 542 |
if hasattr(obj, 'base_filename'): |
543 |
return {
|
|
543 |
out = {
|
|
544 | 544 |
'filename': obj.base_filename, |
545 | 545 |
'content_type': obj.content_type or 'application/octet-stream', |
546 | 546 |
'content': base64.b64encode(obj.get_content()), |
547 |
} |
|
547 |
} |
|
548 |
if hasattr(obj, 'storage_attrs'): # remote storage |
|
549 |
out['storage_attrs'] = obj.storage_attrs |
|
550 |
return out |
|
548 | 551 | |
549 | 552 |
# Let the base class default method raise the TypeError |
550 | 553 |
return json.JSONEncoder.default(self, obj) |
wcs/qommon/publisher.py | ||
---|---|---|
393 | 393 |
except ConfigParser.NoOptionError: |
394 | 394 |
return None |
395 | 395 | |
396 |
def get_site_storages(self): |
|
397 |
if self.site_options is None: |
|
398 |
self.load_site_options() |
|
399 |
storages = {} |
|
400 |
for section, definition in self.site_options.items(): |
|
401 |
if section.startswith('storage-') and 'label' in definition and 'class' in definition: |
|
402 |
storage_id = section[8:] |
|
403 |
storages[storage_id] = dict(definition) |
|
404 |
storages[storage_id]['id'] = storage_id |
|
405 |
return storages |
|
406 | ||
396 | 407 |
def set_config(self, request = None): |
397 | 408 |
self.reload_cfg() |
398 | 409 |
self.site_options = None # reset at the beginning of a request |
wcs/qommon/sessions.py | ||
---|---|---|
31 | 31 |
from . import misc |
32 | 32 |
from .storage import StorableObject |
33 | 33 |
from .publisher import get_cfg |
34 |
from .upload_storage import get_storage_object |
|
34 | 35 |
from quixote.publish import get_session, get_session_manager, get_request |
35 | 36 | |
36 | 37 | |
... | ... | |
256 | 257 |
def get_signer(self): |
257 | 258 |
return Signer(settings.SECRET_KEY + self.id) |
258 | 259 | |
259 |
def add_tempfile(self, upload): |
|
260 |
from wcs.qommon.form import PicklableUpload |
|
261 |
token = randbytes(8) |
|
262 |
upload.__class__ = PicklableUpload |
|
263 |
upload.time = time.time() |
|
264 |
upload.token = token |
|
265 |
# and saves it |
|
260 |
def add_tempfile(self, upload, storage=None): |
|
266 | 261 |
dirname = os.path.join(get_publisher().app_dir, 'tempfiles') |
267 | 262 |
if not os.path.exists(dirname): |
268 | 263 |
os.mkdir(dirname) |
269 |
filename = os.path.join(dirname, token) |
|
270 |
fd = open(filename, 'wb') |
|
271 |
upload.get_file_pointer().seek(0) |
|
272 |
fd.write(upload.get_file_pointer().read()) |
|
273 |
size = fd.tell() |
|
274 |
fd.close() |
|
264 |
token = randbytes(8) |
|
265 |
upload.time = time.time() |
|
266 |
upload.token = token |
|
267 |
upload.storage = storage |
|
268 |
get_storage_object(upload.storage).save_tempfile(upload) |
|
275 | 269 | |
276 | 270 |
self.has_uploads = True |
277 | 271 |
if not self.id: |
... | ... | |
285 | 279 |
'base_filename': upload.base_filename, |
286 | 280 |
'content_type': upload.content_type, |
287 | 281 |
'charset': upload.charset, |
288 |
'size': size,
|
|
282 |
'size': getattr(upload, 'size', None),
|
|
289 | 283 |
'session': self.id, |
290 | 284 |
'token': signer.sign(token), |
291 | 285 |
'unsigned_token': token, |
286 |
'storage': upload.storage, |
|
287 |
'storage-attrs': getattr(upload, 'storage_attrs', None), |
|
292 | 288 |
} |
289 |
filename = os.path.join(get_publisher().app_dir, 'tempfiles', upload.token) |
|
293 | 290 |
with open(filename + '.json', 'w') as fd: |
294 | 291 |
json.dump(data, fd, indent=2) |
295 | 292 | |
... | ... | |
324 | 321 |
if not temp: |
325 | 322 |
return temp |
326 | 323 | |
327 |
from wcs.qommon.form import PicklableUpload |
|
328 |
value = PicklableUpload(temp['orig_filename'], |
|
329 |
temp['content_type'], temp['charset']) |
|
330 |
dirname = os.path.join(get_publisher().app_dir, 'tempfiles') |
|
331 |
filename = os.path.join(dirname, temp['unsigned_token']) |
|
332 |
value.token = token |
|
333 |
value.fp = open(filename, 'rb') |
|
334 |
return value |
|
324 |
return get_storage_object(temp.get('storage')).get_tempfile(temp) |
|
335 | 325 | |
336 | 326 |
def add_extra_variables(self, **kwargs): |
337 | 327 |
if not self.extra_variables: |
wcs/qommon/upload_storage.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import base64 |
|
18 |
import os |
|
19 | ||
20 |
from quixote import get_publisher |
|
21 |
from quixote.http_request import Upload |
|
22 | ||
23 |
from django.utils.module_loading import import_string |
|
24 |
from django.utils.six import StringIO |
|
25 | ||
26 |
from .errors import ConnectionError |
|
27 |
from .misc import json_loads, file_digest |
|
28 |
from .storage import atomic_write |
|
29 | ||
30 | ||
31 |
class PicklableUpload(Upload): |
|
32 |
def __getstate__(self): |
|
33 |
odict = self.__dict__.copy() |
|
34 |
if 'fp' in odict: |
|
35 |
del odict['fp'] |
|
36 |
get_storage_object(getattr(self, 'storage', None)).save(self) |
|
37 |
odict['qfilename'] = getattr(self, 'qfilename', None) |
|
38 |
return odict |
|
39 | ||
40 |
def get_file_pointer(self): |
|
41 |
if 'fp' in self.__dict__ and self.__dict__.get('fp') is not None: |
|
42 |
return self.__dict__.get('fp') |
|
43 |
elif hasattr(self, 'qfilename'): |
|
44 |
basedir = os.path.join(get_publisher().app_dir, 'uploads') |
|
45 |
self.fp = open(os.path.join(basedir, self.qfilename), 'rb') |
|
46 |
return self.fp |
|
47 |
return None |
|
48 | ||
49 |
def __setstate__(self, dict): |
|
50 |
self.__dict__.update(dict) |
|
51 |
if hasattr(self, 'data'): |
|
52 |
# backward compatibility with older w.c.s. version |
|
53 |
self.fp = StringIO(self.data) |
|
54 |
del self.data |
|
55 | ||
56 |
def get_file(self): |
|
57 |
# quack like UploadedFile |
|
58 |
return self.get_file_pointer() |
|
59 | ||
60 |
def get_filename(self): |
|
61 |
if not hasattr(self, 'qfilename'): |
|
62 |
raise AttributeError('filename') |
|
63 |
basedir = os.path.join(get_publisher().app_dir, 'uploads') |
|
64 |
return os.path.join(basedir, self.qfilename) |
|
65 | ||
66 |
def get_content(self): |
|
67 |
if hasattr(self, 'storage_attrs'): # remote storage |
|
68 |
return b'' |
|
69 |
if hasattr(self, 'qfilename'): |
|
70 |
filename = os.path.join(get_publisher().app_dir, 'uploads', self.qfilename) |
|
71 |
return open(filename, 'rb').read() |
|
72 |
return None |
|
73 | ||
74 |
def get_base64_content(self): |
|
75 |
content = self.get_content() |
|
76 |
if content: |
|
77 |
return base64.encodestring(content) |
|
78 |
return b'' |
|
79 | ||
80 | ||
81 |
class UploadStorageError(Exception): |
|
82 |
pass |
|
83 | ||
84 | ||
85 |
class UploadStorage(object): |
|
86 |
def save_tempfile(self, upload): |
|
87 |
upload.__class__ = PicklableUpload |
|
88 |
dirname = os.path.join(get_publisher().app_dir, 'tempfiles') |
|
89 |
filename = os.path.join(dirname, upload.token) |
|
90 |
fd = open(filename, 'wb') |
|
91 |
upload.get_file_pointer().seek(0) |
|
92 |
fd.write(upload.get_file_pointer().read()) |
|
93 |
upload.size = fd.tell() |
|
94 |
fd.close() |
|
95 | ||
96 |
def get_tempfile(self, temp_data): |
|
97 |
value = PicklableUpload( |
|
98 |
temp_data['orig_filename'], |
|
99 |
temp_data['content_type'], |
|
100 |
temp_data['charset']) |
|
101 |
value.storage = temp_data.get('storage') |
|
102 |
dirname = os.path.join(get_publisher().app_dir, 'tempfiles') |
|
103 |
filename = os.path.join(dirname, temp_data['unsigned_token']) |
|
104 |
value.token = temp_data['token'] |
|
105 |
value.fp = open(filename, 'rb') |
|
106 |
return value |
|
107 | ||
108 |
def save(self, upload): |
|
109 |
basedir = os.path.join(get_publisher().app_dir, 'uploads') |
|
110 |
if not os.path.exists(basedir): |
|
111 |
os.mkdir(basedir) |
|
112 |
if getattr(upload, 'qfilename', None): |
|
113 |
filepath = os.path.join(basedir, upload.qfilename) |
|
114 |
else: |
|
115 |
upload.qfilename = file_digest(upload.fp) |
|
116 |
filepath = os.path.join(basedir, upload.qfilename) |
|
117 | ||
118 |
if getattr(upload, 'fp', None) and not upload.fp.closed: |
|
119 |
upload.fp.seek(0) |
|
120 |
atomic_write(filepath, upload.fp, async_op=False) |
|
121 | ||
122 | ||
123 |
class RemoteOpaqueUploadStorage(object): |
|
124 |
def __init__(self, ws, **kwargs): |
|
125 |
self.ws = ws |
|
126 | ||
127 |
def save_tempfile(self, upload): |
|
128 |
if getattr(upload, 'storage_attrs', None): |
|
129 |
# upload is already a remote PicklableUpload, it does not |
|
130 |
# have content. We are certainly restoring a draft. |
|
131 |
return |
|
132 | ||
133 |
upload.__class__ = PicklableUpload |
|
134 |
dirname = os.path.join(get_publisher().app_dir, 'tempfiles') |
|
135 |
filename = os.path.join(dirname, upload.token) |
|
136 |
fd = open(filename, 'wb') |
|
137 |
upload.get_file_pointer().seek(0) |
|
138 |
base64content = base64.b64encode(upload.get_file_pointer().read()) |
|
139 |
fd.close() |
|
140 | ||
141 |
post_data = { |
|
142 |
'file': { |
|
143 |
'filename': upload.base_filename, |
|
144 |
'content_type': upload.content_type or 'application/octet-stream', |
|
145 |
'content': base64content, |
|
146 |
} |
|
147 |
} |
|
148 |
try: |
|
149 |
from wcs.wscalls import call_webservice |
|
150 |
response, status, data = call_webservice(self.ws, method='POST', post_data=post_data) |
|
151 |
except ConnectionError as e: |
|
152 |
raise UploadStorageError('remote storage connection error (%r)' % e) |
|
153 |
if status not in (200, 201): |
|
154 |
raise UploadStorageError('remote storage returns status %s' % status) |
|
155 |
try: |
|
156 |
ws_result = json_loads(data) |
|
157 |
except (ValueError, TypeError) as e: |
|
158 |
raise UploadStorageError('remote storage returns invalid JSON') |
|
159 |
if not isinstance(ws_result, dict): |
|
160 |
raise UploadStorageError('remote storage returns non-dict JSON') |
|
161 |
if ws_result.get('err') != 0: |
|
162 |
raise UploadStorageError('remote storage returns err = %s' % ws_result.get('err')) |
|
163 |
ws_result_data = ws_result.get('data', {}) |
|
164 |
if not ws_result_data.get('redirect_url'): |
|
165 |
raise UploadStorageError('remote storage returns data.redirect_url= %s' % |
|
166 |
ws_result_data.get('redirect_url')) |
|
167 | ||
168 |
upload.storage_attrs = ws_result_data |
|
169 | ||
170 |
def get_tempfile(self, temp_data): |
|
171 |
value = PicklableUpload( |
|
172 |
temp_data['orig_filename'], |
|
173 |
temp_data['content_type'], |
|
174 |
temp_data['charset']) |
|
175 |
value.storage = temp_data.get('storage') |
|
176 |
value.storage_attrs = temp_data['storage-attrs'] |
|
177 |
value.token = temp_data['token'] |
|
178 |
value.fp = None |
|
179 |
return value |
|
180 | ||
181 |
def save(self, upload): |
|
182 |
pass |
|
183 | ||
184 | ||
185 |
def get_storage_object(storage): |
|
186 |
if not storage or storage == 'default': |
|
187 |
return UploadStorage() |
|
188 |
storage_cfg = get_publisher().get_site_storages().get(storage) |
|
189 |
if not storage_cfg: |
|
190 |
raise UploadStorageError('unknown storage %s' % storage) |
|
191 |
try: |
|
192 |
storage_class = import_string(storage_cfg['class']) |
|
193 |
except ImportError: |
|
194 |
raise UploadStorageError('failed to import storage class %s' % storage_class) |
|
195 |
return storage_class(**storage_cfg) |
wcs/root.py | ||
---|---|---|
292 | 292 | |
293 | 293 |
def tmp_upload(self): |
294 | 294 |
results = [] |
295 |
storage = get_request().form.get('storage') |
|
295 | 296 |
for k, v in get_request().form.items(): |
296 | 297 |
if hasattr(v, 'fp'): |
297 |
tempfile = get_session().add_tempfile(v) |
|
298 |
tempfile = get_session().add_tempfile(v, storage=storage)
|
|
298 | 299 |
results.append({'name': tempfile.get('base_filename'), |
299 | 300 |
'type': tempfile.get('content_type'), |
300 | 301 |
'size': tempfile.get('size'), |
301 |
- |