github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/file.go (about)

     1  package filesystem
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  
     8  	model "github.com/cloudreve/Cloudreve/v3/models"
     9  	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
    10  	"github.com/cloudreve/Cloudreve/v3/pkg/conf"
    11  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
    12  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
    13  	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
    14  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
    15  	"github.com/juju/ratelimit"
    16  )
    17  
    18  /* ============
    19  	 文件相关
    20     ============
    21  */
    22  
    23  // 限速后的ReaderSeeker
    24  type lrs struct {
    25  	response.RSCloser
    26  	r io.Reader
    27  }
    28  
    29  func (r lrs) Read(p []byte) (int, error) {
    30  	return r.r.Read(p)
    31  }
    32  
    33  // withSpeedLimit 给原有的ReadSeeker加上限速
    34  func (fs *FileSystem) withSpeedLimit(rs response.RSCloser) response.RSCloser {
    35  	// 如果用户组有速度限制,就返回限制流速的ReaderSeeker
    36  	if fs.User.Group.SpeedLimit != 0 {
    37  		speed := fs.User.Group.SpeedLimit
    38  		bucket := ratelimit.NewBucketWithRate(float64(speed), int64(speed))
    39  		lrs := lrs{rs, ratelimit.Reader(rs, bucket)}
    40  		return lrs
    41  	}
    42  	// 否则返回原始流
    43  	return rs
    44  
    45  }
    46  
    47  // AddFile 新增文件记录
    48  func (fs *FileSystem) AddFile(ctx context.Context, parent *model.Folder, file fsctx.FileHeader) (*model.File, error) {
    49  	// 添加文件记录前的钩子
    50  	err := fs.Trigger(ctx, "BeforeAddFile", file)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	uploadInfo := file.Info()
    56  	newFile := model.File{
    57  		Name:               uploadInfo.FileName,
    58  		SourceName:         uploadInfo.SavePath,
    59  		UserID:             fs.User.ID,
    60  		Size:               uploadInfo.Size,
    61  		FolderID:           parent.ID,
    62  		PolicyID:           fs.Policy.ID,
    63  		MetadataSerialized: uploadInfo.Metadata,
    64  		UploadSessionID:    uploadInfo.UploadSessionID,
    65  	}
    66  
    67  	err = newFile.Create()
    68  
    69  	if err != nil {
    70  		if err := fs.Trigger(ctx, "AfterValidateFailed", file); err != nil {
    71  			util.Log().Debug("AfterValidateFailed hook execution failed: %s", err)
    72  		}
    73  		return nil, ErrFileExisted.WithError(err)
    74  	}
    75  
    76  	fs.User.Storage += newFile.Size
    77  	return &newFile, nil
    78  }
    79  
    80  // GetPhysicalFileContent 根据文件物理路径获取文件流
    81  func (fs *FileSystem) GetPhysicalFileContent(ctx context.Context, path string) (response.RSCloser, error) {
    82  	// 重设上传策略
    83  	fs.Policy = &model.Policy{Type: "local"}
    84  	_ = fs.DispatchHandler()
    85  
    86  	// 获取文件流
    87  	rs, err := fs.Handler.Get(ctx, path)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	return fs.withSpeedLimit(rs), nil
    93  }
    94  
    95  // Preview 预览文件
    96  //
    97  //	path   -   文件虚拟路径
    98  //	isText -   是否为文本文件,文本文件会忽略重定向,直接由
    99  //	           服务端拉取中转给用户,故会对文件大小进行限制
   100  func (fs *FileSystem) Preview(ctx context.Context, id uint, isText bool) (*response.ContentResponse, error) {
   101  	err := fs.resetFileIDIfNotExist(ctx, id)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// 如果是文本文件预览,需要检查大小限制
   107  	sizeLimit := model.GetIntSetting("maxEditSize", 2<<20)
   108  	if isText && fs.FileTarget[0].Size > uint64(sizeLimit) {
   109  		return nil, ErrFileSizeTooBig
   110  	}
   111  
   112  	// 是否直接返回文件内容
   113  	if isText || fs.Policy.IsDirectlyPreview() {
   114  		resp, err := fs.GetDownloadContent(ctx, id)
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  		return &response.ContentResponse{
   119  			Redirect: false,
   120  			Content:  resp,
   121  		}, nil
   122  	}
   123  
   124  	// 否则重定向到签名的预览URL
   125  	ttl := model.GetIntSetting("preview_timeout", 60)
   126  	previewURL, err := fs.SignURL(ctx, &fs.FileTarget[0], int64(ttl), false)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	return &response.ContentResponse{
   131  		Redirect: true,
   132  		URL:      previewURL,
   133  		MaxAge:   ttl,
   134  	}, nil
   135  
   136  }
   137  
   138  // GetDownloadContent 获取用于下载的文件流
   139  func (fs *FileSystem) GetDownloadContent(ctx context.Context, id uint) (response.RSCloser, error) {
   140  	// 获取原始文件流
   141  	rs, err := fs.GetContent(ctx, id)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	// 返回限速处理后的文件流
   147  	return fs.withSpeedLimit(rs), nil
   148  
   149  }
   150  
   151  // GetContent 获取文件内容,path为虚拟路径
   152  func (fs *FileSystem) GetContent(ctx context.Context, id uint) (response.RSCloser, error) {
   153  	err := fs.resetFileIDIfNotExist(ctx, id)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	ctx = context.WithValue(ctx, fsctx.FileModelCtx, fs.FileTarget[0])
   158  
   159  	// 获取文件流
   160  	rs, err := fs.Handler.Get(ctx, fs.FileTarget[0].SourceName)
   161  	if err != nil {
   162  		return nil, ErrIO.WithError(err)
   163  	}
   164  
   165  	return rs, nil
   166  }
   167  
   168  // deleteGroupedFile 对分组好的文件执行删除操作,
   169  // 返回每个分组失败的文件列表
   170  func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*model.File) map[uint][]string {
   171  	// 失败的文件列表
   172  	// TODO 并行删除
   173  	failed := make(map[uint][]string, len(files))
   174  	thumbs := make([]string, 0)
   175  
   176  	for policyID, toBeDeletedFiles := range files {
   177  		// 列举出需要物理删除的文件的物理路径
   178  		sourceNamesAll := make([]string, 0, len(toBeDeletedFiles))
   179  		uploadSessions := make([]*serializer.UploadSession, 0, len(toBeDeletedFiles))
   180  
   181  		for i := 0; i < len(toBeDeletedFiles); i++ {
   182  			sourceNamesAll = append(sourceNamesAll, toBeDeletedFiles[i].SourceName)
   183  
   184  			if toBeDeletedFiles[i].UploadSessionID != nil {
   185  				if session, ok := cache.Get(UploadSessionCachePrefix + *toBeDeletedFiles[i].UploadSessionID); ok {
   186  					uploadSession := session.(serializer.UploadSession)
   187  					uploadSessions = append(uploadSessions, &uploadSession)
   188  				}
   189  			}
   190  
   191  			// Check if sidecar thumb file exist
   192  			if model.IsTrueVal(toBeDeletedFiles[i].MetadataSerialized[model.ThumbSidecarMetadataKey]) {
   193  				thumbs = append(thumbs, toBeDeletedFiles[i].ThumbFile())
   194  			}
   195  		}
   196  
   197  		// 切换上传策略
   198  		fs.Policy = toBeDeletedFiles[0].GetPolicy()
   199  		err := fs.DispatchHandler()
   200  		if err != nil {
   201  			failed[policyID] = sourceNamesAll
   202  			continue
   203  		}
   204  
   205  		// 取消上传会话
   206  		for _, upSession := range uploadSessions {
   207  			if err := fs.Handler.CancelToken(ctx, upSession); err != nil {
   208  				util.Log().Warning("Failed to cancel upload session for %q: %s", upSession.Name, err)
   209  			}
   210  
   211  			cache.Deletes([]string{upSession.Key}, UploadSessionCachePrefix)
   212  		}
   213  
   214  		// 执行删除
   215  		toBeDeletedSrcs := append(sourceNamesAll, thumbs...)
   216  		failedFile, _ := fs.Handler.Delete(ctx, toBeDeletedSrcs)
   217  
   218  		// Exclude failed results related to thumb file
   219  		failed[policyID] = util.SliceDifference(failedFile, thumbs)
   220  	}
   221  
   222  	return failed
   223  }
   224  
   225  // GroupFileByPolicy 将目标文件按照存储策略分组
   226  func (fs *FileSystem) GroupFileByPolicy(ctx context.Context, files []model.File) map[uint][]*model.File {
   227  	var policyGroup = make(map[uint][]*model.File)
   228  
   229  	for key := range files {
   230  		if file, ok := policyGroup[files[key].PolicyID]; ok {
   231  			// 如果已存在分组,直接追加
   232  			policyGroup[files[key].PolicyID] = append(file, &files[key])
   233  		} else {
   234  			// 分组不存在,创建
   235  			policyGroup[files[key].PolicyID] = make([]*model.File, 0)
   236  			policyGroup[files[key].PolicyID] = append(policyGroup[files[key].PolicyID], &files[key])
   237  		}
   238  	}
   239  
   240  	return policyGroup
   241  }
   242  
   243  // GetDownloadURL 创建文件下载链接, timeout 为数据库中存储过期时间的字段
   244  func (fs *FileSystem) GetDownloadURL(ctx context.Context, id uint, timeout string) (string, error) {
   245  	err := fs.resetFileIDIfNotExist(ctx, id)
   246  	if err != nil {
   247  		return "", err
   248  	}
   249  	fileTarget := &fs.FileTarget[0]
   250  
   251  	// 生成下載地址
   252  	ttl := model.GetIntSetting(timeout, 60)
   253  	source, err := fs.SignURL(
   254  		ctx,
   255  		fileTarget,
   256  		int64(ttl),
   257  		true,
   258  	)
   259  	if err != nil {
   260  		return "", err
   261  	}
   262  
   263  	return source, nil
   264  }
   265  
   266  // GetSource 获取可直接访问文件的外链地址
   267  func (fs *FileSystem) GetSource(ctx context.Context, fileID uint) (string, error) {
   268  	// 查找文件记录
   269  	err := fs.resetFileIDIfNotExist(ctx, fileID)
   270  	if err != nil {
   271  		return "", ErrObjectNotExist.WithError(err)
   272  	}
   273  
   274  	// 检查存储策略是否可以获得外链
   275  	if !fs.Policy.IsOriginLinkEnable {
   276  		return "", serializer.NewError(
   277  			serializer.CodePolicyNotAllowed,
   278  			"This policy is not enabled for getting source link",
   279  			nil,
   280  		)
   281  	}
   282  
   283  	source, err := fs.SignURL(ctx, &fs.FileTarget[0], 0, false)
   284  	if err != nil {
   285  		return "", serializer.NewError(serializer.CodeNotSet, "Failed to get source link", err)
   286  	}
   287  
   288  	return source, nil
   289  }
   290  
   291  // SignURL 签名文件原始 URL
   292  func (fs *FileSystem) SignURL(ctx context.Context, file *model.File, ttl int64, isDownload bool) (string, error) {
   293  	fs.FileTarget = []model.File{*file}
   294  	ctx = context.WithValue(ctx, fsctx.FileModelCtx, *file)
   295  
   296  	err := fs.resetPolicyToFirstFile(ctx)
   297  	if err != nil {
   298  		return "", err
   299  	}
   300  
   301  	// 签名最终URL
   302  	// 生成外链地址
   303  	source, err := fs.Handler.Source(ctx, fs.FileTarget[0].SourceName, ttl, isDownload, fs.User.Group.SpeedLimit)
   304  	if err != nil {
   305  		return "", serializer.NewError(serializer.CodeNotSet, "Failed to get source link", err)
   306  	}
   307  
   308  	return source, nil
   309  }
   310  
   311  // ResetFileIfNotExist 重设当前目标文件为 path,如果当前目标为空
   312  func (fs *FileSystem) ResetFileIfNotExist(ctx context.Context, path string) error {
   313  	// 找到文件
   314  	if len(fs.FileTarget) == 0 {
   315  		exist, file := fs.IsFileExist(path)
   316  		if !exist {
   317  			return ErrObjectNotExist
   318  		}
   319  		fs.FileTarget = []model.File{*file}
   320  	}
   321  
   322  	// 将当前存储策略重设为文件使用的
   323  	return fs.resetPolicyToFirstFile(ctx)
   324  }
   325  
   326  // ResetFileIfNotExist 重设当前目标文件为 id,如果当前目标为空
   327  func (fs *FileSystem) resetFileIDIfNotExist(ctx context.Context, id uint) error {
   328  	// 找到文件
   329  	if len(fs.FileTarget) == 0 {
   330  		file, err := model.GetFilesByIDs([]uint{id}, fs.User.ID)
   331  		if err != nil || len(file) == 0 {
   332  			return ErrObjectNotExist
   333  		}
   334  		fs.FileTarget = []model.File{file[0]}
   335  	}
   336  
   337  	// 如果上下文限制了父目录,则进行检查
   338  	if parent, ok := ctx.Value(fsctx.LimitParentCtx).(*model.Folder); ok {
   339  		if parent.ID != fs.FileTarget[0].FolderID {
   340  			return ErrObjectNotExist
   341  		}
   342  	}
   343  
   344  	// 将当前存储策略重设为文件使用的
   345  	return fs.resetPolicyToFirstFile(ctx)
   346  }
   347  
   348  // resetPolicyToFirstFile 将当前存储策略重设为第一个目标文件文件使用的
   349  func (fs *FileSystem) resetPolicyToFirstFile(ctx context.Context) error {
   350  	if len(fs.FileTarget) == 0 {
   351  		return ErrObjectNotExist
   352  	}
   353  
   354  	// 从机模式不进行操作
   355  	if conf.SystemConfig.Mode == "slave" {
   356  		return nil
   357  	}
   358  
   359  	fs.Policy = fs.FileTarget[0].GetPolicy()
   360  	err := fs.DispatchHandler()
   361  	if err != nil {
   362  		return err
   363  	}
   364  	return nil
   365  }
   366  
   367  // Search 搜索文件
   368  func (fs *FileSystem) Search(ctx context.Context, keywords ...interface{}) ([]serializer.Object, error) {
   369  	parents := make([]uint, 0)
   370  
   371  	// 如果限定了根目录,则只在这个根目录下搜索。
   372  	if fs.Root != nil {
   373  		allFolders, err := model.GetRecursiveChildFolder([]uint{fs.Root.ID}, fs.User.ID, true)
   374  		if err != nil {
   375  			return nil, fmt.Errorf("failed to list all folders: %w", err)
   376  		}
   377  
   378  		for _, folder := range allFolders {
   379  			parents = append(parents, folder.ID)
   380  		}
   381  	}
   382  
   383  	files, _ := model.GetFilesByKeywords(fs.User.ID, parents, keywords...)
   384  	fs.SetTargetFile(&files)
   385  
   386  	return fs.listObjects(ctx, "/", files, nil, nil), nil
   387  }