first commit
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// Package upload 文件上传、校验、落盘、公开 token 生成的工具函数。
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
)
|
||||
|
||||
func FileGetExt(name string) string {
|
||||
return strings.ToLower(path.Ext(name))
|
||||
}
|
||||
|
||||
// 校验扩展名是否在白名单内。
|
||||
func FileCheckAllowExt(ext string, allowed []string) bool {
|
||||
ext = strings.TrimPrefix(strings.ToLower(ext), ".")
|
||||
for _, e := range allowed {
|
||||
if strings.ToLower(e) == ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 校验文件大小。maxMB <= 0 表示不限制。
|
||||
func FileCheckMaxSize(size, maxMB int64) bool {
|
||||
if maxMB <= 0 {
|
||||
return true
|
||||
}
|
||||
return size <= maxMB*1024*1024
|
||||
}
|
||||
|
||||
// FileCheckImageMagic 校验文件头 magic bytes,判断内容是否为真实图片(jpeg/png/gif/webp)。
|
||||
// 用于防止伪造扩展名上传非图片文件(如脚本、可执行文件、HTML)。
|
||||
func FileCheckImageMagic(data []byte) bool {
|
||||
if len(data) < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// JPEG: FF D8 FF
|
||||
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
|
||||
return true
|
||||
}
|
||||
|
||||
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
||||
if len(data) >= 8 &&
|
||||
data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 &&
|
||||
data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A {
|
||||
return true
|
||||
}
|
||||
|
||||
// GIF: "GIF8"
|
||||
if data[0] == 'G' && data[1] == 'I' && data[2] == 'F' && data[3] == '8' {
|
||||
return true
|
||||
}
|
||||
|
||||
// WebP: "RIFF" .... "WEBP"
|
||||
if len(data) >= 12 &&
|
||||
data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
|
||||
data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P' {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GenFileName 生成防冲突随机相对文件名。
|
||||
func FileGenName(originName string) (string, error) {
|
||||
ext := FileGetExt(originName)
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b) + ext, nil
|
||||
}
|
||||
|
||||
// 临时保存从前端获取的图片
|
||||
func FileSaveTmp(ctx context.Context, file *multipart.FileHeader, fileName string, userID int64) (string, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "upload.FileSaveTmp")
|
||||
defer span.End()
|
||||
|
||||
dir := filepath.Join(global.UploadSetting.TmpSavePath, fmt.Sprint(userID))
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
span.RecordError(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpFile := filepath.Join(dir, fileName)
|
||||
|
||||
dst, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpFile, nil
|
||||
|
||||
}
|
||||
|
||||
// 从后端抠图返回的图片保存到临时文件(subDir 为相对子目录,如 "用户UUID/用户ID")。
|
||||
func FileSaveTmpBytes(ctx context.Context, data []byte, fileName string, subDir string) (string, error) {
|
||||
_, span := global.Tracer.Start(ctx, "upload.FileSaveTmpBytes")
|
||||
defer span.End()
|
||||
|
||||
dir := filepath.Join(global.UploadSetting.TmpSavePath, subDir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
span.RecordError(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpFile := filepath.Join(dir, fileName)
|
||||
if err := os.WriteFile(tmpFile, data, 0o644); err != nil {
|
||||
span.RecordError(err)
|
||||
return "", err
|
||||
}
|
||||
return tmpFile, nil
|
||||
}
|
||||
|
||||
// 将抠图后的文件上传到cos
|
||||
func FileUploadCos(ctx context.Context, fileName string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "upload.FileUploadCos")
|
||||
defer span.End()
|
||||
|
||||
// COS 客户端初始化失败时降级为 nil,此处做保护避免 nil 指针 panic。
|
||||
if global.CosClient == nil {
|
||||
err := fmt.Errorf("cos client not initialized")
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, err := global.CosClient.Object.Upload(ctx, fileName, fmt.Sprintf("%s/%s", global.UploadSetting.TmpSavePath, fileName), nil)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 删除 COS 对象。key 为对象键(即 processed_image 存储值)。
|
||||
// key 为空直接返回 nil;client 未初始化返回 error。
|
||||
// 删除为尽力而为的清理操作,内部使用短超时避免网络异常长时间阻塞调用方。
|
||||
func FileDeleteCos(ctx context.Context, key string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "upload.FileDeleteCos")
|
||||
defer span.End()
|
||||
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// COS 客户端初始化失败时降级为 nil,此处做保护避免 nil 指针 panic。
|
||||
if global.CosClient == nil {
|
||||
err := fmt.Errorf("cos client not initialized")
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
delCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := global.CosClient.Object.Delete(delCtx, key)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取cos访问url
|
||||
func GetCosUrl(fileName string) string {
|
||||
ts := time.Now().Unix()
|
||||
raw := global.CosSetting.CdnDownloadSecretKey + "/" + fileName + strconv.FormatInt(ts, 10)
|
||||
sign := fmt.Sprintf("%x", md5.Sum([]byte(raw)))
|
||||
|
||||
return fmt.Sprintf("https://%s/%s?sign=%s&t=%d",
|
||||
global.CosSetting.CosDownloadDomain,
|
||||
fileName,
|
||||
sign,
|
||||
ts,
|
||||
)
|
||||
}
|
||||
|
||||
// AvatarURL 将头像存储值转为可访问 URL:
|
||||
// 历史通过修改资料接口传入的 http(s) URL 直接透传;COS key 统一转为 CDN 签名 URL。
|
||||
func AvatarURL(raw string) string {
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
|
||||
return raw
|
||||
}
|
||||
return GetCosUrl(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user