300 lines
8.6 KiB
Go
300 lines
8.6 KiB
Go
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: 错误信息
|
||
//
|
||
// 注意:
|
||
// - 返回的域名包括七牛云提供的默认域名和用户绑定的自定义域名
|
||
// - 默认域名格式: <bucket>.<region>.qiniudns.com 或 <bucket>.<region>.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=<bucketName>
|
||
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
|
||
}
|
||
|
||
// 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=<bucketName>&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))
|
||
}
|