Projet

Général

Profil

0001-add-alternative-storage-system-39517.patch

Thomas Noël, 05 février 2020 12:35

Télécharger (29,4 ko)

Voir les différences:

Subject: [PATCH] add alternative storage system (#39517)

 tests/test_upload_storage.py | 126 ++++++++++++++++++++++
 wcs/fields.py                |  18 +++-
 wcs/forms/common.py          |  20 +++-
 wcs/qommon/form.py           |  82 +++------------
 wcs/qommon/misc.py           |   7 +-
 wcs/qommon/publisher.py      |  11 ++
 wcs/qommon/sessions.py       |  34 +++---
 wcs/qommon/upload_storage.py | 195 +++++++++++++++++++++++++++++++++++
 wcs/root.py                  |   3 +-
 9 files changed, 399 insertions(+), 97 deletions(-)
 create mode 100644 tests/test_upload_storage.py
 create mode 100644 wcs/qommon/upload_storage.py
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
-