[Django] 음원 및 영상 파일 s3 업로드

nikevapormax·2023년 12월 19일
0

TIL

목록 보기
114/116
post-custom-banner

음원 및 영상 파일 업로드

  • 현재 프로젝트에서 음원 및 영상 파일 s3 업로드 기능 구현을 하게 되었다.
  • 첫 번째 방법은 로컬에서만 성공하였다. 해당 방법으로 s3에 파일을 압축해 올리고 싶었으나 경로를 찾지 못하는 src/tmp/초특가 야놀자 15s.wav: No such file or directory 에러를 얻게 되었다.
  • 두 번째 방법은 로컬과 개발 서버 모두 성공하였다. 하지만 해당 방법은 파일을 압축하지 않고 곧바로 s3에 업로드하는 방식이다.
    • 서버에서 받는 파일의 크기를 애초에 제한한다면 나쁘지 않은 방법이라 생각한다.
    • 큰 사이즈의 파일에 대한 테스트가 필요하다.
  • 두 방법의 공통점은 파일 경로를 활용한다는 점이다. 파일을 우선 로컬에 저장하고, 해당 경로를 전달해 그곳에 있는 파일을 가져다 압축 후 업로드 혹은 바로 업로드하는 방식이다.

s3 함수

  • 두 가지 방법 모두에서 공통적으로 쓰였으며, s3에 파일을 업로드해준다.
def upload_to_s3(local_file_path, s3_bucket, s3_key):

    try:
        s3 = boto3.client("s3")

        # 로컬 파일을 S3에 업로드
        s3.upload_file(local_file_path, s3_bucket, s3_key)

        return True

    except NoCredentialsError:
        print("Credentials not available")
        return False

ffmpeg subprocess 를 사용한 압축

celery 함수

  • ffmpeg를 사용해 파일을 압축하는 방식을 채택하였다. 음원과 영상 모두 이름만 다르고 기능은 똑같다.
  • 해당 방법은 ffmpeg 의 subprocess 를 사용해 음원 및 영상 파일을 압축한다.
  • 해당 방법이 개발 서버에서 실패했던 이유는 경로를 찾지 못해서 이다.
  • 이를 해결하기 위해 Dockerfile에 tmp/ 경로를 만들어봤으나 여전히 경로를 찾지 못하여 사용하지 못하게 되었다. (서버는 ec2)
  • 음원의 경우 테스트한 파일들의 크기가 크지 않아 눈에 띄게 압축되지는 않았지만, 영상의 경우 크기가 확연히 줄어든 것을 확인할 수 있었다.
import logging
import os
import subprocess

from celery import shared_task
from django.conf import settings
from django.core.files.temp import NamedTemporaryFile

from app.contact.models import ContactAudio
from app.filmography.models import FilmographyAudio, FilmographyVideo, TaskPoller
from app.filmography.v1.utils import upload_to_s3
from app.message.models import Message

logger = logging.getLogger(__name__)


