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)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|