github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/driver/cos/handler.go (about) 1 package cos 2 3 import ( 4 "context" 5 "crypto/hmac" 6 "crypto/sha1" 7 "encoding/base64" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "path" 15 "path/filepath" 16 "strings" 17 "time" 18 19 model "github.com/cloudreve/Cloudreve/v3/models" 20 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver" 21 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" 22 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" 23 "github.com/cloudreve/Cloudreve/v3/pkg/request" 24 "github.com/cloudreve/Cloudreve/v3/pkg/serializer" 25 "github.com/cloudreve/Cloudreve/v3/pkg/util" 26 "github.com/google/go-querystring/query" 27 cossdk "github.com/tencentyun/cos-go-sdk-v5" 28 ) 29 30 // UploadPolicy 腾讯云COS上传策略 31 type UploadPolicy struct { 32 Expiration string `json:"expiration"` 33 Conditions []interface{} `json:"conditions"` 34 } 35 36 // MetaData 文件元信息 37 type MetaData struct { 38 Size uint64 39 CallbackKey string 40 CallbackURL string 41 } 42 43 type urlOption struct { 44 Speed int `url:"x-cos-traffic-limit,omitempty"` 45 ContentDescription string `url:"response-content-disposition,omitempty"` 46 } 47 48 // Driver 腾讯云COS适配器模板 49 type Driver struct { 50 Policy *model.Policy 51 Client *cossdk.Client 52 HTTPClient request.Client 53 } 54 55 // List 列出COS文件 56 func (handler Driver) List(ctx context.Context, base string, recursive bool) ([]response.Object, error) { 57 // 初始化列目录参数 58 opt := &cossdk.BucketGetOptions{ 59 Prefix: strings.TrimPrefix(base, "/"), 60 EncodingType: "", 61 MaxKeys: 1000, 62 } 63 // 是否为递归列出 64 if !recursive { 65 opt.Delimiter = "/" 66 } 67 // 手动补齐结尾的slash 68 if opt.Prefix != "" { 69 opt.Prefix += "/" 70 } 71 72 var ( 73 marker string 74 objects []cossdk.Object 75 commons []string 76 ) 77 78 for { 79 res, _, err := handler.Client.Bucket.Get(ctx, opt) 80 if err != nil { 81 return nil, err 82 } 83 objects = append(objects, res.Contents...) 84 commons = append(commons, res.CommonPrefixes...) 85 // 如果本次未列取完,则继续使用marker获取结果 86 marker = res.NextMarker 87 // marker 为空时结果列取完毕,跳出 88 if marker == "" { 89 break 90 } 91 } 92 93 // 处理列取结果 94 res := make([]response.Object, 0, len(objects)+len(commons)) 95 // 处理目录 96 for _, object := range commons { 97 rel, err := filepath.Rel(opt.Prefix, object) 98 if err != nil { 99 continue 100 } 101 res = append(res, response.Object{ 102 Name: path.Base(object), 103 RelativePath: filepath.ToSlash(rel), 104 Size: 0, 105 IsDir: true, 106 LastModify: time.Now(), 107 }) 108 } 109 // 处理文件 110 for _, object := range objects { 111 rel, err := filepath.Rel(opt.Prefix, object.Key) 112 if err != nil { 113 continue 114 } 115 res = append(res, response.Object{ 116 Name: path.Base(object.Key), 117 Source: object.Key, 118 RelativePath: filepath.ToSlash(rel), 119 Size: uint64(object.Size), 120 IsDir: false, 121 LastModify: time.Now(), 122 }) 123 } 124 125 return res, nil 126 127 } 128 129 // CORS 创建跨域策略 130 func (handler Driver) CORS() error { 131 _, err := handler.Client.Bucket.PutCORS(context.Background(), &cossdk.BucketPutCORSOptions{ 132 Rules: []cossdk.BucketCORSRule{{ 133 AllowedMethods: []string{ 134 "GET", 135 "POST", 136 "PUT", 137 "DELETE", 138 "HEAD", 139 }, 140 AllowedOrigins: []string{"*"}, 141 AllowedHeaders: []string{"*"}, 142 MaxAgeSeconds: 3600, 143 ExposeHeaders: []string{}, 144 }}, 145 }) 146 147 return err 148 } 149 150 // Get 获取文件 151 func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) { 152 // 获取文件源地址 153 downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0) 154 if err != nil { 155 return nil, err 156 } 157 158 // 获取文件数据流 159 resp, err := handler.HTTPClient.Request( 160 "GET", 161 downloadURL, 162 nil, 163 request.WithContext(ctx), 164 request.WithTimeout(time.Duration(0)), 165 ).CheckHTTPResponse(200).GetRSCloser() 166 if err != nil { 167 return nil, err 168 } 169 170 resp.SetFirstFakeChunk() 171 172 // 尝试自主获取文件大小 173 if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { 174 resp.SetContentLength(int64(file.Size)) 175 } 176 177 return resp, nil 178 } 179 180 // Put 将文件流保存到指定目录 181 func (handler Driver) Put(ctx context.Context, file fsctx.FileHeader) error { 182 defer file.Close() 183 184 opt := &cossdk.ObjectPutOptions{} 185 _, err := handler.Client.Object.Put(ctx, file.Info().SavePath, file, opt) 186 return err 187 } 188 189 // Delete 删除一个或多个文件, 190 // 返回未删除的文件,及遇到的最后一个错误 191 func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) { 192 obs := []cossdk.Object{} 193 for _, v := range files { 194 obs = append(obs, cossdk.Object{Key: v}) 195 } 196 opt := &cossdk.ObjectDeleteMultiOptions{ 197 Objects: obs, 198 Quiet: true, 199 } 200 201 res, _, err := handler.Client.Object.DeleteMulti(context.Background(), opt) 202 if err != nil { 203 return files, err 204 } 205 206 // 整理删除结果 207 failed := make([]string, 0, len(files)) 208 for _, v := range res.Errors { 209 failed = append(failed, v.Key) 210 } 211 212 if len(failed) == 0 { 213 return failed, nil 214 } 215 216 return failed, errors.New("delete failed") 217 } 218 219 // Thumb 获取文件缩略图 220 func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) { 221 // quick check by extension name 222 // https://cloud.tencent.com/document/product/436/44893 223 supported := []string{"png", "jpg", "jpeg", "gif", "bmp", "webp", "heif", "heic"} 224 if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 { 225 supported = handler.Policy.OptionsSerialized.ThumbExts 226 } 227 228 if !util.IsInExtensionList(supported, file.Name) || file.Size > (32<<(10*2)) { 229 return nil, driver.ErrorThumbNotSupported 230 } 231 232 var ( 233 thumbSize = [2]uint{400, 300} 234 ok = false 235 ) 236 if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok { 237 return nil, errors.New("failed to get thumbnail size") 238 } 239 240 thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85) 241 242 thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d/quality/%d", thumbSize[0], thumbSize[1], thumbEncodeQuality) 243 244 source, err := handler.signSourceURL( 245 ctx, 246 file.SourceName, 247 int64(model.GetIntSetting("preview_timeout", 60)), 248 &urlOption{}, 249 ) 250 if err != nil { 251 return nil, err 252 } 253 254 thumbURL, _ := url.Parse(source) 255 thumbQuery := thumbURL.Query() 256 thumbQuery.Add(thumbParam, "") 257 thumbURL.RawQuery = thumbQuery.Encode() 258 259 return &response.ContentResponse{ 260 Redirect: true, 261 URL: thumbURL.String(), 262 }, nil 263 } 264 265 // Source 获取外链URL 266 func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) { 267 // 尝试从上下文获取文件名 268 fileName := "" 269 if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok { 270 fileName = file.Name 271 } 272 273 // 添加各项设置 274 options := urlOption{} 275 if speed > 0 { 276 if speed < 819200 { 277 speed = 819200 278 } 279 if speed > 838860800 { 280 speed = 838860800 281 } 282 options.Speed = speed 283 } 284 if isDownload { 285 options.ContentDescription = "attachment; filename=\"" + url.PathEscape(fileName) + "\"" 286 } 287 288 return handler.signSourceURL(ctx, path, ttl, &options) 289 } 290 291 func (handler Driver) signSourceURL(ctx context.Context, path string, ttl int64, options *urlOption) (string, error) { 292 cdnURL, err := url.Parse(handler.Policy.BaseURL) 293 if err != nil { 294 return "", err 295 } 296 297 // 公有空间不需要签名 298 if !handler.Policy.IsPrivate { 299 file, err := url.Parse(path) 300 if err != nil { 301 return "", err 302 } 303 304 // 非签名URL不支持设置响应header 305 options.ContentDescription = "" 306 307 optionQuery, err := query.Values(*options) 308 if err != nil { 309 return "", err 310 } 311 file.RawQuery = optionQuery.Encode() 312 sourceURL := cdnURL.ResolveReference(file) 313 314 return sourceURL.String(), nil 315 } 316 317 presignedURL, err := handler.Client.Object.GetPresignedURL(ctx, http.MethodGet, path, 318 handler.Policy.AccessKey, handler.Policy.SecretKey, time.Duration(ttl)*time.Second, options) 319 if err != nil { 320 return "", err 321 } 322 323 // 将最终生成的签名URL域名换成用户自定义的加速域名(如果有) 324 presignedURL.Host = cdnURL.Host 325 presignedURL.Scheme = cdnURL.Scheme 326 327 return presignedURL.String(), nil 328 } 329 330 // Token 获取上传策略和认证Token 331 func (handler Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) { 332 // 生成回调地址 333 siteURL := model.GetSiteURL() 334 apiBaseURI, _ := url.Parse("/api/v3/callback/cos/" + uploadSession.Key) 335 apiURL := siteURL.ResolveReference(apiBaseURI).String() 336 337 // 上传策略 338 savePath := file.Info().SavePath 339 startTime := time.Now() 340 endTime := startTime.Add(time.Duration(ttl) * time.Second) 341 keyTime := fmt.Sprintf("%d;%d", startTime.Unix(), endTime.Unix()) 342 postPolicy := UploadPolicy{ 343 Expiration: endTime.UTC().Format(time.RFC3339), 344 Conditions: []interface{}{ 345 map[string]string{"bucket": handler.Policy.BucketName}, 346 map[string]string{"$key": savePath}, 347 map[string]string{"x-cos-meta-callback": apiURL}, 348 map[string]string{"x-cos-meta-key": uploadSession.Key}, 349 map[string]string{"q-sign-algorithm": "sha1"}, 350 map[string]string{"q-ak": handler.Policy.AccessKey}, 351 map[string]string{"q-sign-time": keyTime}, 352 }, 353 } 354 355 if handler.Policy.MaxSize > 0 { 356 postPolicy.Conditions = append(postPolicy.Conditions, 357 []interface{}{"content-length-range", 0, handler.Policy.MaxSize}) 358 } 359 360 res, err := handler.getUploadCredential(ctx, postPolicy, keyTime, savePath) 361 if err == nil { 362 res.SessionID = uploadSession.Key 363 res.Callback = apiURL 364 res.UploadURLs = []string{handler.Policy.Server} 365 } 366 367 return res, err 368 369 } 370 371 // 取消上传凭证 372 func (handler Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error { 373 return nil 374 } 375 376 // Meta 获取文件信息 377 func (handler Driver) Meta(ctx context.Context, path string) (*MetaData, error) { 378 res, err := handler.Client.Object.Head(ctx, path, &cossdk.ObjectHeadOptions{}) 379 if err != nil { 380 return nil, err 381 } 382 return &MetaData{ 383 Size: uint64(res.ContentLength), 384 CallbackKey: res.Header.Get("x-cos-meta-key"), 385 CallbackURL: res.Header.Get("x-cos-meta-callback"), 386 }, nil 387 } 388 389 func (handler Driver) getUploadCredential(ctx context.Context, policy UploadPolicy, keyTime string, savePath string) (*serializer.UploadCredential, error) { 390 // 编码上传策略 391 policyJSON, err := json.Marshal(policy) 392 if err != nil { 393 return nil, err 394 } 395 policyEncoded := base64.StdEncoding.EncodeToString(policyJSON) 396 397 // 签名上传策略 398 hmacSign := hmac.New(sha1.New, []byte(handler.Policy.SecretKey)) 399 _, err = io.WriteString(hmacSign, keyTime) 400 if err != nil { 401 return nil, err 402 } 403 signKey := fmt.Sprintf("%x", hmacSign.Sum(nil)) 404 405 sha1Sign := sha1.New() 406 _, err = sha1Sign.Write(policyJSON) 407 if err != nil { 408 return nil, err 409 } 410 stringToSign := fmt.Sprintf("%x", sha1Sign.Sum(nil)) 411 412 // 最终签名 413 hmacFinalSign := hmac.New(sha1.New, []byte(signKey)) 414 _, err = hmacFinalSign.Write([]byte(stringToSign)) 415 if err != nil { 416 return nil, err 417 } 418 signature := hmacFinalSign.Sum(nil) 419 420 return &serializer.UploadCredential{ 421 Policy: policyEncoded, 422 Path: savePath, 423 AccessKey: handler.Policy.AccessKey, 424 Credential: fmt.Sprintf("%x", signature), 425 KeyTime: keyTime, 426 }, nil 427 }