Spring Boot でAmazon S3にファイルをアップロードする機能の実装

# はじめに
Spring Bootで作成したアプリケーション(ローカル環境)から、Amazon S3に接続して所定のバケットにファイルをアップロードする機能を実装するまでの流れをまとめました。

# 使用技術
Spring Boot 2.5.6
Spring Security
spring-cloud-starter-aws
Thymeleaf
maven
AWS(IAM,S3)

# AWS側の準備
Spring BootからS3へ接続するために、AWSで設定するのは以下2点です。
1.IAMでS3への権限を持ったユーザを作成し、アクセスキーを確認する
2.パブリックアクセスをオフにしたバケットを作成する

## IAMでS3への権限を持ったユーザを作成し、アクセスキーを確認する
IAMから「ユーザーを追加」を選択します。
![Inkedqiita-aws01._LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/007442dd-d074-1672-21b8-b7623a6edd7c.jpeg)
「アクセスキー・プログラムによるアクセス」にチェックを入れます。

![Inkedqiita-aws02_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/62cbc455-94d7-5784-5cfe-bbb24cbddfe9.jpeg)
S3を操作出来る権限を付与します。タグの追加は省略しても大丈夫です。

![Inkedqiita-aws03_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/65b91c9a-1413-6af4-010d-7568f40a48e4.jpeg)
作成が完了すると、アクセスキーとシークレットアクセスキーが確認できます。スクショかメモかcsvで保存しましょう。

![Inkedqiita-aws04_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/0cdad008-8232-6df5-c58a-623dfabe7513.jpeg)
シークレットアクセスキーの確認を忘れた場合は、「ユーザー名」をクリック→「認証情報」タブから、アクセスキーを再度生成することが出来ます。

## パブリックアクセスをオフにしたバケットを作成する
![qiita-aws-s306.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/1d762b86-a66b-bdae-0c78-e449568114e9.jpeg)
バケットを作成する際は「パブリックアクセスをすべてブロック」のチェックを外して作成します。チェック入れたままだと外部(アプリケーション)からのアップロードが出来ません。

# バックエンド
## 依存関係
以下を追加
```pox.xml
        <dependency>
		    <groupId>org.springframework.cloud</groupId>
		    <artifactId>spring-cloud-starter-aws</artifactId>
		    <version>2.2.6.RELEASE</version>
		</dependency>
```

## properties
application.propertiesに以下を追加
```application.properties
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.max-file-size=10MB

cloud.aws.credentials.accessKey=
cloud.aws.credentials.secretKey=
cloud.aws.stack.auto=false
cloud.aws.region.auto=false
cloud.aws.region.static=ap-northeast-1
```
上記ではaccessKeyとsecretKeyに値を記載していませんが、実際にアプリを動かす場合は「AWS側の準備」で確認したアクセスキー、シークレットキーの値を記載してください。

**<font color="Red">※プロパティファイルにシークレットキーを記載したままGithub等へ連携すると悪用される危険性がありますので、ご注意ください。</font>**

