Projet

Général

Profil

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

Nicolas Roche, 13 juillet 2020 19:19

Télécharger (10,4 ko)

Voir les différences:

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

 combo/apps/assets/utils.py | 74 +++++++++++++++++++++++++++++++++
 combo/apps/assets/views.py | 31 ++------------
 tests/test_assets.py       | 84 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 161 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
def add_tar_content(tar, filename, content):
34
    file = tarfile.TarInfo(filename)
35
    fd = BytesIO()
36
    fd.write(content.encode('utf-8'))
37
    file.size = fd.tell()
38
    fd.seek(0)
39
    tar.addfile(file, fileobj=fd)
40
    fd.close()
41

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

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

  
65
def import_assets(fd, overwrite=False):
66
    tar = tarfile.open(mode='r', fileobj=fd)
67
    data = untar_assets_files(tar, overwrite=overwrite)
68
    Asset.load_serialized_objects(data.get('assets') or [])
69
    tar.close()
70

  
71
def export_assets(fd):
72
    tar = tarfile.open(mode='w', fileobj=fd)
73
    tar_assets_files(tar)
74
    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)
10 17

  
11 18
pytestmark = pytest.mark.django_db
12 19

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

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

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

  
66
def test_clean_assets_files(some_assets):
67
    assert count_asset_files() == 2
68
    clean_assets_files()
69
    assert count_asset_files() == 0
70

  
71
def test_add_tar_content(tmpdir):
72
    filename = os.path.join(str(tmpdir), 'file.tar')
73
    tar = tarfile.open(filename, 'w')
74
    add_tar_content(tar, 'foo.txt', 'bar')
75
    tar.close()
76

  
77
    tar = tarfile.open(filename, 'r')
78
    tarinfo = tar.getmember('foo.txt')
79
    assert tar.extractfile(tarinfo).read().decode('utf-8') == 'bar'
80

  
81
def test_tar_untar_assets(some_assets):
82
    assert Asset.objects.count() == 2
83
    assert count_asset_files() == 2
84
    fd = BytesIO()
85

  
86
    tar = tarfile.open(mode='w', fileobj=fd)
87
    tar_assets_files(tar)
88
    tar_bytes = fd.getvalue()
89
    tar.close()
90

  
91
    path = default_storage.path('')
92
    os.remove('%s/assets/test.png' % path)
93
    open('%s/assets/test2.png' % path, 'w').write('foo')
94
    assert count_asset_files() == 1
95
    Asset.objects.all().delete()
96
    assert Asset.objects.count() == 0
97
    fd = BytesIO(tar_bytes)
98

  
99
    tar = tarfile.open(mode='r', fileobj=fd)
100
    data = untar_assets_files(tar)
101
    assert [x['fields']['key'] for x in data['assets']] == ['banner', 'favicon']
102
    assert count_asset_files() == 2
103
    assert open('%s/assets/test.png' % path, 'r').read() == 'test'
104
    assert open('%s/assets/test2.png' % path, 'r').read() == 'foo'
105
    clean_assets_files()
106

  
107
def test_import_export_assets(some_assets, tmpdir):
108
    filename = os.path.join(str(tmpdir), 'file.tar')
109
    assert Asset.objects.count() == 2
110
    assert count_asset_files() == 2
111
    fd = open(filename, 'wb')
112
    export_assets(fd)
113

  
114
    path = default_storage.path('')
115
    os.remove('%s/assets/test.png' % path)
116
    open('%s/assets/test2.png' % path, 'w').write('foo')
117
    assert count_asset_files() == 1
118
    Asset.objects.all().delete()
119
    assert Asset.objects.count() == 0
120

  
121
    fd = open(filename, 'rb')
122
    import_assets(fd, overwrite=True)
123
    assert count_asset_files() == 2
124
    assert open('%s/assets/test.png' % path, 'r').read() == 'test'
125
    assert open('%s/assets/test2.png' % path, 'r').read() == 'test2'
126
    clean_assets_files()
127
    assert count_asset_files() == 0
128
    clean_assets_files()
45
-