Djangoを使ってブラウザからAWS S3に直接アップロードする

GoogleのGAE(Google App Engin) やAWSのElastic BeanstalkなどのPaaS(Platform as a Service)の中には、一回の通信に制限がついています。そのため動画ファイルなどの大きなファイルをアップロードするとError413(ファイルサイズが大きすぎて発生するエラー)が起きます。自分のサイトでブラウザから動画をアップロードをしたいときには、S3などのクラウドストレージに一定時間有効なURLを使ってブラウザから直接アップロードする必要がありますが、公式ドキュメントやstack overflowなどの質問サイトを覗いても署名付きURLまでの作成については書かれていても、それを使ってブラウザから直接アップロードする方法までは書かれていないことが多いです。そこで時間をかけてやっとブラウザからアップロードすることができたのでもう二度と忘れないようにメモとして残します。( アップロードしたファイルをどうやってdjangoモデルにつなげてブラウザで閲覧できるようにするかは考える必要がありますが、とりあえずブラウザからアップロードだけできたので)

前提

今回こちらのサイトを参考に実装を行っていますが、作成時期が少し古いので、その通りにやっていると引っかかる部分がかなりあったので自分が変更した箇所を含めて記録しています。

今回の使用したものは次の通りになります。

  • Django-3.1
  • AWS S3
  • Django-rest_framework
  • Javascript

を使っていきます。

省略事項

今回はdjangoのプロジェクトの作成方法とアプリケーションの作成方法カットさせていただきます。

AWSのアカウント作成もまた同様にカットさせていただきます。

boostarp

アカウントユーザーの作成

作成方法を確認したい方は、こちらのページをご覧ください。

AWS S3のアクセス許可を設定する。

AWSでバケットを作成後名前の項目をクリックしたら表示されるpermmissionsを選択したらその下の方にCross-origin resource sharing (CORS)という項目があります。JSON形式で下記のように書き換えていきます。

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

これらを指示することで自分で設定したドメインからブラウザからバケットに直接アップロードすることを許可することができます。しかしそれだけでは許可が足りず、アップロードすることはできないのでS3で登録してあるグループに権限を持たせる必要があります。

IAMを開きUserGroupsを開き、自分のグループを選択、permissonを開きcreate policyを選択します。

ポリシー作成画面でJson形式にしてから以下のように入力します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucketMultipartUploads",
                "s3:ListBucketVersions",
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::<your-bucket_name>"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": "arn:aws:s3:::<your-bucket_name>/*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "s3:ListAccessPointsForObjectLambda",
                "s3:ListAllMyBuckets"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor3",
            "Effect": "Allow",
            "Action": "s3:*Object*",
            "Resource": [
                "arn:aws:s3:::<your-bucket_name>",
                "arn:aws:s3:::<your-bucket_name>*",
                "arn:aws:s3-object-lambda:<your-_region_name>:<acsesspoint_id>:accesspoint/*"
            ]
        }
    ]
}

Policyを作成後グループにこの許可を認可するようにしますやり方としては、グループにS3の権限を付与するのと同じでし

こうすることでブラウザからS3に直接アップロードする下準備は整いました。次にdjangoの方でアップロードできるサイトを作っていきます。

Djangoの設定

djangoインストールをしたらdjango-storagesとdjango rest framework をインストールを行います

pip install django-storages

pip install djangorestframework

アプリケーションを作る

python manage.py startapp upload

Storage とRest-frame-workをsettings.pyに認知させる

settings.py を開いて、INSTALLED_APPSが書いてある[]内に作ったアプリ名と”storage”、”rest_framework”と書き込みます。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bootstrap4',
    'django.forms',
    'rest_framework', # 追加
    'django_cleanup.apps.CleanupConfig',
    'storages',#追加
    'django_grpc_framework',
    'video'

    

]

django-Storages(AWS)の設定

settings.pyのまま、Storageのアクセス等を書いていきます。

AWS_ACCESS_KEY_ID = 'AWS_UPLOAD_ACCESS_KEY_ID'  
AWS_SECRET_ACCESS_KEY =env('AWS_SECRET_KEY')
AWS_STORAGE_BUCKET_NAME = 'your_bucket_name'
AWS_LOCATION = 'static'
AWS_S3_SIGNATURE_VERSION = 's3v4'
AWS_S3_REGION_NAME = 'ap-northeast-1'
AWS_DEFAULT_ACL = None

