Private
Public Access
1
0

新增: 云OSS存储集成(七牛云+阿里云)+多桶导航+GBK编码自动转换

This commit is contained in:
2026-05-05 03:18:47 +08:00
parent eb5b85e007
commit b4f4b4627d
34 changed files with 5225 additions and 48 deletions

View File

@@ -0,0 +1,299 @@
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))
}