Skip to content

Commit

Permalink
feat: プロファイル画像取得APIの実装
Browse files Browse the repository at this point in the history
 (#257)

### **User description**
## 概要

プロファイル画像を取得する API を作成

## 変更点

- Lambda, API GW の CDK コードを追加
- Lambda 関数の中身の Python コードを追加

## 影響範囲

既存の機能には影響なし

## テスト

- pytest によりテストを実行

## 関連 Issue

- 関連 Issue: #244


___

### **PR Type**
Enhancement


___

### **Description**
- プロファイル画像を取得するAPIを新たに実装しました。
- Lambda関数を追加し、S3から画像を取得してBase64エンコードした結果を返します。
- API GatewayにGETメソッドを追加し、ユーザーのプロフィール画像を取得できるようにしました。
- 画像取得関数に対するユニットテストを実装し、エラーハンドリングも確認しました。



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>

<summary><strong>settings.ts</strong><dd><code>プロファイル画像取得用LambdaとAPIの追加</code>&nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

src/backend/lib/constructs/settings.ts

- プロファイル画像取得用のLambda関数を追加
- API GatewayにGETメソッドを追加



</details>


  </td>
<td><a
href="https://github.com/Kota8102/diary-app/pull/257/files#diff-e94c547bfdc2d716da1a4540ffc6f7208769c059a5af1bb9e241946b25fca7d4">+15/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>

<summary><strong>get_profile_image.py</strong><dd><code>S3から画像を取得するLambda関数の実装</code>&nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/backend/lambda/get_profile_image/get_profile_image.py

- S3から画像を取得する関数を実装
- 画像をBase64エンコードして返す処理を追加



</details>


  </td>
<td><a
href="https://github.com/Kota8102/diary-app/pull/257/files#diff-d68e6102071444eb1156813d7061c6340ba7f7872d1bab66ac96ec0bdb90a7e1">+88/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>

<summary><strong>test_get_profile_image.py</strong><dd><code>プロファイル画像取得関数のテスト追加</code>&nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

src/backend/test/pytest/test_get_profile_image.py

- 画像取得関数のユニットテストを追加
- エラーハンドリングのテストを実装



</details>


  </td>
<td><a
href="https://github.com/Kota8102/diary-app/pull/257/files#diff-716d4c0ab2eb690df4faea3760dfc5c231811f2e7cefcf1e444a874feb11d9b6">+65/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**: Comment `/help "your question"` on any pull
request to receive relevant information
  • Loading branch information
mt-osho-san authored Dec 28, 2024
1 parent 81b8fdb commit 8a2f23d
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 2 deletions.
88 changes: 88 additions & 0 deletions src/backend/lambda/get_profile_image/get_profile_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import base64
import logging
import os

import boto3
from botocore.exceptions import ClientError

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)


def get_image_from_s3(key):
"""
S3 から画像データを取得
Args:
key (string): S3 バケット内のファイルのキー(パス)。
Returns:
bytes: 取得した画像データ。
string: 画像のコンテンツタイプ。
Raises:
RuntimeError: S3 からデータを取得中にエラーが発生した場合。
"""
# S3 クライアントの初期化
s3_client = boto3.client("s3")
bucket = os.environ["USER_SETTINGS_BUCKET"]

try:
response = s3_client.get_object(Bucket=bucket, Key=key)
data = response["Body"].read()
content_type = response["ContentType"]
return data, content_type
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchKey":
raise FileNotFoundError(
f"The key {key} does not exist in bucket {bucket}"
) from e
raise RuntimeError(f"Failed to retrieve image from S3: {str(e)}") from e


def lambda_handler(event, context):
"""
Lambda 関数のエントリーポイント。API Gateway からのリクエストを処理し、ユーザーのプロフィール画像を S3 から取得。
Args:
event (dict): API Gateway からのリクエストイベント。
context (object): Lambda 実行環境の情報。
Returns:
dict: レスポンスコードとメッセージを含む辞書。
"""
logger.info("get profile image function")
try:
# Cognito Authorizer から user_id を取得
user_id = event["requestContext"]["authorizer"]["claims"]["sub"]

