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  }