これでstorageの設定は完了しました。次はモデルの設定を行います。

Modelの作成

次に大きなファイルをアップロードに名前やらアップロードされた日を記入したりするモデルを作成します。今回はビデオにします。

class Video(models.Model):
    user = models.ForeignKey( settings.AUTH_USER_MODEL,on_delete=models.CASCADE)
    video = models.FileField(upload_to='media/user/video',verbose_name='upload to moive ',
    help_text="test",)
    title = models.CharField(_('タイトル'),max_length=50,blank=True)
    text = models.TextField(verbose_name='text',blank=True,)
    create_at = models.DateTimeField('CreateDay',null=True,default=timezone.now)

    def __str__(self):
        return self.title
    

Views.pyの設定

ブラウザからアップロードできる様にいろいろしていきます。

import base64
import hashlib
import hmac
import os
import time
from rest_framework import permissions, status, authentication
from rest_framework.response import Response
from rest_framework.views import APIView
from django.conf import settings

from .models import Video

class FilePolicy(APIView):
    """
    This view is to get the AWS Upload Policy for our s3 bucket.
    What we do here is first create a FileItem object instance in our
    Django backend. This is to include the FileItem instance in the path
    we will use within our bucket as you'll see below.
    """
    permission_classes = [permissions.IsAuthenticated]
    authentication_classes = [authentication.SessionAuthentication]

    def post(self, request, *args, **kwargs):
        """
        The initial post request includes the filename
        and auth credientails. In our case, we'll use
        Session Authentication but any auth should work.
        """
        filename_req = request.data.get('filename')
        if not filename_req:
                return Response({"message": "A filename is required"}, status=status.HTTP_400_BAD_REQUEST)
        policy_expires = int(time.time()+5000)
        user = request.user
        username_str = str(request.user.username)

        video_type = os.path.splitext(filename_req)
        video_str = str(filename_req)
        """
        Below we create the Django object. We'll use this
        in our upload path to AWS. 

        Example:
        To-be-uploaded file's name: Some Random File.mp4
        Eventual Path on S3: <bucket>/username/2312/2312.mp4
        """
        file_obj = Video.objects.create(user=user, title=filename_req,)
        file_obj_id = file_obj.id

        upload_start_path = "media/media/user/video/".format(
                    username = username_str,
                    file_obj_id=file_obj_id,
                    video_str=video_str
            )
        _, file_extension = os.path.splitext(filename_req)
        filename_final = "{file_obj_id}".format(
                    file_obj_id= video_str,
                    file_extension=file_extension

                )
        """
        Eventual file_upload_path includes the renamed file to the 
        Django-stored FileItem instance ID. Renaming the file is 
        done to prevent issues with user generated formatted names.
        """
        final_upload_path = "{upload_start_path}{filename_final}".format(
                                 upload_start_path=video_str,
                                 filename_final=filename_final,
                            )
        video =   'media/user/video/{filename_final}'.format(
                        bucket=settings.AWS_STORAGE_BUCKET_NAME,  
                        region=settings.AWS_UPLOAD_REGION,
                        user = username_str,
                        filename_final=filename_final,
                        video_str=video_str
                        
        )       
        file_obj.video = video
        if filename_req and file_extension:
            """
            Save the eventual path to the Django-stored FileItem instance
            """
            file_obj.path = final_upload_path
            file_obj.save()

        policy_document_context = {
            "expire": policy_expires,
            "bucket_name":settings.AWS_STORAGE_BUCKET_NAME,
            "key_name": "",
            "acl_name": "private",
            "content_name": "",
            "content_length": 42880000,最大アップロード容量
            "upload_start_path": upload_start_path,

            }
        policy_document = """
        {"expiration": "2026-01-01T00:00:00Z",
          "conditions": [ 
            {"bucket": "%(bucket_name)s"}, 
            ["starts-with", "$key", "%(upload_start_path)s"],
            {"acl": "%(acl_name)s"},
            
            ["starts-with", "$Content-Type", "%(content_name)s"],
            ["starts-with", "$filename", ""],
            ["content-length-range", 0, %(content_length)d]
          ]
        }
        """ % policy_document_context
        aws_secret = str.encode(AWS_SECRET_ACCESS_KEY)
        policy_document_str_encoded = str.encode(policy_document.replace(" ", ""))
        url = 'https://{bucket}.s3-{region}.amazonaws.com/'.format(
                        bucket=settings.AWS_STORAGE_BUCKET_NAME,  
                        region=settings.AWS_UPLOAD_REGION
                        )
        policy = base64.b64encode(policy_document_str_encoded)
        signature = base64.b64encode(hmac.new(aws_secret, policy, hashlib.sha1).digest())
        data = {
            "policy": policy,
            "signature": signature,
            "key": settings.AWS_ACCESS_KEY_ID,
            "file_bucket_path": upload_start_path,
            "file_id": file_obj_id,
            "filename": filename_final,
            "url": url,
            "username": username_str,
        }
        return Response(data, status=status.HTTP_200_OK)