# S3 パスを設定
s3_key = f"profile/image/{user_id}.png"

# S3 から画像を取得
image_data, content_type = get_image_from_s3(s3_key)

# Base64 エンコード
encoded_image = base64.b64encode(image_data).decode("utf-8")

# 成功レスポンスを返す
return {
"statusCode": 200,
"headers": {
"Content-Type": content_type,
"Content-Encoding": "base64",
},
"body": encoded_image,
"isBase64Encoded": True,
}

except FileNotFoundError as fe:
logger.error(f"File not found: {str(fe)}")
return {"statusCode": 404, "body": f"Image not found: {str(fe)}"}
except RuntimeError as re:
logger.error(f"Runtime error: {str(re)}")
return {"statusCode": 500, "body": f"Server error: {str(re)}"}
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return {"statusCode": 500, "body": f"Failed to retrieve image: {str(e)}"}
15 changes: 15 additions & 0 deletions src/backend/lib/constructs/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,26 @@ export class Settings extends Construct {
})
userSettingsBucket.grantPut(uploadProfileImageFunction)

//プロフィール画像取得用Lambda関数の定義
const getProfileImageFunction = new lambda.Function(this, 'getProfileImageFunction', {
runtime: lambda.Runtime.PYTHON_3_11,
handler: 'get_profile_image.lambda_handler',
timeout: cdk.Duration.seconds(15),
code: lambda.Code.fromAsset('lambda/get_profile_image'),
environment: {
USER_SETTINGS_BUCKET: userSettingsBucket.bucketName,
},
})
userSettingsBucket.grantRead(getProfileImageFunction)
// /APIの設定
const settingsApi = props.api.root.addResource('settings')

settingsApi.addMethod('POST', new apigateway.LambdaIntegration(uploadProfileImageFunction), {
authorizer: props.cognitoAuthorizer,
})

