파일 업로드는 보통 요청의 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.')
이 방법을 사용하여 메모리에 부담을 주지 않고 대용량 파일을 업로드 받을 수 있다.