署名付きURLとマルチパートアップロードを利用したS3へのファイルアップロード

AWS

マルチパートアップロード機能を利用するとS3へのファイルアップロードを
並列に行うことや中断・再開といったオペレーションが可能となります。
また通常のアップロードと比較しファイルサイズの上限も緩和されますので
大容量のファイルアップロードには必須の機能になります。

今回はLambdaとブラウザ(RESTAPI)を利用してマルチパートアップロードを行ってみます。
作業の流れは以下になります。

  1. S3バケットの作成とCORS設定
  2. マルチパートアップロードのID発行(マルチパートアップロードの開始)
  3. 署名付きURLの発行  ・・・ファイル分割数分実行
  4. パートのアップロード ・・・ファイル分割数分実行
  5. マルチパートアップロードの完了

方式は色々ありますが今回は2,3,5はLambdaで行い、4をブラウザで実装します。

マルチパートアップロードを使用したオブジェクトのアップロードとコピー

1.S3バケットの作成とCORS設定

マルチアップロードでは「ETag」を利用するため、ExposeHeadersの指定を行います。
アップロード用に任意のバケットを作成し、
バケットのCORS設定を以下のように設定します。
CORS設定はコンソールの
  [アクセス許可] > [Cross-Origin Resource Sharing (CORS)]
から設定出来ます。

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag",
            "x-amz-meta-custom-header"
        ]
    }
]

2.マルチパートアップロードのID発行

create_multipart_uploadを呼び出し、「アップロードID」を取得します。
パートのアップロード、完了、中止等にはこの「アップロードID」が必要です。
Lambdaで発行しクライアントに返却します。

マルチパートアップロードを開始して1つ以上のパーツをアップロードした後、アップロードされたパーツの保存に対する課金を停止するには、マルチパートアップロードを完了するか中止する必要があります。 S3のバケットポリシーで期間による削除指定が可能なので指定しておきましょう。

boto3リファレンス create_multipart_upload

import boto3
import json
from botocore.client import Config

REGION_NAME = 'ap-northeast-1'
S3_BUCKET_NAME = 'yourBacketName'
S3_OBJECT_NAME = 'fileupload/'

def lambda_handler(event, context):

    file_name = event['queryStringParameters']['fileName']

    s3 = boto3.client(
        service_name='s3',
        region_name=REGION_NAME,
        config=Config(signature_version='s3v4')
    )    

    # マルチアップロード用IDを生成
    response = s3.create_multipart_upload(
        Bucket=S3_BUCKET_NAME, 
        Key=S3_OBJECT_NAME + file_name,
        ContentType='multipart/form-data')

    upload_id = response['UploadId']

    resonse_body = {
        'upload_id': upload_id,
    }

    return {
       'statusCode': 200,
       'isBase64Encoded': False,
        'headers': {
            'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'OPTIONS,POST,GET,PUT',
        },
        'body': json.dumps(resonse_body)
    }

3.署名付きURLの発行

マルチパートアップロードを行う際、分割したファイル毎にパートNoを付与します。
クライアントからは分割した数だけ本APIを呼び出しを行い、
アップロードデータと一緒にパートNoをクライアントからリクエストで受け取ります。
署名付きURLには以下を含めておく必要があります。
・マルチパートアップロードID
・パートNo
・S3オブジェクトキー(アップロード先)

import boto3
import json
import logging
from botocore.client import Config

REGION_NAME = 'ap-northeast-1'
S3_BUCKET_NAME = 'yourBacketName'
S3_OBJECT_NAME = 'fileupload/'
DURATION_SECONDS = 60*60*24

def lambda_handler(event, context):

    # 署名付きURLに含めるファイル名、パートNo、アップロードIDを取得
    file_name = event['queryStringParameters']['fileName']
    part_number = int(event['queryStringParameters']['partNumber'])
    upload_id = event['queryStringParameters']['uploadId']
    upload_user = event['queryStringParameters']['uploadUser']

    s3 = boto3.client(
        service_name='s3',
        region_name=REGION_NAME,
        config=Config(signature_version='s3v4')
    )

    # 署名付きURLを生成
    preSignedUrl = s3.generate_presigned_url(
                                     ClientMethod = 'upload_part', 
                                    Params = {
                                        'Bucket': S3_BUCKET_NAME,
                                        'Key': S3_OBJECT_NAME + file_name,
                                        'UploadId': upload_id,
                                        'PartNumber': part_number,
                                    }, 
                                    ExpiresIn = DURATION_SECONDS, 
                                    HttpMethod = 'PUT')

    response={
        'presigned_url': preSignedUrl,
    }

    return {
       'statusCode': 200,
       'isBase64Encoded': False,
        'headers': {
            'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'OPTIONS,GET',
        },
        'body': json.dumps(response)
    }

4.パートのアップロード

Lambdaから受け取った署名付きURLに対してブラウザからPUTを行います。
ファイル分割処理はややこしいので割愛しますが、アップロード自体は
以下のようにPUTを呼び出すだけです。

 presigned_url:Lambdaから受け取った署名付きURL
 data:送信データ

PUTメソッドのレスポンスで「ETag」が返却されます。
このEtagはパートNoと紐づくものになります。
アップロード完了を行うために、完了したすべての「ETag」「パートNo」が
必要になるので保持しておきます。

    fetch(presigned_url, {
      method: 'PUT',
      headers,
      body: data,
    }).then((res) => {
      multipartMap.Parts.push({
        ETag: res.headers.get('ETag'),
        PartNumber: partNumber,
      });

送信データの分割、読み込みは以下のように行えます。

  function readBlobAsByte(partNumber, blob, offset, size, callback) {
    var reader = new FileReader();
    reader.onload = function(e) {
      let u8array = new Uint8Array(e.target.result);
      callback(e.target.readyState, u8array, partNumber);
    }

    var readsize = size;
    if(blob.size < offset + size) {
      readsize = blob.size - offset;
    }

    slice = blob.slice(offset, offset + readsize, blob.type);
    reader.readAsArrayBuffer(slice);
  }  

5.マルチパートアップロードの完了

マルチパートアップロードの完了を通知します。
この完了通知を行うことで、パート単位で送信していたデータが連結されて
正式なS3オブジェクトが生成されます。
この操作を行わない場合、送信した中途半端なデータがS3上に残ってしまいます。
課金対象にもなるので注意してください。

完了にはアップロードID、パート番号とそれに対応する ETag 値のリストが必要です。

import json
import boto3

REGION_NAME = 'ap-northeast-1'
S3_BUCKET_NAME = 'yourBucketName'
S3_OBJECT_NAME = 'fileupload/'

def lambda_handler(event, context):

    body_json = json.loads(event['body'])
    parts = body_json['Parts']
    upload_id = body_json['UploadId']
    file_name = body_json['file_Name']

    client = boto3.client('s3')
    response = client.complete_multipart_upload(
        Bucket=S3_BUCKET_NAME,
        Key=S3_OBJECT_NAME + file_name,
        MultipartUpload={'Parts' : parts},
        UploadId=upload_id
    )

    response = client.get_object(
        Bucket=S3_BUCKET_NAME,
        Key=S3_OBJECT_NAME + file_name
    )

    return {
       'statusCode': 200,
        'headers': {
            'Access-Control-Allow-Headers': 'Host,Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'OPTIONS,POST,GET,PUT'
        },
        'body': json.dumps('OK')
    }

boto3リファレンス complete_multipart_upload

以上

タイトルとURLをコピーしました