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) }