settingsApi.addMethod('GET', new apigateway.LambdaIntegration(getProfileImageFunction), {
authorizer: props.cognitoAuthorizer,
})
}
}
234 changes: 232 additions & 2 deletions src/backend/test/__snapshots__/snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ exports[`Snapshot test 1`] = `
"Type": "AWS::IAM::Role",
"UpdateReplacePolicy": "Delete",
},
"ApiDiaryApiDeploymentCA0DCBF5b04204490493f7bae0d0fbb51bda4809": {
"ApiDiaryApiDeploymentCA0DCBF5ddc2bf13dbc3d7fbb845a336b665119f": {
"DependsOn": [
"ApiDiaryApibouquetGET7AC93032",
"ApiDiaryApibouquetPOST2DEB7999",
Expand All @@ -350,6 +350,7 @@ exports[`Snapshot test 1`] = `
"ApiDiaryApiflowerGETEAC01B09",
"ApiDiaryApiflowerOPTIONS2A81112B",
"ApiDiaryApiflower7C61878D",
"ApiDiaryApisettingsGET4E223DBA",
"ApiDiaryApisettingsPOST5E845A8B",
"ApiDiaryApisettings70D21EB5",
"ApiDiaryApititleGET40FCE039",
Expand Down Expand Up @@ -380,7 +381,7 @@ exports[`Snapshot test 1`] = `
"Format": "{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","user":"$context.identity.user","caller":"$context.identity.caller","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}",
},
"DeploymentId": {
"Ref": "ApiDiaryApiDeploymentCA0DCBF5b04204490493f7bae0d0fbb51bda4809",
"Ref": "ApiDiaryApiDeploymentCA0DCBF5ddc2bf13dbc3d7fbb845a336b665119f",
},
"MethodSettings": [
{
Expand Down Expand Up @@ -1400,6 +1401,127 @@ exports[`Snapshot test 1`] = `
},
"Type": "AWS::ApiGateway::Resource",
},
"ApiDiaryApisettingsGET4E223DBA": {
"Properties": {
"AuthorizationType": "COGNITO_USER_POOLS",
"AuthorizerId": {
"Ref": "ApiCognitoAuthorizer23B91BA2",
},
"HttpMethod": "GET",
"Integration": {
"IntegrationHttpMethod": "POST",
"Type": "AWS_PROXY",
"Uri": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":apigateway:",
{
"Ref": "AWS::Region",
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"SettingsgetProfileImageFunctionDBA40214",
"Arn",
],
},
"/invocations",
],
],
},
},
"ResourceId": {
"Ref": "ApiDiaryApisettings70D21EB5",
},
"RestApiId": {
"Ref": "ApiDiaryApi1E03348A",
},
},
"Type": "AWS::ApiGateway::Method",
},
"ApiDiaryApisettingsGETApiPermissionCdkSampleStackApiDiaryApi4AB3EF3AGETsettings7E6B2258": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"SettingsgetProfileImageFunctionDBA40214",
"Arn",
],
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":execute-api:",
{
"Ref": "AWS::Region",
},
":",
{
"Ref": "AWS::AccountId",
},
":",
{
"Ref": "ApiDiaryApi1E03348A",
},
"/",
{
"Ref": "ApiDiaryApiDeploymentStageprodD9912276",
},
"/GET/settings",
],
],
},
},
"Type": "AWS::Lambda::Permission",
},
"ApiDiaryApisettingsGETApiPermissionTestCdkSampleStackApiDiaryApi4AB3EF3AGETsettings86A23BEE": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"SettingsgetProfileImageFunctionDBA40214",
"Arn",
],
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":execute-api:",
{
"Ref": "AWS::Region",
},
":",
{
"Ref": "AWS::AccountId",
},
":",
{
"Ref": "ApiDiaryApi1E03348A",
},
"/test-invoke-stage/GET/settings",
],
],
},
},
"Type": "AWS::Lambda::Permission",
},
"ApiDiaryApisettingsPOST5E845A8B": {
"Properties": {
"AuthorizationType": "COGNITO_USER_POOLS",
Expand Down Expand Up @@ -4050,6 +4172,114 @@ exports[`Snapshot test 1`] = `
},
"Type": "AWS::IAM::Policy",
},
"SettingsgetProfileImageFunctionDBA40214": {
"DependsOn": [
"SettingsgetProfileImageFunctionServiceRoleDefaultPolicyBC909A15",
"SettingsgetProfileImageFunctionServiceRole36952281",
],
"Properties": {
"Code": {
"S3Bucket": {
"Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}",
},
"S3Key": "HASH_REPLACED.zip",
},
"Environment": {
"Variables": {
"USER_SETTINGS_BUCKET": {
"Ref": "SettingsuserSettingsBucket1D2B648F",
},
},
},
"Handler": "get_profile_image.lambda_handler",
"Role": {
"Fn::GetAtt": [
"SettingsgetProfileImageFunctionServiceRole36952281",
"Arn",
],
},
"Runtime": "python3.11",
"Timeout": 15,
},
"Type": "AWS::Lambda::Function",
},
"SettingsgetProfileImageFunctionServiceRole36952281": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
],
],
},
],
},
"Type": "AWS::IAM::Role",
},
"SettingsgetProfileImageFunctionServiceRoleDefaultPolicyBC909A15": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*",
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"SettingsuserSettingsBucket1D2B648F",
"Arn",
],
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"SettingsuserSettingsBucket1D2B648F",
"Arn",
],
},
"/*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "SettingsgetProfileImageFunctionServiceRoleDefaultPolicyBC909A15",
"Roles": [
{
"Ref": "SettingsgetProfileImageFunctionServiceRole36952281",
},
],
},
"Type": "AWS::IAM::Policy",
},
"SettingsuploadProfileImageFunction5C7EC09C": {
"DependsOn": [
"SettingsuploadProfileImageFunctionServiceRoleDefaultPolicy8B4E4832",
Expand Down
Loading

0 comments on commit 8a2f23d

Please sign in to comment.