文件上传下载

文件上传下载是 Web 应用常见的功能,Gin 对此有很好的支持。

单文件上传

最简单的单文件上传:

package main

import (
    "net/http"
    "path/filepath"
    
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    
    r.MaxMultipartMemory = 8 << 20
    
    r.POST("/upload", func(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        
        filename := filepath.Base(file.Filename)
        dst := filepath.Join("./uploads", filename)
        
        if err := c.SaveUploadedFile(file, dst); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(http.StatusOK, gin.H{
            "message":  "上传成功",
            "filename": filename,
            "size":     file.Size,
        })
    })
    
    r.Run(":8080")
}

前端表单:

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <button type="submit">上传</button>
</form>

多文件上传

上传多个文件:

r.POST("/uploads", func(c *gin.Context) {
    form, err := c.MultipartForm()
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    files := form.File["files"]
    
    var uploaded []string
    for _, file := range files {
        filename := filepath.Base(file.Filename)
        dst := filepath.Join("./uploads", filename)
        
        if err := c.SaveUploadedFile(file, dst); err != nil {
            continue
        }
        uploaded = append(uploaded, filename)
    }
    
    c.JSON(http.StatusOK, gin.H{
        "message":  "上传完成",
        "uploaded": uploaded,
        "count":    len(uploaded),
    })
})

文件类型验证

上传前验证文件类型:

func allowedFileType(filename string) bool {
    ext := strings.ToLower(filepath.Ext(filename))
    allowed := map[string]bool{
        ".jpg":  true,
        ".jpeg": true,
        ".png":  true,
        ".gif":  true,
        ".pdf":  true,
    }
    return allowed[ext]
}

r.POST("/upload/image", func(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"})
        return
    }
    
    if !allowedFileType(file.Filename) {
        c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"})
        return
    }
    
    if file.Size > 5*1024*1024 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小不能超过5MB"})
        return
    }
    
    ext := filepath.Ext(file.Filename)
    newFilename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
    dst := filepath.Join("./uploads", newFilename)
    
    c.SaveUploadedFile(file, dst)
    
    c.JSON(http.StatusOK, gin.H{"url": "/uploads/" + newFilename})
})

文件下载

简单的文件下载:

r.GET("/download/:filename", func(c *gin.Context) {
    filename := c.Param("filename")
    filepath := filepath.Join("./uploads", filename)
    
    c.Header("Content-Description", "File Transfer")
    c.Header("Content-Transfer-Encoding", "binary")
    c.Header("Content-Disposition", "attachment; filename="+filename)
    c.Header("Content-Type", "application/octet-stream")
    
    c.File(filepath)
})

流式下载大文件

大文件建议流式传输:

r.GET("/download/large/:filename", func(c *gin.Context) {
    filename := c.Param("filename")
    filepath := filepath.Join("./uploads", filename)
    
    file, err := os.Open(filepath)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
        return
    }
    defer file.Close()
    
    info, _ := file.Stat()
    
    c.Header("Content-Disposition", "attachment; filename="+filename)
    c.Header("Content-Type", "application/octet-stream")
    c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
    
    c.DataFromReader(http.StatusOK, info.Size(), "application/octet-stream", file, nil)
})

断点续传下载

支持断点续传:

r.GET("/download/resumable/:filename", func(c *gin.Context) {
    filename := c.Param("filename")
    filepath := filepath.Join("./uploads", filename)
    
    file, err := os.Open(filepath)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
        return
    }
    defer file.Close()
    
    info, _ := file.Stat()
    
    c.Header("Accept-Ranges", "bytes")
    c.Header("Content-Disposition", "attachment; filename="+filename)
    
    rangeHeader := c.GetHeader("Range")
    if rangeHeader == "" {
        c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
        c.DataFromReader(http.StatusOK, info.Size(), "application/octet-stream", file, nil)
        return
    }
    
    var start, end int64
    fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
    
    if end == 0 || end >= info.Size() {
        end = info.Size() - 1
    }
    
    file.Seek(start, 0)
    
    c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, info.Size()))
    c.Header("Content-Length", fmt.Sprintf("%d", end-start+1))
    
    c.DataFromReader(http.StatusPartialContent, end-start+1, "application/octet-stream", file, nil)
})

图片上传并生成缩略图

实际项目中经常需要处理图片:

import (
    "image"
    "image/jpeg"
    "image/png"
    
    "github.com/disintegration/imaging"
)

r.POST("/upload/avatar", func(c *gin.Context) {
    file, err := c.FormFile("avatar")
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "请选择头像"})
        return
    }
    
    src, err := file.Open()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    defer src.Close()
    
    img, _, err := image.Decode(src)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效的图片文件"})
        return
    }
    
    thumbnail := imaging.Resize(img, 200, 200, imaging.Lanczos)
    
    filename := fmt.Sprintf("%d.jpg", time.Now().UnixNano())
    dst := filepath.Join("./uploads/avatars", filename)
    
    out, _ := os.Create(dst)
    defer out.Close()
    jpeg.Encode(out, thumbnail, &jpeg.Options{Quality: 85})
    
    c.JSON(http.StatusOK, gin.H{"url": "/uploads/avatars/" + filename})
})

小结

Gin 的文件上传下载功能足够满足大部分需求。上传时注意设置 MaxMultipartMemory 限制内存使用,下载大文件时使用流式传输。别忘了验证文件类型和大小,防止恶意上传。