naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Djangoで、複数ファイルをダウンロードする(ZIPにする)

プログラミング関連 Django Python 約566日前
2016年5月25日0:04
今回は、複数ファイルのダウンロードです
複数ファイルをZIPにし、HTTPレスポンスでダウンロードさせる、といった流れです
https://torina.top/detail/245/
こちらを改変した内容です。
基本的に改変部分だけ書いていきます


前回同様ですが、モデルは載せときます
models.py
from django.db import models
 
 
class Photo(models.Model):
    title = models.CharField(max_length=255, )
    image1 = models.ImageField(upload_to='images/', null=True, blank=True,)
    image2 = models.ImageField(upload_to='images/', null=True, blank=True, )
    image3 = models.ImageField(upload_to='images/', null=True, blank=True, )
 




一括ダウンロードのリンクが増えました
index.html
{% extends "multi/base.html" %}
{% block body %}
<div class="container">
  {% for photo in photos %}    
    <h2 class="page-header">{{ photo.title }}</h2>
    <div class="row">
      <div class="col-xs-4">
        <img class="img-responsive" src="/media/{{ photo.image1 }}">
      </div>
      <div class="col-xs-4">
        <img class="img-responsive" src="/media/{{ photo.image2 }}">
      </div>
      <div class="col-xs-4">
        <img class="img-responsive" src="/media/{{ photo.image3 }}">
      </div>
    </div>
    <a class="btn btn-primary" href="{% url 'multi:upload' photo.id %}">一括アップ</a>
    <a class="btn btn-danger" href="{% url 'multi:download' photo.id %}">一括ダウンロード</a>
  {% endfor %}
</div>
{% endblock %}


urls.py
from django.conf.urls import url
from . import views
 
 
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^upload/(?P<p_id>\d+)/$', views.upload, name='upload'),
    url(r'^upload_save/$', views.upload_save, name='upload_save'),
    url(r'^download/(?P<p_id>\d+)/$', views.download, name='download'),  # 増えたのこれ
]



views.py
from io import BytesIO  # 増えました
import zipfile  # 増えました
from django.shortcuts import render, redirect
from django.http import HttpResponse
from .models import Photo
 
 
# これが増えた
def download(request, p_id):
    photo_obj = Photo.objects.get(id=p_id)
 
    memory_file = BytesIO()
    photo_zip = zipfile.ZipFile(memory_file, 'w')
 
    photo_zip.writestr(photo_obj.image1.name, photo_obj.image1.read())
    photo_zip.writestr(photo_obj.image2.name, photo_obj.image2.read())
    photo_zip.writestr(photo_obj.image3.name, photo_obj.image3.read())
 
    photo_zip.close()
    response = HttpResponse(memory_file.getvalue(), content_type="application/zip")
    response['Content-Disposition'] = 'attachment; filename={}.zip'.format(photo_obj.id)
    return response
 
 
#  ここから、前回同様
def index(request):
    d = {
        'photos': Photo.objects.all(),
    }
    return render(request, 'multi/index.html', d)
 
def upload(request, p_id):
    d = {
        'p_id': p_id,
    }
    return render(request, 'multi/upload.html', d)
 
def upload_save(request):
    photo_id = request.POST.get("p_id", "")
    photo_obj = Photo.objects.get(id=photo_id)
    files = request.FILES.getlist("files[]")
 
    for idx, file in enumerate(files):
        program = "photo_obj.image{} = file".format(str(idx+1))
        exec(program)
 
    photo_obj.save()
        
    return redirect("multi:index")




一括ダウンロードを押すと、1.zipがダウンロードされました(画面下)



upload_to='images/',にしているため、imagesディレクトリができています
(これは、photo_obj.image1.name が"images/namae.png"となっているためです)


このように、きちんと保存されていますね


今回は、いちいちZIPファイルをローカルに作る必要はなかったので(容量増えそうですし)
メモリ内にZIPを作りました
ImageField.read()はバイトが帰ってくるので、BytesIOを使います
    memory_file = BytesIO()
    photo_zip = zipfile.ZipFile(memory_file, 'w')



.read()でファイル内容を読み込んでいます
.nameで、ファイル名を取得しています
その後は、クローズです
    photo_zip.writestr(photo_obj.image1.name, photo_obj.image1.read())
    photo_zip.writestr(photo_obj.image2.name, photo_obj.image2.read())
    photo_zip.writestr(photo_obj.image3.name, photo_obj.image3.read())
 
    photo_zip.close()




content_type="application/zip"は、ブラウザくんにこいつはZIPだよと教えています。text/htmlなんかだと、ブラウザはあ、こいつ開けるやつだ!と判断しブラウザ上で開こうとします
attachment;は、ファイル添付してるよ、とブラウザくんに教えています
https://torina.top/detail/228/
でもちょっと触れました
    response = HttpResponse(memory_file.getvalue(), content_type="application/zip")
    response['Content-Disposition'] = 'attachment; filename={}.zip'.format(photo_obj.id)
    return response


ZIPをローカルに作りつつ、更にそれをダウンロードさせるならこんな感じでしょうか
def download(request, p_id):
    photo_obj = Photo.objects.get(id=p_id)
 
    photo_zip = zipfile.ZipFile("local.zip", 'w')
    photo_zip.writestr(photo_obj.image1.name, photo_obj.image1.read())
    photo_zip.writestr(photo_obj.image2.name, photo_obj.image2.read())
    photo_zip.writestr(photo_obj.image3.name, photo_obj.image3.read())
    photo_zip.close()
    
    response = HttpResponse(open("local.zip", "rb").read(), content_type="application/zip")
    response['Content-Disposition'] = 'attachment; filename={}.zip'.format(photo_obj.id)
    return response


ローカルにZIPを作らずにダウンロードさせたいなら、こういった方法もあるようです。うつくしい!
zipfile.ZipFileは、ファイルっぽいものなら何でもいい、器の広い男です
def download(request, p_id):
    photo_obj = Photo.objects.get(id=p_id)
 
    response = HttpResponse(content_type="application/zip")
    response['Content-Disposition'] = 'attachment; filename={}.zip'.format(photo_obj.id)
 
    photo_zip = zipfile.ZipFile(response, 'w')
    photo_zip.writestr(photo_obj.image1.name, photo_obj.image1.read())
    photo_zip.writestr(photo_obj.image2.name, photo_obj.image2.read())
    photo_zip.writestr(photo_obj.image3.name, photo_obj.image3.read())
    photo_zip.close()
 
    return response


参考
http://stackoverflow.com/questions/21264848/image-to-zip-in-python
http://stackoverflow.com/questions/32075135/python-3-in-memory-zipfile-error-string-argument-expected-got-bytes
http://d.hatena.ne.jp/nullpobug/20100616/1276700129
約172日前 ainamori 2017年6月23日0:36 返信する
いつも参考に拝読しております。
読みやすく、またテーマがハッキリしており
大変有り難く思っております。

ところで
====
今回は、複数ファイルのダウンロードです
複数ファイルをZIPにし、HTTPレスポンスでダウンロードさせる、といった流れです
https://torina.top/main/245/
こちらを改変した内容です。
基本的に改変部分だけ書いていきます
====

以下の誤りでは無いでしょうか。
 https://torina.top/detail/245/

本文の通りだと Not found となってしまいます。
約165日前 narito 2017年6月29日18:03
ainamori様

ご指摘のとおりです。
https://torina.top/detail/245/
に修正しました。
また、もう一箇所リンクの誤りがあったのでそちらも修正しました。

ご連絡ありがとうございました。