Private
Public Access
1
0
Files
u-desk/internal/oss/qiniu/bucket.go

300 lines
8.6 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 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))
}