package qiniu import ( "context" "crypto/hmac" "crypto/sha1" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "u-desk/internal/oss" ) // BucketAccessControl 空间访问控制类型 type BucketAccessControl int const ( // BucketAccessControlPublic 公开空间 (0) BucketAccessControlPublic BucketAccessControl = 0 // BucketAccessControlPrivate 私有空间 (1) BucketAccessControlPrivate BucketAccessControl = 1 ) // String 返回访问控制的字符串表示 func (a BucketAccessControl) String() string { switch a { case BucketAccessControlPublic: return "公开" case BucketAccessControlPrivate: return "私有" default: return "未知" } } // GetBucketDomains 获取空间绑定的域名列表 // 根据: https://developer.qiniu.com/kodo/api/3949/get-the-bucket-space-domain // // 返回: // - []string: 域名列表 // - error: 错误信息 // // 注意: // - 返回的域名包括七牛云提供的默认域名和用户绑定的自定义域名 // - 默认域名格式: ..qiniudns.com 或 ..clouddn.com func (c *Client) GetBucketDomains(ctx context.Context) ([]string, error) { // 构建查询参数 params := url.Values{} params.Set("tbl", c.config.Bucket) // 构建 URL // 格式: GET /v6/domain/list?tbl= apiURL := fmt.Sprintf("%s/v6/domain/list?%s", c.apiAPI, params.Encode()) // 创建请求 req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, oss.NewError("BUCKET_ERROR", "failed to create request", err) } // 使用 API 服务的 host 生成认证 // API 接口使用简单的查询字符串认证 path := "/v6/domain/list" queryString := params.Encode() host := "api.qiniu.com" authToken := c.generateAuthTokenWithQuery("GET", path, queryString, host, "application/x-www-form-urlencoded", nil) req.Header.Set("Host", host) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", authToken) // 发送请求 resp, err := c.httpClient.Do(req) if err != nil { return nil, oss.NewError("BUCKET_ERROR", "failed to get bucket domains", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, oss.NewError("BUCKET_ERROR", "failed to read response", err) } if resp.StatusCode != 200 { return nil, oss.NewError("BUCKET_ERROR", fmt.Sprintf("get bucket domains failed with status %d: %s", resp.StatusCode, string(body)), nil) } // 解析响应(JSON 数组) var domains []string if err := json.Unmarshal(body, &domains); err != nil { return nil, oss.NewError("BUCKET_ERROR", "failed to parse response", err) } return domains, nil } // GetBucketRegion 查询桶的真实区域 // API: POST https://uc.qbox.me/v2/buckets → 遍历匹配桶名获取 region func (c *Client) GetBucketRegion(ctx context.Context) (string, error) { // 使用 UC API 获取所有桶列表(含 region) req, err := http.NewRequestWithContext(ctx, "POST", "https://uc.qbox.me/v2/buckets", nil) if err != nil { return "", oss.NewError("BUCKET_ERROR", "failed to create request", err) } path := "/v2/buckets" host := "uc.qbox.me" authToken := c.generateAuthTokenWithQuery("POST", path, "", host, "application/x-www-form-urlencoded", nil) req.Header.Set("Host", host) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", authToken) resp, err := c.httpClient.Do(req) if err != nil { return "", oss.NewError("BUCKET_ERROR", "failed to query bucket region", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", oss.NewError("BUCKET_ERROR", "failed to read response", err) } if resp.StatusCode != 200 { return "", oss.NewError("BUCKET_ERROR", fmt.Sprintf("query bucket region failed with status %d: %s", resp.StatusCode, string(body)), nil) } var buckets []struct { ID string `json:"id"` Region string `json:"region"` } if err := json.Unmarshal(body, &buckets); err != nil { return "", oss.NewError("BUCKET_ERROR", "failed to parse response", err) } for _, b := range buckets { if b.ID == c.config.Bucket { return b.Region, nil } } return "", oss.NewError("BUCKET_ERROR", fmt.Sprintf("bucket %s not found in account", c.config.Bucket), nil) } // SetBucketAccess 设置空间访问权限(公开/私有) // 根据: https://developer.qiniu.com/kodo/api/3946/set-bucket-private // // 参数: // - access: BucketAccessControlPublic(公开) 或 BucketAccessControlPrivate(私有) // // 注意: // - 公开空间:文件可通过 URL 直接访问 // - 私有空间:文件访问需要下载凭证 // - 修改权限会影响该空间下所有文件的访问方式 func (c *Client) SetBucketAccess(ctx context.Context, access BucketAccessControl) error { // 构建查询参数 params := url.Values{} params.Set("bucket", c.config.Bucket) params.Set("private", fmt.Sprintf("%d", access)) // 构建 URL // 格式: POST /private?bucket=&private=<0|1> apiURL := fmt.Sprintf("%s/private?%s", c.apiAPI, params.Encode()) // 创建请求 req, err := http.NewRequestWithContext(ctx, "POST", apiURL, nil) if err != nil { return oss.NewError("BUCKET_ERROR", "failed to create request", err) } // 使用 API 服务的 host 生成认证 path := "/private" queryString := params.Encode() host := "api.qiniu.com" authToken := c.generateAuthTokenWithQuery("POST", path, queryString, host, "application/x-www-form-urlencoded", nil) req.Header.Set("Host", host) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", authToken) // 发送请求 resp, err := c.httpClient.Do(req) if err != nil { return oss.NewError("BUCKET_ERROR", "failed to set bucket access", err) } defer resp.Body.Close() if resp.StatusCode == 200 { return nil } body, _ := io.ReadAll(resp.Body) return oss.NewError("BUCKET_ERROR", fmt.Sprintf("set bucket access failed with status %d: %s", resp.StatusCode, string(body)), nil) } // SetBucketPublic 设置空间为公开空间 // 便捷方法:将空间设置为公开访问 func (c *Client) SetBucketPublic(ctx context.Context) error { return c.SetBucketAccess(ctx, BucketAccessControlPublic) } // SetBucketPrivate 设置空间为私有空间 // 便捷方法:将空间设置为私有访问 func (c *Client) SetBucketPrivate(ctx context.Context) error { return c.SetBucketAccess(ctx, BucketAccessControlPrivate) } // BucketInfo 空间信息 type BucketInfo struct { Name string // 空间名称 Region string // 区域 Domains []string // 绑定的域名列表 IsPrivate bool // 是否为私有空间 } // GetBucketInfo 获取空间信息 // 组合方法:获取空间的域名列表和访问权限等信息 // // 注意: // - 该方法会调用多个 API 接口获取完整信息 // - IsPrivate 字段无法通过 API 直接获取,需要通过测试文件访问来确定 func (c *Client) GetBucketInfo(ctx context.Context) (*BucketInfo, error) { // 获取域名列表 domains, err := c.GetBucketDomains(ctx) if err != nil { return nil, err } // 构建基本信息 info := &BucketInfo{ Name: c.config.Bucket, Region: c.config.Region, Domains: domains, // IsPrivate 需要通过其他方式确定 } return info, nil } // CheckBucketAccess 检查空间访问权限 // 通过尝试访问一个不存在的文件来判断空间是否为私有 // // 返回: // - bool: true=私有空间, false=公开空间 // - error: 错误信息 // // 注意: // - 该方法会发送一个测试请求来判断空间权限 // - 如果空间内没有文件,可能无法准确判断 func (c *Client) CheckBucketAccess(ctx context.Context) (bool, error) { // 尝试获取一个不存在的文件的信息 // 如果是公开空间,会返回明确的"文件不存在"错误 // 如果是私有空间,会返回认证错误 testKey := fmt.Sprintf("__qiniu_test_access_check_%d__", time.Now().UnixNano()) _, err := c.GetFileInfo(ctx, testKey) if err == oss.ErrFileNotFound { // 返回文件不存在,说明是公开空间 return false, nil } // 其他错误情况,可能需要根据错误信息判断 if err != nil { // 检查错误信息中是否包含认证相关的内容 errStr := err.Error() if contains(errStr, "permission") || contains(errStr, "unauthorized") || contains(errStr, "token") { return true, nil // 私有空间 } } // 默认假设为公开空间 return false, nil } // contains 辅助函数:检查字符串是否包含子串(忽略大小写) func contains(str, substr string) bool { return len(str) >= len(substr) && (str == substr || len(str) > len(substr) && containsIgnoreCase(str, substr)) } func containsIgnoreCase(str, substr string) bool { // 简化实现,实际使用时可以使用 strings.ToLower for i := 0; i <= len(str)-len(substr); i++ { match := true for j := 0; j < len(substr); j++ { c1 := str[i+j] c2 := substr[j] if c1 >= 'A' && c1 <= 'Z' { c1 += 32 } if c2 >= 'A' && c2 <= 'Z' { c2 += 32 } if c1 != c2 { match = false break } } if match { return true } } return false } // ListBuckets 列出所有存储桶 func ListBuckets(accessKey, secretKey string) ([]oss.BucketEntry, error) { signingStr := "/buckets\n" token := accessKey + ":" + signHmacSha1(secretKey, signingStr) req, _ := http.NewRequest("POST", "https://rs.qbox.me/buckets", nil) req.Header.Set("Authorization", "QBox "+token) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("列举存储桶失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("列举存储桶失败: HTTP %d: %s", resp.StatusCode, string(body)) } var names []string if err := json.NewDecoder(resp.Body).Decode(&names); err != nil { return nil, fmt.Errorf("解析存储桶列表失败: %w", err) } entries := make([]oss.BucketEntry, len(names)) for i, name := range names { entries[i] = oss.BucketEntry{Name: name} } return entries, nil } func signHmacSha1(secretKey, data string) string { mac := hmac.New(sha1.New, []byte(secretKey)) mac.Write([]byte(data)) return base64.URLEncoding.EncodeToString(mac.Sum(nil)) }