class FileUploadCompleateHander(APIView):
    permmison_classes = [authentication.SessionAuthentication]
    def post(self,*args, **kwargs):
        video_id = self.request.POST.get('file')
        size = self.request.POST.get('fileSize')
        
        data = {}
        type_ = self.request.POST.get('fileType')
        if video_id:
            video = Video.objects.get(id=int(video_id))
            video.save()
            data['id'] = video.id
            data['saved'] = True
        return Response(data, status=status.HTTP_200_OK)

URL.pyの設定

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls import url
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.contrib.staticfiles.urls import static
from GRC.views import FilePolicy,FileUploadCompleateHander
import GRC.views
urlpatterns = [
    path('fhrshsssff/', admin.site.urls),
    path('', include('video.urls')),
    path('api/', include('rest_framework.urls')),
    url(r'^api/files/policy/$', FilePolicy.as_view(), name='upload-policy'),
    url(r'^api/files/complete/$',FileUploadCompleateHander.as_view(), name='upload-complete'),
]
urlpatterns += staticfiles_urlpatterns()

javascriptとhtmlの設定

base.htmlを設定し、テンプレートタグを入力すればどのページでもこの画面で表示できる様にします。

<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>CFE Upload</title>
  </head>
  <body>
    <div class='container main-container'>
      {% block content %}{% endblock content %}
    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <!-- Ensure it's not jquery-3.2.1.slim.min.js -->
  </body>
 </html>

アップロードページの作成

<script src="{% static 'app/video_upload.js' %}"></script>
<form  method="POST" enctype="multipart/form-data" class='cfeproj-upload-form' id="my-awesome-dropzone" enctype="multipart/form-data" >
    <input class="cfeproj-upload-file" id="video"type="file" style="display:none" accept=".MOV, .MP4, .mpeg4">
    <div class="image-upload-wrap">
        <div class="drag-text" id="my-awesome-dropzone">
            <label for="video"><h3>Select add Movie</h3></label>
        
      </div>
</div>
</form>

label for=”id” で自分の望むデザインでアップロードができます。

javascript

