github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/driver/upyun/handler.go (about) 1 package upyun 2 3 import ( 4 "context" 5 "crypto/hmac" 6 "crypto/md5" 7 "crypto/sha1" 8 "encoding/base64" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "net/http" 13 "net/url" 14 "path" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 model "github.com/cloudreve/Cloudreve/v3/models" 21 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver" 22 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" 23 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" 24 "github.com/cloudreve/Cloudreve/v3/pkg/request" 25 "github.com/cloudreve/Cloudreve/v3/pkg/serializer" 26 "github.com/cloudreve/Cloudreve/v3/pkg/util" 27 "github.com/upyun/go-sdk/upyun" 28 ) 29 30 // UploadPolicy 又拍云上传策略 31 type UploadPolicy struct { 32 Bucket string `json:"bucket"` 33 SaveKey string `json:"save-key"` 34 Expiration int64 `json:"expiration"` 35 CallbackURL string `json:"notify-url"` 36 ContentLength uint64 `json:"content-length"` 37 ContentLengthRange string `json:"content-length-range,omitempty"` 38 AllowFileType string `json:"allow-file-type,omitempty"` 39 } 40 41 // Driver 又拍云策略适配器 42 type Driver struct { 43 Policy *model.Policy 44 } 45 46 func (handler Driver) List(ctx context.Context, base string, recursive bool) ([]response.Object, error) { 47 base = strings.TrimPrefix(base, "/") 48 49 // 用于接受SDK返回对象的chan 50 objChan := make(chan *upyun.FileInfo) 51 objects := []*upyun.FileInfo{} 52 53 // 列取配置 54 listConf := &upyun.GetObjectsConfig{ 55 Path: "/" + base, 56 ObjectsChan: objChan, 57 MaxListTries: 1, 58 } 59 // 递归列取时不限制递归次数 60 if recursive { 61 listConf.MaxListLevel = -1 62 } 63 64 // 启动一个goroutine收集列取的对象信 65 wg := &sync.WaitGroup{} 66 wg.Add(1) 67 go func(input chan *upyun.FileInfo, output *[]*upyun.FileInfo, wg *sync.WaitGroup) { 68 defer wg.Done() 69 for { 70 file, ok := <-input 71 if !ok { 72 return 73 } 74 *output = append(*output, file) 75 } 76 }(objChan, &objects, wg) 77 78 up := upyun.NewUpYun(&upyun.UpYunConfig{ 79 Bucket: handler.Policy.BucketName, 80 Operator: handler.Policy.AccessKey, 81 Password: handler.Policy.SecretKey, 82 }) 83 84 err := up.List(listConf) 85 if err != nil { 86 return nil, err 87 } 88 89 wg.Wait() 90 91 // 汇总处理列取结果 92 res := make([]response.Object, 0, len(objects)) 93 for _, object := range objects { 94 res = append(res, response.Object{ 95 Name: path.Base(object.Name), 96 RelativePath: object.Name, 97 Source: path.Join(base, object.Name), 98 Size: uint64(object.Size), 99 IsDir: object.IsDir, 100 LastModify: object.Time, 101 }) 102 } 103 104 return res, nil 105 } 106 107 // Get 获取文件 108 func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { 109 // 获取文件源地址 110 downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0) 111 if err != nil { 112 return nil, err 113 } 114 115 // 获取文件数据流 116 client := request.NewClient() 117 resp, err := client.Request( 118 "GET", 119 downloadURL, 120 nil, 121 request.WithContext(ctx), 122 request.WithHeader( 123 http.Header{"Cache-Control": {"no-cache", "no-store", "must-revalidate"}}, 124 ), 125 request.WithTimeout(time.Duration(0)), 126 ).CheckHTTPResponse(200).GetRSCloser() 127 if err != nil { 128 return nil, err 129 } 130 131 resp.SetFirstFakeChunk() 132 133 // 尝试自主获取文件大小 134 if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { 135 resp.SetContentLength(int64(file.Size)) 136 } 137 138 return resp, nil 139 140 } 141 142 // Put 将文件流保存到指定目录 143 func (handler Driver) Put(ctx context.Context, file fsctx.FileHeader) error { 144 defer file.Close() 145 146 up := upyun.NewUpYun(&upyun.UpYunConfig{ 147 Bucket: handler.Policy.BucketName, 148 Operator: handler.Policy.AccessKey, 149 Password: handler.Policy.SecretKey, 150 }) 151 err := up.Put(&upyun.PutObjectConfig{ 152 Path: file.Info().SavePath, 153 Reader: file, 154 }) 155 156 return err 157 } 158 159 // Delete 删除一个或多个文件, 160 // 返回未删除的文件,及遇到的最后一个错误 161 func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) { 162 up := upyun.NewUpYun(&upyun.UpYunConfig{ 163 Bucket: handler.Policy.BucketName, 164 Operator: handler.Policy.AccessKey, 165 Password: handler.Policy.SecretKey, 166 }) 167 168 var ( 169 failed = make([]string, 0, len(files)) 170 lastErr error 171 currentIndex = 0 172 indexLock sync.Mutex 173 failedLock sync.Mutex 174 wg sync.WaitGroup 175 routineNum = 4 176 ) 177 wg.Add(routineNum) 178 179 // upyun不支持批量操作,这里开四个协程并行操作 180 for i := 0; i < routineNum; i++ { 181 go func() { 182 for { 183 // 取得待删除文件 184 indexLock.Lock() 185 if currentIndex >= len(files) { 186 // 所有文件处理完成 187 wg.Done() 188 indexLock.Unlock() 189 return 190 } 191 path := files[currentIndex] 192 currentIndex++ 193 indexLock.Unlock() 194 195 // 发送异步删除请求 196 err := up.Delete(&upyun.DeleteObjectConfig{ 197 Path: path, 198 Async: true, 199 }) 200 201 // 处理错误 202 if err != nil { 203 failedLock.Lock() 204 lastErr = err 205 failed = append(failed, path) 206 failedLock.Unlock() 207 } 208 } 209 }() 210 } 211 212 wg.Wait() 213 214 return failed, lastErr 215 } 216 217 // Thumb 获取文件缩略图 218 func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { 219 // quick check by extension name 220 // https://help.upyun.com/knowledge-base/image/ 221 supported := []string{"png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"} 222 if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 { 223 supported = handler.Policy.OptionsSerialized.ThumbExts 224 } 225 226 if !util.IsInExtensionList(supported, file.Name) { 227 return nil, driver.ErrorThumbNotSupported 228 } 229 230 var ( 231 thumbSize = [2]uint{400, 300} 232 ok = false 233 ) 234 if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok { 235 return nil, errors.New("failed to get thumbnail size") 236 } 237 238 thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85) 239 240 thumbParam := fmt.Sprintf("!/fwfh/%dx%d/quality/%d", thumbSize[0], thumbSize[1], thumbEncodeQuality) 241 thumbURL, err := handler.Source(ctx, file.SourceName+thumbParam, int64(model.GetIntSetting("preview_timeout", 60)), false, 0) 242 if err != nil { 243 return nil, err 244 } 245 246 return &response.ContentResponse{ 247 Redirect: true, 248 URL: thumbURL, 249 }, nil 250 } 251 252 // Source 获取外链URL 253 func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) { 254 // 尝试从上下文获取文件名 255 fileName := "" 256 if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { 257 fileName = file.Name 258 } 259 260 sourceURL, err := url.Parse(handler.Policy.BaseURL) 261 if err != nil { 262 return "", err 263 } 264 265 fileKey, err := url.Parse(url.PathEscape(path)) 266 if err != nil { 267 return "", err 268 } 269 270 sourceURL = sourceURL.ResolveReference(fileKey) 271 272 // 如果是下载文件URL 273 if isDownload { 274 query := sourceURL.Query() 275 query.Add("_upd", fileName) 276 sourceURL.RawQuery = query.Encode() 277 } 278 279 return handler.signURL(ctx, sourceURL, ttl) 280 } 281 282 func (handler Driver) signURL(ctx context.Context, path *url.URL, TTL int64) (string, error) { 283 if !handler.Policy.IsPrivate { 284 // 未开启Token防盗链时,直接返回 285 return path.String(), nil 286 } 287 288 etime := time.Now().Add(time.Duration(TTL) * time.Second).Unix() 289 signStr := fmt.Sprintf( 290 "%s&%d&%s", 291 handler.Policy.OptionsSerialized.Token, 292 etime, 293 path.Path, 294 ) 295 signMd5 := fmt.Sprintf("%x", md5.Sum([]byte(signStr))) 296 finalSign := signMd5[12:20] + strconv.FormatInt(etime, 10) 297 298 // 将签名添加到URL中 299 query := path.Query() 300 query.Add("_upt", finalSign) 301 path.RawQuery = query.Encode() 302 303 return path.String(), nil 304 } 305 306 // Token 获取上传策略和认证Token 307 func (handler Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) { 308 // 生成回调地址 309 siteURL := model.GetSiteURL() 310 apiBaseURI, _ := url.Parse("/api/v3/callback/upyun/" + uploadSession.Key) 311 apiURL := siteURL.ResolveReference(apiBaseURI) 312 313 // 上传策略 314 fileInfo := file.Info() 315 putPolicy := UploadPolicy{ 316 Bucket: handler.Policy.BucketName, 317 // TODO escape 318 SaveKey: fileInfo.SavePath, 319 Expiration: time.Now().Add(time.Duration(ttl) * time.Second).Unix(), 320 CallbackURL: apiURL.String(), 321 ContentLength: fileInfo.Size, 322 ContentLengthRange: fmt.Sprintf("0,%d", fileInfo.Size), 323 AllowFileType: strings.Join(handler.Policy.OptionsSerialized.FileType, ","), 324 } 325 326 // 生成上传凭证 327 policyJSON, err := json.Marshal(putPolicy) 328 if err != nil { 329 return nil, err 330 } 331 policyEncoded := base64.StdEncoding.EncodeToString(policyJSON) 332 333 // 生成签名 334 elements := []string{"POST", "/" + handler.Policy.BucketName, policyEncoded} 335 signStr := handler.Sign(ctx, elements) 336 337 return &serializer.UploadCredential{ 338 SessionID: uploadSession.Key, 339 Policy: policyEncoded, 340 Credential: signStr, 341 UploadURLs: []string{"https://v0.api.upyun.com/" + handler.Policy.BucketName}, 342 }, nil 343 } 344 345 // 取消上传凭证 346 func (handler Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error { 347 return nil 348 } 349 350 // Sign 计算又拍云的签名头 351 func (handler Driver) Sign(ctx context.Context, elements []string) string { 352 password := fmt.Sprintf("%x", md5.Sum([]byte(handler.Policy.SecretKey))) 353 mac := hmac.New(sha1.New, []byte(password)) 354 value := strings.Join(elements, "&") 355 mac.Write([]byte(value)) 356 signStr := base64.StdEncoding.EncodeToString((mac.Sum(nil))) 357 return fmt.Sprintf("UPYUN %s:%s", handler.Policy.AccessKey, signStr) 358 }