## Config
```S3Config.java
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

@Configuration
public class S3Config {
	
	@Value("${cloud.aws.region.static}")
	private String s3ClientRegion;
	
	@Value("${cloud.aws.credentials.access-key}")
	private String accessKey;

	@Value("${cloud.aws.credentials.secret-key}")
	private String secretKey;
	  
	@Bean
	public AmazonS3 s3Client() {
		AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
	    return AmazonS3ClientBuilder.standard()
	    		.withCredentials(new AWSStaticCredentialsProvider(credentials))
	            .withRegion(s3ClientRegion).build();
	    }

}
```
@Valueでプロパティファイルに記載したアクセスキー等の値を取得します。
Beanに登録しているAmazonS3型ですが、[参考文献](https://javadoc.io/doc/com.amazonaws/aws-java-sdk-s3/1.9.18/com/amazonaws/services/s3/AmazonS3.html)によると「AmazonS3Webサービスにアクセスするためのインターフェース」とのことです。

## Form
```Form.java
import org.springframework.web.multipart.MultipartFile;

public class FileUploadForm {
	
    private MultipartFile multipartFile;

    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
	private LocalDateTime createAt;

    ///getter & setter
```

## Service
```FileUploadService.java
@Service
public class FileUploadService {
	
	private final AmazonS3 s3Client;
	
	public FileUploadService(AmazonS3 s3Client) {
		this.s3Client = s3Client;
	}
	
    public String fileUpload(FileUploadForm fileUploadForm,String s3PathName) 
			       throws IOException{
		DateTimeFormatter fm = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss");
		String extension = FilenameUtils.getExtension(fileUploadForm.getMultipartFile().getOriginalFilename()).toLowerCase();
        String fileName = fileUploadForm.getCreateAt().format(fm) +"." + extension;
        File uploadFile = new File(fileName);
    
        //try-with-resources
        try (FileOutputStream uploadFileStream = new FileOutputStream(uploadFile)){
    		byte[] bytes = fileUploadForm.getMultipartFile().getBytes();
	        uploadFileStream.write(bytes);
        	
        	//引数：S3の格納先オブジェクト名,ファイル名,ファイル
        	s3Client.putObject(s3PathName, fileName, uploadFile);
        	uploadFile.delete();
        	return fileName;
        } catch (AmazonServiceException e) {
        	e.printStackTrace();
        	throw e;
        }  catch (IOException e) {
            e.printStackTrace();
            throw e;
        }
        
	}
}
```
fileUploadメソッドでは、ファイルを受け取ったFormクラスとS3の格納先名を引数で受け取り、ファイルをアップロードする処理を行っています。
あくまでもサンプルコードなので、画像をアップロードする前の「ファイル形式が正しいか」等のチェックは省略しております。

## Controller
```DiaryRecordController.java
@Controller
public class DiaryRecordController {
	
	private final DiaryRecordService diaryRecordService;
	
	private final FileUploadService fileUploadService;
	
	public DiaryRecordController(DiaryRecordService diaryRecordService,
			FileUploadService fileUploadService) {
		this.diaryRecordService = diaryRecordService;
		this.fileUploadService = fileUploadService;
	}

    @GetMapping("/index/create")
	public String showCreateContent(@ModelAttribute("diaryRecordForm") DiaryRecordForm form,
			                        @ModelAttribute("fileUploadForm") FileUploadForm file,Model model) {
		return "UserCalendar/Create";
	}

    @PostMapping("/index/create/insert")
	public String createContent(@AuthenticationPrincipal AccountUserDetails details,
			                    @ModelAttribute("diaryRecordForm") @Validated DiaryRecordForm form,BindingResult resultForm,
			                    @ModelAttribute("fileUploadForm")  FileUploadForm file,
			                    Model model) throws Exception{
		if(resultForm.hasErrors()) {
			return "UserCalendar/Create";
		}
		
		String imageName = null;
		LocalDateTime dateTime = LocalDateTime.now();
		//ファイルが空でない場合に、ファイルをアップロードする
		if(!Objects.isNull(file.getMultipartFile())){
			String s3Path = "spring-infra-wp-copy/" + details.getUsername();
			file.setCreateAt(dateTime);
			imageName = fileUploadService.fileUpload(file, s3Path);	
		}
		
		//~~~データベースへレコードをinsertする処理
		
		return "redirect:/index";
	}
}
```
Controllerクラスでは、ファイルをPOST通信で受け取ってServiceクラスのアップロード処理を呼び出しています。ファイルのバリデーションも必要ですが、今回は省略しています。
fileUploadメソッドの引数に渡すS3の格納先は、「バケット名/ログイン中のユーザ名」としています。


# フロントエンド

## html

```create.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>sample</title>
    <meta charset="UTF-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" 
		  integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>

<body>
<div th:replace="flagment/header :: user-header"></div><hr>
<h2>食事記録登録</h2>
<div class="container">
    <form class="row" id="category" enctype="multipart/form-data" th:action="@{/index/create/insert}" method="post" >
    <div class="col-sm-6" th:object="${diaryRecordForm}">
    <!-->省略<-->
	    <div th:object="${fileUploadForm}">
            <input type="file" th:field="*{multipartFile}">
        </div>
	    <button type="submit">登録</button>
	</div>
	</form>
</div>
</body>
</html>
```

# 結果・動作確認
![qiita-aws-s307.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/1a56cfae-c96e-5cc5-2c3e-2a72b25218da.jpeg)
今回作成したばかりのS3バケットには、ファイルが何も入っていません。

![qiita-aws-s308.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/70b5d18f-e7da-c02c-1b0f-fbaa877c1c6d.jpeg)
ローカルでアプリを起動させ、ファイルを選択して「登録」を押下すると...

![qiita-aws-s309.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448205/39bb1cf6-a805-a10e-d52d-10647ca1c156.jpeg)
S3バケットにファイルが格納されました。ファイル名もServiceクラス内で指定した名前になっています。
今回の格納先は「バケット名/ログイン中のユーザ名」としていますが、指定のフォルダ(ユーザ名のフォルダ)が無い場合はフォルダを作って格納されるみたいです。便利～！

# 終わりに
本来は「画像形式以外は受け付けない」といったバリデーションを設けて不正なファイルを弾く処理が必要ですが、本記事では省略しております。気になった方は調べてみてください。
ファイルアップロード処理のとっかかりになれば幸いです。

# 参考文献
https://reasonable-code.com/spring-boot-s3/

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/examples-s3-objects.html

https://javadoc.io/doc/com.amazonaws/aws-java-sdk-s3/1.9.18/com/amazonaws/services/s3/AmazonS3.html