kookmin-sw / capstone-2023-08

capstone-2023-08 created by GitHub Classroom
1 stars 4 forks source link

[FE] 카메라로 촬영한 사진 S3에 업로드 #55

Closed lucyya99 closed 1 year ago

lucyya99 commented 1 year ago

목표 flow

  1. Camera_Screen에서는 카메라로 이미지를 촬영합니다. path_provider를 이용해 임시 디렉토리(local)를 생성하고, 그 임시 디렉토리에 이미지를 저장합니다.
  2. Camera_Screen에서 이미지를 불러와 Camera_result에서 결과 화면(이미지)을 보여주고, 저장 버튼을 누르면 S3에 업로드 됩니다.

그래서 해당 스크린에서 S3에 업로드하는 코드를 추가하는 중입니다.

추가한 코드

Future<void> _uploadImageToS3() async {
Uri url = Uri.parse(_url);
    http.Response response = await http.post(
      url, body: 
   );
}

문제점

1. 이미지 파일이 아니라 이미지 경로가 들어가서 오류가 발생합니다.

Unhandled Exception: Invalid argument(s): Invalid request body "File: '/data/user/0/com.example.client/cache/2023-03-24 05:15:39.313366.png'"

가정 1) File()로 감싸면 이미지가 아니라 경로가 불러와진다

flutter 공식문서에는 File(image path)을 하면 이미지 객체가 결과로 들어간다고 설명되어있습니다.

예시로, Image.file(File(image_path))로 이미지를 화면에 나타내면 결과 화면은 잘 나옵니다.

가정 2) 이미지 경로가 제대로 안 지정되어 있다

가정 3) Camera Plugin 자체의 문제라 다른 플러그인을 사용해야 한다

위의 가정들을 바탕으로 해결 방법을 모색 중입니다.

2. presigned URL을 생성한 것이 맞는지 확인 필요

현영님이 올려주신 presigned URL을 생성하여 업로드하는 방법의 참고자료를 보았는데 엑세스키가 없어 업로드가 안됩니다. 그래서 python에서 boto3로 생성하는 코드에 access_key, secret_access_key를 넣었는데 이게 presigned URL을 생성하는 것이 맞는지 모르겠습니다. 검색해보니 presigned URL은 임시 사용자가 해당 버킷을 이용할 수 있도록 임시 URL을 생성해주는 것인데, 엑세스키를 이용해 접근하는게 맞는지 확인해주시면 좋을 것 같습니다..!

wynter122 commented 1 year ago

관련해서 Presigned URL 생성하는 API 만들도록 하겠습니다.

lucyya99 commented 1 year ago

Camera plugin -> image_pick plugin로 변경

  1. 참고자료의 풍부함
  2. 타이머가 기본기능에 구현되어 있음
  3. 카메라 촬영 이후, [체크/취소버튼]으로 이 사진을 쓸건지 확인
  4. Camera plugin을 사용하기 위해 main 함수에서 가능한 카메라 목록을 받아오는데, 어플에서 카메라를 사용하지 않는 동안에도 돌아가서 buffer 관련 메세지가 계속 떠서 오류 메세지 캐치가 어려움

특히 4번 이유 때문에 플러그인을 바꿨습니다.

image_picker에 대한 이해

pickImage()의 결과인 XFile에 대한 이해가 필요합니다.

XFile

A CrossFile is a cross-platform, simplified File abstraction. It wraps the bytes of a selected file, and its (platform-dependant) path. [bytes 데이터, path]를 묶은 클래스

XFile Properties

실제로 확인하기

hashCode: 475567355    // 이미지 고유 번호
name: 1679982458454.jpg    // 저장된 이미지 파일명
mimeType: null    // web상에 전송하기 위해 필요한 형식 (예- img/png, img/jpeg 등)
path: /data/user/0/com.example.ya/cache/4302e32d-4e12-4568-a13b-a0a8ec83a8d8/1679982458454.jpg    // 이미지 임시 경로 (플랫폼에 따라 다르게 지정됨)
runtimeType: XFile

이 중에서 필요한 것 = mimeType, http 전송시 mimeType 지정해서 전송하기 때문.

