文件上传下载 #

一、文件上传 #

1.1 单文件上传 #

go
e.POST("/upload", func(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return err
    }
    
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()
    
    dst, err := os.Create("uploads/" + file.Filename)
    if err != nil {
        return err
    }
    defer dst.Close()
    
    if _, err = io.Copy(dst, src); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "filename": file.Filename,
        "message":  "上传成功",
    })
})

1.2 多文件上传 #

go
e.POST("/uploads", func(c echo.Context) error {
    form, err := c.MultipartForm()
    if err != nil {
        return err
    }
    
    files := form.File["files"]
    
    os.MkdirAll("uploads", 0755)
    
    for _, file := range files {
        src, err := file.Open()
        if err != nil {
            return err
        }
        
        dst, err := os.Create("uploads/" + file.Filename)
        if err != nil {
            src.Close()
            return err
        }
        
        io.Copy(dst, src)
        
        src.Close()
        dst.Close()
    }
    
    return c.JSON(http.StatusOK, map[string]int{
        "count": len(files),
    })
})

1.3 文件信息 #

go
type FileInfo struct {
    Filename string `json:"filename"`
    Size     int64  `json:"size"`
    MIME     string `json:"mime"`
}

e.POST("/upload", func(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return err
    }
    
    info := FileInfo{
        Filename: file.Filename,
        Size:     file.Size,
        MIME:     file.Header.Get("Content-Type"),
    }
    
    return c.JSON(http.StatusOK, info)
})

二、文件验证 #

2.1 文件大小限制 #

go
e.Use(middleware.BodyLimit("10M"))

2.2 文件类型验证 #

go
func validateFileType(file *multipart.FileHeader, allowedTypes []string) error {
    ext := strings.ToLower(filepath.Ext(file.Filename))
    
    for _, t := range allowedTypes {
        if ext == t {
            return nil
        }
    }
    
    return fmt.Errorf("不允许的文件类型: %s", ext)
}

e.POST("/upload", func(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return err
    }
    
    allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif"}
    if err := validateFileType(file, allowedTypes); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "filename": file.Filename,
    })
})

2.3 MIME类型验证 #

go
func validateMIME(file *multipart.FileHeader, allowedMIMEs []string) error {
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()
    
    buffer := make([]byte, 512)
    _, err = src.Read(buffer)
    if err != nil {
        return err
    }
    
    mimeType := http.DetectContentType(buffer)
    
    for _, m := range allowedMIMEs {
        if mimeType == m {
            return nil
        }
    }
    
    return fmt.Errorf("不允许的MIME类型: %s", mimeType)
}

三、文件存储 #

3.1 本地存储 #

go
func saveFile(file *multipart.FileHeader, dest string) error {
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()
    
    os.MkdirAll(filepath.Dir(dest), 0755)
    
    dst, err := os.Create(dest)
    if err != nil {
        return err
    }
    defer dst.Close()
    
    _, err = io.Copy(dst, src)
    return err
}

e.POST("/upload", func(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return err
    }
    
    filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
    dest := filepath.Join("uploads", filename)
    
    if err := saveFile(file, dest); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "filename": filename,
        "path":     dest,
    })
})

3.2 按日期存储 #

go
func getUploadPath() string {
    now := time.Now()
    return filepath.Join("uploads", now.Format("2006/01/02"))
}

e.POST("/upload", func(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return err
    }
    
    uploadPath := getUploadPath()
    filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
    dest := filepath.Join(uploadPath, filename)
    
    if err := saveFile(file, dest); err != nil {
        return err
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "filename": filename,
        "path":     dest,
    })
})

3.3 唯一文件名 #

go
func generateFilename(original string) string {
    ext := filepath.Ext(original)
    return uuid.New().String() + ext
}

四、文件下载 #

4.1 基本下载 #

go
e.GET("/download/:filename", func(c echo.Context) error {
    filename := c.Param("filename")
    return c.Attachment("uploads/"+filename, filename)
})

4.2 内联显示 #

go
e.GET("/view/:filename", func(c echo.Context) error {
    filename := c.Param("filename")
    return c.Inline("uploads/"+filename, filename)
})

4.3 文件服务 #

go
e.Static("/files", "uploads")

4.4 安全下载 #

go
e.GET("/download/:filename", func(c echo.Context) error {
    filename := c.Param("filename")
    
    ext := strings.ToLower(filepath.Ext(filename))
    forbidden := []string{".php", ".asp", ".jsp", ".exe", ".sh"}
    for _, f := range forbidden {
        if ext == f {
            return echo.NewHTTPError(http.StatusForbidden, "不允许下载此类型文件")
        }
    }
    
    filepath := "uploads/" + filename
    
    if _, err := os.Stat(filepath); os.IsNotExist(err) {
        return echo.NewHTTPError(http.StatusNotFound, "文件不存在")
    }
    
    return c.Attachment(filepath, filename)
})

五、图片处理 #

5.1 图片缩放 #

go
func resizeImage(src io.Reader, width, height int) ([]byte, error) {
    img, _, err := image.Decode(src)
    if err != nil {
        return nil, err
    }
    
    resized := resize.Resize(uint(width), uint(height), img, resize.Lanczos3)
    
    var buf bytes.Buffer
    err = jpeg.Encode(&buf, resized, nil)
    
    return buf.Bytes(), err
}