@shared_task
def audio(
    task_poller_id=None,
    filmography_audio_id=None,
    message_id=None,
    contact_audio_id=None,
    files=None,
    s3_object_key=None,
):
    """
    files = request.FILES["audio_file"]
    - "-y"  -> 파일 덮어쓰기 설정 (해당 설정이 없을 때 같은 음원이 여러번 들어오면 덮어쓸 것인지 물어보며 대답을 해야 압축이 진행됨)
    - "-i"  -> 입력파일 지정 (해당 플래그 뒤에 입력 파일의 경로가 와야 함)
    - "-b:a", "64k"  -> 오디오 비트레이트 설정
    - "-ar", "22050"  -> 오디오 샘플레이트 설정
    """
    task_poller = TaskPoller.objects.get(id=task_poller_id)

    try:
        logger.info("오디오 압축 시작")
        logger.info(
            f"filmography_audio_id : {filmography_audio_id}, message_id : {message_id}, contact_audio_id: {contact_audio_id}"
        )

        with NamedTemporaryFile(suffix=".wav", delete=True) as temp_file:
            output_file = temp_file.name
            command = [
                "ffmpeg",
                "-y",
                "-i",
                files,
                "-b:a",
                "64k",
                "-ar",
                "22050",
                output_file,
            ]
            subprocess.run(command, check=True)

            if upload_to_s3(output_file, settings.AWS_STORAGE_BUCKET_NAME, f"_media/{s3_object_key}"):
                task_poller.is_completed = True
                if filmography_audio_id:
                    task_poller.audio_id = filmography_audio_id
                elif message_id:
                    task_poller.message_id = message_id
                elif contact_audio_id:
                    task_poller.contact_audio_id = contact_audio_id
                task_poller.save()

                logger.info(f"task_poller 완료 여부 : {task_poller.is_completed}")

                # 로컬에서 파일 삭제
                os.remove(files)

                return s3_object_key
            else:
                logger.info("File upload to S3 failed")

    except subprocess.CalledProcessError as e:
        logger.info(f"오디오 압축 실패 <- {e}")

        task_poller.fail_reason = e
        task_poller.save()

        FilmographyAudio.objects.filter(id=filmography_audio_id).delete()
        Message.objects.filter(id=message_id).delete()
        ContactAudio.objects.filter(id=contact_audio_id).delete()

        raise Exception(e)

    finally:
        temp_file.close()

serializer

  • delay()를 사용해 celery에게 음원 및 영상 파일 압축을 맡겼다.
  • 사용자에게 음원 또는 영상 파일이 올라가고 있다는 것을 알려주기 위해 일단 task_poller_id를 넘겼고, 해당 task_poller_id를 통해 task_poller를 retrieve 하도록 하였다.
    • is_completed=True 인 경우, 파일 업로드가 완료된 상태이다.
  • FileSystemStorage()를 활용해 파일을 로컬에 저장한다. 이곳에 저장된 파일을 celery에서 끌어다 압축을 하게 된다.
    def create(self, validated_data):
        with transaction.atomic():
            instrument_set = validated_data.pop("instrument_id_list", None)

            task_poller = TaskPoller.objects.create(type=TaskPollerChoices.AUDIO)
            audio_file = validated_data.pop("audio")
            uploaded_file, exp = audio_file._name.split(".")
            s3_object_key = f"filmography/audio/{uploaded_file}.flac"

            storage = FileSystemStorage()
            storage.save(uploaded_file, File(audio_file))

            validated_data["audio"] = s3_object_key

            # 필모그래피 오디오 생성
            instance = FilmographyAudio.objects.create(**validated_data)

            # 오디오 비동기 업로드
            audio.delay(
                task_poller_id=task_poller.id,
                filmography_audio_id=instance.id,
                message_id=None,
                files=storage.path(uploaded_file),
                s3_object_key=s3_object_key,
            )

            # 필모그래피 오디오 태그 생성
            if instrument_set:
                instance.tag_set.set(instrument_set)

        return task_poller

s3에 바로 업로드

    def update(self, instance, validated_data):
        with transaction.atomic():
            audio_file = validated_data.pop("audio", None)

            if audio_file:
                s3_object_key = f"contact/audio/{audio_file._name}"
                tmp_dir = f"/tmp/{audio_file._name}"

                with open(tmp_dir, "wb") as tmp_audio_file:
                    tmp_audio_file.write(audio_file.read())

                # upload_to_s3() 함수는 성공 시 True를 반환한다.
                if upload_to_s3(tmp_dir, settings.AWS_STORAGE_BUCKET_NAME, f"_media/{s3_object_key}"):
                    contact_audio = ContactAudio.objects.create(contact=self.instance, audio=s3_object_key)

                    # 로컬에 있는 파일 삭제
                    os.remove(tmp_dir)

                else:
                    raise ValidationError("File upload to S3 failed")

                return contact_audio
profile
https://github.com/nikevapormax
post-custom-banner

0개의 댓글