Private
Public Access
1
0
Files
u-desk/internal/oss/qiniu/upload.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)
}