[Django] ファイルアップロード機能の使い方 [基本設定編]

## 概要

Djangoにはファイルアップロードを簡単に実装する仕組みがあります。
`FileField`、`ImageField`というモデルフィールドがあり、これをモデルに定義するとモデルフォーム内にファイル選択欄が表示されます。アップロードされたファイルはサーバ内やAmazonS3やDropbox等の任意のストレージに保存することができます。

参考：https://docs.djangoproject.com/en/2.0/topics/http/file-uploads/

## アップロードの流れ

１．モデルにFileField(ImageField)を定義すると

```models.py

    attach = models.FileField(
        upload_to='uploads/%Y/%m/%d/',
        verbose_name='添付ファイル',
        validators=[FileExtensionValidator(['pdf', ])],
    )
```

２．モデルフォームにファイル入力欄が追加されるので

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/45952254-f220-a7f2-0368-f71be4b464c8.png)

３．データを登録すると`MEDIA_ROOT`配下の`upload_to`の場所にファイルが保存される

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/521a3966-ee21-2183-f2a4-a0bdb7ccf9fe.png)

４．データベースには上記のパスが登録される

```
attach : uploads/2018/06/25/添付資料.pdf
```

５．`http://ドメイン名 + MEDIA_URL + upload_to のパス`というURLでアップロードしたファイルにアクセスすることができる。

## 利用方法

