236 lines
6.1 KiB
Go
236 lines
6.1 KiB
Go
package qiniu
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"time"
|
|
|
|
"u-desk/internal/oss"
|
|
)
|
|
|
|
// UploadResult 七牛云上传结果
|
|
type qiniuUploadResult struct {
|
|
Key string `json:"key"`
|
|
Hash string `json:"hash"`
|
|
Size int64 `json:"fsize"`
|
|
Bucket string `json:"bucket"`
|
|
}
|
|
|
|
// UploadWithUploader 使用表单上传文件
|
|
func (c *Client) UploadWithUploader(ctx context.Context, key string, reader io.Reader, options *oss.UploadOptions) (*oss.UploadResult, error) {
|
|
// 生成上传 token
|
|
token := c.generateUploadToken(key)
|
|
|
|
// 创建 multipart form
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
|
|
// 添加字段
|
|
_ = writer.WriteField("token", token)
|
|
_ = writer.WriteField("key", key)
|
|
|
|
// 添加文件
|
|
part, err := writer.CreateFormFile("file", key)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to create form file", err)
|
|
}
|
|
|
|
// 读取数据并写入
|
|
// 为了获取文件大小,先读取到内存
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to read file data", err)
|
|
}
|
|
|
|
if _, err := part.Write(data); err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to write file data", err)
|
|
}
|
|
|
|
// 设置 Content-Type
|
|
if options != nil && options.ContentType != "" {
|
|
_ = writer.WriteField("mimeType", options.ContentType)
|
|
}
|
|
|
|
if err := writer.Close(); err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to close multipart writer", err)
|
|
}
|
|
|
|
// 上传 URL - 根据配置或区域选择
|
|
uploadURL := c.getUploadDomain()
|
|
|
|
// 创建请求
|
|
req, err := http.NewRequestWithContext(ctx, "POST", uploadURL, body)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to create request", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
// 发送请求
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to upload file", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// 读取响应
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to read response", err)
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, oss.NewError("UPLOAD_ERROR", fmt.Sprintf("upload failed with status %d: %s", resp.StatusCode, string(respBody)), nil)
|
|
}
|
|
|
|
// 解析结果
|
|
var result qiniuUploadResult
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to parse response", err)
|
|
}
|
|
|
|
return &oss.UploadResult{
|
|
Key: result.Key,
|
|
ETag: result.Hash,
|
|
Size: result.Size,
|
|
}, nil
|
|
}
|
|
|
|
// UploadClient 专用的上传客户端
|
|
type UploadClient struct {
|
|
config *Config
|
|
client *http.Client
|
|
}
|
|
|
|
// NewUploadClient 创建上传客户端
|
|
func NewUploadClient(config *Config) *UploadClient {
|
|
return &UploadClient{
|
|
config: config,
|
|
client: &http.Client{
|
|
Timeout: 5 * time.Minute,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Upload 上传文件
|
|
func (uc *UploadClient) Upload(ctx context.Context, key string, reader io.Reader) (*oss.UploadResult, error) {
|
|
token := uc.generateUploadToken()
|
|
|
|
// 创建 multipart form
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
|
|
_ = writer.WriteField("token", token)
|
|
_ = writer.WriteField("key", key)
|
|
|
|
part, err := writer.CreateFormFile("file", key)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to create form file", err)
|
|
}
|
|
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to read file data", err)
|
|
}
|
|
|
|
if _, err := part.Write(data); err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to write file data", err)
|
|
}
|
|
|
|
if err := writer.Close(); err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to close multipart writer", err)
|
|
}
|
|
|
|
// 上传 URL - 根据配置或区域选择
|
|
scheme := "https://"
|
|
if !uc.config.UseHTTPS {
|
|
scheme = "http://"
|
|
}
|
|
|
|
var uploadURL string
|
|
if uc.config.UploadDomain != "" {
|
|
uploadURL = scheme + uc.config.UploadDomain
|
|
} else {
|
|
// 根据区域选择
|
|
switch uc.config.Region {
|
|
case "z0":
|
|
uploadURL = scheme + "up-z0.qiniup.com"
|
|
case "z1":
|
|
uploadURL = scheme + "up-z1.qiniup.com"
|
|
case "z2":
|
|
uploadURL = scheme + "up-z2.qiniup.com"
|
|
case "na0":
|
|
uploadURL = scheme + "up-na0.qiniup.com"
|
|
case "as0":
|
|
uploadURL = scheme + "up-as0.qiniup.com"
|
|
default:
|
|
uploadURL = scheme + "up-z0.qiniup.com"
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", uploadURL, body)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to create request", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
resp, err := uc.client.Do(req)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to upload file", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to read response", err)
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, oss.NewError("UPLOAD_ERROR", fmt.Sprintf("upload failed with status %d: %s", resp.StatusCode, string(respBody)), nil)
|
|
}
|
|
|
|
var result qiniuUploadResult
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, oss.NewError("UPLOAD_ERROR", "failed to parse response", err)
|
|
}
|
|
|
|
return &oss.UploadResult{
|
|
Key: result.Key,
|
|
ETag: result.Hash,
|
|
Size: result.Size,
|
|
}, nil
|
|
}
|
|
|
|
// generateUploadToken 生成上传 token
|
|
func (uc *UploadClient) generateUploadToken() string {
|
|
// 1. 创建 putPolicy
|
|
putPolicy := fmt.Sprintf(`{"scope":"%s","deadline":%d}`,
|
|
uc.config.Bucket, time.Now().Add(1*time.Hour).Unix())
|
|
|
|
// 2. 对 putPolicy 进行 base64 URL 编码
|
|
encodedPutPolicy := base64.URLEncoding.EncodeToString([]byte(putPolicy))
|
|
|
|
// 3. 对 encodedPutPolicy 进行 HMAC-SHA1 签名
|
|
h := uc.hmacSHA1([]byte(encodedPutPolicy))
|
|
encodedSign := base64.URLEncoding.EncodeToString(h)
|
|
|
|
// 4. 组合 token
|
|
return uc.config.AccessKey + ":" + encodedSign + ":" + encodedPutPolicy
|
|
}
|
|
|
|
// hmacSHA1 HMAC-SHA1 签名
|
|
func (uc *UploadClient) hmacSHA1(data []byte) []byte {
|
|
h := hmac.New(sha1.New, []byte(uc.config.SecretKey))
|
|
h.Write(data)
|
|
return h.Sum(nil)
|
|
}
|