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) }