139 lines
3.6 KiB
Go
139 lines
3.6 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|
||
}
|