5.2 生成缩略图 #

go
e.POST("/upload/thumbnail", func(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return err
    }
    
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()
    
    thumbnail, err := resizeImage(src, 200, 200)
    if err != nil {
        return err
    }
    
    filename := fmt.Sprintf("thumb_%s", file.Filename)
    os.WriteFile("uploads/"+filename, thumbnail, 0644)
    
    return c.JSON(http.StatusOK, map[string]string{
        "thumbnail": filename,
    })
})

六、完整示例 #

go
package main

import (
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
    "strings"
    "time"
    "github.com/google/uuid"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()
    
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    
    e.Use(middleware.BodyLimit("10M"))
    
    os.MkdirAll("uploads", 0755)
    
    e.Static("/files", "uploads")
    
    e.POST("/upload", uploadFile)
    e.POST("/uploads", uploadFiles)
    e.GET("/download/:filename", downloadFile)
    e.DELETE("/files/:filename", deleteFile)
    
    e.Logger.Fatal(e.Start(":8080"))
}

func uploadFile(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "请选择文件")
    }
    
    if err := validateFile(file); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    
    filename, err := saveFile(file)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "保存文件失败")
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "filename": filename,
        "url":      "/files/" + filename,
        "message":  "上传成功",
    })
}

func uploadFiles(c echo.Context) error {
    form, err := c.MultipartForm()
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "无效的表单数据")
    }
    
    files := form.File["files"]
    if len(files) == 0 {
        return echo.NewHTTPError(http.StatusBadRequest, "请选择文件")
    }
    
    var uploadedFiles []string
    
    for _, file := range files {
        if err := validateFile(file); err != nil {
            continue
        }
        
        filename, err := saveFile(file)
        if err != nil {
            continue
        }
        
        uploadedFiles = append(uploadedFiles, filename)
    }
    
    return c.JSON(http.StatusOK, map[string]interface{}{
        "count":    len(uploadedFiles),
        "files":    uploadedFiles,
        "message":  fmt.Sprintf("成功上传 %d 个文件", len(uploadedFiles)),
    })
}

func downloadFile(c echo.Context) error {
    filename := c.Param("filename")
    
    if err := validateDownload(filename); err != nil {
        return echo.NewHTTPError(http.StatusForbidden, err.Error())
    }
    
    filepath := "uploads/" + filename
    
    if _, err := os.Stat(filepath); os.IsNotExist(err) {
        return echo.NewHTTPError(http.StatusNotFound, "文件不存在")
    }
    
    return c.Attachment(filepath, filename)
}

func deleteFile(c echo.Context) error {
    filename := c.Param("filename")
    
    filepath := "uploads/" + filename
    
    if _, err := os.Stat(filepath); os.IsNotExist(err) {
        return echo.NewHTTPError(http.StatusNotFound, "文件不存在")
    }
    
    if err := os.Remove(filepath); err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "删除失败")
    }
    
    return c.NoContent(http.StatusNoContent)
}

func validateFile(file *multipart.FileHeader) error {
    if file.Size > 10*1024*1024 {
        return fmt.Errorf("文件大小不能超过10MB")
    }
    
    ext := strings.ToLower(filepath.Ext(file.Filename))
    allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx"}
    
    valid := false
    for _, t := range allowedTypes {
        if ext == t {
            valid = true
            break
        }
    }
    
    if !valid {
        return fmt.Errorf("不允许的文件类型: %s", ext)
    }
    
    return nil
}

func validateDownload(filename string) error {
    ext := strings.ToLower(filepath.Ext(filename))
    forbidden := []string{".php", ".asp", ".jsp", ".exe", ".sh", ".bat"}
    
    for _, f := range forbidden {
        if ext == f {
            return fmt.Errorf("不允许下载此类型文件")
        }
    }
    
    if strings.Contains(filename, "..") {
        return fmt.Errorf("非法文件名")
    }
    
    return nil
}

func saveFile(file *multipart.FileHeader) (string, error) {
    src, err := file.Open()
    if err != nil {
        return "", err
    }
    defer src.Close()
    
    ext := filepath.Ext(file.Filename)
    filename := uuid.New().String() + ext
    
    uploadPath := getUploadPath()
    os.MkdirAll(uploadPath, 0755)
    
    dst := filepath.Join(uploadPath, filename)
    
    dstFile, err := os.Create(dst)
    if err != nil {
        return "", err
    }
    defer dstFile.Close()
    
    if _, err = io.Copy(dstFile, src); err != nil {
        return "", err
    }
    
    return filepath.Join(uploadPath, filename), nil
}

func getUploadPath() string {
    now := time.Now()
    return filepath.Join("uploads", now.Format("2006/01/02"))
}

七、总结 #

文件上传下载要点:

要点 说明
FormFile 获取单个文件
MultipartForm 获取多个文件
BodyLimit 限制请求体大小
Attachment 文件下载
Inline 内联显示
Static 静态文件服务

准备好学习WebSocket了吗?让我们进入下一章!

最后更新:2026-03-28