新增: SFTP直连+网站预览+OSS区域嗅探+热键+BGM播放
This commit is contained in:
@@ -99,6 +99,55 @@ func (c *Client) GetBucketDomains(ctx context.Context) ([]string, error) {
|
||||
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
|
||||
//
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,12 +19,12 @@ import (
|
||||
|
||||
// Config 七牛云配置
|
||||
type Config struct {
|
||||
AccessKey string // 访问密钥
|
||||
SecretKey string // 秘钥
|
||||
Bucket string // 存储空间名称
|
||||
Region string // 区域 z0=华东, as0=亚太0区
|
||||
UseHTTPS bool // 是否使用 HTTPS
|
||||
UploadDomain string // 上传域名(可选,默认根据 Region 自动选择)
|
||||
AccessKey string // 访问密钥
|
||||
SecretKey string // 秘钥
|
||||
Bucket string // 存储空间名称
|
||||
Region string // 区域 z0=华东, z2=华南, as0=亚太0区
|
||||
UseHTTPS bool // 是否使用 HTTPS
|
||||
DownloadDomain string // 缓存的下载域名(由 resolveDownloadDomain 自动设置)
|
||||
}
|
||||
|
||||
// Client 七牛云客户端
|
||||
@@ -61,84 +62,31 @@ func NewClient(config *Config) (*Client, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateSignature 生成七牛云管理凭证签名
|
||||
// 根据官方文档:https://developer.qiniu.com/kodo/1201/access-token
|
||||
func (c *Client) generateSignature(method, path, host, contentType string, body []byte) string {
|
||||
// 七牛云管理凭证签名格式:
|
||||
// signingStr = Method + " " + Path + "\nHost: " + Host + "\n" + [Content-Type] + "\n\n" + [body]
|
||||
var signingStr string
|
||||
|
||||
// 1. Method + " " + Path
|
||||
signingStr = method + " " + path
|
||||
|
||||
// 2. Host header
|
||||
signingStr += "\nHost: " + host
|
||||
|
||||
// 3. Content-Type header (如果设置了)
|
||||
if contentType != "" {
|
||||
signingStr += "\nContent-Type: " + contentType
|
||||
}
|
||||
|
||||
// 4. 两个连续换行符
|
||||
signingStr += "\n\n"
|
||||
|
||||
// 5. Body (如果设置了 Content-Type 且不是 application/octet-stream)
|
||||
if contentType != "" && contentType != "application/octet-stream" && len(body) > 0 {
|
||||
signingStr += string(body)
|
||||
}
|
||||
|
||||
// 使用 HMAC-SHA1 签名
|
||||
h := hmac.New(sha1.New, []byte(c.config.SecretKey))
|
||||
h.Write([]byte(signingStr))
|
||||
|
||||
// Base64 URL 安全编码
|
||||
signature := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
// generateAuthToken 生成管理认证 Token
|
||||
func (c *Client) generateAuthToken(method, path, host, contentType string, body []byte) string {
|
||||
signature := c.generateSignature(method, path, host, contentType, body)
|
||||
return "Qiniu " + c.config.AccessKey + ":" + signature
|
||||
return c.generateAuthTokenWithQuery(method, path, "", host, contentType, body)
|
||||
}
|
||||
|
||||
// generateAuthTokenWithQuery 生成管理认证 Token(支持 query string)
|
||||
// https://developer.qiniu.com/kodo/1201/access-token
|
||||
func (c *Client) generateAuthTokenWithQuery(method, path, query, host, contentType string, body []byte) string {
|
||||
// 七牛云管理凭证签名格式:
|
||||
// 如果 query 为非空字符串: signingStr = Method + " " + Path + "?" + query + "\nHost: " + Host + ...
|
||||
// 如果 query 为空: signingStr = Method + " " + Path + "\nHost: " + Host + ...
|
||||
var signingStr string
|
||||
|
||||
// 1. Method + " " + Path
|
||||
signingStr = method + " " + path
|
||||
|
||||
// 2. Query string (如果有)
|
||||
if query != "" {
|
||||
signingStr += "?" + query
|
||||
}
|
||||
|
||||
// 3. Host header
|
||||
signingStr += "\nHost: " + host
|
||||
|
||||
// 4. Content-Type header (如果设置了)
|
||||
if contentType != "" {
|
||||
signingStr += "\nContent-Type: " + contentType
|
||||
}
|
||||
|
||||
// 5. 两个连续换行符
|
||||
signingStr += "\n\n"
|
||||
|
||||
// 6. Body (如果设置了 Content-Type 且不是 application/octet-stream)
|
||||
if contentType != "" && contentType != "application/octet-stream" && len(body) > 0 {
|
||||
signingStr += string(body)
|
||||
}
|
||||
|
||||
// 使用 HMAC-SHA1 签名
|
||||
h := hmac.New(sha1.New, []byte(c.config.SecretKey))
|
||||
h.Write([]byte(signingStr))
|
||||
|
||||
// Base64 URL 安全编码
|
||||
signature := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
return "Qiniu " + c.config.AccessKey + ":" + signature
|
||||
@@ -152,12 +100,11 @@ func (c *Client) encodeEntry(key string) string {
|
||||
|
||||
// getUploadDomain 获取上传域名
|
||||
func (c *Client) getUploadDomain() string {
|
||||
// 如果配置了自定义上传域名,使用自定义的
|
||||
if c.config.UploadDomain != "" {
|
||||
if c.config.DownloadDomain != "" {
|
||||
if c.config.UseHTTPS {
|
||||
return "https://" + c.config.UploadDomain
|
||||
return "https://" + c.config.DownloadDomain
|
||||
}
|
||||
return "http://" + c.config.UploadDomain
|
||||
return "http://" + c.config.DownloadDomain
|
||||
}
|
||||
|
||||
// 根据区域选择默认上传域名
|
||||
@@ -264,85 +211,169 @@ func (c *Client) Upload(ctx context.Context, key string, reader io.Reader, optio
|
||||
return uploadClient.Upload(ctx, key, reader)
|
||||
}
|
||||
|
||||
// generateUploadToken 生成上传凭证
|
||||
func (c *Client) generateUploadToken(key string) string {
|
||||
// 七牛云上传凭证的生成
|
||||
// 1. 创建 putPolicy
|
||||
putPolicy := fmt.Sprintf(`{"scope":"%s:%s","deadline":%d}`,
|
||||
c.config.Bucket, key, time.Now().Add(1*time.Hour).Unix())
|
||||
|
||||
// 2. 对 putPolicy 进行 base64 URL 编码
|
||||
encodedPutPolicy := base64.URLEncoding.EncodeToString([]byte(putPolicy))
|
||||
|
||||
// 3. 对 encodedPutPolicy 进行 HMAC-SHA1 签名
|
||||
// generateToken 生成上传凭证
|
||||
func (c *Client) generateToken(scope string) string {
|
||||
putPolicy := fmt.Sprintf(`{"scope":"%s","deadline":%d}`, scope, time.Now().Add(1*time.Hour).Unix())
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(putPolicy))
|
||||
h := hmac.New(sha1.New, []byte(c.config.SecretKey))
|
||||
h.Write([]byte(encodedPutPolicy))
|
||||
encodedSign := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
// 4. 组合 token
|
||||
return c.config.AccessKey + ":" + encodedSign + ":" + encodedPutPolicy
|
||||
h.Write([]byte(encoded))
|
||||
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
return c.config.AccessKey + ":" + sign + ":" + encoded
|
||||
}
|
||||
|
||||
func (c *Client) generateUploadToken(key string) string {
|
||||
return c.generateToken(c.config.Bucket + ":" + key)
|
||||
}
|
||||
|
||||
// generateBucketToken 生成 bucket 级别的上传凭证(用于分片上传 v2)
|
||||
func (c *Client) generateBucketToken() string {
|
||||
// 分片上传 v2 需要 bucket 级别的 token
|
||||
// 1. 创建 putPolicy
|
||||
putPolicy := fmt.Sprintf(`{"scope":"%s","deadline":%d}`,
|
||||
c.config.Bucket, time.Now().Add(1*time.Hour).Unix())
|
||||
return c.generateToken(c.config.Bucket)
|
||||
}
|
||||
|
||||
// 2. 对 putPolicy 进行 base64 URL 编码
|
||||
encodedPutPolicy := base64.URLEncoding.EncodeToString([]byte(putPolicy))
|
||||
// 七牛云临时域名后缀(平台分配的 CDN 域名,稳定性高)
|
||||
var qiniuTempSuffixes = []string{
|
||||
".qiniudns.com", ".clouddn.com", ".qbox.me",
|
||||
".qnssl.com", ".qnybgz.cn", ".qiniudns.com.cn",
|
||||
}
|
||||
|
||||
// 3. 对 encodedPutPolicy 进行 HMAC-SHA1 签名
|
||||
h := hmac.New(sha1.New, []byte(c.config.SecretKey))
|
||||
h.Write([]byte(encodedPutPolicy))
|
||||
encodedSign := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
// extractHost 从 URL 提取主机名(去掉 scheme、path、port)
|
||||
func extractHost(domainURL string) string {
|
||||
host := strings.TrimPrefix(domainURL, "http://")
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
if idx := strings.Index(host, "/"); idx >= 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
return h
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// 4. 组合 token
|
||||
return c.config.AccessKey + ":" + encodedSign + ":" + encodedPutPolicy
|
||||
// isTempDomain 判断是否为七牛平台分配的临时域名(后缀匹配)
|
||||
func (c *Client) isTempDomain(domain string) bool {
|
||||
host := strings.ToLower(extractHost(domain))
|
||||
for _, s := range qiniuTempSuffixes {
|
||||
if strings.HasSuffix(host, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// classifyDomains 将域名列表分为临时域名和自定义域名
|
||||
func (c *Client) classifyDomains(domains []string) (tempDomains, customDomains []string) {
|
||||
for _, d := range domains {
|
||||
if !strings.HasPrefix(d, "http://") && !strings.HasPrefix(d, "https://") {
|
||||
d = "http://" + d
|
||||
}
|
||||
if c.isTempDomain(d) {
|
||||
tempDomains = append(tempDomains, d)
|
||||
} else {
|
||||
customDomains = append(customDomains, d)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// resolveDownloadDomain 解析并缓存下载域名
|
||||
// 策略:API 域名列表(临时优先→自定义)→ 兜底默认 CDN
|
||||
// 不做 HTTP 探测:Download 使用签名 URL,即使有防盗链也能通过
|
||||
func (c *Client) resolveDownloadDomain() (string, error) {
|
||||
if c.config.UploadDomain != "" {
|
||||
return c.config.UploadDomain, nil
|
||||
if c.config.DownloadDomain != "" {
|
||||
return c.config.DownloadDomain, nil
|
||||
}
|
||||
|
||||
domains, err := c.GetBucketDomains(context.Background())
|
||||
if err != nil || len(domains) == 0 {
|
||||
return "", fmt.Errorf("无法获取桶 %s 的下载域名: %v", c.config.Bucket, err)
|
||||
|
||||
if err == nil && len(domains) > 0 {
|
||||
tempDomains, customDomains := c.classifyDomains(domains)
|
||||
|
||||
// 精准获取桶的真实区域
|
||||
c.resolveRegion(tempDomains)
|
||||
|
||||
// 优先使用临时域名(平台分配,稳定性高)
|
||||
if len(tempDomains) > 0 {
|
||||
d := tempDomains[0]
|
||||
c.config.DownloadDomain = d
|
||||
return d, nil
|
||||
}
|
||||
// 降级到自定义域名
|
||||
if len(customDomains) > 0 {
|
||||
d := customDomains[0]
|
||||
c.config.DownloadDomain = d
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
domain := domains[0]
|
||||
if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") {
|
||||
domain = "http://" + domain
|
||||
}
|
||||
c.config.UploadDomain = domain
|
||||
return domain, nil
|
||||
|
||||
// 无域名 → 兜底默认 CDN(可能不存在,但给一个机会)
|
||||
fallback := c.defaultCDNDomain()
|
||||
c.config.DownloadDomain = fallback
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
// Download 下载文件
|
||||
// defaultCDNDomain 构造七牛默认 CDN 域名
|
||||
func (c *Client) defaultCDNDomain() string {
|
||||
return fmt.Sprintf("http://%s-%s.qiniudns.com", c.config.Bucket, c.config.Region)
|
||||
}
|
||||
|
||||
// ClearDownloadDomain 清除缓存的下载域名(下载失败时调用,下次重新解析)
|
||||
func (c *Client) ClearDownloadDomain() {
|
||||
c.config.DownloadDomain = ""
|
||||
}
|
||||
|
||||
// resolveRegion 精准获取桶的真实区域
|
||||
// 优先从临时域名提取 → 查询 API → 使用配置值兜底
|
||||
func (c *Client) resolveRegion(tempDomains []string) {
|
||||
// 1. 从临时域名提取
|
||||
bucketLower := strings.ToLower(c.config.Bucket)
|
||||
for _, d := range tempDomains {
|
||||
host := extractHost(d)
|
||||
host = strings.ToLower(host)
|
||||
if !strings.HasPrefix(host, bucketLower+"-") {
|
||||
continue
|
||||
}
|
||||
rest := host[len(bucketLower)+1:]
|
||||
if idx := strings.Index(rest, "."); idx > 0 {
|
||||
c.config.Region = rest[:idx]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 查询七牛 API
|
||||
if region, err := c.GetBucketRegion(context.Background()); err == nil && region != "" {
|
||||
c.config.Region = region
|
||||
}
|
||||
}
|
||||
|
||||
// Download 下载文件(使用签名 URL,绕过防盗链)
|
||||
func (c *Client) Download(ctx context.Context, key string, writer io.Writer) error {
|
||||
baseURL, err := c.resolveDownloadDomain()
|
||||
signedURL, err := c.GetSignedURL(ctx, key, 1*time.Hour)
|
||||
if err != nil {
|
||||
return oss.NewError("DOWNLOAD_ERROR", err.Error(), err)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", baseURL, key)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", signedURL, nil)
|
||||
if err != nil {
|
||||
return oss.NewError("DOWNLOAD_ERROR", "failed to create request", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
c.ClearDownloadDomain()
|
||||
return oss.NewError("DOWNLOAD_ERROR", "failed to download file", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return oss.NewError("DOWNLOAD_ERROR", fmt.Sprintf("download failed with status %d", resp.StatusCode), nil)
|
||||
c.ClearDownloadDomain()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return oss.NewError("DOWNLOAD_ERROR",
|
||||
fmt.Sprintf("download failed with status %d: %s", resp.StatusCode, string(body[:min(len(body), 200)])), nil)
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, resp.Body)
|
||||
if err != nil {
|
||||
c.ClearDownloadDomain()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -407,11 +438,27 @@ func (c *Client) GetFileInfo(ctx context.Context, key string) (*oss.FileInfo, er
|
||||
return nil, oss.NewError("STAT_ERROR", fmt.Sprintf("stat failed with status %d: %s", resp.StatusCode, string(body)), nil)
|
||||
}
|
||||
|
||||
// 解析响应 (简化实现)
|
||||
// 实际响应格式: {"hash":"xxx","fsize":123,"mimeType":"xxx","putTime":123}
|
||||
// 这里返回一个简化的 FileInfo
|
||||
var statResp struct {
|
||||
Hash string `json:"hash"`
|
||||
Fsize int64 `json:"fsize"`
|
||||
MimeType string `json:"mimeType"`
|
||||
PutTime int64 `json:"putTime"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &statResp); err != nil {
|
||||
return nil, oss.NewError("STAT_ERROR", "failed to parse response", err)
|
||||
}
|
||||
|
||||
var modTime time.Time
|
||||
if statResp.PutTime > 0 {
|
||||
modTime = time.Unix(0, statResp.PutTime)
|
||||
}
|
||||
|
||||
return &oss.FileInfo{
|
||||
Key: key,
|
||||
Key: key,
|
||||
Size: statResp.Fsize,
|
||||
ETag: statResp.Hash,
|
||||
ContentType: statResp.MimeType,
|
||||
LastModified: modTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -471,11 +518,16 @@ func (c *Client) ListFiles(ctx context.Context, options *oss.ListOptions) (*oss.
|
||||
// 转换为统一格式
|
||||
files := make([]oss.FileInfo, 0, len(listResp.Items))
|
||||
for _, item := range listResp.Items {
|
||||
var modTime time.Time
|
||||
if item.PutTime > 0 {
|
||||
modTime = time.Unix(0, item.PutTime)
|
||||
}
|
||||
files = append(files, oss.FileInfo{
|
||||
Key: item.Key,
|
||||
Size: item.Fsize,
|
||||
ETag: item.Hash,
|
||||
ContentType: item.MimeType,
|
||||
Key: item.Key,
|
||||
Size: item.Fsize,
|
||||
ETag: item.Hash,
|
||||
ContentType: item.MimeType,
|
||||
LastModified: modTime,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -488,27 +540,22 @@ func (c *Client) ListFiles(ctx context.Context, options *oss.ListOptions) (*oss.
|
||||
}
|
||||
|
||||
// GetSignedURL 获取预签名URL
|
||||
// 签名格式: hmac_sha1(SecretKey, "<downloadURL>?e=<deadline>")
|
||||
func (c *Client) GetSignedURL(ctx context.Context, key string, expiresIn time.Duration) (string, error) {
|
||||
// 七牛云私有空间下载需要生成私有下载 URL
|
||||
deadline := time.Now().Add(expiresIn).Unix()
|
||||
|
||||
// 构建 download URL
|
||||
baseURL, err := c.resolveDownloadDomain()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
downloadURL := fmt.Sprintf("%s/%s", baseURL, key)
|
||||
|
||||
// 生成签名
|
||||
// 签名字符串 = 完整 URL + ?e=deadline
|
||||
urlToSign := fmt.Sprintf("%s/%s?e=%d", baseURL, key, deadline)
|
||||
h := hmac.New(sha1.New, []byte(c.config.SecretKey))
|
||||
signStr := fmt.Sprintf("%s\n%d", downloadURL, deadline)
|
||||
h.Write([]byte(signStr))
|
||||
h.Write([]byte(urlToSign))
|
||||
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
// 构建最终 URL
|
||||
signedURL := fmt.Sprintf("%s?e=%d&token=%s:%s", downloadURL, deadline, c.config.AccessKey, sign)
|
||||
|
||||
return signedURL, nil
|
||||
return fmt.Sprintf("%s&token=%s:%s", urlToSign, c.config.AccessKey, sign), nil
|
||||
}
|
||||
|
||||
// Copy 复制文件
|
||||
|
||||
190
internal/oss/qiniu/client_test.go
Normal file
190
internal/oss/qiniu/client_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package qiniu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/oss"
|
||||
)
|
||||
|
||||
// 临时测试配置 — 提交前删除此文件
|
||||
func testConfig() *Config {
|
||||
return &Config{
|
||||
AccessKey: "eUjiDJGy9CkRb3-Ad3jCubPrm49xeBTesHYckIwc",
|
||||
SecretKey: "LE8XL-LmoMkpy0jNK-kDhgL_w7A6MRXD1Msqd1Y4",
|
||||
Bucket: "u-res",
|
||||
Region: "as0",
|
||||
UseHTTPS: true,
|
||||
}
|
||||
}
|
||||
|
||||
const testKey = "music/03.一人一首成名曲【特调音源】/001.雨一直下-张宇.mp3"
|
||||
|
||||
// TestListBuckets 列举桶
|
||||
func TestListBuckets(t *testing.T) {
|
||||
buckets, err := ListBuckets("eUjiDJGy9CkRb3-Ad3jCubPrm49xeBTesHYckIwc", "LE8XL-LmoMkpy0jNK-kDhgL_w7A6MRXD1Msqd1Y4")
|
||||
if err != nil {
|
||||
t.Fatalf("ListBuckets 失败: %v", err)
|
||||
}
|
||||
for _, b := range buckets {
|
||||
t.Logf("桶: %s 区域: %s", b.Name, b.Region)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetBucketDomains 获取桶域名
|
||||
func TestGetBucketDomains(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
domains, err := c.GetBucketDomains(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("获取域名失败: %v", err)
|
||||
}
|
||||
t.Logf("桶域名: %v", domains)
|
||||
}
|
||||
|
||||
// TestDownloadDirect 裸 URL 下载(测试桶公开/私有)
|
||||
func TestDownloadDirect(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
domain, err := c.resolveDownloadDomain()
|
||||
if err != nil {
|
||||
t.Fatalf("获取下载域名失败: %v", err)
|
||||
}
|
||||
t.Logf("下载域名: %s", domain)
|
||||
|
||||
rawURL := fmt.Sprintf("%s/%s", domain, testKey)
|
||||
t.Logf("裸 URL: %s", rawURL)
|
||||
|
||||
httpResp, err := http.Get(rawURL)
|
||||
if err != nil {
|
||||
t.Fatalf("请求失败: %v", err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
t.Logf("裸 URL 状态码: %d", httpResp.StatusCode)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(httpResp.Body)
|
||||
t.Logf("响应大小: %d bytes", buf.Len())
|
||||
}
|
||||
|
||||
// TestDownloadSigned 签名 URL 下载
|
||||
func TestDownloadSigned(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
signedURL, err := c.GetSignedURL(context.Background(), testKey, 1*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("生成签名 URL 失败: %v", err)
|
||||
}
|
||||
t.Logf("签名 URL: %s...", signedURL[:min(120, len(signedURL))])
|
||||
|
||||
httpResp, err := http.Get(signedURL)
|
||||
if err != nil {
|
||||
t.Fatalf("请求失败: %v", err)
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
t.Logf("签名 URL 状态码: %d", httpResp.StatusCode)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(httpResp.Body)
|
||||
t.Logf("下载大小: %d bytes", buf.Len())
|
||||
|
||||
if httpResp.StatusCode != 200 {
|
||||
t.Errorf("下载失败: %d, body: %s", httpResp.StatusCode, buf.String()[:min(200, buf.Len())])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDownloadViaClient 通过 Client.Download 方法下载
|
||||
func TestDownloadViaClient(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = c.Download(context.Background(), testKey, &buf)
|
||||
if err != nil {
|
||||
t.Errorf("Client.Download 失败: %v", err)
|
||||
} else {
|
||||
t.Logf("Client.Download 成功,大小: %d bytes (预期 ~7MB)", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetFileInfo 获取文件信息
|
||||
func TestGetFileInfo(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
info, err := c.GetFileInfo(context.Background(), testKey)
|
||||
if err != nil {
|
||||
t.Errorf("GetFileInfo 失败: %v", err)
|
||||
} else {
|
||||
t.Logf("GetFileInfo: key=%s size=%d", info.Key, info.Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListFiles 列举文件
|
||||
func TestListFiles(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
result, err := c.ListFiles(context.Background(), &oss.ListOptions{Prefix: "music/", MaxKeys: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles 失败: %v", err)
|
||||
}
|
||||
for _, f := range result.Files {
|
||||
t.Logf("文件: %-80s size: %d", f.Key, f.Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListFilesRaw 原始 RSF 请求查看响应结构
|
||||
func TestListFilesRaw(t *testing.T) {
|
||||
c, err := NewClient(testConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
resp, err := c.doRSFRequest("GET", fmt.Sprintf("/list?bucket=%s&limit=3&prefix=music/", testConfig().Bucket))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(resp.Body)
|
||||
|
||||
var pretty bytes.Buffer
|
||||
json.Indent(&pretty, buf.Bytes(), "", " ")
|
||||
t.Logf("原始响应:\n%s", pretty.String())
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -156,8 +156,8 @@ func (uc *UploadClient) Upload(ctx context.Context, key string, reader io.Reader
|
||||
}
|
||||
|
||||
var uploadURL string
|
||||
if uc.config.UploadDomain != "" {
|
||||
uploadURL = scheme + uc.config.UploadDomain
|
||||
if uc.config.DownloadDomain != "" {
|
||||
uploadURL = scheme + uc.config.DownloadDomain
|
||||
} else {
|
||||
// 根据区域选择
|
||||
switch uc.config.Region {
|
||||
|
||||
Reference in New Issue
Block a user