**とりあえず開発環境で動かすための設定になります。クラウドサービスを使う本番環境の設定は[ファイルアップロード機能の使い方 クラウドストレージ編](https://qiita.com/okoppe8/items/7ecbc92187171926238f) で説明します。**

### 事前準備

FileField、ImageFieldでアップロードされるファイルを「メディアファイル」と呼びます。メディアファイルを扱うにはプロジェクト側でいくつかの設定が必要です。

### １．保存場所の作成

メディアファイルを格納するディレクトリ（メディアルートと呼ぶ）が必要です。開発環境ではプロジェクトルート直下に**`media`**というディレクトリを作りメディアルートとするのが一般的です。作成後はプロジェクトの**`.gitignore`**を修正してバージョン管理から除外します。

本番環境ではAmazon S3のバゲット等を設定します。

### ２．settings.pyの変更

設定ファイル（settings.py）に以下の変数を追加します。

- MEDIA_ROOT 
 - サーバから見たメディアルートの絶対パスです。開発環境では１で作ったディレクトリを指定します。
    
- MEDIA_URL
 - メディアファイル公開時のURLのプレフィクスです。
メディアファイルのURLは「http://アプリのドメイン+MEDIA_URL+メディアファイル名」となります。
    
設定例：

```python:settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
```    

###３．urls.pyの変更
    
ディスパッチャの設定(`urls.py`)にメディアファイル公開用のURLを追加します。

```python:urls.py
urlpatterns = [
...
]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```
参考：[Django公式 静的ファイルの管理](https://docs.djangoproject.com/ja/2.0/howto/static-files/#serving-files-uploaded-by-a-user-during-development)

### ４．formタグの変更

テンプレート内の入力フォームのHTMLを修正します。フォームタグのenctype属性を設定します。

```HTML:
変更前：
<form method="post" id="myform">
変更後：
<form method="post" id="myform" enctype="multipart/form-data">
```

### ５．Viewのロジック変更 [任意]


関数ベースビューを使っている場合は、フォームに渡す引数を増やす必要があります。**クラスベースビューでは修正不要です。**


```python:views.py
# 変更前
form = SampleForm(request.POST)
# 変更後
form = SampleForm(request.POST, request.FILES)
```


## フィールド定義

### 基本定義

FileFieldの基本的な定義方法です。

```models.py

    attach = models.FileField(
        upload_to='uploads/%Y/%m/%d/',
        verbose_name='添付ファイル',
        validators=[FileExtensionValidator(['pdf', ])],
    )
```

- `upload_to`属性で`MEDIA_ROOT`配下の保存先を指定します。[`strftime`](https://docs.python.jp/3.6/library/datetime.html#strftime-and-strptime-behavior)フォーマットの指定が使える他、関数での指定が可能です。関数を使う場合は以下の資料が参考になります。

 - [公式：Django 公式 upload_toについて](https://docs.djangoproject.com/ja/2.0/ref/models/fields/#django.db.models.FileField.upload_to)

 - [Qiita : Django の ImageField にUUIDを使う](https://qiita.com/kojionilk/items/da20c732642ee7377a78#uuid-%E3%81%A8%E3%81%8B%E3%81%AB%E3%81%97%E3%81%A6%E3%81%8A%E3%81%91%E3%81%B0%E3%81%84%E3%81%84%E3%81%A0%E3%82%8D%E3%81%86)
 

- デフォルトのファイル名の制限は100文字までとなっています。もっと長いファイル名を使う場合は`max_length`属性で調整します。
- ビルトインのバリデーション「FileExtensionValidator」で拡張子の制限をかけることができます。上記はpdfの例です。


### ImageFieldについて

ImageFieldは画像を扱うことに特化したFileFieldの派生フィールドです

ImageFieldの利用には追加パッケージとして`Pillow`が要求されます。

```shell
pip install Pillow
```

ImageFieldとFileFieldの主な違いは以下の点です。

- 登録時のバリデーションで画像ファイルのチェックをする。画像ファイルでないもの、データの破損があるものは登録エラーとする（専用エラーメッセージあり）。

- 管理用のフィールドを事前に用意すると、登録時に画像の高さと幅(pixel単位)を取得して保存する。

定義例：

- 登録時にurl_height と url_width に高さと幅が保存される

```python:models.py
    image = models.ImageField(
        upload_to='files/',
        verbose_name='添付画像',
        height_field='url_height',
        width_field='url_width',
    )

    url_height = models.IntegerField(
        editable=False,
    )

    url_width = models.IntegerField(
        editable=False,
    )
```

### Django ImageKit について

スマホで撮った高画質な画像をそのままアプリで扱うと、ファイルサイズが大きすぎて色々支障が出ることが危惧されます。追加パッケージの`Django ImageKit`を使うと、アップロード時にリサイズ処理やサムネイル作成処理をしてくれます。その他便利な機能が盛り込まれているので、画像ファイルを扱う際は`Django ImageKit`の利用を前提としましょう。

参考：[Django ImageKit 公式](https://github.com/matthewwithanm/django-imagekit)
参考：[DjangoでImageFieldからサムネイルをImageKitで自動生成する](https://qiita.com/felyce/items/57421ea191ab89175e9e)

## 問題の修正方法

### 保存時の問題

FileFieldのデフォルトの動作では、ファイルの削除・更新時に古いファイルは消されずそのまま残ります（同名ファイルで更新した場合、古いファイルがリネームされて残る）。これはこれでバックアップが残って安全な動作ですが、古いファイルが不要な場合は余計に容量が使われてしまいます。

この問題に対して「django-cleanup」というパッケージが提供されています。インストールして設定に入れるだけで、レコードの削除・更新時にファイルも削除されるようになります。

```shell
pip install django-cleanup
```

```settings.py
INSTALLED_APPS = [
    ...
    'django_cleanup',
    ...
]
```

- 参考：[django-cleanup公式](https://github.com/un1t/django-cleanup)
- 参考：[Sugar Maple's blog 【Django】メディアファイルを更新・削除したときに、古いファイルを自動で削除してくれる「django-cleanup」が便利](https://sugar-maple.herokuapp.com/django-cleanup/)

### 表示面での問題

#### ファイル名だけを表示する

FileFieldには`MEDIA_ROOT`からの相対パスが保存されます。内容にディレクトリも含むので、そのまま画面表示で使うとユーザーが混乱します。ファイル名だけを表示させるには、テンプレートタグを使うのがスマートです。

``` python:app/templatetags/filename.py
import os

from django import template

register = template.Library()


@register.filter
def get_filename(value):
    return os.path.basename(value.file.name)

```

```app/templates/sample.html
{% load filename %}
  
{{item.file | getfilename }}
```

参考：[Django: How to display only a filename in your template](https://matthiasomisore.com/web-programming/django-how-to-display-only-a-filename-in-your-template/)

#### エラーメッセージを正しく表示する

**※django-crispy-forms利用時のみ**

`django-crispy-forms`を使っている場合、入力フォームのファイル入力欄のエラーメッセージが表示されません。`django-crispy-forms`がファイル入力欄に対応していないのが原因のようです。

このままでは不便なので`css`に以下の記述を追加してエラーメッセージを表示させます。

```css
input[type="file"] + span.invalid-feedback {
    display: block;
}
```

※[Djagno] ファイルアップロード機能の使い方 [クラウドストレージ編](https://qiita.com/okoppe8/items/7ecbc92187171926238f) に続きます。