示例说明
ParseMultipartForm 会把整个请求体读进内存或临时文件,恶意上传能直接打爆磁盘。MultipartReader 是流式替代:逐个交付 part,io.Copy 把文件流直接对接磁盘写流,内存占用与文件大小无关。MaxBytesReader 在 HTTP 层就掐断超限请求体;临时文件加 rename 的组合保证存储上永远只有完整文件。
技术要点
- MultipartReader 逐 part 流式交付
- MaxBytesReader 在传输层截断超限
- 临时文件加 rename 原子收尾
代码解读
处理器先限定方法与请求体上限;循环里普通字段被消费丢弃——不读掉会卡住解析器。文件 part 的名字先取 Base 剥离路径再创建临时文件,io.Copy 落盘后按字节数复核上限,rename 原子定稿。任一步失败都删除临时文件防止半成品堆积。
使用建议
并发上传要限流保护磁盘带宽。类型白名单在落盘前校验 MIME。临时文件目录定期巡检兜底孤儿文件。
代码示例
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)
const (
maxUploadSize = 25 << 20 // 25 MiB
memThreshold = 1 << 20 // 1 MiB 以下暂存内存
)
func uploadHandler(uploadDir string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "post only", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize+1024)
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, "expected multipart", http.StatusBadRequest)
return
}
saved := []string{}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
http.Error(w, "read part failed", http.StatusBadRequest)
return
}
if part.FileName() == "" {
io.Copy(io.Discard, part) // 普通字段:必须消费掉
continue
}
safe := filepath.Base(part.FileName()) // 剥离路径防穿越
dst, err := os.CreateTemp(uploadDir, safe+".part-")
if err != nil {
http.Error(w, "storage error", http.StatusInternalServerError)
return
}
written, copyErr := io.Copy(dst, part) // stream to disk without buffering
closeErr := dst.Close()
if copyErr != nil || closeErr != nil {
os.Remove(dst.Name()) // 失败清理半成品
http.Error(w, "write failed", http.StatusInternalServerError)
return
}
if written > maxUploadSize {
os.Remove(dst.Name())
http.Error(w, "file too large", http.StatusRequestEntityTooLarge)
return
}
final := filepath.Join(uploadDir, safe)
if err := os.Rename(dst.Name(), final); err != nil {
os.Remove(dst.Name())
http.Error(w, "finalize failed", http.StatusInternalServerError)
return
}
saved = append(saved, fmt.Sprintf("%s(%d)", safe, written))
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "saved: %v\n", saved)
}
}
func main() {
http.HandleFunc("/upload", uploadHandler("./uploads"))
http.ListenAndServe(":8080", nil)
}