S3에서 원본 파일명 그대로 다운로드해 클라이언트에 전달하기 (Django)

Seungwoo Kim·2022년 2월 6일
0

How I Solved

목록 보기
1/1
post-thumbnail

나는 창업동아리 팀에서 백엔드 개발자 포지션으로 Django를 이용해 API 개발을 진행하고 있었다.
개발 기능 중 S3 bucket에 업로드한 파일을 다운로드 받아 클라이언트에게 전달하는 것이 있었다.
이 과정에서 마주한 문제 상황을 소개하고 해결 과정을 정리해보고자 한다.

❗️ 문제 상황

S3에 업로드 되는 파일명의 중복 이슈

s3에 파일을 업로드 하면 해당 파일을 다운로드 할 수 있는 객체 url이 아래와 같이 생성된다.

해당 url에 접속하여 파일을 다운로드 할 시 s3에 업로드된 파일명 그대로 파일을 다운로드 할 수 있다.
위 사진에서는 files/ 뒤의 블러 처리한 부분이 파일명이다.

하지만 개발하는 서비스의 특성상 s3에 업로드되는 파일명이 중복될 수 있다는 문제점이 있었고 이를 해결하고 넘어가야 했다.

💡 해결 과정

uuid를 이용한 파일명 중복 방지

문제상황을 해결하고자 s3에 업로드 할 시 uuid를 이용하여 중복되지 않는 파일명을 생성했다.

해당 이슈는 같은 백엔드 팀원 친구가 해결하였으며 s3utils.py를 생성해 uuid로 fileId를 생성한 것을 확인할 수 있다.

import boto3
import uuid
from ownroom.settings import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME

PREFIX = 'files/'


class S3Client:
    def __init__(self, access_key, secret_key, bucket_name):
        self.s3client = boto3.client(
            's3',
            aws_access_key_id = access_key,
            aws_secret_access_key = secret_key
        )
        self.bucket_name = bucket_name

    def upload(self, file):
        try:
            fileId = str(uuid.uuid4())
            extra_args = {'ContentType': file.content_type}
            final_path = PREFIX + fileId
            self.s3client.upload_fileobj(
                file,
                self.bucket_name,
                final_path,
                ExtraArgs = extra_args
            )
            return final_path
        except:
            return None


s3client = S3Client(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME)

또한 클라이언트가 첨부한 파일의 파일명을 따로 관리해야 했기에 모델의 name필드(원본 파일명)를 다음과 같이 추가했다. 참고로 아래 사진은 우리 서비스의 ERD중 일부를 캡쳐한 것이며 AWS RDS를 통해 MySQL 데이터베이스를 구축했다.

올바른 파일명으로 파일 다운로드 후 클라이언트에 넘겨주기

내가 구현하고 싶었던 기능은 웹에서 파일 다운로드 API를 호출하면 응답으로 파일이 자동 다운로드 되게끔 하는 것 이었다. json 형태의 응답값만 클라이언트에 전달했기에 처음 개발하는 기능이었고, 구글링 끝에 이 것이 가능하다고 판단했다.

생각한 방법은 다음과 같다. S3에 uuid를 사용해 파일을 업로드 하고, 원본 파일명은 MySQL DB를 통해 따로 저장했다. 따라서 클라이언트에 S3의 파일을 넘겨줄 시에는 DB를 조회해 원본 파일명을 얻고, 파일명을 알맞게 변경해야만 할 것이다. 도식화하자면 위와 같다.

다음은 실제 작성한 views.py의 일부다.

class ConsultingApplicationDownloadView(APIView):
    permission_classes = (IsAuthenticated,)
    authentication_classes = (JSONWebTokenAuthentication,)

    def get(self, request):
        # 현재 로그인한 계정의 userId를 불러옴
        jwt_value = JSONWebTokenAuthentication().get_jwt_value(request)
        payload = JWT_DECODE_HANDLER(jwt_value)
        userId = JWT_PAYLOAD_GET_USER_ID_HANDLER(payload)

        # 유저의 isConsultant 조회
        user = get_object_or_404(User, id=userId)
        isConsultant = user.isConsultant

        # body로 넘겨받은 userId를 조회
        opponent = User.objects.get(nickname=self.request.query_params.get('nickname'))

        if isConsultant:
            # 현재 컨설턴트 일 때
            contact = get_object_or_404(Contact, consultant_id=userId, owner=opponent)
        else:
            # 현재 오너 일 때
            contact = get_object_or_404(Contact, owner_id=userId, consultant=opponent)

        # 컨설팅 신청서(isReport = False)
        file = contact.files.get(isReport=False)
        url = file.url
        filename = file.filename

        # s3에서 파일 다운로드
        client = boto3.client('s3')
        client.download_file(AWS_STORAGE_BUCKET_NAME, url, 'media/'+filename)

        # Response에 파일 첨부
        with open('media/' + filename, 'rb') as fh:
            mime_type, _ = mimetypes.guess_type('media/' + filename)
            response = HttpResponse(fh.read(), content_type=mime_type)
            quote_file_name = urllib.parse.quote(filename.encode('utf-8'))
            response['Content-Disposition'] = 'attachment;filename*=UTF-8\'\'%s' % quote_file_name
            os.remove('media/' + filename) # 서버 내의 파일 삭제
            return response

코드 중간의 file.url, file.filename 각각 DB에 저장된 uuid와 원본 파일명이다.

client = boto3.client('s3')
client.download_file(AWS_STORAGE_BUCKET_NAME, url, 'media/'+filename)

이를 바탕으로 서버 내의 media/ 에 원하는 파일명(filename)으로 파일을 다운로드 받을 수 있다.

그 후, 'media/파일명' 파일을 byte 타입으로 open 하고, mimetypes.guess_type으로 content_type을 추론한다.

HttpResponse를 이용해 파일을 첨부한 뒤 클라이언트에 보낼 response를 생성한다. 이 때 urllib.parse를 사용한 이유는 한글 파일명의 경우 인코딩이 필요하기 때문이다. 여기에서 Content-Disposition은 Response Body에 오는 내용의 성향을 알려주는데, 이를 파일명과 함께 attachment로 넘겨주는 경우 해당 내용을 다운로드 하라는 의미가 된다.

마지막으로 서버에 파일이 누적 저장되는 일을 막고자 os.remove로 서버 내에 저장된 파일을 삭제한 뒤 클라이언트에 응답을 보냈다.

0개의 댓글