522 lines
14 KiB
Go
522 lines
14 KiB
Go
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)
|
||
}
|