Djangoでのファイルアップロード

2019-08-01Python,TIPSDjango

Djangoでの基本的なファイルアップロードの概念とModelFormを使用してファイルアップロードする方法を説明します。
今回もGitHubにサンプルコードをアップしています。

ファイルアップロードの概念

DjangoではフォームからファイルがPOSTされるとそのファイルデータはrequest.FILESに格納されます。
このためにはHTMLのformタグではenctype="multipart/form-data"をセットする必要があります。これを忘れてしまうとPOSTされたファイルは格納されず、request.FILESは空っぽのままになります。

request.FILES自体はdict型のオブジェクトでありキーは
input type="files" name="hoge"のname部が割り当てられます。
そして値には実際のファイルインスタンスが割り当てられます。

モデルを使用せずにファイルをアップロードすることも可能ですがDjangoにはファイルアップロードに適したFileFieldとImageFileldというフィールドが用意されています。
この2つのフィールドにアップロードされたファイルはデータベースには保存されず、ファイルシステムへ保存されます。
そして保存されたファイルへの参照情報が文字列フィールドとしてデータベースへ保存されます。

よってデータベースからファイル参照を削除してもファイルシステムへ保存されたファイル本体は削除されません。

まず開発用サーバーでMEDIAを有効にするには以下2つの準備が必要になります。

1.) MEDIA_ROOT と MEDIA_URLを設定する

settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

記述する場所はどこでも大丈夫ですが、BASE_DIRを定数宣言している箇所よりも下にしなければエラーが発生します。

2.) MEDIAへのアクセスを可能にする

urls.py

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # 略
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)



basicなアップロード方法

FileSystemStrageを使用したミニマムでのアップロード例です。
フォームを使用しない形式ですが個人的にこの方法はあまり好きではありません。 バリデーションなどを自前で用意しなければいけないからです。

basic_upload.html

{% extends ‘base.html’ %}

{% load static %}

{% block content %}
  <form method=“POST” enctype=“multipart/form-data”>
    {% csrf_token %}
    <input type=“file” name=“testfile”>
    <button type=“submit”>アップロード</button>
  </form>

  {% if upload_file_url %}
    <p>アップロード時刻<a href=“{{ upload_file_url }}”>{{ upload_file_url }}</a></p>
  {% endif %}

  <p><a href=“{% url ‘index’ %}”>戻る</a></p>

{% endlock %}

views.py

from django.shortcuts import render
from django.conf import settings
from django.core.files.storage import FileSystemStorage

def simple_upload(request):
    if request.method == 'POST' and request.FILES['myfile']:
        myfile = request.FILES['myfile']
        fs = FileSystemStorage()
        filename = fs.save(myfile.name, myfile)
        uploaded_file_url = fs.url(filename)
        return render(request, 'core/simple_upload.html', {
            'uploaded_file_url': uploaded_file_url
        })
    return render(request, 'core/simple_upload.html')



あまりオススメはできませんが業務アプリ等で既存のExcelにシステム側の情報を追記したいといった具合にアップロードされたファイルをサーバ側へ保存する必要がないケースでは以下の様にファイルを操作できます。

views.py

import openpyxl

def append_excel(request):
    ...
    ...
    workbook = load_workbook(request.FILES['hogehoge'].file)
    # Excelを編集する処理など
    



FORMを使用したファイルアップロード

こちらはDjango標準のModelFormを使用してファイルをアップロードできます。
バリデーションや保存先の絶対パスを細かく指定(分岐も可)することができたりと基本的にはこちらの方式で実装することをオススメします。

models.py

from django.db import models

class Document(models.Model):
    description = models.CharField(max_length=255, blank=True)
    document = models.FileField(upload_to='documents/')
    uploaded_at = models.DateTimeField(auto_now_add=True)

forms.py

from django import forms
from upload.models import Document

class DocumentForm(forms.ModelForm):
    class Meta:
        model = Document
        fields = ('description', 'document', )

views.py

def modelform_upload(request):
    if request.method == 'POST':
        form = DocumentForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect('index')
    else:
        form = DocumentForm()
    return render(request, 'modelform_upload.html', {
        'form': form
    })



ファイル保存先の設定方法

models.pyにフィールドを定義する際に保存先を指定することができます。
先程の例では以下の様に設定していました。

document = models.FileField(upload_to='documents/')

この場合だとファイルは自動的にMEDIA_ROOT/documents/ に保存されます。
以下の様にするとタイムゾーンに合わせて年月日でパスを自動生成することもできます。

document = models.FileField(upload_to='documents/%Y/%m/%d/')

この場合だと MEDIA_ROOT/documents/2019/06/28/ といった具合に保存されます。

upload_toには関数を指定することもできます。
例えば以下の様にすると、関数[path_branch]で指定されたパスにファイルが保存されます。

models.py

def path_branch(instance, filename):
    # PDF・CSV・その他でパスを変更する場合
    root_ext_pair = os.path.splitext(filename)
    if root_ext_pair[1] = '.pdf':
        path = os.path.join('documents/%Y/%m/%d/PDF/', filename)
    elif root_ext_pair[1] = '.csv':
        path = os.path.join('documents/%Y/%m/%d/CSV/', filename)
    else:
        path = os.path.join('documents/%Y/%m/%d/Others/', filename)

    return path

class FileDocuments(models.Model):
    upload = models.FileField(upload_to=path_branch)



今回作成したコードもGitHubにアップロードしてあります。

2019-08-01Python,TIPSDjango

Posted by Kenny