Private
Public Access
1
0
Files
u-desk/internal/oss/aliyun/lifecycle.go

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