diff --git a/app/controllers/objectController/upload.go b/app/controllers/objectController/upload.go index 900ec9f..03d67fa 100644 --- a/app/controllers/objectController/upload.go +++ b/app/controllers/objectController/upload.go @@ -1,8 +1,8 @@ package objectController import ( + "bytes" "errors" - "io" "mime/multipart" "4u-go/app/apiException" @@ -26,22 +26,15 @@ func UploadFile(c *gin.Context) { } uploadType := data.UploadType - fileHeader := data.File - // 获取文件流 - file, err := data.File.Open() + fileSize := data.File.Size + fileData, err := objectService.ReadFileToBytes(data.File) if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) + apiException.AbortWithException(c, apiException.UploadFileError, err) return } - defer func(file multipart.File) { - err := file.Close() - if err != nil { - zap.L().Error("文件流关闭错误", zap.Error(err)) - } - }(file) // 获取文件信息 - contentType, fileExt, err := objectService.GetFileInfo(file, fileHeader, uploadType) + contentType, fileExt, err := objectService.GetFileInfo(fileData, fileSize, uploadType) if errors.Is(err, objectService.ErrSizeExceeded) { apiException.AbortWithException(c, apiException.FileSizeExceedError, err) return @@ -58,15 +51,22 @@ func UploadFile(c *gin.Context) { apiException.AbortWithException(c, apiException.ServerError, err) return } - _, err = file.Seek(0, io.SeekStart) - if err != nil { - apiException.AbortWithException(c, apiException.ServerError, err) - return + + if uploadType == objectService.TypeImage { + d, s, err := objectService.ConvertToWebP(fileData) + if err != nil { + zap.L().Error("转换图片到 WebP 失败", zap.Error(err)) + } else { // 若转换成功则替代原文件 + fileData = d + fileSize = s + fileExt = ".webp" + contentType = "image/webp" + } } // 上传文件 objectKey := objectService.GenerateObjectKey(uploadType, fileExt) - objectUrl, err := objectService.PutObject(objectKey, file, fileHeader.Size, contentType) + objectUrl, err := objectService.PutObject(objectKey, bytes.NewReader(fileData), fileSize, contentType) if err != nil { apiException.AbortWithException(c, apiException.UploadFileError, err) return diff --git a/app/services/objectService/objectService.go b/app/services/objectService/objectService.go index d0507d5..20a0f3a 100644 --- a/app/services/objectService/objectService.go +++ b/app/services/objectService/objectService.go @@ -1,15 +1,20 @@ package objectService import ( + "bytes" "errors" "fmt" + "io" "mime/multipart" "strings" "time" + "github.com/chai2010/webp" + "github.com/disintegration/imaging" "github.com/dustin/go-humanize" "github.com/gabriel-vasile/mimetype" uuid "github.com/satori/go.uuid" + "go.uber.org/zap" ) var ( @@ -23,15 +28,23 @@ var ( ErrNotImage = errors.New("file isn't a image") ) +const ( + // TypeImage 图片 + TypeImage = "image" + + // TypeAttachment 附件 + TypeAttachment = "attachment" +) + var uploadTypeLimits = map[string]int64{ - "public/image": humanize.MByte * 10, - "public/attachment": humanize.MByte * 100, + TypeImage: humanize.MByte * 10, + TypeAttachment: humanize.MByte * 100, } // GetFileInfo 获取文件基本信息 func GetFileInfo( - file multipart.File, - fileHeader *multipart.FileHeader, + fileData []byte, + fileSize int64, uploadType string, ) ( contentType string, @@ -39,18 +52,15 @@ func GetFileInfo( err error, ) { // 检查文件大小 - if err = checkFileSize(uploadType, fileHeader.Size); err != nil { + if err = checkFileSize(uploadType, fileSize); err != nil { return "", "", err } // 通过文件头获取类型和扩展名 - mimeType, mimeExt, err := getFileTypeAndExt(file) - if err != nil { - return "", "", err - } + mimeType, mimeExt := getFileTypeAndExt(fileData) // 检查是否为图像类型 - if uploadType == "public/image" && !strings.HasPrefix(mimeType, "image") { + if uploadType == TypeImage && !strings.HasPrefix(mimeType, "image") { return "", "", ErrNotImage } @@ -75,10 +85,42 @@ func checkFileSize(uploadType string, size int64) error { } // getFileTypeAndExt 根据文件头(Magic Number)判断文件类型和扩展名 -func getFileTypeAndExt(file multipart.File) (mimeType string, mimeExt string, err error) { - mime, err := mimetype.DetectReader(file) +func getFileTypeAndExt(fileData []byte) (mimeType string, mimeExt string) { + mime := mimetype.Detect(fileData) + return mime.String(), mime.Extension() +} + +// ConvertToWebP 将图片转换为 WebP 格式 +func ConvertToWebP(fileData []byte) ([]byte, int64, error) { + img, err := imaging.Decode(bytes.NewReader(fileData)) if err != nil { - return "", "", err + return nil, 0, err + } + + var buf bytes.Buffer + err = webp.Encode(&buf, img, &webp.Options{Quality: 100}) + if err != nil { + return nil, 0, err + } + return buf.Bytes(), int64(buf.Len()), nil +} + +// ReadFileToBytes 读取文件数据 +func ReadFileToBytes(fileHeader *multipart.FileHeader) ([]byte, error) { + file, err := fileHeader.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer func(file multipart.File) { + err := file.Close() + if err != nil { + zap.L().Warn("文件关闭失败", zap.Error(err)) + } + }(file) + + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) } - return mime.String(), mime.Extension(), nil + return data, nil } diff --git a/docs/README.md b/docs/README.md index bbccada..b6b353f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,7 +33,9 @@ ### 如何参与开发 -1. 克隆该项目并切换到dev分支,然后切出自己的分支进行开发 +1. 安装`GCC`或`MinGW`并配置好环境([下载](http://tdm-gcc.tdragon.net/download)) + +2. 克隆该项目并切换到`dev`分支,然后切出自己的分支进行开发 ```shell git clone https://github.com/zjutjh/4UOnline-Go.git @@ -58,7 +60,7 @@ git reset --hard origin/dev git push --force ``` -2. 复制示例配置,并按注释要求填写配置文件(`user`配置询问部长团,并要提供个人学号) +3. 复制示例配置,并按注释要求填写配置文件(`user`配置询问部长团,并要提供个人学号) ```shell /* Linux */ @@ -68,25 +70,16 @@ cp config.example.yaml config.yaml copy config.example.yaml config.yaml ``` -3. 启动程序 +4. 启动程序 ```shell go run main.go ``` -4. 每次提交 commit 前,先运行以下命令来格式化代码并检查规范(需要安装 [gci](https://github.com/daixiang0/gci) 和 [golangci-lint](https://golangci-lint.run/)) +5. 每次提交 commit 前,先运行以下命令来格式化代码并检查规范(需要安装 [gci](https://github.com/daixiang0/gci) 和 [golangci-lint](https://golangci-lint.run/)) ``` gofmt -w . gci write . -s standard -s default golangci-lint run --config .golangci.yml ``` - -5. 打包后端到服务器运行 - -``` -SET CGO_ENABLE=0 -SET GOOS=linux -SET GOARCH=amd64 -go build -o 4u main.go -``` diff --git a/go.mod b/go.mod index 1711940..42970bb 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module 4u-go go 1.22.9 require ( + github.com/chai2010/webp v1.1.1 + github.com/disintegration/imaging v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/gabriel-vasile/mimetype v1.4.6 github.com/gin-contrib/cors v1.7.2 @@ -12,7 +14,6 @@ require ( github.com/go-resty/resty/v2 v2.16.0 github.com/json-iterator/go v1.1.12 github.com/minio/minio-go/v7 v7.0.80 - github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.0 github.com/silenceper/wechat/v2 v2.1.7 github.com/spf13/viper v1.19.0 @@ -77,6 +78,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.12.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/image v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect diff --git a/go.sum b/go.sum index 476e02c..ebbcc16 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= +github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -30,6 +32,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -159,8 +163,6 @@ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -235,6 +237,9 @@ golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= +golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=