f
This commit is contained in:
66
app/main/api/internal/logic/upload/serveuploadedfilelogic.go
Normal file
66
app/main/api/internal/logic/upload/serveuploadedfilelogic.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"tyc-server/app/main/api/internal/svc"
|
||||
"tyc-server/app/main/api/internal/types"
|
||||
"tyc-server/common/xerr"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type ServeUploadedFileLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewServeUploadedFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServeUploadedFileLogic {
|
||||
return &ServeUploadedFileLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ServeUploadedFileLogic) ServeUploadedFile(req *types.ServeUploadedFileReq) (resp *types.ServeUploadedFileResp, err error) {
|
||||
fileName := strings.TrimSpace(req.FileName)
|
||||
if fileName == "" {
|
||||
return nil, errors.Wrap(xerr.NewErrMsg("缺少文件名"), "fileName empty")
|
||||
}
|
||||
// 只允许文件名,禁止路径穿越
|
||||
if strings.Contains(fileName, "..") || filepath.Base(fileName) != fileName {
|
||||
return nil, errors.Wrap(xerr.NewErrMsg("非法文件名"), fileName)
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
"data/uploads",
|
||||
"../data/uploads",
|
||||
"../../data/uploads",
|
||||
"../../../data/uploads",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
abs, _ := filepath.Abs(c)
|
||||
fullPath := filepath.Join(abs, fileName)
|
||||
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
|
||||
contentType := "image/jpeg"
|
||||
if strings.HasSuffix(strings.ToLower(fileName), ".png") {
|
||||
contentType = "image/png"
|
||||
} else if strings.HasSuffix(strings.ToLower(fileName), ".gif") {
|
||||
contentType = "image/gif"
|
||||
} else if strings.HasSuffix(strings.ToLower(fileName), ".webp") {
|
||||
contentType = "image/webp"
|
||||
}
|
||||
return &types.ServeUploadedFileResp{
|
||||
FilePath: fullPath,
|
||||
ContentType: contentType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("文件不存在"), "fileName=%s", fileName)
|
||||
}
|
||||
138
app/main/api/internal/logic/upload/uploadimagelogic.go
Normal file
138
app/main/api/internal/logic/upload/uploadimagelogic.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"tyc-server/app/main/api/internal/svc"
|
||||
"tyc-server/app/main/api/internal/types"
|
||||
"tyc-server/common/xerr"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
const maxImageSize = 3 * 1024 * 1024 // 3MB
|
||||
const defaultTempFileMaxAgeH = 24
|
||||
|
||||
type UploadImageLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewUploadImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadImageLogic {
|
||||
return &UploadImageLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UploadImageLogic) UploadImage(req *types.UploadImageReq) (resp *types.UploadImageResp, err error) {
|
||||
decoded, decErr := base64.StdEncoding.DecodeString(req.ImageBase64)
|
||||
if decErr != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("图片 base64 格式错误"), "%v", decErr)
|
||||
}
|
||||
if len(decoded) > maxImageSize {
|
||||
return nil, errors.Wrapf(xerr.NewErrMsg("图片不能超过 3M"), "size=%d", len(decoded))
|
||||
}
|
||||
|
||||
// 按文件内容 hash 命名,相同文件复用同一 URL,避免重复传输与刷流量
|
||||
hashSum := sha256.Sum256(decoded)
|
||||
hashHex := hex.EncodeToString(hashSum[:])
|
||||
fileName := hashHex + ".jpg"
|
||||
|
||||
dir := l.uploadStoragePath()
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建上传目录失败: %v", err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dir, fileName)
|
||||
// 若已存在同 hash 文件,直接返回 URL,不重复写入
|
||||
if _, statErr := os.Stat(filePath); statErr == nil {
|
||||
url := l.buildURL(fileName)
|
||||
logx.Infof("upload image dedup by hash, file=%s", fileName)
|
||||
return &types.UploadImageResp{Url: url}, nil
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, decoded, 0644); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 异步清理过期临时文件,不阻塞响应
|
||||
go l.deleteOldUploads(dir)
|
||||
|
||||
url := l.buildURL(fileName)
|
||||
logx.Infof("upload image ok, file=%s", fileName)
|
||||
return &types.UploadImageResp{Url: url}, nil
|
||||
}
|
||||
|
||||
func (l *UploadImageLogic) buildURL(fileName string) string {
|
||||
baseURL := l.svcCtx.Config.Upload.FileBaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = l.svcCtx.Config.AdminPromotion.URLDomain
|
||||
if baseURL != "" {
|
||||
baseURL = baseURL + "/api/v1/upload/file"
|
||||
}
|
||||
}
|
||||
if baseURL == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", baseURL, fileName)
|
||||
}
|
||||
|
||||
func (l *UploadImageLogic) uploadStoragePath() string {
|
||||
candidates := []string{
|
||||
"data/uploads",
|
||||
"../data/uploads",
|
||||
"../../data/uploads",
|
||||
"../../../data/uploads",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
abs, _ := filepath.Abs(c)
|
||||
if err := os.MkdirAll(abs, 0755); err == nil {
|
||||
return abs
|
||||
}
|
||||
}
|
||||
abs, _ := filepath.Abs(candidates[0])
|
||||
return abs
|
||||
}
|
||||
|
||||
// deleteOldUploads 删除目录下超过保留时长的临时文件
|
||||
func (l *UploadImageLogic) deleteOldUploads(dir string) {
|
||||
maxAgeH := l.svcCtx.Config.Upload.TempFileMaxAgeH
|
||||
if maxAgeH <= 0 {
|
||||
maxAgeH = defaultTempFileMaxAgeH
|
||||
}
|
||||
cutoff := time.Now().Add(-time.Duration(maxAgeH) * time.Hour)
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
l.Errorf("deleteOldUploads ReadDir: %v", err)
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, e.Name())
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
l.Errorf("deleteOldUploads Remove %s: %v", path, err)
|
||||
} else {
|
||||
l.Infof("deleteOldUploads removed %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user