Files
nl-blogs/server/utils/oss.go

489 lines
13 KiB
Go
Raw Normal View History

2026-01-19 16:14:08 +08:00
package utils
import (
2026-01-19 20:21:09 +08:00
"bytes"
"context"
2026-01-19 16:14:08 +08:00
"fmt"
"io"
"mime/multipart"
2026-01-19 20:21:09 +08:00
"net/http"
"net/url"
2026-01-19 16:14:08 +08:00
"os"
"path/filepath"
"strings"
"time"
2026-01-19 20:21:09 +08:00
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
cos "github.com/tencentyun/cos-go-sdk-v5"
2026-01-19 16:14:08 +08:00
)
// StorageType 存储类型
type StorageType string
const (
StorageLocal StorageType = "local"
StorageQCloud StorageType = "qcloud"
StorageAliyun StorageType = "aliyun"
StorageQiniu StorageType = "qiniu"
)
// OSSConfig OSS配置
type OSSConfig struct {
StorageType string
2026-01-19 20:21:09 +08:00
// 通用字段(向后兼容)
AccessKey string
SecretKey string
Bucket string
Region string
Domain string
// 阿里云OSS专用字段
OSSAccessKeyID string
OSSAccessKeySecret string
OSSEndpoint string
OSSBucket string
OSSDomain string
// 腾讯云COS专用字段
QCloudSecretID string
QCloudSecretKey string
QCloudRegion string
QCloudBucket string
QCloudDomain string
// 七牛云专用字段
QiniuAccessKey string
QiniuSecretKey string
QiniuBucket string
QiniuRegion string
QiniuDomain string
2026-01-19 16:14:08 +08:00
}
// OSSUploader OSS上传接口
type OSSUploader interface {
Upload(file multipart.File, filename string, size int64) (string, string, error) // 返回 filePath, fileURL, error
Delete(filePath string) error
}
// GetOSSUploader 根据存储类型获取上传器
func GetOSSUploader(config *OSSConfig) (OSSUploader, error) {
switch StorageType(config.StorageType) {
case StorageLocal:
2026-01-20 10:36:27 +08:00
// 从环境变量读取配置,如果未设置则使用默认值
basePath := os.Getenv("UPLOADS_BASE_PATH")
if basePath == "" {
basePath = "./uploads"
}
baseURL := os.Getenv("UPLOADS_BASE_URL")
if baseURL == "" {
baseURL = "/uploads"
}
2026-01-19 16:14:08 +08:00
return &LocalUploader{
2026-01-20 10:36:27 +08:00
BasePath: basePath,
BaseURL: baseURL,
2026-01-19 16:14:08 +08:00
}, nil
case StorageQCloud:
2026-01-19 20:21:09 +08:00
// 优先使用专用字段,如果为空则使用通用字段(向后兼容)
secretID := config.QCloudSecretID
secretKey := config.QCloudSecretKey
region := config.QCloudRegion
bucket := config.QCloudBucket
domain := config.QCloudDomain
if secretID == "" {
secretID = config.AccessKey
}
if secretKey == "" {
secretKey = config.SecretKey
}
if region == "" {
region = config.Region
}
if bucket == "" {
bucket = config.Bucket
}
if domain == "" {
domain = config.Domain
}
if secretID == "" || secretKey == "" || bucket == "" || region == "" {
return nil, fmt.Errorf("qcloud config incomplete: secretID, secretKey, bucket, region are required")
}
u, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region))
if err != nil {
return nil, fmt.Errorf("invalid qcloud config: %v", err)
}
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: secretID,
SecretKey: secretKey,
},
})
return &QCloudUploader{
Client: client,
Bucket: bucket,
Domain: domain,
}, nil
2026-01-19 16:14:08 +08:00
case StorageAliyun:
2026-01-19 20:21:09 +08:00
// 优先使用专用字段,如果为空则使用通用字段(向后兼容)
accessKeyID := config.OSSAccessKeyID
accessKeySecret := config.OSSAccessKeySecret
endpoint := config.OSSEndpoint
bucket := config.OSSBucket
domain := config.OSSDomain
if accessKeyID == "" {
accessKeyID = config.AccessKey
}
if accessKeySecret == "" {
accessKeySecret = config.SecretKey
}
if bucket == "" {
bucket = config.Bucket
}
if domain == "" {
domain = config.Domain
}
if accessKeyID == "" || accessKeySecret == "" || bucket == "" {
return nil, fmt.Errorf("aliyun config incomplete: accessKeyID, accessKeySecret, bucket are required")
}
// 如果endpoint为空从region构建向后兼容
if endpoint == "" {
region := config.Region
if region == "" {
return nil, fmt.Errorf("aliyun config incomplete: endpoint or region is required")
}
endpoint = fmt.Sprintf("https://oss-%s.aliyuncs.com", region)
}
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
return nil, fmt.Errorf("failed to create aliyun oss client: %v", err)
}
ossBucket, err := client.Bucket(bucket)
if err != nil {
return nil, fmt.Errorf("failed to get aliyun bucket: %v", err)
}
return &AliyunUploader{
Bucket: ossBucket,
Domain: domain,
}, nil
2026-01-19 16:14:08 +08:00
case StorageQiniu:
2026-01-19 20:21:09 +08:00
// 优先使用专用字段,如果为空则使用通用字段(向后兼容)
accessKey := config.QiniuAccessKey
secretKey := config.QiniuSecretKey
bucket := config.QiniuBucket
region := config.QiniuRegion
domain := config.QiniuDomain
if accessKey == "" {
accessKey = config.AccessKey
}
if secretKey == "" {
secretKey = config.SecretKey
}
if bucket == "" {
bucket = config.Bucket
}
if domain == "" {
domain = config.Domain
}
if accessKey == "" || secretKey == "" || bucket == "" {
return nil, fmt.Errorf("qiniu config incomplete: accessKey, secretKey, bucket are required")
}
mac := qbox.NewMac(accessKey, secretKey)
// 根据region选择Zone如果region为空使用通用字段的region否则默认华东
var zone *storage.Zone
if region != "" {
switch region {
case "z0", "华东":
zone = &storage.ZoneHuadong
case "z1", "华北":
zone = &storage.ZoneHuabei
case "z2", "华南":
zone = &storage.ZoneHuanan
case "na0", "北美":
zone = &storage.ZoneBeimei
case "as0", "东南亚":
zone = &storage.ZoneXinjiapo
default:
zone = &storage.ZoneHuadong // 默认华东
}
} else if config.Region != "" {
// 使用通用字段的region
switch config.Region {
case "z0", "华东":
zone = &storage.ZoneHuadong
case "z1", "华北":
zone = &storage.ZoneHuabei
case "z2", "华南":
zone = &storage.ZoneHuanan
case "na0", "北美":
zone = &storage.ZoneBeimei
case "as0", "东南亚":
zone = &storage.ZoneXinjiapo
default:
zone = &storage.ZoneHuadong
}
} else {
zone = &storage.ZoneHuadong // 默认华东
}
cfg := storage.Config{
Zone: zone,
UseHTTPS: true,
UseCdnDomains: false,
}
formUploader := storage.NewFormUploader(&cfg)
bucketManager := storage.NewBucketManager(mac, &cfg)
return &QiniuUploader{
FormUploader: formUploader,
BucketManager: bucketManager,
Bucket: bucket,
Domain: domain,
Mac: mac,
}, nil
2026-01-19 16:14:08 +08:00
default:
return nil, fmt.Errorf("unsupported storage type: %s", config.StorageType)
}
}
// LocalUploader 本地存储上传器
type LocalUploader struct {
BasePath string
BaseURL string
}
// Upload 上传文件到本地
func (l *LocalUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
// 生成唯一文件名
ext := filepath.Ext(filename)
timestamp := time.Now().Unix()
randomStr := fmt.Sprintf("%d", timestamp)
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
// 按日期创建目录
dateDir := time.Now().Format("2006/01/02")
uploadDir := filepath.Join(l.BasePath, dateDir)
// 创建目录
if err := os.MkdirAll(uploadDir, 0755); err != nil {
return "", "", fmt.Errorf("failed to create upload directory: %v", err)
}
// 完整文件路径
filePath := filepath.Join(uploadDir, newFilename)
// 创建目标文件
dst, err := os.Create(filePath)
if err != nil {
return "", "", fmt.Errorf("failed to create file: %v", err)
}
defer dst.Close()
// 复制文件内容
if _, err := io.Copy(dst, file); err != nil {
return "", "", fmt.Errorf("failed to copy file: %v", err)
}
// 生成访问URL
fileURL := fmt.Sprintf("%s/%s/%s", l.BaseURL, dateDir, newFilename)
return filePath, fileURL, nil
}
// Delete 删除本地文件
func (l *LocalUploader) Delete(filePath string) error {
// 确保文件路径在BasePath内安全措施
absBasePath, err := filepath.Abs(l.BasePath)
if err != nil {
return err
}
absFilePath, err := filepath.Abs(filePath)
if err != nil {
return err
}
if !strings.HasPrefix(absFilePath, absBasePath) {
return fmt.Errorf("invalid file path: outside base directory")
}
return os.Remove(filePath)
}
2026-01-19 20:21:09 +08:00
// AliyunUploader 阿里云OSS上传器
type AliyunUploader struct {
Bucket *oss.Bucket
Domain string
}
// Upload 上传文件到阿里云OSS
func (a *AliyunUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
// 生成唯一文件名
ext := filepath.Ext(filename)
timestamp := time.Now().Unix()
randomStr := fmt.Sprintf("%d", timestamp)
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
// 按日期创建目录
dateDir := time.Now().Format("2006/01/02")
objectKey := fmt.Sprintf("%s/%s", dateDir, newFilename)
// 上传文件
err := a.Bucket.PutObject(objectKey, file)
if err != nil {
return "", "", fmt.Errorf("failed to upload to aliyun oss: %v", err)
}
// 生成访问URL
var fileURL string
if a.Domain != "" {
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(a.Domain, "/"), objectKey)
} else {
// 从endpoint中提取region格式为 https://oss-region.aliyuncs.com
endpoint := a.Bucket.Client.Config.Endpoint
fileURL = fmt.Sprintf("%s/%s", endpoint, objectKey)
}
return objectKey, fileURL, nil
}
// Delete 删除阿里云OSS文件
func (a *AliyunUploader) Delete(objectKey string) error {
err := a.Bucket.DeleteObject(objectKey)
if err != nil {
return fmt.Errorf("failed to delete from aliyun oss: %v", err)
}
return nil
}
// QCloudUploader 腾讯云COS上传器
type QCloudUploader struct {
Client *cos.Client
Bucket string
Domain string
}
// Upload 上传文件到腾讯云COS
func (q *QCloudUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
// 生成唯一文件名
ext := filepath.Ext(filename)
timestamp := time.Now().Unix()
randomStr := fmt.Sprintf("%d", timestamp)
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
// 按日期创建目录
dateDir := time.Now().Format("2006/01/02")
objectKey := fmt.Sprintf("%s/%s", dateDir, newFilename)
// 上传文件
_, err := q.Client.Object.Put(context.Background(), objectKey, file, nil)
if err != nil {
return "", "", fmt.Errorf("failed to upload to qcloud cos: %v", err)
}
// 生成访问URL
var fileURL string
if q.Domain != "" {
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(q.Domain, "/"), objectKey)
} else {
// 使用BucketURL生成URL
fileURL = fmt.Sprintf("%s/%s", q.Client.BaseURL.BucketURL.String(), objectKey)
}
return objectKey, fileURL, nil
}
// Delete 删除腾讯云COS文件
func (q *QCloudUploader) Delete(objectKey string) error {
_, err := q.Client.Object.Delete(context.Background(), objectKey)
if err != nil {
return fmt.Errorf("failed to delete from qcloud cos: %v", err)
}
return nil
}
// QiniuUploader 七牛云上传器
type QiniuUploader struct {
FormUploader *storage.FormUploader
BucketManager *storage.BucketManager
Bucket string
Domain string
Mac *qbox.Mac
}
// Upload 上传文件到七牛云
func (q *QiniuUploader) Upload(file multipart.File, filename string, size int64) (string, string, error) {
// 生成唯一文件名
ext := filepath.Ext(filename)
timestamp := time.Now().Unix()
randomStr := fmt.Sprintf("%d", timestamp)
newFilename := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(filename, ext), randomStr, ext)
// 按日期创建目录
dateDir := time.Now().Format("2006/01/02")
key := fmt.Sprintf("%s/%s", dateDir, newFilename)
// 生成上传凭证
putPolicy := storage.PutPolicy{
Scope: q.Bucket,
}
upToken := putPolicy.UploadToken(q.Mac)
// 读取文件内容
fileData := make([]byte, size)
_, err := file.Read(fileData)
if err != nil {
return "", "", fmt.Errorf("failed to read file: %v", err)
}
// 上传文件
ret := storage.PutRet{}
err = q.FormUploader.Put(context.Background(), &ret, upToken, key, bytes.NewReader(fileData), size, nil)
if err != nil {
return "", "", fmt.Errorf("failed to upload to qiniu: %v", err)
}
// 生成访问URL
var fileURL string
if q.Domain != "" {
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(q.Domain, "/"), key)
} else {
// 七牛云需要配置域名如果没有配置则返回key
fileURL = key
}
return key, fileURL, nil
}
// Delete 删除七牛云文件
func (q *QiniuUploader) Delete(key string) error {
err := q.BucketManager.Delete(q.Bucket, key)
if err != nil {
return fmt.Errorf("failed to delete from qiniu: %v", err)
}
return nil
}
2026-01-19 16:14:08 +08:00
// GetFileType 根据MIME类型判断文件类型
func GetFileType(mimeType string) string {
if strings.HasPrefix(mimeType, "image/") {
return "image"
} else if strings.HasPrefix(mimeType, "video/") {
return "video"
} else if strings.HasPrefix(mimeType, "application/pdf") ||
strings.HasPrefix(mimeType, "application/msword") ||
strings.HasPrefix(mimeType, "application/vnd.openxmlformats") ||
strings.HasPrefix(mimeType, "text/") {
return "document"
}
return "other"
}