Private
Public Access
1
0

新增: 云OSS存储集成(七牛云+阿里云)+多桶导航+GBK编码自动转换

This commit is contained in:
2026-05-05 03:18:47 +08:00
parent eb5b85e007
commit b4f4b4627d
34 changed files with 5225 additions and 48 deletions

View File

@@ -0,0 +1,521 @@
package aliyun
import (
"bytes"
"context"
"crypto/md5"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/http"
"strings"
"time"
"u-desk/internal/oss"
)
// ============ 生命周期相关数据结构 ============
// LifecycleStorageClass 存储类型枚举
type LifecycleStorageClass string
const (
// StorageClassStandard 标准存储
StorageClassStandard LifecycleStorageClass = "Standard"
// StorageClassIA 低频存储 (Infrequent Access)
StorageClassIA LifecycleStorageClass = "IA"
// StorageClassArchive 归档存储
StorageClassArchive LifecycleStorageClass = "Archive"
// StorageClassColdArchive 冷归档存储
StorageClassColdArchive LifecycleStorageClass = "ColdArchive"
)
// LifecycleRule 生命周期规则
type LifecycleRule struct {
ID string `xml:"ID"` // 规则 ID
Prefix string `xml:"Prefix"` // 前缀(应用于匹配的文件)
Status string `xml:"Status"` // 状态Enabled 或 Disabled
// Expiration 过期删除配置
Expiration *LifecycleExpiration `xml:"Expiration,omitempty"`
// Transition 存储类型转换配置(可以有多个)
Transitions []LifecycleTransition `xml:"Transition,omitempty"`
// AbortMultipartUpload 中止未完成的分片上传
AbortMultipartUpload *LifecycleAbortMultipartUpload `xml:"AbortMultipartUpload,omitempty"`
// Filter 过滤器(与 Prefix 二选一)
Filter *LifecycleFilter `xml:"Filter,omitempty"`
}
// LifecycleExpiration 过期删除配置
type LifecycleExpiration struct {
Days int `xml:"Days,omitempty"` // 多少天后过期
CreatedBeforeDate string `xml:"CreatedBeforeDate,omitempty"` // 指定日期之前创建的文件过期格式2023-01-01T00:00:00.000Z
ExpiredObjectDeleteMarker bool `xml:"ExpiredObjectDeleteMarker,omitempty"` // 删除过期删除标记
}
// LifecycleTransition 存储类型转换配置
type LifecycleTransition struct {
Days int `xml:"Days,omitempty"` // 多少天后转换
CreatedBeforeDate string `xml:"CreatedBeforeDate,omitempty"` // 指定日期之前创建的文件转换
StorageClass LifecycleStorageClass `xml:"StorageClass"` // 目标存储类型
}
// LifecycleAbortMultipartUpload 中止分片上传配置
type LifecycleAbortMultipartUpload struct {
Days int `xml:"Days,omitempty"` // 多少天后中止
CreatedBeforeDate string `xml:"CreatedBeforeDate,omitempty"` // 指定日期之前创建的分片上传中止
}
// LifecycleFilter 过滤器(用于更精细的规则匹配)
type LifecycleFilter struct {
// Prefix 前缀
Prefix string `xml:"Prefix,omitempty"`
// Tag 标签(可以有多个)
Tag []LifecycleTag `xml:"Tag,omitempty"`
// Not 非匹配条件
Not *LifecycleNotFilter `xml:"Not,omitempty"`
}
// LifecycleTag 标签
type LifecycleTag struct {
Key string `xml:"Key"`
Value string `xml:"Value"`
}
// LifecycleNotFilter 非匹配条件
type LifecycleNotFilter struct {
Prefix string `xml:"Prefix,omitempty"`
Tag LifecycleTag `xml:"Tag,omitempty"`
}
// LifecycleConfiguration 生命周期配置
type LifecycleConfiguration struct {
XMLName xml.Name `xml:"LifecycleConfiguration"`
Rules []LifecycleRule `xml:"Rule"`
}
// ============ 生命周期管理方法 ============
// SetBucketLifecycle 设置生命周期规则
// 参考: https://help.aliyun.com/zh/oss/developer-reference/put-bucket-lifecycle
func (c *Client) SetBucketLifecycle(ctx context.Context, rules []LifecycleRule) error {
date := time.Now().UTC().Format(http.TimeFormat)
// 构建请求体
config := LifecycleConfiguration{
Rules: rules,
}
bodyBytes, err := xml.Marshal(config)
if err != nil {
return oss.NewError("LIFECYCLE_ERROR", "failed to marshal lifecycle config", err)
}
// 添加 XML 声明
bodyWithHeader := []byte(xml.Header + string(bodyBytes))
// 构建签名字符串 - 对于 bucket 级别操作,需要计算 Content-MD5
contentType := "application/xml"
path := "/" + c.config.Bucket + "/?lifecycle"
// 计算 Content-MD5
hash := md5.Sum(bodyWithHeader)
contentMD5 := base64.StdEncoding.EncodeToString(hash[:])
signature := c.generateSignature("PUT", path, contentType, date, contentMD5)
scheme := "https://"
if !c.config.UseHTTPS {
scheme = "http://"
}
url := scheme + c.config.Bucket + "." + c.config.Endpoint + "/?lifecycle"
req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewReader(bodyWithHeader))
if err != nil {
return oss.NewError("LIFECYCLE_ERROR", "failed to create request", err)
}
req.Header.Set("Date", date)
req.Header.Set("Authorization", "OSS "+c.config.AccessKeyID+":"+signature)
req.Header.Set("Content-Type", contentType)
req.Header.Set("Content-MD5", contentMD5)
resp, err := c.httpClient.Do(req)
if err != nil {
return oss.NewError("LIFECYCLE_ERROR", "failed to set lifecycle", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return oss.NewError("LIFECYCLE_ERROR",
fmt.Sprintf("set lifecycle failed with status %d: %s", resp.StatusCode, string(body)), nil)
}
return nil
}
// GetBucketLifecycle 获取生命周期规则
// 参考: https://help.aliyun.com/zh/oss/developer-reference/get-bucket-lifecycle
func (c *Client) GetBucketLifecycle(ctx context.Context) ([]LifecycleRule, error) {
date := time.Now().UTC().Format(http.TimeFormat)
// 构建签名字符串 - 使用 bucket/ 前缀
path := "/" + c.config.Bucket + "/?lifecycle"
signature := c.generateSignature("GET", path, "", date, "")
scheme := "https://"
if !c.config.UseHTTPS {
scheme = "http://"
}
url := scheme + c.config.Bucket + "." + c.config.Endpoint + "/?lifecycle"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, oss.NewError("LIFECYCLE_ERROR", "failed to create request", err)
}
req.Header.Set("Date", date)
req.Header.Set("Authorization", "OSS "+c.config.AccessKeyID+":"+signature)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, oss.NewError("LIFECYCLE_ERROR", "failed to get lifecycle", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, oss.NewError("LIFECYCLE_ERROR", "failed to read response", err)
}
if resp.StatusCode == 404 {
// 没有设置生命周期规则
return nil, nil
}
if resp.StatusCode != 200 {
return nil, oss.NewError("LIFECYCLE_ERROR",
fmt.Sprintf("get lifecycle failed with status %d: %s", resp.StatusCode, string(body)), nil)
}
// 解析 XML 响应
var config LifecycleConfiguration
if err := xml.Unmarshal(body, &config); err != nil {
return nil, oss.NewError("LIFECYCLE_ERROR", "failed to parse response", err)
}
return config.Rules, nil
}
// DeleteBucketLifecycle 删除生命周期规则
// 参考: https://help.aliyun.com/zh/oss/developer-reference/delete-bucket-lifecycle
func (c *Client) DeleteBucketLifecycle(ctx context.Context) error {
date := time.Now().UTC().Format(http.TimeFormat)
// 构建签名字符串 - 使用 bucket/ 前缀
path := "/" + c.config.Bucket + "/?lifecycle"
signature := c.generateSignature("DELETE", path, "", date, "")
scheme := "https://"
if !c.config.UseHTTPS {
scheme = "http://"
}
url := scheme + c.config.Bucket + "." + c.config.Endpoint + "/?lifecycle"
req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil)
if err != nil {
return oss.NewError("LIFECYCLE_ERROR", "failed to create request", err)
}
req.Header.Set("Date", date)
req.Header.Set("Authorization", "OSS "+c.config.AccessKeyID+":"+signature)
resp, err := c.httpClient.Do(req)
if err != nil {
return oss.NewError("LIFECYCLE_ERROR", "failed to delete lifecycle", err)
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
body, _ := io.ReadAll(resp.Body)
return oss.NewError("LIFECYCLE_ERROR",
fmt.Sprintf("delete lifecycle failed with status %d: %s", resp.StatusCode, string(body)), nil)
}
return nil
}
// ============ 便捷方法 ============
// SetExpirationRule 设置过期删除规则
// 为指定前缀的文件设置过期删除天数
func (c *Client) SetExpirationRule(ctx context.Context, ruleID, prefix string, days int) error {
// 获取现有规则
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
// 如果没有现有规则,创建新的规则列表
rules = []LifecycleRule{}
}
// 检查是否已存在相同 ID 的规则,如果存在则更新,否则添加
found := false
for i, r := range rules {
if r.ID == ruleID {
// 更新现有规则
rules[i].Prefix = prefix
rules[i].Status = "Enabled"
rules[i].Expiration = &LifecycleExpiration{Days: days}
found = true
break
}
}
if !found {
// 添加新规则
rule := LifecycleRule{
ID: ruleID,
Prefix: prefix,
Status: "Enabled",
Expiration: &LifecycleExpiration{
Days: days,
},
}
rules = append(rules, rule)
}
return c.SetBucketLifecycle(ctx, rules)
}
// SetTransitionRule 设置存储类型转换规则
// 为指定前缀的文件设置存储类型转换
func (c *Client) SetTransitionRule(ctx context.Context, ruleID, prefix string, days int, storageClass LifecycleStorageClass) error {
// 获取现有规则
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
// 如果没有现有规则,创建新的规则列表
rules = []LifecycleRule{}
}
// 检查是否已存在相同 ID 的规则,如果存在则更新,否则添加
found := false
for i, r := range rules {
if r.ID == ruleID {
// 更新现有规则
rules[i].Prefix = prefix
rules[i].Status = "Enabled"
rules[i].Transitions = []LifecycleTransition{
{Days: days, StorageClass: storageClass},
}
found = true
break
}
}
if !found {
// 添加新规则
rule := LifecycleRule{
ID: ruleID,
Prefix: prefix,
Status: "Enabled",
Transitions: []LifecycleTransition{
{
Days: days,
StorageClass: storageClass,
},
},
}
rules = append(rules, rule)
}
return c.SetBucketLifecycle(ctx, rules)
}
// SetAbortMultipartUploadRule 设置中止分片上传规则
// 为指定前缀的文件设置中止未完成的分片上传
func (c *Client) SetAbortMultipartUploadRule(ctx context.Context, ruleID, prefix string, days int) error {
rule := LifecycleRule{
ID: ruleID,
Prefix: prefix,
Status: "Enabled",
AbortMultipartUpload: &LifecycleAbortMultipartUpload{
Days: days,
},
}
return c.SetBucketLifecycle(ctx, []LifecycleRule{rule})
}
// SetCombinedRule 设置组合生命周期规则
// 同时支持过期删除和存储类型转换
func (c *Client) SetCombinedRule(ctx context.Context, ruleID, prefix string, expirationDays int, transitionDays int, storageClass LifecycleStorageClass) error {
rule := LifecycleRule{
ID: ruleID,
Prefix: prefix,
Status: "Enabled",
}
// 设置过期删除
if expirationDays > 0 {
rule.Expiration = &LifecycleExpiration{
Days: expirationDays,
}
}
// 设置存储类型转换
if transitionDays > 0 && storageClass != "" {
rule.Transitions = []LifecycleTransition{
{
Days: transitionDays,
StorageClass: storageClass,
},
}
}
return c.SetBucketLifecycle(ctx, []LifecycleRule{rule})
}
// SetTempFileRule 设置临时文件规则
// 为临时文件目录设置规则:先转为低频存储,然后删除
func (c *Client) SetTempFileRule(ctx context.Context, prefix string, toIADays int, deleteDays int) error {
ruleID := fmt.Sprintf("temp-files-%s", strings.ReplaceAll(prefix, "/", "-"))
// 获取现有规则
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
return err
}
// 创建新规则
rule := LifecycleRule{
ID: ruleID,
Prefix: prefix,
Status: "Enabled",
}
// 设置存储类型转换
if toIADays > 0 {
rule.Transitions = []LifecycleTransition{
{
Days: toIADays,
StorageClass: StorageClassIA,
},
}
}
// 设置过期删除
if deleteDays > 0 {
rule.Expiration = &LifecycleExpiration{
Days: deleteDays,
}
}
// 添加到现有规则
rules = append(rules, rule)
return c.SetBucketLifecycle(ctx, rules)
}
// ClearTempFileRule 清除临时文件规则
func (c *Client) ClearTempFileRule(ctx context.Context, prefix string) error {
ruleID := fmt.Sprintf("temp-files-%s", strings.ReplaceAll(prefix, "/", "-"))
// 获取现有规则
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
return err
}
// 过滤掉要删除的规则
newRules := make([]LifecycleRule, 0, len(rules))
for _, rule := range rules {
if rule.ID != ruleID {
newRules = append(newRules, rule)
}
}
// 更新规则
if len(newRules) == 0 {
return c.DeleteBucketLifecycle(ctx)
}
return c.SetBucketLifecycle(ctx, newRules)
}
// ListLifecycleRules 列出所有生命周期规则(带详细信息)
func (c *Client) ListLifecycleRules(ctx context.Context) ([]LifecycleRule, error) {
return c.GetBucketLifecycle(ctx)
}
// DisableLifecycleRule 禁用生命周期规则
func (c *Client) DisableLifecycleRule(ctx context.Context, ruleID string) error {
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
return err
}
// 找到并禁用规则
found := false
for i := range rules {
if rules[i].ID == ruleID {
rules[i].Status = "Disabled"
found = true
break
}
}
if !found {
return oss.NewError("LIFECYCLE_ERROR", "rule not found: "+ruleID, nil)
}
return c.SetBucketLifecycle(ctx, rules)
}
// EnableLifecycleRule 启用生命周期规则
func (c *Client) EnableLifecycleRule(ctx context.Context, ruleID string) error {
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
return err
}
// 找到并启用规则
found := false
for i := range rules {
if rules[i].ID == ruleID {
rules[i].Status = "Enabled"
found = true
break
}
}
if !found {
return oss.NewError("LIFECYCLE_ERROR", "rule not found: "+ruleID, nil)
}
return c.SetBucketLifecycle(ctx, rules)
}
// DeleteLifecycleRule 删除生命周期规则
func (c *Client) DeleteLifecycleRule(ctx context.Context, ruleID string) error {
rules, err := c.GetBucketLifecycle(ctx)
if err != nil {
return err
}
// 过滤掉要删除的规则
newRules := make([]LifecycleRule, 0, len(rules))
for _, rule := range rules {
if rule.ID != ruleID {
newRules = append(newRules, rule)
}
}
// 更新规则
if len(newRules) == 0 {
return c.DeleteBucketLifecycle(ctx)
}
return c.SetBucketLifecycle(ctx, newRules)
}