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

     1  package filesystem
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path"
     7  	"time"
     8  
     9  	model "github.com/cloudreve/Cloudreve/v3/models"
    10  	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
    11  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
    12  	"github.com/cloudreve/Cloudreve/v3/pkg/request"
    13  	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
    14  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
    15  	"github.com/gin-gonic/gin"
    16  	"github.com/gofrs/uuid"
    17  )
    18  
    19  /* ================
    20  	 上传处理相关
    21     ================
    22  */
    23  
    24  const (
    25  	UploadSessionMetaKey     = "upload_session"
    26  	UploadSessionCtx         = "uploadSession"
    27  	UserCtx                  = "user"
    28  	UploadSessionCachePrefix = "callback_"
    29  )
    30  
    31  // Upload 上传文件
    32  func (fs *FileSystem) Upload(ctx context.Context, file *fsctx.FileStream) (err error) {
    33  	// 上传前的钩子
    34  	err = fs.Trigger(ctx, "BeforeUpload", file)
    35  	if err != nil {
    36  		request.BlackHole(file)
    37  		return err
    38  	}
    39  
    40  	// 生成文件名和路径,
    41  	var savePath string
    42  	if file.SavePath == "" {
    43  		// 如果是更新操作就从上下文中获取
    44  		if originFile, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
    45  			savePath = originFile.SourceName
    46  		} else {
    47  			savePath = fs.GenerateSavePath(ctx, file)
    48  		}
    49  		file.SavePath = savePath
    50  	}
    51  
    52  	// 保存文件
    53  	if file.Mode&fsctx.Nop != fsctx.Nop {
    54  		// 处理客户端未完成上传时,关闭连接
    55  		go fs.CancelUpload(ctx, savePath, file)
    56  
    57  		err = fs.Handler.Put(ctx, file)
    58  		if err != nil {
    59  			fs.Trigger(ctx, "AfterUploadFailed", file)
    60  			return err
    61  		}
    62  	}
    63  
    64  	// 上传完成后的钩子
    65  	err = fs.Trigger(ctx, "AfterUpload", file)
    66  
    67  	if err != nil {
    68  		// 上传完成后续处理失败
    69  		followUpErr := fs.Trigger(ctx, "AfterValidateFailed", file)
    70  		// 失败后再失败...
    71  		if followUpErr != nil {
    72  			util.Log().Debug("AfterValidateFailed hook execution failed: %s", followUpErr)
    73  		}
    74  
    75  		return err
    76  	}
    77  
    78  	return nil
    79  }
    80  
    81  // GenerateSavePath 生成要存放文件的路径
    82  // TODO 完善测试
    83  func (fs *FileSystem) GenerateSavePath(ctx context.Context, file fsctx.FileHeader) string {
    84  	fileInfo := file.Info()
    85  	return path.Join(
    86  		fs.Policy.GeneratePath(
    87  			fs.User.Model.ID,
    88  			fileInfo.VirtualPath,
    89  		),
    90  		fs.Policy.GenerateFileName(
    91  			fs.User.Model.ID,
    92  			fileInfo.FileName,
    93  		),
    94  	)
    95  
    96  }
    97  
    98  // CancelUpload 监测客户端取消上传
    99  func (fs *FileSystem) CancelUpload(ctx context.Context, path string, file fsctx.FileHeader) {
   100  	var reqContext context.Context
   101  	if ginCtx, ok := ctx.Value(fsctx.GinCtx).(*gin.Context); ok {
   102  		reqContext = ginCtx.Request.Context()
   103  	} else if reqCtx, ok := ctx.Value(fsctx.HTTPCtx).(context.Context); ok {
   104  		reqContext = reqCtx
   105  	} else {
   106  		return
   107  	}
   108  
   109  	select {
   110  	case <-reqContext.Done():
   111  		select {
   112  		case <-ctx.Done():
   113  			// 客户端正常关闭,不执行操作
   114  		default:
   115  			// 客户端取消上传,删除临时文件
   116  			util.Log().Debug("Client canceled upload.")
   117  			if fs.Hooks["AfterUploadCanceled"] == nil {
   118  				return
   119  			}
   120  			err := fs.Trigger(ctx, "AfterUploadCanceled", file)
   121  			if err != nil {
   122  				util.Log().Debug("AfterUploadCanceled hook execution failed: %s", err)
   123  			}
   124  		}
   125  
   126  	}
   127  }
   128  
   129  // CreateUploadSession 创建上传会话
   130  func (fs *FileSystem) CreateUploadSession(ctx context.Context, file *fsctx.FileStream) (*serializer.UploadCredential, error) {
   131  	// 获取相关有效期设置
   132  	callBackSessionTTL := model.GetIntSetting("upload_session_timeout", 86400)
   133  
   134  	callbackKey := uuid.Must(uuid.NewV4()).String()
   135  	fileSize := file.Size
   136  
   137  	// 创建占位的文件,同时校验文件信息
   138  	file.Mode = fsctx.Nop
   139  	if callbackKey != "" {
   140  		file.UploadSessionID = &callbackKey
   141  	}
   142  
   143  	fs.Use("BeforeUpload", HookValidateFile)
   144  	fs.Use("BeforeUpload", HookValidateCapacity)
   145  
   146  	// 验证文件规格
   147  	if err := fs.Upload(ctx, file); err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	uploadSession := &serializer.UploadSession{
   152  		Key:            callbackKey,
   153  		UID:            fs.User.ID,
   154  		Policy:         *fs.Policy,
   155  		VirtualPath:    file.VirtualPath,
   156  		Name:           file.Name,
   157  		Size:           fileSize,
   158  		SavePath:       file.SavePath,
   159  		LastModified:   file.LastModified,
   160  		CallbackSecret: util.RandStringRunes(32),
   161  	}
   162  
   163  	// 获取上传凭证
   164  	credential, err := fs.Handler.Token(ctx, int64(callBackSessionTTL), uploadSession, file)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	// 创建占位符
   170  	if !fs.Policy.IsUploadPlaceholderWithSize() {
   171  		fs.Use("AfterUpload", HookClearFileHeaderSize)
   172  	}
   173  	fs.Use("AfterUpload", GenericAfterUpload)
   174  	ctx = context.WithValue(ctx, fsctx.IgnoreDirectoryConflictCtx, true)
   175  	if err := fs.Upload(ctx, file); err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// 创建回调会话
   180  	err = cache.Set(
   181  		UploadSessionCachePrefix+callbackKey,
   182  		*uploadSession,
   183  		callBackSessionTTL,
   184  	)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	// 补全上传凭证其他信息
   190  	credential.Expires = time.Now().Add(time.Duration(callBackSessionTTL) * time.Second).Unix()
   191  
   192  	return credential, nil
   193  }
   194  
   195  // UploadFromStream 从文件流上传文件
   196  func (fs *FileSystem) UploadFromStream(ctx context.Context, file *fsctx.FileStream, resetPolicy bool) error {
   197  	if resetPolicy {
   198  		// 重设存储策略
   199  		fs.Policy = &fs.User.Policy
   200  		err := fs.DispatchHandler()
   201  		if err != nil {
   202  			return err
   203  		}
   204  	}
   205  
   206  	// 给文件系统分配钩子
   207  	fs.Lock.Lock()
   208  	if fs.Hooks == nil {
   209  		fs.Use("BeforeUpload", HookValidateFile)
   210  		fs.Use("BeforeUpload", HookValidateCapacity)
   211  		fs.Use("AfterUploadCanceled", HookDeleteTempFile)
   212  		fs.Use("AfterUpload", GenericAfterUpload)
   213  		fs.Use("AfterValidateFailed", HookDeleteTempFile)
   214  	}
   215  	fs.Lock.Unlock()
   216  
   217  	// 开始上传
   218  	return fs.Upload(ctx, file)
   219  }
   220  
   221  // UploadFromPath 将本机已有文件上传到用户的文件系统
   222  func (fs *FileSystem) UploadFromPath(ctx context.Context, src, dst string, mode fsctx.WriteMode) error {
   223  	file, err := os.Open(util.RelativePath(src))
   224  	if err != nil {
   225  		return err
   226  	}
   227  	defer file.Close()
   228  
   229  	// 获取源文件大小
   230  	fi, err := file.Stat()
   231  	if err != nil {
   232  		return err
   233  	}
   234  	size := fi.Size()
   235  
   236  	// 开始上传
   237  	return fs.UploadFromStream(ctx, &fsctx.FileStream{
   238  		File:        file,
   239  		Seeker:      file,
   240  		Size:        uint64(size),
   241  		Name:        path.Base(dst),
   242  		VirtualPath: path.Dir(dst),
   243  		Mode:        mode,
   244  	}, true)
   245  }