文件上传下载 #
一、文件上传 #
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