【AWS】S3署名付きURLによる画像保存
1. 使用するサービス
(AWS)
- S3
- Lambda
2.概要
S3に画像をアップロードする際にいくつか手法があると思いますが、API GatewayとLambdaのサーバレスの構成の際、Lambdaに対してbase64でエンコードして送るなど考えがちかと思います。少し、時間はかかると思いますが、悪くない選択だと思います。しかし、API Gatewayの制約として、入力ペイロードの最大が10MBまでということもあり、複数画像の一括アップロードや動画の保存は現実的に不可能です。
そこで、今回、クライアント側から直接S3へセキュアな方法でアップロードする方法を説明します。
全体的な流れは下記のようになります。
API Gatewayの設定等は前回の記事参考
buffalokusojima.hatenablog.com
3. 実装
【バックエンド実装】
3-1. S3署名付きURL発行Lambda
S3に署名付きURLの発行をS3に依頼し、署名付きURLと各種認証情報等を受け取り、レスポンスとして返すLambdaを作成します。
import json import os import boto3 import uuid s3Client = boto3.client('s3') FOLDER = '/tmp' BUCKET_NAME = os.environ["PhotoBucketName"] def handler(event, context): print(event) body = event["queryStringParameters"] file_name = body["file_name"] file_type = body["file_type"] file_num = body["file_num"] userName = event['requestContext']['authorizer']['claims']['cognito:username'] file_extention = file_name.split(".")[-1] folder = os.path.join(userName, file_num) key = os.path.join(folder, str(uuid.uuid4())+"."+file_extention) presigned_post = s3Client.generate_presigned_post( Bucket = BUCKET_NAME, Key = key, Fields = {"acl": "public-read", "Content-Type": file_type}, Conditions = [ {"acl": "public-read"}, {"Content-Type": file_type} ], ExpiresIn = 3600 ) return { "statusCode": 200, "body": json.dumps({"success": True, "url": presigned_post["url"], "data": presigned_post}), "headers": { "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS" } }
3-2. S3のPUTトリガーが設定されたLambda
これはおまけですが、S3のPUTに対して反応するLambdaを作成します。イメージとしては、S3にPUTされた画像のURLをRDS等に保存するといった感じです。(今回はSQSに投げるまでにします)
import json import os import boto3 import uuid queueClient = boto3.client('sqs') QUEUE_URL = os.environ["ImageDataWriteQueue"] def handler(event, context): print(event) records = event["Records"] for record in records: s3 = record["s3"] bucket_name = s3["bucket"]["name"] + ".s3.amazonaws.com" key = s3["object"]["key"] folder = "/".join(key.split("/")[:-1]) url = "https://"+os.path.join(bucket_name, key) msg = {'messageId': str(uuid.uuid4()), "image_url": url, "folder": folder} queueClient.send_message(QueueUrl=QUEUE_URL, MessageBody=json.dumps(msg), MessageGroupId="group")
【フロントエンド】
3-3. 画像のアップロードとダウンロード
フロント側の実装になります。画像の表示とアップロードを行っています。
注意点としては、アップロード中に更新等で処理を中断すると、写真が古いままになる可能性があります。フロント側で処理が中断されないようにする必要があります。
getImageファンクションで叩いているAPIは別途作成する必要あります。RDS等に画像のURLを保存するなりして、そこからURLを引っ張り、レスポンスとして返すLambdaが理想です。機会があれば別途、そこら辺も紹介出来たらと思います。
<html> <head> <meta http-equiv="Cache-Control" content="no-store"> <title>test</title> </head> <body onload="begining()"> <input type="file" id="postfile1"/> <input type="file" id="postfile2"/> <input type="file" id="postfile3"/> <input type="file" id="postfile4"/> <input type="file" id="postfile5"/> <input type="file" id="postfile6"/> <input type="file" id="postfile7"/> <input type="file" id="postfile8"/> <input type="file" id="postfile9"/> <input type="file" id="postfile10"/> <input type="button" value="submit" onclick="postImage()" /> <div id="image_field"></div> <script> const URL = "https://URL"; function sendRequest(data, callback, callbackValue){ var request = new XMLHttpRequest(); let storage = localStorage; let id_token = storage.getItem("idToken"); request.open(data.method, data.url); request.setRequestHeader("Content-Type", "application/json"); request.onload = function(err){ console.log(err); if(request.readyState == 4){ if(request.status == 200){ console.log("success"); var res = JSON.parse(request.responseText); console.log(res); callback(res, callbackValue); }else if(request.status == 401){ var res = JSON.parse(request.responseText); console.log(res); singnIn(); }else{ console.log(request.status, request.response); console.log(request.responseText); } } } request.onerror = function(){ console.log("err:" + request.status); console.log(request.responseText) } console.log("sending"); var body; if(data.body){ /* body = { "body": JSON.stringify(data.body) };*/ body = JSON.stringify(data.body); //body = data.body } console.log(body); request.send(body); } function begining(){ getImage(); } function getImage(){ var data = { "method": "GET", "url": URL + "getImageURL", "body": null } sendRequest(data, showImage, null); } function showImage(data){ var url_list = data.url_list; var image_filed = document.getElementById("image_field"); for(var i=0; i<url_list.length; i++){ var imgSrc = document.createElement("img"); imgSrc.id = i+1; imgSrc.src = url_list[i]; image_filed.appendChild(imgSrc); } } function postImage(){ for(var i=1; i<11; i++){ var file = document.getElementById("postfile"+i); console.log(file); file = file.files[0]; if(!file){ continue; } var data = { "method": "GET", "url": URL + "getImagePresignedURL?file_name=" + file.name + "&file_type=" +file.type + "&file_num=" + i, "body": null } sendRequest(data, uploadFile, file); } } function uploadFile(s3Data, file){ var xhr = new XMLHttpRequest(); xhr.open("POST", s3Data.url); var postData = new FormData(); for(var key in s3Data.data.fields){ //console.log(key, s3Data.data.fields[key]) postData.append(key, s3Data.data.fields[key]); } postData.append('file', file); xhr.onreadystatechange = function() { if(xhr.readyState === 4){ if(xhr.status === 200 || xhr.status === 204){ //document.getElementById("preview").src = url; //document.getElementById("avatar-url").value = url; } else{ alert("Could not upload file."); } } }; xhr.send(postData); } </script> </body> </html>
4. おわりに
以上で簡単なS3の署名付きURLによる画像アップロードになります。
次回以降、PUTトリガー、SQSによるRDSとの連携とかも出来たらと思います。