GoogleのGAE(Google App Engin) やAWSのElastic BeanstalkなどのPaaS(Platform as a Service)の中には、一回の通信に制限がついています。そのため動画ファイルなどの大きなファイルをアップロードするとError413(ファイルサイズが大きすぎて発生するエラー)が起きます。自分のサイトでブラウザから動画をアップロードをしたいときには、S3などのクラウドストレージに一定時間有効なURLを使ってブラウザから直接アップロードする必要がありますが、公式ドキュメントやstack overflowなどの質問サイトを覗いても署名付きURLまでの作成については書かれていても、それを使ってブラウザから直接アップロードする方法までは書かれていないことが多いです。そこで時間をかけてやっとブラウザからアップロードすることができたのでもう二度と忘れないようにメモとして残します。( アップロードしたファイルをどうやってdjangoモデルにつなげてブラウザで閲覧できるようにするかは考える必要がありますが、とりあえずブラウザからアップロードだけできたので)
Table of Contents
前提
今回こちらのサイトを参考に実装を行っていますが、作成時期が少し古いので、その通りにやっていると引っかかる部分がかなりあったので自分が変更した箇所を含めて記録しています。
今回の使用したものは次の通りになります。
- 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);
})
}});
これでアップロードの完了しました。応用していくとこんな感じでできます。
これをやりたがる人はあまりいなさそうですが参考になれば幸いです。