Files
2025-11-17 13:32:54 +08:00

274 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package storage
import (
"fmt"
"io"
"mime"
"mime/multipart"
"path/filepath"
"strings"
"time"
"dianshang/internal/config"
"dianshang/pkg/logger"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/google/uuid"
)
// OSSStorage 阿里云OSS存储服务
type OSSStorage struct {
client *oss.Client
bucket *oss.Bucket
config *config.OSSConfig
}
// NewOSSStorage 创建OSS存储服务实例
func NewOSSStorage(cfg *config.OSSConfig) (*OSSStorage, error) {
// 创建OSSClient实例
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("创建OSS客户端失败: %w", err)
}
// 获取存储空间
bucket, err := client.Bucket(cfg.BucketName)
if err != nil {
return nil, fmt.Errorf("获取OSS Bucket失败: %w", err)
}
return &OSSStorage{
client: client,
bucket: bucket,
config: cfg,
}, nil
}
// UploadFile 上传文件到OSS
func (s *OSSStorage) UploadFile(file multipart.File, filename string, objectPath string) (string, error) {
// 生成OSS对象路径
objectKey := s.generateObjectKey(objectPath, filename)
logger.Infof("[OSS] 开始上传文件,对象路径: %s", objectKey)
// 获取文件MIME类型
contentType := getContentType(filename)
logger.Debugf("[OSS] 文件MIME类型: %s", contentType)
// 上传文件到OSS设置Content-Type和其他元数据
// 使用 ObjectACL 设置为公共读,确保文件可以直接访问
err := s.bucket.PutObject(objectKey, file,
oss.ContentType(contentType),
oss.ObjectACL(oss.ACLPublicRead),
oss.ContentDisposition("inline"), // 关键设置为inline而不是attachment
)
if err != nil {
logger.Errorf("[OSS] 上传文件失败,对象路径: %s, 错误: %v", objectKey, err)
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
}
// 获取OSS返回的文件URL
fileURL, err := s.GetFileURL(objectKey)
if err != nil {
logger.Errorf("[OSS] 获取文件URL失败对象路径: %s, 错误: %v", objectKey, err)
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
}
logger.Infof("[OSS] 上传成功,对象路径: %s, OSS返回URL: %s", objectKey, fileURL)
return fileURL, nil
}
// UploadFromBytes 从字节数组上传文件到OSS
func (s *OSSStorage) UploadFromBytes(data []byte, filename string, objectPath string) (string, error) {
// 生成OSS对象路径
objectKey := s.generateObjectKey(objectPath, filename)
logger.Infof("[OSS] 开始上传字节数组,对象路径: %s, 大小: %d bytes", objectKey, len(data))
// 获取文件MIME类型
contentType := getContentType(filename)
logger.Debugf("[OSS] 文件MIME类型: %s", contentType)
// 上传文件到OSS设置Content-Type和其他元数据
err := s.bucket.PutObject(objectKey, strings.NewReader(string(data)),
oss.ContentType(contentType),
oss.ObjectACL(oss.ACLPublicRead),
oss.ContentDisposition("inline"),
)
if err != nil {
logger.Errorf("[OSS] 上传字节数组失败,对象路径: %s, 错误: %v", objectKey, err)
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
}
// 获取OSS返回的文件URL
fileURL, err := s.GetFileURL(objectKey)
if err != nil {
logger.Errorf("[OSS] 获取文件URL失败对象路径: %s, 错误: %v", objectKey, err)
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
}
logger.Infof("[OSS] 上传成功,对象路径: %s, OSS返回URL: %s", objectKey, fileURL)
return fileURL, nil
}
// UploadFromReader 从Reader上传文件到OSS
func (s *OSSStorage) UploadFromReader(reader io.Reader, filename string, objectPath string) (string, error) {
// 生成OSS对象路径
objectKey := s.generateObjectKey(objectPath, filename)
logger.Infof("[OSS] 开始上传Reader数据对象路径: %s", objectKey)
// 获取文件MIME类型
contentType := getContentType(filename)
logger.Debugf("[OSS] 文件MIME类型: %s", contentType)
// 上传文件到OSS设置Content-Type和其他元数据
err := s.bucket.PutObject(objectKey, reader,
oss.ContentType(contentType),
oss.ObjectACL(oss.ACLPublicRead),
oss.ContentDisposition("inline"),
)
if err != nil {
logger.Errorf("[OSS] 上传Reader数据失败对象路径: %s, 错误: %v", objectKey, err)
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
}
// 获取OSS返回的文件URL
fileURL, err := s.GetFileURL(objectKey)
if err != nil {
logger.Errorf("[OSS] 获取文件URL失败对象路径: %s, 错误: %v", objectKey, err)
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
}
logger.Infof("[OSS] 上传成功,对象路径: %s, OSS返回URL: %s", objectKey, fileURL)
return fileURL, nil
}
// DeleteFile 删除OSS中的文件
func (s *OSSStorage) DeleteFile(objectKey string) error {
err := s.bucket.DeleteObject(objectKey)
if err != nil {
return fmt.Errorf("删除OSS文件失败: %w", err)
}
return nil
}
// IsObjectExist 检查对象是否存在
func (s *OSSStorage) IsObjectExist(objectKey string) (bool, error) {
exist, err := s.bucket.IsObjectExist(objectKey)
if err != nil {
return false, fmt.Errorf("检查OSS对象是否存在失败: %w", err)
}
return exist, nil
}
// GetFileURL 获取文件访问URL使用OSS SDK标准方法
func (s *OSSStorage) GetFileURL(objectKey string) (string, error) {
// 如果配置了自定义域名,使用自定义域名
if s.config.Domain != "" {
url := fmt.Sprintf("%s/%s", strings.TrimRight(s.config.Domain, "/"), objectKey)
logger.Debugf("[OSS] 使用自定义域名生成URL: %s", url)
return url, nil
}
// 使用OSS SDK获取对象的公共URL
// 对于公共读的Bucket直接拼接URL
url := fmt.Sprintf("https://%s.%s/%s", s.config.BucketName, s.config.Endpoint, objectKey)
logger.Debugf("[OSS] 使用OSS标准域名生成URL: %s", url)
return url, nil
}
// GeneratePresignedURL 生成临时访问URL带签名
func (s *OSSStorage) GeneratePresignedURL(objectKey string, expireSeconds int64) (string, error) {
signedURL, err := s.bucket.SignURL(objectKey, oss.HTTPGet, expireSeconds)
if err != nil {
return "", fmt.Errorf("生成预签名URL失败: %w", err)
}
return signedURL, nil
}
// generateObjectKey 生成OSS对象键名
func (s *OSSStorage) generateObjectKey(objectPath string, filename string) string {
// 组合基础路径和对象路径
basePath := strings.TrimRight(s.config.BasePath, "/")
objectPath = strings.TrimLeft(objectPath, "/")
// OSS 路径统一使用斜杠,避免 Windows 下使用反斜杠
if basePath == "" {
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(objectPath, filename)), "\\", "/")
}
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(basePath, objectPath, filename)), "\\", "/")
}
// GenerateFilename 生成唯一文件名
func GenerateFilename(originalFilename string) string {
ext := filepath.Ext(originalFilename)
name := strings.TrimSuffix(originalFilename, ext)
// 生成时间戳和UUID
timestamp := time.Now().Format("20060102150405")
uuidStr := uuid.New().String()[:8]
// 清理文件名,移除特殊字符
name = strings.ReplaceAll(name, " ", "_")
name = strings.ReplaceAll(name, "(", "")
name = strings.ReplaceAll(name, ")", "")
return fmt.Sprintf("%s_%s_%s%s", name, timestamp, uuidStr, ext)
}
// IsValidImageType 验证图片文件类型
func IsValidImageType(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"}
for _, validExt := range validExts {
if ext == validExt {
return true
}
}
return false
}
// getContentType 根据文件名获取MIME类型
func getContentType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
// 常见图片类型
switch ext {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
case ".svg":
return "image/svg+xml"
case ".bmp":
return "image/bmp"
case ".ico":
return "image/x-icon"
// 常见文档类型
case ".pdf":
return "application/pdf"
case ".doc":
return "application/msword"
case ".docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
case ".xls":
return "application/vnd.ms-excel"
case ".xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case ".txt":
return "text/plain; charset=utf-8"
case ".json":
return "application/json"
case ".xml":
return "application/xml"
default:
// 使用Go标准库自动检测
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
return mimeType
}
// 默认类型
return "application/octet-stream"
}
}