阿里云OSS设置
This commit is contained in:
@@ -4,51 +4,59 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AESKey 从环境变量或配置中获取,这里使用默认密钥(生产环境应该从配置读取)
|
||||
var defaultAESKey = []byte("your-32-byte-secret-key-here!!") // 32 bytes for AES-256
|
||||
// defaultAESKey 必须精确为 16/24/32 字节;此处为 AES-256(32 字节)
|
||||
var defaultAESKey = []byte("art-code-blog-aes256-key-32b!!!!") // len == 32
|
||||
|
||||
// GetAESKey 获取AES密钥(应该从配置文件或环境变量读取)
|
||||
// GetAESKey 获取 AES 密钥:优先 AES_ENCRYPTION_KEY,否则使用默认密钥;长度非法时用 SHA-256 规范化为 32 字节
|
||||
func GetAESKey() []byte {
|
||||
// TODO: 从配置文件或环境变量读取密钥
|
||||
// key := os.Getenv("AES_ENCRYPTION_KEY")
|
||||
// if key == "" {
|
||||
// return defaultAESKey
|
||||
// }
|
||||
// return []byte(key)
|
||||
return defaultAESKey
|
||||
key := strings.TrimSpace(os.Getenv("AES_ENCRYPTION_KEY"))
|
||||
if key == "" {
|
||||
return normalizeAESKey(defaultAESKey)
|
||||
}
|
||||
return normalizeAESKey([]byte(key))
|
||||
}
|
||||
|
||||
func normalizeAESKey(key []byte) []byte {
|
||||
switch len(key) {
|
||||
case 16, 24, 32:
|
||||
return key
|
||||
default:
|
||||
sum := sha256.Sum256(key)
|
||||
out := make([]byte, 32)
|
||||
copy(out, sum[:])
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptAES 使用AES-256-GCM加密数据
|
||||
func EncryptAES(plaintext string) (string, error) {
|
||||
key := GetAESKey()
|
||||
|
||||
// Create cipher block
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create nonce
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
|
||||
// Encode to base64
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
@@ -56,25 +64,21 @@ func EncryptAES(plaintext string) (string, error) {
|
||||
func DecryptAES(encrypted string) (string, error) {
|
||||
key := GetAESKey()
|
||||
|
||||
// Decode from base64
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create cipher block
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract nonce
|
||||
nonceSize := aesGCM.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return "", errors.New("ciphertext too short")
|
||||
@@ -82,7 +86,6 @@ func DecryptAES(encrypted string) (string, error) {
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -3,6 +3,8 @@ package utils
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
@@ -64,6 +66,63 @@ type OSSUploader interface {
|
||||
Delete(filePath string) error
|
||||
}
|
||||
|
||||
// NormalizeOSSEndpoint 补全 endpoint 协议前缀
|
||||
func NormalizeOSSEndpoint(endpoint string) string {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
|
||||
return "https://" + endpoint
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// LoadAliyunConfigFromEnv 从环境变量加载阿里云 OSS 配置;前 4 项齐全返回 true
|
||||
func LoadAliyunConfigFromEnv() (*OSSConfig, bool) {
|
||||
accessKeyID := strings.TrimSpace(os.Getenv("OSS_ACCESS_KEY_ID"))
|
||||
accessKeySecret := strings.TrimSpace(os.Getenv("OSS_ACCESS_KEY_SECRET"))
|
||||
endpoint := NormalizeOSSEndpoint(os.Getenv("OSS_ENDPOINT"))
|
||||
bucket := strings.TrimSpace(os.Getenv("OSS_BUCKET"))
|
||||
domain := strings.TrimSpace(os.Getenv("OSS_DOMAIN"))
|
||||
|
||||
if accessKeyID == "" || accessKeySecret == "" || endpoint == "" || bucket == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &OSSConfig{
|
||||
StorageType: string(StorageAliyun),
|
||||
OSSAccessKeyID: accessKeyID,
|
||||
OSSAccessKeySecret: accessKeySecret,
|
||||
OSSEndpoint: endpoint,
|
||||
OSSBucket: bucket,
|
||||
OSSDomain: domain,
|
||||
}, true
|
||||
}
|
||||
|
||||
// HasAliyunEnvConfig 环境变量中是否具备完整的阿里云 OSS 配置
|
||||
func HasAliyunEnvConfig() bool {
|
||||
_, ok := LoadAliyunConfigFromEnv()
|
||||
return ok
|
||||
}
|
||||
|
||||
// BuildBlogObjectKey 生成博客上传对象键:blog/{YYYYMMDD}/{随机名}{ext}
|
||||
func BuildBlogObjectKey(originalFilename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(originalFilename))
|
||||
randomName := randomHex(16)
|
||||
dateDir := time.Now().Format("20060102")
|
||||
return fmt.Sprintf("blog/%s/%s%s", dateDir, randomName, ext)
|
||||
}
|
||||
|
||||
func randomHex(nBytes int) string {
|
||||
buf := make([]byte, nBytes)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
// 退化到时间戳,仍保证文件名唯一性
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// GetOSSUploader 根据存储类型获取上传器
|
||||
func GetOSSUploader(config *OSSConfig) (OSSUploader, error) {
|
||||
switch StorageType(config.StorageType) {
|
||||
@@ -158,6 +217,7 @@ func GetOSSUploader(config *OSSConfig) (OSSUploader, error) {
|
||||
}
|
||||
endpoint = fmt.Sprintf("https://oss-%s.aliyuncs.com", region)
|
||||
}
|
||||
endpoint = NormalizeOSSEndpoint(endpoint)
|
||||
|
||||
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
|
||||
if err != nil {
|
||||
@@ -262,39 +322,25 @@ type LocalUploader struct {
|
||||
|
||||
// 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)
|
||||
objectKey := BuildBlogObjectKey(filename)
|
||||
filePath := filepath.Join(l.BasePath, filepath.FromSlash(objectKey))
|
||||
uploadDir := filepath.Dir(filePath)
|
||||
|
||||
// 按日期创建目录
|
||||
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)
|
||||
|
||||
fileURL := fmt.Sprintf("%s/%s", strings.TrimSuffix(l.BaseURL, "/"), objectKey)
|
||||
return filePath, fileURL, nil
|
||||
}
|
||||
|
||||
@@ -326,30 +372,19 @@ type AliyunUploader struct {
|
||||
|
||||
// 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)
|
||||
objectKey := BuildBlogObjectKey(filename)
|
||||
|
||||
// 按日期创建目录
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
objectKey := fmt.Sprintf("%s/%s", dateDir, newFilename)
|
||||
|
||||
// 上传文件
|
||||
err := a.Bucket.PutObject(objectKey, file)
|
||||
err := a.Bucket.PutObject(objectKey, file, oss.ObjectACL(oss.ACLPublicRead))
|
||||
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)
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(NormalizeOSSEndpoint(a.Domain), "/"), objectKey)
|
||||
} else {
|
||||
// 从endpoint中提取region,格式为 https://oss-region.aliyuncs.com
|
||||
endpoint := a.Bucket.Client.Config.Endpoint
|
||||
fileURL = fmt.Sprintf("%s/%s", endpoint, objectKey)
|
||||
endpoint := NormalizeOSSEndpoint(a.Bucket.Client.Config.Endpoint)
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(endpoint, "/"), objectKey)
|
||||
}
|
||||
|
||||
return objectKey, fileURL, nil
|
||||
@@ -364,6 +399,42 @@ func (a *AliyunUploader) Delete(objectKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestOSSConnection 验证云存储配置是否可用(ListObjects / 等价只读探测)
|
||||
func TestOSSConnection(config *OSSConfig) error {
|
||||
uploader, err := GetOSSUploader(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u := uploader.(type) {
|
||||
case *LocalUploader:
|
||||
if err := os.MkdirAll(u.BasePath, 0755); err != nil {
|
||||
return fmt.Errorf("local storage path not writable: %v", err)
|
||||
}
|
||||
return nil
|
||||
case *AliyunUploader:
|
||||
_, err := u.Bucket.ListObjects(oss.MaxKeys(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("aliyun oss connection failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
case *QCloudUploader:
|
||||
_, _, err := u.Client.Bucket.Get(context.Background(), &cos.BucketGetOptions{MaxKeys: 1})
|
||||
if err != nil {
|
||||
return fmt.Errorf("qcloud cos connection failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
case *QiniuUploader:
|
||||
_, _, _, _, err := u.BucketManager.ListFiles(u.Bucket, "", "", "", 1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("qiniu connection failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported storage type: %s", config.StorageType)
|
||||
}
|
||||
}
|
||||
|
||||
// QCloudUploader 腾讯云COS上传器
|
||||
type QCloudUploader struct {
|
||||
Client *cos.Client
|
||||
@@ -373,29 +444,18 @@ type QCloudUploader struct {
|
||||
|
||||
// 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)
|
||||
objectKey := BuildBlogObjectKey(filename)
|
||||
|
||||
// 按日期创建目录
|
||||
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)
|
||||
fileURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(q.Client.BaseURL.BucketURL.String(), "/"), objectKey)
|
||||
}
|
||||
|
||||
return objectKey, fileURL, nil
|
||||
@@ -421,17 +481,8 @@ type QiniuUploader struct {
|
||||
|
||||
// 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)
|
||||
key := BuildBlogObjectKey(filename)
|
||||
|
||||
// 按日期创建目录
|
||||
dateDir := time.Now().Format("2006/01/02")
|
||||
key := fmt.Sprintf("%s/%s", dateDir, newFilename)
|
||||
|
||||
// 生成上传凭证
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: q.Bucket,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user