문제상황
해결방안
방식
- 한글 이름을 그대로 사용하여 presigned_url을 생성한다.
- 동일 이름의 파일이 존재하는지 먼저 체크한다.
s3_client.head_object(Bucket=bucket_name, Key=object_key)
를 사용하였다.
- head_object() 는 HTTP method의 HEAD와 비슷하다. 실제로 프린트를 찍어보면 객체를 직접 가져오지는 않지만, 내가 찾는 객체가 존재한다면 해당 객체의 메타데이터 등을 리턴해준다.
- 객체가 존재하지 않는다면 404 에러를 뱉기 때문에
try except
처리가 필수이다.
- 나의 경우, 파일이 존재하는지만 알면 되기 때문에 최적이었다.
- 동일 이름의 파일이 존재하지 않는다면 내가 받은 그대로 object_key를 만들어준다.
- 만약 겹치는 이름이 있다면,
초특가_야놀자_15s_XXT6GPX.wav
와 같이 _{random_string}
을 붙여주었다.
- django admin에서 s3에 파일을 올릴 때와 동일하게 7자리의 랜덤 스트링을 채택하였다.
url 예시
- 확인하기 쉽도록 버킷, 경로, 파일명으로 분리해 보았다.
https://sample-bucket.s3.ap-northeast-2.amazonaws.com/
_media/message/audio/
%E1%84%8E%E1%85%A9%E1%84%90%E1%85%B3%E1%86%A8%E1%84%80%E1%85%A1_%E1%84%8B%E1%85%A3%E1%84%82%E1%85%A9%E1%86%AF%E1%84%8C%E1%85%A1_15s_XXT6GPX.wav
- 프론트앤드에서는 위의 파일명
%E1%84%8E%E1%85%A9%E1%84%90%E1%85%B3%E1%86%A8%E1%84%80%E1%85%A1_%E1%84%8B%E1%85%A3%E1%84%82%E1%85%A9%E1%86%AF%E1%84%8C%E1%85%A1_15s_XXT6GPX.wav
을 가져와 디코딩해주면 된다.
코드
class PresignedUrlSerializer(serializers.Serializer):
name = serializers.CharField(write_only=True)
category = serializers.CharField(write_only=True, label="category")
url = serializers.URLField(read_only=True)
def validate(self, attrs):
attrs["url"] = self.create_presigned_url(attrs["name"], attrs["category"])
return attrs
def create(self, validated_data):
return validated_data
def check_object_existence(self, s3_client=None, bucket_name=None, object_key=None, name=None, category=None):
try:
s3_client.head_object(Bucket=bucket_name, Key=object_key)
ran_string = "".join(random.choices("".join([string.ascii_uppercase, string.digits]), k=7))
file_name = name.split(".")
new_object_key = "/".join([f"_media/{category}", f"{file_name[0]}_{ran_string}.{file_name[-1]}"])
return self.check_object_existence(
s3_client=s3_client, bucket_name=bucket_name, object_key=new_object_key, name=name, category=category
)
except ClientError as e:
return object_key
def create_presigned_url(self, name, category):
s3_config = Config(
region_name="ap-northeast-2",
signature_version="s3v4",
)
s3_client = boto3.client("s3", config=s3_config)
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
object_key = self.check_object_existence(
s3_client=s3_client,
bucket_name=bucket_name,
object_key="/".join([f"_media/{category}", f"{name}"]),
name=name,
category=category,
)
try:
url = s3_client.generate_presigned_url(
"put_object",
Params={
"Bucket": bucket_name,
"Key": object_key,
},
ExpiresIn=300,
)
except ClientError as e:
raise ValidationError({"s3": ["S3 Client Error"]})
return url
결과
- 이미 존재하는 이름인
초특가_야놀자_15s.wav
의 경우, 뒤에 랜덤 스트링을 붙여 중복을 피할 수 있다. 즉, 같은 내용의 다른 이름의 파일들이 되는 것이다.
- 클라이언트가 원했던
다운로드 시 기존 파일의 이름 유지가 가능
해졌다.
개선방향
- 사실 내가 사용하고 있던
generate_presigned_url()
로도 put_object를 할 수 있지만, 아래의 generate_presigned_post()
을 활용하는 것으로도 변경해보고 싶다.
- 기능적으로는 전혀 문제없다.
- 처음에 유저에게 보여지는 파일의 이름을 어떻게든 유지하려고 uuid의 틀을 사용하면서
Metadata
를 같이 껴서 보내려고 노력했었다.
- 아래의 generate_presigned_post() 를 사용하면 Metadata를 함께 사용할 수 있다.
def generate_presigned_post(
self,
Bucket: str,
Key: str,
Fields: Optional[Dict[str, Any]] = ...,
Conditions: Union[List[Any], Dict[str, Any], None] = ...,
ExpiresIn: int = 3600,
) -> Dict[str, Any]:
"""
Builds the url and the form fields used for a presigned s3 post.
[Show boto3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.generate_presigned_post)
[Show boto3-stubs documentation](https://youtype.github.io/boto3_stubs_docs/mypy_boto3_s3/client/#generate_presigned_post)
"""