aiohttp에서 큰 파일을 업로드하는 법

파일 업로드는 보통 요청의 body에 인코딩된 파일 데이터를 넣어서 POST 요청으로 서버에 전달되는데, aiohttp에서는 다음과 같이 post를 처리하는 핸들러를 사용해서 이를 처리할 수 있다.

async def store_mp3_handler(request):
  data = await request.post()
  mp3 = data['mp3']
  filename = mp3.filename
  mp3_file = mp3.file
  content = mp3_file.read()
  return web.Response(body=content, headers=
     MultiDict({ 'CONTENT-DISPOSITION': mp3_file}))

여기서 문제는 request.post() 메소드가 요청 데이터를 한꺼번에 메모리로 읽어들이기 때문에 메모리 부족으로 서버가 죽을 수 있는 상황이 있다는 것이다. 따라서 aiohttp에서 일반적으로 처리할 수 있는 요청의 크기는 2MB로 제한된다. 하지만 이 크기는 어지간한 사진 하나의 용량도 감당하기 어렵기 때문에 뭔가 다른 방법이 필요하다. (보통은 일종의 옵션 값 같은 걸로 최대 처리 요청 크기를 변경할 수 있을 줄 알았는데, 없었다.)

request.multipart는 이러한 문제를 피하는 멀티파트 리더로 기능할 수 있다. 리더 객체를 생성한 다음에는 멀티파트 요청을 단위 콘텐츠 별로 읽어들일 수 있다. requrest.multipart() 메소드는 멀티파트 요청을 각 파트별로 리턴하는 비동기 제너레이터인데, 각각의 파트(필드)는 read() 메소드를 통해서 통째로 읽어들이거나, read_chunk() 메소드를 통해서 필드의 일부분을 순차적으로 버퍼에 읽어들일 수 있다.

BodyPartReader.read_chunk(size=chunk_size::int)


https://docs.aiohttp.org/en/stable/multipart_reference.html#aiohttp.BodyPartReader.read_chunk

다음 코드는 mp3 파일을 폼 필드에서 mp3라는 필드 이름으로 업로드했을 때, 이를 받아서 저장하는 서버쪽 코드이다. 바이너리 파일을 버퍼로 읽어들여서 순차적으로 저장하는 것과 동일한 방식으로 처리한다. 기존 코드와 차이점이 있다면 awiat request.read()는 HTTP요청 자체를 하나의 사전과 비슷한 객체로 읽어들이는 반면, 개별 필드를 각각 요청하고 처리해야 한다.

async def store_mp3_handler(request):
  reader = await request.multipart()

  field = await reader.next()
  assert field.name == 'name'
  name = await field.read(decode=True)
  
  field = await reader.next()
  assert field.name == 'mp3'
  filename = filed.filename

  size = 0
  with open(os.path.join('/spool/yarrr-media/mp3/', filename), 'wb') as f:
    while True:
      chunk = await field.read_chunk()
      if not chunck:
        break
      size += len(chunk)
      f.write(chunk)
  return web.Response(text=f'{filename} sized of {size} successfully stored.')

이 방법을 사용하여 메모리에 부담을 주지 않고 대용량 파일을 업로드 받을 수 있다.