From af4f78928be1cc794692669c64fa83573a30c9db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sun, 28 Apr 2019 10:48:32 +0200
Subject: [PATCH] assets: add basic export/import of assets in tar file (#6756)

---
 combo/apps/assets/forms.py                    |  5 ++
 .../templates/combo/manager_assets.html       |  5 ++
 combo/apps/assets/urls.py                     |  2 +
 combo/apps/assets/views.py                    | 45 ++++++++++-
 tests/test_manager.py                         | 75 +++++++++++++++++++
 5 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/combo/apps/assets/forms.py b/combo/apps/assets/forms.py
index aba7a548..414e0c02 100644
--- a/combo/apps/assets/forms.py
+++ b/combo/apps/assets/forms.py
@@ -20,3 +20,8 @@ from django.utils.translation import ugettext_lazy as _
 
 class AssetUploadForm(forms.Form):
     upload = forms.FileField(label=_('File'))
+
+
+class AssetsImportForm(forms.Form):
+    assets_file = forms.FileField(label=_('Assets File'))
+    overwrite = forms.BooleanField(label=_('Overwrite Existing Files'), required=False)
diff --git a/combo/apps/assets/templates/combo/manager_assets.html b/combo/apps/assets/templates/combo/manager_assets.html
index 423d0d2d..1c8d9de6 100644
--- a/combo/apps/assets/templates/combo/manager_assets.html
+++ b/combo/apps/assets/templates/combo/manager_assets.html
@@ -4,7 +4,12 @@
 {% block appbar %}
 <h2>{% trans 'Assets' %}</h2>
 <span class="actions">
+<a class="extra-actions-menu-opener"></a>
 <a href="{% url 'combo-manager-asset-upload' %}" rel="popup">{% trans 'Upload' %}</a>
+<ul class="extra-actions-menu">
+  <li><a href="{% url 'combo-manager-assets-export' %}">{% trans 'Export Assets' %}</a></li>
+  <li><a rel="popup" href="{% url 'combo-manager-assets-import' %}">{% trans 'Import Assets' %}</a></li>
+</ul>
 </span>
 {% endblock %}
 
diff --git a/combo/apps/assets/urls.py b/combo/apps/assets/urls.py
index 087716b4..38770b91 100644
--- a/combo/apps/assets/urls.py
+++ b/combo/apps/assets/urls.py
@@ -27,6 +27,8 @@ assets_manager_urls = [
     url(r'^upload/$', views.asset_upload, name='combo-manager-asset-upload'),
     url(r'^upload/(?P<key>[\w_:-]+)/$', views.slot_asset_upload, name='combo-manager-slot-asset-upload'),
     url(r'^delete/(?P<key>[\w_:-]+)/$', views.slot_asset_delete, name='combo-manager-slot-asset-delete'),
+    url(r'^export/$', views.assets_export, name='combo-manager-assets-export'),
+    url(r'^import/$', views.assets_import, name='combo-manager-assets-import'),
 ]
 
 urlpatterns = [
diff --git a/combo/apps/assets/views.py b/combo/apps/assets/views.py
index 7d39214d..3270dc6e 100644
--- a/combo/apps/assets/views.py
+++ b/combo/apps/assets/views.py
@@ -14,14 +14,18 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import tarfile
 import os
 
 from django.conf import settings
+from django.contrib import messages
 from django.core.exceptions import PermissionDenied
 from django.core.files.storage import default_storage
 from django.core.urlresolvers import reverse, reverse_lazy
-from django.http import Http404
+from django.http import Http404, HttpResponse
 from django.shortcuts import redirect
+from django.utils.six import BytesIO
+from django.utils.translation import ugettext_lazy as _
 from django.views.generic import TemplateView, ListView, FormView
 
 import ckeditor
@@ -29,7 +33,7 @@ from sorl.thumbnail.shortcuts import get_thumbnail
 
 from combo.data.models import CellBase
 
-from .forms import AssetUploadForm
+from .forms import AssetUploadForm, AssetsImportForm
 from .models import Asset
 
 
@@ -240,6 +244,43 @@ class SlotAssetDelete(TemplateView):
 slot_asset_delete = SlotAssetDelete.as_view()
 
 
+class AssetsImport(FormView):
+    form_class = AssetsImportForm
+    template_name = 'combo/manager_assets_import.html'
+    success_url = reverse_lazy('combo-manager-assets')
+
+    def form_valid(self, form):
+        overwrite = form.cleaned_data.get('overwrite')
+        try:
+            assets = tarfile.open(fileobj=form.cleaned_data['assets_file'])
+        except tarfile.TarError:
+            messages.error(self.request, _('The assets file is not valid.'))
+            return super(AssetsImport, self).form_valid(form)
+        media_prefix = default_storage.path('')
+        for tarinfo in assets.getmembers():
+            filepath = default_storage.path(tarinfo.name)
+            if not overwrite and os.path.exists(filepath):
+                continue
+            assets.extract(tarinfo, path=media_prefix)
+        messages.success(self.request, _('The assets file has been imported.'))
+        return super(AssetsImport, self).form_valid(form)
+
+assets_import = AssetsImport.as_view()
+
+
+def assets_export(request, *args, **kwargs):
+    fd = BytesIO()
+    assets_file = tarfile.open('assets.tar', 'w', fileobj=fd)
+    media_prefix = default_storage.path('')
+    for basedir, dirnames, filenames in os.walk(media_prefix):
+        for filename in filenames:
+            assets_file.add(
+                    os.path.join(basedir, filename),
+                    os.path.join(basedir, filename)[len(media_prefix):])
+    assets_file.close()
+    return HttpResponse(fd.getvalue(), content_type='application/x-tar')
+
+
 def serve_asset(request, key):
     try:
         asset = Asset.objects.get(key=key)
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 3f4185d3..0303901e 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -2,6 +2,7 @@ import base64
 import json
 import os
 import re
+import shutil
 
 import mock
 
@@ -823,6 +824,80 @@ def test_asset_slots_management(app, admin_user):
         assert '>Delete<' in resp.text
         assert Asset.objects.filter(key='collectivity:cgu').count() == 1
 
+
+def test_asset_export_import(app, admin_user):
+    for path in ('uploads', 'assets'):
+        if os.path.exists(default_storage.path(path)):
+            shutil.rmtree(default_storage.path(path))
+
+    app = login(app)
+
+    # upload a file
+    resp = app.get('/manage/assets/')
+    resp = resp.click('Upload')
+    resp.form['upload'] = Upload('test.png',
+            base64.decodestring(b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='),
+            'image/png')
+    resp = resp.form.submit()
+
+    resp = app.get('/manage/assets/')
+    resp = resp.click('Export')
+    assert resp.content_type == 'application/x-tar'
+    content = resp.content
+
+    for path in ('uploads', 'assets'):
+        if os.path.exists(default_storage.path(path)):
+            shutil.rmtree(default_storage.path(path))
+
+    resp = app.get('/manage/assets/')
+    assert 'have any asset yet.' in resp.text
+    resp = resp.click('Import')
+    resp.form['assets_file'] = Upload('test.tar', content)
+    resp = resp.form.submit()
+    assert sum([len(x[2]) for x in os.walk(default_storage.path(''))]) == 2
+    resp = resp.follow()
+    assert 'The assets file has been imported.' in resp.text
+
+    # test no overwrite
+    filename = re.findall('data-href="(.*?)"', resp.text)[0][7:]  # strip /media/
+    with open(default_storage.path(filename), 'w') as fd:
+        fd.write('test')  # 4 bytes
+    assert os.stat(default_storage.path(filename)).st_size == 4
+
+    resp = app.get('/manage/assets/')
+    resp = resp.click('Import')
+    resp.form['assets_file'] = Upload('test.tar', content)
+    resp = resp.form.submit()
+    resp = resp.follow()
+    assert 'The assets file has been imported.' in resp.text
+
+    assert os.stat(default_storage.path(filename)).st_size == 4
+
+    # test overwrite
+    resp = app.get('/manage/assets/')
+    resp = resp.click('Import')
+    resp.form['overwrite'] = True
+    resp.form['assets_file'] = Upload('test.tar', content)
+    resp = resp.form.submit()
+    resp = resp.follow()
+    assert 'The assets file has been imported.' in resp.text
+
+    assert os.stat(default_storage.path(filename)).st_size == 67
+
+    # test uploading garbage
+    for path in ('uploads', 'assets'):
+        if os.path.exists(default_storage.path(path)):
+            shutil.rmtree(default_storage.path(path))
+
+    resp = app.get('/manage/assets/')
+    resp = resp.click('Import')
+    resp.form['assets_file'] = Upload('test.tar', b'garbage')
+    resp = resp.form.submit()
+    assert sum([len(x[2]) for x in os.walk(default_storage.path(''))]) == 0
+    resp = resp.follow()
+    assert 'The assets file is not valid.' in resp.text
+
+
 def test_menu_json(app, admin_user):
     app.get('/manage/menu.json', status=302)
 
-- 
2.20.1

