본문 바로가기
Study/golang

gin 서버 multipart form data 업로드시 유의사항

by Melpin 2024. 1. 28.

go 1.21 버전, gin 을 이용해 파일 업로드 관련 핸들을 작성해 테스트를 하던 도중,  팟에서 용량이 없어서 죽는 현상을 발견했다.

원인을 쭉 찾아보니 gin context parameter bind 과정 중, multipartForm data 요청이 왔다면, 파일에 대해서 아래의 코드를 수행하게 되는데, 임시파일로 먼저 저장하고, 파일핸들을 io.ReadCloseSeeker 로 반환해준다

if n > maxFileMemoryBytes {
			if file == nil {
				file, err = os.CreateTemp(r.tempDir, "multipart-")
				if err != nil {
					return nil, err
				}
			}
			numDiskFiles++
			if _, err := file.Write(b.Bytes()); err != nil {
				return nil, err
			}
			if copyBuf == nil {
				copyBuf = make([]byte, 32*1024) // same buffer size as io.Copy uses
			}
			// os.File.ReadFrom will allocate its own copy buffer if we let io.Copy use it.
			type writerOnly struct{ io.Writer }
			remainingSize, err := io.CopyBuffer(writerOnly{file}, p, copyBuf)
			if err != nil {
				return nil, err
			}
			fh.tmpfile = file.Name()
			fh.Size = int64(b.Len()) + remainingSize
			fh.tmpoff = fileOff
			fileOff += fh.Size
			if !combineFiles {
				if err := file.Close(); err != nil {
					return nil, err
				}
				file = nil
			}
		} else {
			fh.content = b.Bytes()
			fh.Size = int64(len(fh.content))
			maxFileMemoryBytes -= n
			maxMemoryBytes -= n
		}
		form.File[name] = append(form.File[name], fh)
	}
    // src/mime/multipart/formdata.go

당연한 생각으로 사용한 임시파일에 대해서 자동으로 삭제를 할 것 같은데, 디버깅해봐도 아래 이슈에서 해결했다고 하지만, 리퀘스트가 끝났을때 자동으로 파일을 삭제하는 부분이 호출되지 않았다.

 

https://github.com/golang/go/issues/20253

 

mime/multipart: TempFile file hangs around on disk after usage in multipart/formdata.go · Issue #20253 · golang/go

Hey Go team, I'd like to know if this is a bug or expected behavior. If it is a bug, I would like to write a patch to fix it. Please answer these questions before submitting your issue. Thanks! Wha...

github.com

 

그래서 해결책으로는 직접 임시파일 삭제부분을 defer RemoveAll 로 잊지않고 항상 삭제해주는 방법이 있거나

form := c.MultipartForm()
defer form.RemoveAll()
files := form.File["file"]
for _, file := range files{
	defer file.Close()
    ....
	file.Read()
    ....
}

두번째 방법으로는 업로드시 임시파일 용량을 서버에서 차지하고 있을 이유는 없다면, 위 원래 구현된 multiparForm 파싱코드를 참고해 파일을 저장하지않도록 작성해 처리해야할 것 같다

_, param, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
...
multipartReader := multipart.NewReader(ctx.Request.Body, param["boundary"])
...
for{
    part, err := multipartReader.NextPart()
    ...
    if part.FileName() != ""{
    	return part // part 는 io.ReadCloser로 처리가능하고, 업로드한 파일의 내용
    }
}

 

 

댓글