283 lines
8.0 KiB
Go
283 lines
8.0 KiB
Go
package tool
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"planA/planB/initialization/golabl"
|
||
planBTypeModules "planA/planB/type/modules"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/minio/minio-go/v7"
|
||
"github.com/minio/minio-go/v7/pkg/lifecycle"
|
||
)
|
||
|
||
// UploadToMinIo 上传到图片空间
|
||
// @param watermarkFromURLExsBase64Arr 水印图片信息
|
||
// @return []string 上传后的图片链接
|
||
func UploadToMinIo(watermarkFromURLExsBase64Arr []planBTypeModules.ImageResult) ([]string, error) {
|
||
var imageUrlArr []string
|
||
for _, watermarkFromURLExsBase64 := range watermarkFromURLExsBase64Arr {
|
||
imageUrl, uploadBase64ImageErr := UploadBase64Image(watermarkFromURLExsBase64.Data, "")
|
||
if uploadBase64ImageErr != nil {
|
||
return imageUrlArr, nil
|
||
}
|
||
imageUrlArr = append(imageUrlArr, imageUrl)
|
||
}
|
||
return imageUrlArr, nil
|
||
}
|
||
|
||
// UploadBase64Image 传入 base64 图片信息与目录上传到图片空间
|
||
// 参数 base64Data: base64 编码的图片数据(可以是纯 base64 或带 data:image/xxx;base64, 前缀)
|
||
// 参数 fileName: 自定义文件名(可选,为空则自动生成)
|
||
// 返回: 上传后的完整对象名称(含目录)和错误信息
|
||
func UploadBase64Image(base64Data, fileName string) (string, error) {
|
||
// 解析 base64 数据,分离 MIME 类型和纯 base64 内容
|
||
var pureBase64 string
|
||
var contentType string
|
||
|
||
// 检查是否包含 data URL 前缀
|
||
if strings.Contains(base64Data, ";base64,") {
|
||
// 格式: data:image/jpeg;base64,xxxxx
|
||
parts := strings.SplitN(base64Data, ";base64,", 2)
|
||
if len(parts) != 2 {
|
||
return "", fmt.Errorf("无效的 base64 数据格式")
|
||
}
|
||
// 提取 MIME 类型,去掉 "data:" 前缀
|
||
mimePart := strings.TrimPrefix(parts[0], "data:")
|
||
contentType = mimePart
|
||
pureBase64 = parts[1]
|
||
} else {
|
||
// 假设是纯 base64 数据,需要根据内容判断类型
|
||
pureBase64 = base64Data
|
||
// 尝试通过 base64 内容的前几个字节判断图片类型
|
||
contentType = detectImageTypeFromBase64(pureBase64)
|
||
if contentType == "" {
|
||
contentType = "image/jpeg" // 默认类型
|
||
}
|
||
}
|
||
|
||
// 解码 base64 数据
|
||
imageData, err := base64.StdEncoding.DecodeString(pureBase64)
|
||
if err != nil {
|
||
return "", fmt.Errorf("base64 解码失败: %v", err)
|
||
}
|
||
|
||
// 如果未提供文件名,则自动生成
|
||
if fileName == "" {
|
||
// 根据 MIME 类型确定扩展名
|
||
ext := getExtensionFromMimeType(contentType)
|
||
fileName = generateRandomFileName(ext)
|
||
}
|
||
|
||
// 将图片数据转换为 io.Reader
|
||
reader := bytes.NewReader(imageData)
|
||
|
||
// 设置全局生命周期规则(所有文件30天后自动删除)
|
||
err = SetLifecyclePolicy(30)
|
||
if err != nil {
|
||
fmt.Printf("设置生命周期规则失败: %v\n", err)
|
||
// 继续执行,不影响上传
|
||
}
|
||
|
||
// 生成按日期目录的对象名称
|
||
objectName := generateObjectName(string(imageData))
|
||
|
||
// 上传到 MinIO
|
||
url, err := UploadWithExpiry(
|
||
reader,
|
||
objectName,
|
||
contentType,
|
||
int64(len(imageData)),
|
||
time.Now().Add(7*24*time.Hour), // 7天后过期
|
||
)
|
||
if err != nil {
|
||
return "", fmt.Errorf("上传图片失败: %v", err)
|
||
}
|
||
if golabl.Config.Minio.UseSSL {
|
||
url = "https://" + url
|
||
} else {
|
||
url = "http://" + url
|
||
}
|
||
return url, nil
|
||
}
|
||
|
||
// detectImageTypeFromBase64 通过 base64 解码后的前几个字节判断图片类型
|
||
func detectImageTypeFromBase64(base64Str string) string {
|
||
// 解码前几个字节用于判断
|
||
decoded, err := base64.StdEncoding.DecodeString(base64Str[:min(32, len(base64Str))])
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
|
||
// 检查文件头
|
||
if len(decoded) >= 4 {
|
||
// JPEG: FF D8
|
||
if decoded[0] == 0xFF && decoded[1] == 0xD8 {
|
||
return "image/jpeg"
|
||
}
|
||
// PNG: 89 50 4E 47
|
||
if decoded[0] == 0x89 && decoded[1] == 0x50 && decoded[2] == 0x4E && decoded[3] == 0x47 {
|
||
return "image/png"
|
||
}
|
||
// GIF: 47 49 46
|
||
if decoded[0] == 0x47 && decoded[1] == 0x49 && decoded[2] == 0x46 {
|
||
return "image/gif"
|
||
}
|
||
// WEBP: 52 49 46 46
|
||
if decoded[0] == 0x52 && decoded[1] == 0x49 && decoded[2] == 0x46 && decoded[3] == 0x46 {
|
||
return "image/webp"
|
||
}
|
||
// BMP: 42 4D
|
||
if decoded[0] == 0x42 && decoded[1] == 0x4D {
|
||
return "image/bmp"
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// getExtensionFromMimeType 根据 MIME 类型获取文件扩展名
|
||
func getExtensionFromMimeType(mimeType string) string {
|
||
switch mimeType {
|
||
case "image/jpeg":
|
||
return ".jpg"
|
||
case "image/png":
|
||
return ".png"
|
||
case "image/gif":
|
||
return ".gif"
|
||
case "image/webp":
|
||
return ".webp"
|
||
case "image/bmp":
|
||
return ".bmp"
|
||
default:
|
||
return ".jpg"
|
||
}
|
||
}
|
||
|
||
// generateRandomFileName 生成随机文件名
|
||
func generateRandomFileName(ext string) string {
|
||
// 这里使用简单的随机数生成,实际应用中可以改用 UUID 或时间戳
|
||
// 为了示例简单,使用时间戳
|
||
return fmt.Sprintf("%d%s", getTimestampNano(), ext)
|
||
}
|
||
|
||
// getTimestampNano 获取纳秒时间戳
|
||
func getTimestampNano() int64 {
|
||
return time.Now().UnixNano()
|
||
}
|
||
|
||
// SetLifecyclePolicy 设置生命周期规则 - 自动删除超过指定天数的文件
|
||
func SetLifecyclePolicy(expiryDays int) error {
|
||
ctx := context.Background()
|
||
|
||
// 创建生命周期配置
|
||
lifecycleStr := fmt.Sprintf(`{
|
||
"Rules": [
|
||
{
|
||
"ID": "auto-delete-rule",
|
||
"Status": "Enabled",
|
||
"Filter": {
|
||
"Prefix": ""
|
||
},
|
||
"Expiration": {
|
||
"Days": %d
|
||
}
|
||
}
|
||
]
|
||
}`, expiryDays)
|
||
|
||
var config lifecycle.Configuration
|
||
if err := json.Unmarshal([]byte(lifecycleStr), &config); err != nil {
|
||
return fmt.Errorf("解析生命周期配置失败: %v", err)
|
||
}
|
||
|
||
// 设置生命周期配置
|
||
if err := golabl.MinIo.Client.SetBucketLifecycle(ctx, golabl.Config.Minio.BucketName, &config); err != nil {
|
||
return fmt.Errorf("设置生命周期规则失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 生成按日期组织的对象名称(格式:2026-05-21/时间戳.扩展名)
|
||
// imgBase64: Base64编码的图片字符串,格式如 "data:image/jpeg;base64,/9j/4AAQ..." 或直接的Base64字符串
|
||
func generateObjectName(imgBase64 string) string {
|
||
// 获取当前时间的日期字符串(YYYY-MM-DD格式)
|
||
now := time.Now()
|
||
dateDir := now.Format("2006-01-02") // 格式:2026-05-21
|
||
|
||
// 生成时间戳和文件名
|
||
timestamp := now.UnixNano()
|
||
|
||
// 从Base64字符串中提取扩展名
|
||
ext := getExtFromBase64(imgBase64)
|
||
if ext == "" {
|
||
ext = ".jpg"
|
||
}
|
||
|
||
// 构建目录结构: 2026-05-21/时间戳.扩展名
|
||
objectName := fmt.Sprintf("%s/%d%s", dateDir, timestamp, ext)
|
||
|
||
return objectName
|
||
}
|
||
|
||
// 从Base64字符串中获取文件扩展名
|
||
func getExtFromBase64(base64Str string) string {
|
||
// 查找 data:image/ 格式的MIME类型
|
||
if strings.Contains(base64Str, "data:image/") {
|
||
// 提取MIME类型部分,格式如 "data:image/jpeg;base64,"
|
||
parts := strings.SplitN(base64Str, ";", 2)
|
||
if len(parts) > 0 {
|
||
mimePart := parts[0]
|
||
// 提取 image/ 后面的格式
|
||
imageType := strings.TrimPrefix(mimePart, "data:image/")
|
||
|
||
// 根据MIME类型返回对应的扩展名
|
||
switch imageType {
|
||
case "jpeg", "jpg":
|
||
return ".jpg"
|
||
case "png":
|
||
return ".png"
|
||
case "gif":
|
||
return ".gif"
|
||
case "webp":
|
||
return ".webp"
|
||
case "bmp":
|
||
return ".bmp"
|
||
default:
|
||
return "." + imageType
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有MIME信息,默认返回空
|
||
return ""
|
||
}
|
||
|
||
// UploadWithExpiry 上传文件并设置自定义元数据(用于单独控制每个文件的过期时间)
|
||
func UploadWithExpiry(imgData io.Reader, objectName, contentType string, size int64, expiryTime time.Time) (string, error) {
|
||
ctx := context.Background()
|
||
|
||
// 设置过期时间到元数据
|
||
metadata := map[string]string{
|
||
"expiry-date": expiryTime.Format(time.RFC3339),
|
||
"delete-after": fmt.Sprintf("%d", int(time.Until(expiryTime).Hours()/24)),
|
||
}
|
||
|
||
// 上传文件
|
||
_, err := golabl.MinIo.Client.PutObject(ctx, golabl.Config.Minio.BucketName, objectName, imgData, size, minio.PutObjectOptions{
|
||
ContentType: contentType,
|
||
UserMetadata: metadata,
|
||
})
|
||
if err != nil {
|
||
return "", fmt.Errorf("上传失败: %v", err)
|
||
}
|
||
|
||
url := fmt.Sprintf("%s/%s/%s", golabl.Config.Minio.Url, golabl.Config.Minio.BucketName, objectName)
|
||
return url, nil
|
||
}
|