Projet

Général

Profil

0001-assets-factorize-import-export-code-39425.patch

Nicolas Roche, 27 juillet 2020 16:53

Télécharger (10,5 ko)

Voir les différences:

Subject: [PATCH 1/2] assets: factorize import/export code (#39425)

 combo/apps/assets/utils.py | 79 ++++++++++++++++++++++++++++++++
 combo/apps/assets/views.py | 31 ++-----------
 tests/test_assets.py       | 92 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 174 insertions(+), 28 deletions(-)
 create mode 100644 combo/apps/assets/utils.py
combo/apps/assets/utils.py
1
# combo - content management system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18
import os
19
import tarfile
20

  
21
from django.core.files.storage import default_storage
22
from django.utils.six import BytesIO
23

  
24
from .models import Asset
25

  
26

  
27
def clean_assets_files():
28
    media_prefix = default_storage.path('')
29
    for basedir, dirnames, filenames in os.walk(media_prefix):
30
        for filename in filenames:
31
            os.remove('%s/%s' % (basedir, filename))
32

  
33

  
34
def add_tar_content(tar, filename, content):
35
    file = tarfile.TarInfo(filename)
36
    fd = BytesIO()
37
    fd.write(content.encode('utf-8'))
38
    file.size = fd.tell()
39
    fd.seek(0)
40
    tar.addfile(file, fileobj=fd)
41
    fd.close()
42

  
43

  
44
def untar_assets_files(tar, overwrite=False):
45
    media_prefix = default_storage.path('')
46
    data = {}
47
    for tarinfo in tar.getmembers():
48
        filepath = default_storage.path(tarinfo.name)
49
        if not overwrite and os.path.exists(filepath):
50
            continue
51
        if tarinfo.name == '_assets.json':
52
            json_assets = tar.extractfile(tarinfo).read()
53
            data = json.loads(json_assets.decode('utf-8'))
54
        elif tarinfo.name != '_site.json':
55
            tar.extract(tarinfo, path=media_prefix)
56
    return data
57

  
58

  
59
def tar_assets_files(tar):
60
    media_prefix = default_storage.path('')
61
    for basedir, dirnames, filenames in os.walk(media_prefix):
62
        for filename in filenames:
63
            tar.add(os.path.join(basedir, filename),
64
                    os.path.join(basedir, filename)[len(media_prefix):])
65
    export = {'assets': Asset.export_all_for_json()}
66
    add_tar_content(tar, '_assets.json', json.dumps(export, indent=2))
67

  
68

  
69
def import_assets(fd, overwrite=False):
70
    tar = tarfile.open(mode='r', fileobj=fd)
71
    data = untar_assets_files(tar, overwrite=overwrite)
72
    Asset.load_serialized_objects(data.get('assets') or [])
73
    tar.close()
74

  
75

  
76
def export_assets(fd):
77
    tar = tarfile.open(mode='w', fileobj=fd)
78
    tar_assets_files(tar)
79
    tar.close()
combo/apps/assets/views.py
29 29
from django.utils.six import BytesIO
30 30
from django.utils.translation import ugettext_lazy as _
31 31
from django.views.generic import TemplateView, ListView, FormView
32 32

  
33 33
import ckeditor
34 34
from sorl.thumbnail.shortcuts import get_thumbnail
35 35

  
36 36
from combo.data.models import CellBase
37
from combo.data.utils import import_site
37
from combo.apps.assets.utils import import_assets, export_assets
38 38

  
39 39
from .forms import AssetUploadForm, AssetsImportForm
40 40
from .models import Asset
41 41

  
42 42

  
43 43
class CkEditorAsset(object):
44 44
    def __init__(self, filepath):
45 45
        self.filepath = filepath
......
333 333
class AssetsImport(FormView):
334 334
    form_class = AssetsImportForm
335 335
    template_name = 'combo/manager_assets_import.html'
336 336
    success_url = reverse_lazy('combo-manager-assets')
337 337

  
338 338
    def form_valid(self, form):
339 339
        overwrite = form.cleaned_data.get('overwrite')
340 340
        try:
341
            assets = tarfile.open(fileobj=form.cleaned_data['assets_file'])
341
            import_assets(form.cleaned_data['assets_file'], overwrite)
342 342
        except tarfile.TarError:
343 343
            messages.error(self.request, _('The assets file is not valid.'))
344 344
            return super(AssetsImport, self).form_valid(form)
345
        media_prefix = default_storage.path('')
346
        for tarinfo in assets.getmembers():
347
            filepath = default_storage.path(tarinfo.name)
348
            if not overwrite and os.path.exists(filepath):
349
                continue
350
            if tarinfo.name == '_assets.json':
351
                json_assets = assets.extractfile(tarinfo).read()
352
                import_site(json.loads(json_assets.decode('utf-8')))
353
            else:
354
                assets.extract(tarinfo, path=media_prefix)
355 345
        messages.success(self.request, _('The assets file has been imported.'))
356 346
        return super(AssetsImport, self).form_valid(form)
357 347

  
358 348
assets_import = AssetsImport.as_view()
359 349

  
360 350

  
361 351
def assets_export(request, *args, **kwargs):
362 352
    fd = BytesIO()
363
    assets_file = tarfile.open('assets.tar', 'w', fileobj=fd)
364
    media_prefix = default_storage.path('')
365
    for basedir, dirnames, filenames in os.walk(media_prefix):
366
        for filename in filenames:
367
            assets_file.add(
368
                    os.path.join(basedir, filename),
369
                    os.path.join(basedir, filename)[len(media_prefix):])
370
    if Asset.objects.exists():
371
        json_file = tarfile.TarInfo('_assets.json')
372
        json_fd = BytesIO()
373
        export = {'assets': Asset.export_all_for_json(),}
374
        json_fd.write(json.dumps(export).encode('utf-8'))
375
        json_file.size = json_fd.tell()
376
        json_fd.seek(0)
377
        assets_file.addfile(json_file, fileobj=json_fd)
378
    assets_file.close()
353
    export_assets(fd)
379 354
    return HttpResponse(fd.getvalue(), content_type='application/x-tar')
380 355

  
381 356

  
382 357
def serve_asset(request, key):
383 358
    asset = get_object_or_404(Asset, key=key)
384 359

  
385 360
    if not os.path.exists(asset.asset.path):
386 361
        raise Http404()
tests/test_assets.py
1 1
# -*- coding: utf-8 -*-
2 2

  
3 3
import base64
4
import os
5
import tarfile
4 6

  
5 7
from django.urls import reverse
8
from django.core.files.storage import default_storage
9
from django.core.files import File
10
from django.utils.six import BytesIO
6 11

  
7 12
import pytest
8 13

  
9 14
from combo.apps.assets.models import Asset
15
from combo.apps.assets.utils import (add_tar_content, clean_assets_files,
16
         export_assets, import_assets, untar_assets_files, tar_assets_files)
17

  
10 18

  
11 19
pytestmark = pytest.mark.django_db
12 20

  
21

  
22
@pytest.fixture
23
def some_assets():
24
    Asset(key='banner', asset=File(BytesIO(b'test'), 'test.png')).save()
25
    Asset(key='favicon', asset=File(BytesIO(b'test2'), 'test2.png')).save()
26

  
27

  
28
def count_asset_files():
29
    nb_assets = 0
30
    media_prefix = default_storage.path('')
31
    for basedir, dirnames, filenames in os.walk(media_prefix):
32
        nb_assets += len(filenames)
33
    return nb_assets
34

  
35

  
13 36
def test_asset_set_api(app, john_doe):
14 37
    app.authorization = ('Basic', (john_doe.username, john_doe.username))
15 38
    resp = app.post_json(reverse('api-assets-set', kwargs={'key': 'plop'}), params={
16 39
        'asset': {
17 40
            'content': base64.encodebytes(b'plop').decode('ascii'),
18 41
            'content_type': 'text/plain',
19 42
            'filename': 'plop.txt',
20 43
        }
......
37 60
        resp = app.post_json(reverse('api-assets-set', kwargs={'key': 'plop'}), params={
38 61
            'asset': {
39 62
                'content': invalid_value,
40 63
                'content_type': 'text/plain',
41 64
                'filename': 'plop.txt',
42 65
            }
43 66
            }, status=400)
44 67
        assert resp.json.get('err') == 1
68
    clean_assets_files()
69

  
70

  
71
def test_clean_assets_files(some_assets):
72
    assert count_asset_files() == 2
73
    clean_assets_files()
74
    assert count_asset_files() == 0
75

  
76

  
77
def test_add_tar_content(tmpdir):
78
    filename = os.path.join(str(tmpdir), 'file.tar')
79
    tar = tarfile.open(filename, 'w')
80
    add_tar_content(tar, 'foo.txt', 'bar')
81
    tar.close()
82

  
83
    tar = tarfile.open(filename, 'r')
84
    tarinfo = tar.getmember('foo.txt')
85
    assert tar.extractfile(tarinfo).read().decode('utf-8') == 'bar'
86

  
87

  
88
def test_tar_untar_assets(some_assets):
89
    assert Asset.objects.count() == 2
90
    assert count_asset_files() == 2
91
    fd = BytesIO()
92

  
93
    tar = tarfile.open(mode='w', fileobj=fd)
94
    tar_assets_files(tar)
95
    tar_bytes = fd.getvalue()
96
    tar.close()
97

  
98
    path = default_storage.path('')
99
    os.remove('%s/assets/test.png' % path)
100
    open('%s/assets/test2.png' % path, 'w').write('foo')
101
    assert count_asset_files() == 1
102
    Asset.objects.all().delete()
103
    assert Asset.objects.count() == 0
104
    fd = BytesIO(tar_bytes)
105

  
106
    tar = tarfile.open(mode='r', fileobj=fd)
107
    data = untar_assets_files(tar)
108
    assert [x['fields']['key'] for x in data['assets']] == ['banner', 'favicon']
109
    assert count_asset_files() == 2
110
    assert open('%s/assets/test.png' % path, 'r').read() == 'test'
111
    assert open('%s/assets/test2.png' % path, 'r').read() == 'foo'
112
    clean_assets_files()
113

  
114

  
115
def test_import_export_assets(some_assets, tmpdir):
116
    filename = os.path.join(str(tmpdir), 'file.tar')
117
    assert Asset.objects.count() == 2
118
    assert count_asset_files() == 2
119
    fd = open(filename, 'wb')
120
    export_assets(fd)
121

  
122
    path = default_storage.path('')
123
    os.remove('%s/assets/test.png' % path)
124
    open('%s/assets/test2.png' % path, 'w').write('foo')
125
    assert count_asset_files() == 1
126
    Asset.objects.all().delete()
127
    assert Asset.objects.count() == 0
128

  
129
    fd = open(filename, 'rb')
130
    import_assets(fd, overwrite=True)
131
    assert count_asset_files() == 2
132
    assert open('%s/assets/test.png' % path, 'r').read() == 'test'
133
    assert open('%s/assets/test2.png' % path, 'r').read() == 'test2'
134
    clean_assets_files()
135
    assert count_asset_files() == 0
136
    clean_assets_files()
45
-