XFile Methods

  1. [readAsBytes](https://pub.dev/documentation/cross_file/latest/cross_file/XFile/readAsBytes.html)() → [**Future](https://api.dart.dev/stable/2.19.4/dart-async/Future-class.html)<[Uint8List](https://api.dart.dev/stable/2.19.4/dart-typed_data/Uint8List-class.html)>**

    MultipartFile.fromBytes() 와 같이 사용

  2. [readAsString](https://pub.dev/documentation/cross_file/latest/cross_file/XFile/readAsString.html)({[Encoding](https://api.dart.dev/stable/2.19.4/dart-convert/Encoding-class.html) encoding = utf8}) → [Future](https://api.dart.dev/stable/2.19.4/dart-async/Future-class.html)<[String](https://api.dart.dev/stable/2.19.4/dart-core/String-class.html)>

    Asynchronously read the entire file contents as a string using the given [Encoding](https://api.dart.dev/stable/2.19.4/dart-convert/Encoding-class.html).

발견한 문제점

파일 경로를 사용하면 자꾸 오류가 나서,

이미지를 받아올 때 이미지 데이터를 bytes list로 바꾼 후 변수로 저장, 그 변수를 setState할 때 image와 같이 업데이트했더니 성공했습니다!

http 403 오류

The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256

제가 생성한 버킷이 Signature version 4로 설정이 되어있는데, presigned url을 생성할 때 버전 2로 설정되어 있어서 발생하는 오류입니다. 나라마다 버전2도 가능한 나라가 있는데, 제 설정은 'ap-northeast-2'이기 때문에 이걸로 진행했습니다.

presigned url 만들 때 다음과 같은 코드 추가

url = boto3.client('s3', aws_access_key_id=AWS_ACCESS_KEY_ID,
                      aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
                   **config=botocore.client.Config(
                       region_name='ap-northeast-2', # 이 부분은 bucket 설정에 따라 다름
                       signature_version='s3v4'
                   )**).generate_presigned_url(
    ClientMethod='put_object',
    Params={'Bucket': 'flutter-img-test', 'Key': 'image.png'},
    ExpiresIn=36000
)

The request signature we calculated does not match the signature you provided.

s3에서 엑세스키를 발급받을 때 가끔 /나 %같은 특수문제가 들어가서 오류가 나는 경우가 있다고 합니다. 확인해보니 저는 안들어가 있었는데, 혹시 몰라 새로운 엑세스키를 발급받아 사용했습니다.

‘Key’:’{파일명}.{확장자명}’으로 지정

S3 버킷에 예시로 올린 파일을 확인해보면 개요에 해당 내용이 있는 것을 알 수 있습니다.

화면 캡처 2023-03-29 134049-1

S3 동작방식을 잘 몰라서 헤맸는데, 빨간색 네모 박스로된 부분이 ‘키’로 들어간 것을 보아, 키 부분에 {파일이름}.{확장자}로 저장하면 png 파일로 저장되는 형태 같습니다. 처음에는 ‘image’로 지정해서 올렸는데 확장자가 지정되지 않은 상태로 올라가서, ‘image.png’로 올리니 잘 올라갔습니다. 또한 버전 관리 체크를 안한 상태로 파일명이 같으면 바로 위에 덮어씌워지니 버전 관리 체크를 하거나 이름을 항상 다르게 쓰는 로직을 돌려서 사용하면 좋을 것 같습니다.

파일명도 플러터 내부에서 사용되니 Presigned URL 만들때 Output으로 함께 주셔야될 것 같습니다!

Presigend URL과 파일명을 받는다 → 플러터 내부에서 사용한다

Response 결과

response.statusCode == 200, 성공입니다!

파일 깨짐 문제

화면 캡처 2023-03-30 003234

시험삼아 올린 png 파일의 메타데이터는 Content-Type이 image/png로 올려져 있습니다.

메타데이터 형식에 따라 Content-Type을 image/jpeg로 변경해 저장하면 객체 URL 선택하면 다운로드가 아니라 이미지 결과가 띄워집니다!

[Uploaded file to S3 via PreSigned URL from Flutter App. but the file is corrupted when i download it](https://stackoverflow.com/questions/56577788/uploaded-file-to-s3-via-presigned-url-from-flutter-app-but-the-file-is-corrupte)

await dio.put(
    url,
    data: image.openRead(),
    options: Options(
      contentType: "image/jpeg",
      headers: {
        "Content-Length": image.lengthSync(),
      },
    ),
);

해당 링크 나온대로 하니 제대로 올려집니다! 계속 삽질했던 부분이 image.openRead()로 안하고 byte list로 보내서 그런 것 같아요 😥

마지막으로 UI 수정

화면 캡처 2023-03-31 143411

pickImage(ImageSource.Camera) ⇒ 각자 플랫폼에 맞는 기본 카메라가 나옵니다. 플러그인 내에서 사진 촬영 후 가운데에 체크버튼, 오른쪽에 취소 버튼이 알아서 나오도록 되어 있어서 다시찍기와 같은 UI는 삭제했습니다.

아마 camera_screen.dart에서 S3에 업로드, 회원가입 API로 요청 보내기를 둘다 해야할 것 같아요. 그래서 중간에 로딩이 되는 시간이 있을 것 같아서 로딩 후 회원가입 성공, 그 다음 메인메이지 or 마이페이지로 이동되도록 수정했습니다.

현재 2, 3, 4번까지 완료한 상태이고, 앞에 Dialog창 넣는거는 result screen 작업하면서 같이하면 될 것 같아요

서버에서 해주셔야할 일 정리

1. Presigned URL 생성 시 다음과 같은 코드 추가

image_name = '' # 파일명 생성
image_path = f'{image_name}.jpg' # 확장자명 포함

url = boto3.client('s3', aws_access_key_id=AWS_ACCESS_KEY_ID,
                      aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
                   **config=botocore.client.Config(
                       region_name='ap-northeast-2', # 이 부분은 bucket 설정(지역)에 따라 다름
                       signature_version='s3v4'
                   )**).generate_presigned_url(
    ClientMethod='put_object',
    Params={'Bucket': 'flutter-img-test', 'Key': image_path},
    ExpiresIn=36000
)

# image_path 반환

2. 파일 이름 생성 관리

버전 관리 체크를 안한 상태로 파일명이 같으면 바로 위에 덮어씌워지니 버전 관리 체크를 하거나 이름을 항상 다르게 쓰는 로직을 돌려서 사용하면 좋을 것 같습니다.

3. 추가 궁금증

사람 사진도 정해진 크기로 보내면 모델 성능이 잘 나올 것 같은데, flutter에서 보내기 전에 크기 조절이 가능하니 원래 사진 크기 알려주시면 반영해보겠습니다!

끝으로 너무 오래걸렸는데 정말 죄송합니다..ㅠㅠ

wynter122 commented 1 year ago
  1. 파일 이름 생성 관리 버전 관리 체크를 안한 상태로 파일명이 같으면 바로 위에 덮어씌워지니 버전 관리 체크를 하거나 이름을 항상 다르게 쓰는 로직을 돌려서 사용하면 좋을 것 같습니다.

저희 서비스에서 user 별 신체이미지는 1개만 존재하도록 유지할 예정이니 버전관리나 파일명 관리는 신경쓰지 않아도 될것같아요. s3 의 user-human-img 버킷 아래에 {user_id}.jpg 로 저장하면 되겠습니다.

wynter122 commented 1 year ago

데모 서버에 추가된 api 로 url 받아오기-> s3 에 정상적으로 사진 업로드하기 테스트가 완료되면 다시 이슈 close 합니다. @lucyya99 결과 본 이슈에 코멘트로 공유해주세요

lucyya99 commented 1 year ago

진행상황

1. get presigned url + upload to S3

http통신은 단방향이라 클라이언트에서 request를 계속 보내면 서버에서 계속 받아서 결과를 받아오는 형태입니다. 그래서 클라이언트 측에서 계속해서 request가 가능합니다. flutter에서 그렇게 사용해본 결과 presigned url을 받아오는 것은 성공했으나, S3에 업로드하는 과정이 안되는 문제가 발생합니다.

원래 사용중인 플러그인은 Dio로, http와 기능은 완전히 같고 url을 파싱해주는 기능만 대신하는 플러그인입니다. 이런저런 문제로 통신 관련된 부분은 dio에서 http 플러그인으로 변경했습니다.

http 플러그인 코드 내부로 들어가보면 다음과 같은 주석이 있습니다.

If you're planning on making multiple requests to the same server, 
you should use a single [Client] for all of those requests.

원하는 방식으로 하려면, 해당 설명처럼 http로 요청 보내고 바로 닫는 형태가 아니라, client socket을 계속 열어둬서 회원가입 요청까지 보낸 후에 닫는 형식으로 바꿔야할 것 같습니다. 이 부분은 문서도 없고 혼자 해결해보고 안되면 2번 방식으로 진행하겠습니다.

2. 현영님이 주신 api gateway 사용

현영님과 상의해보고, 현영님이 api gateway를 통해 presigned url을 생성하지 않고 바로 s3에 업로드할 수 있도록 url을 보내주셨는데 성공했습니다.

진행방향

오늘까지 1번이 해결이 안되면 2번 방법을 사용할 예정입니다!

wynter122 commented 1 year ago

@lucyya99 혹시 이 부분 이번 프론트 PR에 반영이 되었을까요? 되었다면 이슈 닫아도 될 것 같습니다.

lucyya99 commented 1 year ago

presigned url 방식으로 재시도했는데 성공해서 이 방향으로 진행합니다 현영님 정말 고생하셨습니다..! 😭

회원가입~로그인 남은 To-do

이 정도 남은 것 같고, 남은 거는 많이 어렵지 않아서 먼저 PR 올린 후에 나머지 처리하고 다시 올리겠습니다

wynter122 commented 1 year ago

혹시 성공하게된 이유나 전에 실패한 원인이 정확히 파악될까요? 나중에 시연때 안되면 야단나서... 원인을 잘 알고있어야 대처가 가능할 것 같아서요!

wynter122 commented 1 year ago

80 에서도 언급했는데,

두개 모두 같은 api 를 사용하기 때문에, 버킷명이랑 파일명을 클라에서 request 보낼 때 담아서 같이 보내도록 해주시면 되겠습니다!

lucyya99 commented 1 year ago

다른 문제는 아니고 제 코드에 이상이 있었던 것 같아요.. 원인을 다른곳에서 찾아서 나중에 공부하고 코드 변경하니 문제였던것같구요 일단 해결되었고, 버킷명에 담는걸로 변경되었으니 이슈 닫겠습니다..!