← 中文资源

简体中文版本

Go 多部分流式上传处理器

在 Go 处理器里流式接收 multipart 上传:体积上限前置、文件流直接落盘零整块缓冲、失败清理临时文件、文件名防穿越。

阅读英文原文

示例说明

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)
}