[Flutter] S3 Presigned URL 발급받기 | AWS Lambda & API Gateway
Presigned URL을 발급 반는 과정은 이전 게시글에서 확인할 수 있다. 이제 Presigned URL을 사용해서 최종적으로 S3 버킷에 이미지를 업로드해 보자!
image_picker로 이미지 선택하기
final ImagePicker picker = ImagePicker(); // ImagePicker 초기화
XFile? image; // 사용자가 선택한 이미지
String contentType = ''; // 이미지 파일의 타입
bool isImageUpdated = false; // 이미지 업로드 여부
Future getImage(ImageSource imageSource) async {
final XFile? pickedFile = await picker.pickImage(source: imageSource);
if (pickedFile == null) {
return;
}
setState(() {
image = XFile(pickedFile.path);
contentType = lookupMimeType(pickedFile.path)
isImageUpdated = true;
});
}
우선 사용자가 올린 이미지를 image_picker를 통해 가져오고, Presigned URL을 얻고, 이 Presigned URL을 사용해서 S3에 이미지를 업로드하는 흐름으로 진행된다.
S3 버킷에 이미지 업로드하기
final bytes = await _image!.readAsBytes();
// S3 버킷에 이미지 업로드 요청
final uploadImageResponse = await http.put(
Uri.parse(presignedUrl),
headers: {
'Content-Type': _contentType,
},
body: bytes,
);
Presiend URL에 `Content-Type` 헤더, 이미지 바이트와 함께 `put` 요청을 한다. 이 http 요청이 responstype `200`을 반환하면 이미지 업로드에 성공한 것이다.
업로드한 이미지 URL 얻기
final bucketUrl = "https://test.s3.ap-northeast-2.amazonaws.com";
String objectKey = ''; // Presigned URL 생성 시 filename 저장 예정
final String objectUrl = '$bucketUrl/$objectKey';
S3 버킷에 업로드한 이미지 URL은 http 응답에서 제공되지 않기 때문에 직접 생성해야 한다. 하지만 정해진 형식이 있기 때문에 어렵지 않게 만들 수 있다.
`bucketUrl`은 `"https://test.s3.ap-northeast-2.amazonaws.com"`에서 `test` 부분을 자신의 버킷 이름으로 바꿔주면 된다. `objectKey`는 Presigned URL을 생성할 때 쿼리 파라미터로 전달했던 `filename`을 그대로 사용하면 된다. 이 두 가지를 조합해서 이미지의 URL이 생성된다.
403 에러 해결: S3 버킷 권한 설정하기
HTTP request failed, statusCode: 403
지금 상태로 S3에 이미지를 업로드한다면 아마 다음과 같은 에러를 마주할 것이다. 이를 해결하기 위해 버킷 권한 탭에서 두 가지를 설정해주어야 한다.
먼저 버킷 정책을 편집한다. 버킷 정책 편집 페이지에서 정책 생성기 버튼을 클릭한다.
- Select Type of Policy: `S3 Bucket Policy`
- Effect: `Allow`
- Principal: `*`
- Actions: `GetObject`, `PutObject` 2개 선택
- Amazon Resource Name (ARN): 버킷 정책 페이지에서 `버킷 ARN` 복사
항목을 모두 입력한 후 `Add Statement` 버튼을 클릭하면 정책을 최종적으로 확인할 수 있고, 모두 올바르게 입력했다면 `Generate Policy` 버튼을 클릭하고 화면에 뜨는 JSON 코드를 복사한다. 복사한 JSON 코드를 버킷 정책 페이지에 붙여 넣고 변경 사항을 저장한다.
그리고 추가적으로 CORS까지 설정해 주었다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
전체 코드
Future getImage(ImageSource imageSource) async {
final XFile? pickedFile = await picker.pickImage(source: imageSource);
if (pickedFile == null) {
return;
}
setState(() {
image = XFile(pickedFile.path);
contentType = lookupMimeType(pickedFile.path)
isImageUpdated = true;
});
}
Future<void> _getPresignedUrl() async {
try {
// Generate a unique filename by appending a UUID
final String uniqueFilename =
'${const Uuid().v4()}.${_contentType.split('/').last}';
final String filename = Uri.encodeComponent(uniqueFilename);
final url = Uri.parse(
'https://qznvjmzymb.execute-api.ap-northeast-2.amazonaws.com/dev/getPresignedUrl?filename=$filename&contentType=$_contentType');
final response = await http.get(url); // AWS API에 Presigned URL 만드는 요청
// decode the top-level JSON response
final Map<String, dynamic> responseData = json.decode(response.body);
if (responseData['statusCode'] == 200) {
// decode the nested 'body' field which is also a JSON string
final Map<String, dynamic> bodyData = json.decode(responseData['body']);
final String presignedUrl = bodyData['url'];
_objectKey = filename; // 이미지 URL 얻을 때 필요한 filename
await _uploadImageToS3(presignedUrl);
print('Succeeded to get presigned URL: $presignedUrl');
} else {
print('Failed to get presigned URL: ${response.body}');
}
} catch (e) {
print('Failed to get presigned URL: $e');
}
}
Future<void> _uploadImageToS3(String presignedUrl) async {
try {
final bytes = await _image!.readAsBytes();
// S3 버킷에 이미지 업로드 요청
final uploadImageResponse = await http.put(
Uri.parse(presignedUrl),
headers: {
'Content-Type': _contentType,
},
body: bytes,
);
if (uploadImageResponse.statusCode == 200) {
// Construct the object URL based on the bucket URL and the object key
final String objectUrl = '$bucketUrl/$_objectKey';
await _createOrUpdateData(objectUrl);
} else {
print('Failed to upload a image: ${uploadImageResponse.body}');
}
} catch (e) {
print('Failed to upload a image: $e');
}
}