Django mediaファイルとAWS S3

[Django staticファイルとAWS S3 - Qiita](https://qiita.com/sand/items/2eae781e2b904e0e67b9)
[Django mediaファイルとAWS S3 - Qiita](https://qiita.com/sand/items/b897aa47c304b7fbdcb5)

※今回の記事はDjangoを利用したブログである「Djangoブログ」（仮称）作成時に調査したことをまとめたものです。Djangoは初めてでしたが3週間程度で作りました。α版ですが、どうぞユーザ登録して使ってみてください。
[「Djangブログ」 - GAEにDeployしたものです](https://djangoblog.appspot.com/)



**本記事ではDjangoのmediaファイルの扱いと、AWS S3へのアップロードについてまとめます。**

DjangoではFileField、ImageFieldでアップロードされるファイルを**mediaファイル**と呼びます。


[Form fields - 公式ドキュメント](https://docs.djangoproject.com/ja/2.2/ref/forms/fields/)


#１．localへのアップロード - 開発環境(DEBUG=True)

##１－１．テストプロジェクト作成

```
python -m venv localupload
source localupload/bin/activate
cd localupload/
pip freeze
pip install django
django-admin startproject localupload .
```

Remoteからアクセスするための設定です。

```python:localupload/settings.py
---
ALLOWED_HOSTS = ["www.mypress.jp"]
---
```

ここまででプロジェクトが動作するので確認します。

```
python manage.py migrate
python manage.py runserver 0:8080
```





#１－２．localへのアップロード

localへのアップロードするアプリケーションを作成します。

```
python manage.py startapp uploadapp
```

```python:localupload/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'uploadapp',
]
---
```



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


* MEDIA_URL : メディアファイル公開時のURLのプレフィクス。url=http://アプリのドメイン+MEDIA_URL+メディアファイル名
* MEDIA_ROOT : サーバから見たメディアルートの絶対パス. プロジェクトトップディレクトリ/media


```python:localupload/urls.py
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('uploadapp.urls')),
]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

**開発中(DEBUG=True)の時は最後のスニペットを追加することで、画像はlocalなStorageのMEDIA_ROOTに保存され、localなurl(相対url)のMEDIA_URLで参照できます。** しかし本番環境（DEBUG=False）ではこのやり方は推奨されていません。staticファイルの場合と同じです。

[静的ファイル (画像、JavaScript、CSS など) を管理する - 公式ドキュメント](https://docs.djangoproject.com/ja/2.2/howto/static-files/#serving-files-uploaded-by-a-user-during-development)
[Django staticファイルとAWS S3 - Qiita](https://qiita.com/sand/items/2eae781e2b904e0e67b9)


```python:uploadapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.Photo.as_view()),
]
```



さて今回は、**FileField**でなく**ImageField**を使います。ImageFieldは次のようなものです。

* ImageFieldは画像を扱うことに特化したFileFieldの派生フィールドです
* ImageFieldにはPillowが必要。
* 登録時のバリデーションで画像ファイルのチェックをする。画像ファイルでないもの、データの破損があるものは登録エラーとする（専用エラーメッセージあり）。
* 管理用のフィールドを事前に用意すると、登録時に画像の高さと幅(pixel単位)を取得して保存する

それではPillowをインストールします。

```
pip install Pillow
```

ImageFieldを使ってmodelsを定義します。

```python:uploadapp/models.py
from django.db import models

class PhotoModel(models.Model):
    image = models.ImageField(upload_to='images')

    def __str__(self):
        return self.image.url
```

formsを定義します。

```python:uploadapp/forms.py
from django import forms
from .models import PhotoModel

class PhotoForm(forms.ModelForm):

    class Meta:
        model = PhotoModel
        fields = '__all__'
```

viewsを定義します。

```python:uploadapp/views.py
from django.views import generic
from .forms import PhotoForm
from .models import PhotoModel

class Photo(generic.CreateView):
    model = PhotoModel
    form_class = PhotoForm
    template_name = 'uploadapp/upload.html'
    success_url = '/'

    def get_context_data(self, **kwargs):
        context = super(Photo, self).get_context_data(**kwargs) # はじめに継承元のメソッドを呼び出す
        context["photos"] = PhotoModel.objects.all()
        return context

```

テンプレートのフォルダを作成します。

```
mkdir -p uploadapp/templates/uploadapp/
```

アップロードフォームとアップロード済みの画像リスト表示です。

```html:uploadapp/templates/uploadapp/upload.html
    <form action="" method="POST" enctype="multipart/form-data">{% csrf_token %}        {{ form.as_p }}
        <button type="submit">アップロード</button>
    </form>

    {% for photo in photos %}
        <a href="{{ photo.image.url }}">{{ photo }}</a>
        <hr>
    {% endfor %}
```

プロジェクトを起動します。

```
python manage.py runserver 0:8080
```


2つの画像をアップロード済みの画面です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/5ae127a5-4737-3ba2-4b14-db48b730c280.png)


**MEDIA_URL** の設定を考慮して、以下のHTMLソースを確認してください。**photo.image.url="/media/images/tree5.jpg"** のように展開されています。

```html:HTMLソース
    <form action="" method="POST" enctype="multipart/form-data"><input type="hidden" name="csrfmiddlewaretoken" value="m0qzyYvLhuUckbF5ctHAAJOCThqZ24Q7RE5c70EQ4bnTm05v3F33aYrn7br4jed2">
        <p><label for="id_image">Image:</label> <input type="file" name="image" accept="image/*" required id="id_image"></p>
        <button type="submit">アップロード</button>
    </form>

    
        <a href="/media/images/tree4jpeg.jpeg">/media/images/tree4jpeg.jpeg</a>
        <hr>
    
        <a href="/media/images/tree5.jpg">/media/images/tree5.jpg</a>
        <hr>
```


**MEDIA_ROOT**の設定を確認してください。プロジェクトのトップディレクトリでtreeコマンドを発行した結果です。

```
$ tree media/
media/
├── images
│   ├── tree4jpeg.jpeg
│   └── tree5.jpg
└── tree4jpeg.jpeg
```



#２．Remote の AWS S3 へアップロード - 本番環境(DEBUG=False)

ここから、画像をAWS S3にアップロードする設定を見ていきます。本番環境なので、基本はstatcファイルもAWS S3にdeployする設定を行い、追加でmediaファイルもAWS S3にアップロードする設定を行います。

staticファイルとAWSの設定については[「Django staticファイルとAWS S3 -Qiita」](https://qiita.com/sand/items/2eae781e2b904e0e67b9)を参照してください。


まずは本番環境の設定です。

```python:localupload/settings.py
---
DEBUG = False
---
```

AWS S3に新バケットを作り、以下のポリシーを付与します。


```
 {
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"AddPerm",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::media-test-reiwa/*"]
    }
  ]
}
```


必要なライブラリをインストールします。

```
pip install boto3
pip install django-storages
```

ライブラリを有効にします。

```python:localupload/settings.py
---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'uploadapp',
    'storages',      # 追加
]
---
```




```python:localupload/settings.py
# STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

AWS_ACCESS_KEY_ID = 'AKIAXXXXXXXXXXXXXXXXXXXXX'
AWS_SECRET_ACCESS_KEY = 'YYYYYYYYYYYYYYYYYYYYYY'
AWS_STORAGE_BUCKET_NAME = 'media-test-reiwa'
AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400',
}
AWS_LOCATION = 'static'
AWS_DEFAULT_ACL = None
STATIC_URL = 'https://%s/%s/' % (AWS_S3_CUSTOM_DOMAIN, AWS_LOCATION)
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'


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

DEFAULT_FILE_STORAGE = 'localupload.storage_backends.MediaStorage'
```

staticファイルの時の設定に加えて、**DEFAULT_FILE_STORAGE**の設定を追加しています。


### (説明)　static assets と media assets
* static ファイルは**STATICFILES_STORAGE**=S3Boto3Storageで設定。collectstaticが処理する。
* media ファイルは**DEFAULT_FILE_STORAGE**で設定。S3Boto3Storageを拡張し、'media'ディレクトリにアップロードするよう指定する。

S3Boto3Storageの拡張は**DEFAULT_FILE_STORAGE**で指定したように、以下のstorage_backends.pyで行います。同一ファイル名の画像がアップロードされたら上書きせずに適当にリネームするように設定しています。


```python:localupload/storage_backends.py
from storages.backends.s3boto3 import S3Boto3Storage

# 画像は同ファイル名での上書きを許さない
class MediaStorage(S3Boto3Storage):
    location = 'media'
    file_overwrite = False
```

staticディレクトリを作成し、staticファイルをcollectstaticでAWS S3にdeployします。

```
mkdir static
```

```
python manage.py collectstatic
```


ここでDBテーブルから前にアップロードしたエントリーを全削除しておきます。
プロジェクトを起動し、画像ファイルを2個アップロードします。

```
python manage.py runserver 0:8080
```


以下のような画面が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/98855f1a-b345-93ba-91f6-3e5a7f2d8abe.png)


AWS S3のバケットにはstaticとmediaディレクトリできています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/a81af7ff-5c82-6f1d-60a6-3eaaa37272df.png)


mediaディレクトリには確かに2ファイルがアップロードされています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/f0d3c385-d380-dfa7-9e2d-e7264d5762bc.png)


今回は以上です。