AWS Amplify + Reactで既存のLambda(Python)にファイルを渡して処理する

2022.05.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS Amplify + Reactで既存のLambdaを呼び出す」では、既存のLambdaをAWS Amplify + Reactから呼び出す方法について紹介しました。

今回は、この発展形でLambdaに対してファイルを渡す方法、Reactから見るとファイルアップロードを行う方法について記載します。

解法

フロントエンド(React)

非常にシンプルなReactでの実装例になります。

import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'

const initialState = {
  file: null,
  someMetadata: ''
}

const SomeFileUploader = () => {
  const [formState, setFormState] = useState(initialState)

  const setInput = (key, value) => {
    setFormState({ ...formState, [key]: value })
  }

  const convertBase64 = (file) => {
    return new Promise((resolve, reject) => {
      const fileReader = new FileReader()
      fileReader.readAsDataURL(file)
      fileReader.onload = () => {
        resolve(fileReader.result)
      }
      fileReader.onerror = (error) => {
        reject(error)
      }
    })
  }

  const onFileInputChange = async (event) => {
    const file = event.target.files[0]
    const base64 = await convertBase64(file)
    setInput('file', base64.replace(/^data:\w+\/\w+;base64,/, ''))
  }

  const uploadFile = async() => {
    if (!formState.file) return

    // メタデータも適当に設定(ファイルアップロードには無関係)
    formState.someMetadata = 'メタデータです'

    const credentials = await Auth.currentCredentials()
    const client = new LambdaClient({
      credentials: Auth.essentialCredentials(credentials),
      region: process.env.REACT_APP_LAMBDA_REGION
    })
    const input = {
      FunctionName: 'some-existing-lambda-' + process.env.REACT_APP_STAGE,
      Payload: JSON.stringify(formState)
    }
    const command = new InvokeCommand(input)
    const response = await client.send(command)
    if (response) {
      setFormState(initialState)
    } else {
      console.error(response.error)
    }
  }

  return (
    <Box>
      <input
        type="file"
        onChange={onFileInputChange}
      />
      <Button
        onClick={uploadFile}
        text="Upload File"
      />
    </Box>
  )
}

export default SomeFileUploader

Reactからsome-existing-lambda-devという名前の既存のLambdaを呼び出す例になっています。

.envファイルには下記のような設定をしています。

REACT_APP_STAGE=dev
REACT_APP_LAMBDA_REGION=ap-northeast-1

ファイルアップロードに関する箇所を以下にピックアップします。

      <input
        type="file"
        onChange={onFileInputChange}
      />

でファイル選択のUIを作成します。このUIでは選択したファイルが異なる度にonChangeに指定したonFileInputChangeがコールされます。

onFileInputChangeは次のようになっています。

  const onFileInputChange = async (event) => {
    const file = event.target.files[0]
    const base64 = await convertBase64(file)
    setInput('file', base64.replace(/^data:\w+\/\w+;base64,/, ''))
  }

まず、event.target.files[0]で選択したファイルオブジェクトの参照を取得し、convertBase64関数に渡してファイルコンテンツをBase64エンコードします。Base64エンコード結果は

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA.........

のようにメタ情報が最初についてきます。このメタ情報(〜base64,まで)はファイルコンテンツのデータとしては不要なので、

base64.replace(/^data:\w+\/\w+;base64,/, '')

で除去し、formState.fileに結果をセットしています。

あとはaws-sdkLambda Clientを使ってLambdaを呼び出しています。

See Also

バックエンド(Lambda)

PythonによるLambdaの実装例を示します。 フロントエンドでアップロード指定したファイルを受け取ってS3に保存します。

import os
import logging
import boto3
import botocore.exceptions
import json
import base64

logging.basicConfig(
    format="%(asctime)s %(module)s:%(funcName)s(%(lineno)d) - %(levelname)s - %(message)s"
)
logger = logging.getLogger()
logger.setLevel(logging.INFO)

bucket_name = os.environ['S3_BUCKET']
upload_dir = os.environ['UPLOAD_DIR']

def lambda_handler(event, context):
    s3 = boto3.resource('s3')
    bucket = s3.Bucket(bucket_name)

    key = "%s/%s" % (upload_dir, event['filename'])
    body = base64.b64decode(event['file'])
    try:
        bucket.put_object(
            Key = key,
            Body = body,
            Metadata = {
                'some-metadata': event['someMetadata']
            }
        )
        logger.info("S3にアップロードしました。 key: %s" % key)
    except Exception as e:
        logger.error("S3へのアップロードでエラーが発生しました。処理を中断します。: Exception name is %s. Detail is %s" % (type(e).__name__, e))
        raise

    ret_body = {
        "input": event
    }
    return {
        "statusCode": 200,
        "body": json.dumps(ret_body)
    }

# メイン関数
if __name__ == "__main__":
    lambda_handler({"filename": "", "someMetadata": ""}, {})

event['file']にBase64エンコードしたファイルコンテンツが入っていますので、

    body = base64.b64decode(event['file'])

でデコードしてbucket.put_object()メソッドにBodyとして渡すことでS3に保存できます。 なお、put_objectメソッドではMetadataにdictを渡すと、dictのkey名のカスタムメタデータをオブジェクトに対して設定できます。ここではsome-metadataというカスタムメタデータに対して、Reactで入力したevent['someMetadata']を指定しています。