$(document).ready(function(){

    // setup session cookie data. This is Django-related
    function getCookie(name) {
        var cookieValue = null;
        if (document.cookie && document.cookie !== '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
    var csrftoken = getCookie('csrftoken');
    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
    // end session cookie data setup. 



// declare an empty array for potential uploaded files
var fileItemList = []

// auto-upload on file input change.
$(document).on('change','.cfeproj-upload-file', function(event){
    var selectedFiles = $(this).prop('files');
    formItem = $(this).parent()
    $.each(selectedFiles, function(index, item){
        var myFile = verifyFileIsImageMovieAudio(item)
        if (myFile){
            uploadFile(myFile)
        } else {
            // alert("Some files are invalid uploads.")
        }
    })
    $(this).val('');

})



function verifyFileIsImageMovieAudio(file){
    // verifies the file extension is one we support.
    var extension = file.name.split('.').pop().toLowerCase(); //file.substr( (file.lastIndexOf('.') +1) );
    switch(extension) {
        case 'mov':
        case 'mp4':
        case 'mpeg4':
            return file
        default:
            notAllowedFiles.push(file)
            return null
    }
};

function constructFormPolicyData(policyData, fileItem) {
   var contentType = fileItem.type != '' ? fileItem.type : 'application/octet-stream'
    var url = policyData.url
    var filename = policyData.filename
    var repsonseUser = policyData.user
    // var keyPath = 'www/' + repsonseUser + '/' + filename
    var keyPath = policyData.file_bucket_path
    var fd = new FormData()
    fd.append('key', keyPath + filename);
    fd.append('acl','private');
    fd.append('Content-Type', contentType);
    fd.append("AWSAccessKeyId", policyData.key)
    fd.append('Policy', policyData.policy);
    fd.append('filename', filename);
    fd.append('Signature', policyData.signature);
    fd.append('file', fileItem);
    return fd
}

function fileUploadComplete(fileItem, policyData){
    data = {
        uploaded: true,
        fileSize: fileItem.size,
        file: policyData.file_id,

    }
    $.ajax({
        method:"POST",
        data: data,
        url: "/api/files/complete/",
        success: function(data){
            displayItems(fileItemList)
        },
        error: function(jqXHR, textStatus, errorThrown){ 
            alert("An error occured, please refresh the page.")
        }
    })
}

function displayItems(fileItemList){
    var itemList = $('.item-loading-queue')
    itemList.html("")
    $.each(fileItemList, function(index, obj){
        var item = obj.file
        var id_ = obj.id
        var order_ = obj.order
        var html_ = "<div class=\"progress\"style=\"height: 20px;\">" + 
          "<div class=\"progress-bar bg-info\" role=\"progressbar\" style='width:" + item.progress + "%' aria-valuenow='" + item.progress + "' aria-valuemin=\"0\" aria-valuemax=\"100\"></div></div>"
        itemList.append("<div>" + order_ + ") " + item.name + "<a href='#' class='srvup-item-upload float-right' data-id='" + id_ + ")'>X</a> <br/>" + html_ + "</div><hr/>")

    })


    var InputFile = $('#video-edit')
    var Loading = $('#loading')

    InputFile.hide()
    Loading.show()
    
}


function uploadFile(fileItem){
        var policyData;
        var newLoadingItem;
        // get AWS upload policy for each file uploaded through the POST method
        // Remember we're creating an instance in the backend so using POST is
        // needed.
        $.ajax({
            method:"POST",
            data: {
                filename: fileItem.name
            },
            url: "/api/files/policy/",
            success: function(data){
                    policyData = data
            },
            error: function(data){
                alert("An error occured, please try again later")
            }
        }).done(function(){
            // construct the needed data using the policy for AWS
            var fd = constructFormPolicyData(policyData, fileItem)
            
            // use XML http Request to Send to AWS. 
            var xhr = new XMLHttpRequest()

            // construct callback for when uploading starts
            xhr.upload.onloadstart = function(event){
                var inLoadingIndex = $.inArray(fileItem, fileItemList)
                if (inLoadingIndex == -1){
                    // Item is not loading, add to inProgress queue
                    newLoadingItem = {
                        file: fileItem,
                        id: policyData.file_id,
                        order: fileItemList.length + 1
                    }
                    fileItemList.push(newLoadingItem)
                  }
                fileItem.xhr = xhr
            }

            // Monitor upload progress and attach to fileItem.
            xhr.upload.addEventListener("progress", function(event){
                if (event.lengthComputable) {
                 var progress = Math.round(event.loaded / event.total * 100);
                    fileItem.progress = progress
                    displayItems(fileItemList)
                }
            })

            xhr.upload.addEventListener("load", function(event){
                var EditFile = $("#reload")
                EditFile.html("<div class=\"btn btn-info\" onclick=\"window.location.reload();\">Push me</div>")
                var text = $("#modal-text")
                text.html("Upload complete. Please push reload button or [push me!] Button.")
                // handle FileItem Upload being complete.
                fileUploadComplete(fileItem, policyData)
            })


            xhr.open('POST', policyData.url , true);
            xhr.send(fd);
            
        })
}});

これでアップロードの完了しました。応用していくとこんな感じでできます。

これをやりたがる人はあまりいなさそうですが参考になれば幸いです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA