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  }