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

     1  package explorer
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	model "github.com/cloudreve/Cloudreve/v3/models"
     7  	"github.com/cloudreve/Cloudreve/v3/pkg/auth"
     8  	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
     9  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
    10  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/local"
    11  	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
    12  	"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
    13  	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
    14  	"github.com/cloudreve/Cloudreve/v3/pkg/util"
    15  	"github.com/gin-gonic/gin"
    16  	"io/ioutil"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  )
    21  
    22  // CreateUploadSessionService 获取上传凭证服务
    23  type CreateUploadSessionService struct {
    24  	Path         string `json:"path" binding:"required"`
    25  	Size         uint64 `json:"size" binding:"min=0"`
    26  	Name         string `json:"name" binding:"required"`
    27  	PolicyID     string `json:"policy_id" binding:"required"`
    28  	LastModified int64  `json:"last_modified"`
    29  	MimeType     string `json:"mime_type"`
    30  }
    31  
    32  // Create 创建新的上传会话
    33  func (service *CreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response {
    34  	// 创建文件系统
    35  	fs, err := filesystem.NewFileSystemFromContext(c)
    36  	if err != nil {
    37  		return serializer.Err(serializer.CodeCreateFSError, "", err)
    38  	}
    39  
    40  	// 取得存储策略的ID
    41  	rawID, err := hashid.DecodeHashID(service.PolicyID, hashid.PolicyID)
    42  	if err != nil {
    43  		return serializer.Err(serializer.CodePolicyNotExist, "", err)
    44  	}
    45  
    46  	if fs.Policy.ID != rawID {
    47  		return serializer.Err(serializer.CodePolicyNotAllowed, "存储策略发生变化,请刷新文件列表并重新添加此任务", nil)
    48  	}
    49  
    50  	file := &fsctx.FileStream{
    51  		Size:        service.Size,
    52  		Name:        service.Name,
    53  		VirtualPath: service.Path,
    54  		File:        ioutil.NopCloser(strings.NewReader("")),
    55  		MimeType:    service.MimeType,
    56  	}
    57  	if service.LastModified > 0 {
    58  		lastModified := time.UnixMilli(service.LastModified)
    59  		file.LastModified = &lastModified
    60  	}
    61  	credential, err := fs.CreateUploadSession(ctx, file)
    62  	if err != nil {
    63  		return serializer.Err(serializer.CodeNotSet, err.Error(), err)
    64  	}
    65  
    66  	return serializer.Response{
    67  		Code: 0,
    68  		Data: credential,
    69  	}
    70  }
    71  
    72  // UploadService 本机及从机策略上传服务
    73  type UploadService struct {
    74  	ID    string `uri:"sessionId" binding:"required"`
    75  	Index int    `uri:"index" form:"index" binding:"min=0"`
    76  }
    77  
    78  // LocalUpload 处理本机文件分片上传
    79  func (service *UploadService) LocalUpload(ctx context.Context, c *gin.Context) serializer.Response {
    80  	uploadSessionRaw, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
    81  	if !ok {
    82  		return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
    83  	}
    84  
    85  	uploadSession := uploadSessionRaw.(serializer.UploadSession)
    86  
    87  	fs, err := filesystem.NewFileSystemFromContext(c)
    88  	if err != nil {
    89  		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
    90  	}
    91  
    92  	if uploadSession.UID != fs.User.ID {
    93  		return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
    94  	}
    95  
    96  	// 查找上传会话创建的占位文件
    97  	file, err := model.GetFilesByUploadSession(service.ID, fs.User.ID)
    98  	if err != nil {
    99  		return serializer.Err(serializer.CodeUploadSessionExpired, "", err)
   100  	}
   101  
   102  	// 重设 fs 存储策略
   103  	if !uploadSession.Policy.IsTransitUpload(uploadSession.Size) {
   104  		return serializer.Err(serializer.CodePolicyNotAllowed, "", err)
   105  	}
   106  
   107  	fs.Policy = &uploadSession.Policy
   108  	if err := fs.DispatchHandler(); err != nil {
   109  		return serializer.Err(serializer.CodePolicyNotExist, "", err)
   110  	}
   111  
   112  	expectedSizeStart := file.Size
   113  	actualSizeStart := uint64(service.Index) * uploadSession.Policy.OptionsSerialized.ChunkSize
   114  	if uploadSession.Policy.OptionsSerialized.ChunkSize == 0 && service.Index > 0 {
   115  		return serializer.Err(serializer.CodeInvalidChunkIndex, "Chunk index cannot be greater than 0", nil)
   116  	}
   117  
   118  	if expectedSizeStart < actualSizeStart {
   119  		return serializer.Err(serializer.CodeInvalidChunkIndex, "Chunk must be uploaded in order", nil)
   120  	}
   121  
   122  	if expectedSizeStart > actualSizeStart {
   123  		util.Log().Info("Trying to overwrite chunk[%d] Start=%d", service.Index, actualSizeStart)
   124  	}
   125  
   126  	return processChunkUpload(ctx, c, fs, &uploadSession, service.Index, file, fsctx.Append)
   127  }
   128  
   129  // SlaveUpload 处理从机文件分片上传
   130  func (service *UploadService) SlaveUpload(ctx context.Context, c *gin.Context) serializer.Response {
   131  	uploadSessionRaw, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
   132  	if !ok {
   133  		return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
   134  	}
   135  
   136  	uploadSession := uploadSessionRaw.(serializer.UploadSession)
   137  
   138  	fs, err := filesystem.NewAnonymousFileSystem()
   139  	if err != nil {
   140  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   141  	}
   142  
   143  	fs.Handler = local.Driver{}
   144  
   145  	// 解析需要的参数
   146  	service.Index, _ = strconv.Atoi(c.Query("chunk"))
   147  	mode := fsctx.Append
   148  	if c.GetHeader(auth.CrHeaderPrefix+"Overwrite") == "true" {
   149  		mode |= fsctx.Overwrite
   150  	}
   151  
   152  	return processChunkUpload(ctx, c, fs, &uploadSession, service.Index, nil, mode)
   153  }
   154  
   155  func processChunkUpload(ctx context.Context, c *gin.Context, fs *filesystem.FileSystem, session *serializer.UploadSession, index int, file *model.File, mode fsctx.WriteMode) serializer.Response {
   156  	// 取得并校验文件大小是否符合分片要求
   157  	chunkSize := session.Policy.OptionsSerialized.ChunkSize
   158  	isLastChunk := session.Policy.OptionsSerialized.ChunkSize == 0 || uint64(index+1)*chunkSize >= session.Size
   159  	expectedLength := chunkSize
   160  	if isLastChunk {
   161  		expectedLength = session.Size - uint64(index)*chunkSize
   162  	}
   163  
   164  	fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64)
   165  	if err != nil || (expectedLength != fileSize) {
   166  		return serializer.Err(
   167  			serializer.CodeInvalidContentLength,
   168  			fmt.Sprintf("Invalid Content-Length (expected: %d)", expectedLength),
   169  			err,
   170  		)
   171  	}
   172  
   173  	// 非首个分片时需要允许覆盖
   174  	if index > 0 {
   175  		mode |= fsctx.Overwrite
   176  	}
   177  
   178  	fileData := fsctx.FileStream{
   179  		MimeType:     c.Request.Header.Get("Content-Type"),
   180  		File:         c.Request.Body,
   181  		Size:         fileSize,
   182  		Name:         session.Name,
   183  		VirtualPath:  session.VirtualPath,
   184  		SavePath:     session.SavePath,
   185  		Mode:         mode,
   186  		AppendStart:  chunkSize * uint64(index),
   187  		Model:        file,
   188  		LastModified: session.LastModified,
   189  	}
   190  
   191  	// 给文件系统分配钩子
   192  	fs.Use("AfterUploadCanceled", filesystem.HookTruncateFileTo(fileData.AppendStart))
   193  	fs.Use("AfterValidateFailed", filesystem.HookTruncateFileTo(fileData.AppendStart))
   194  
   195  	if file != nil {
   196  		fs.Use("BeforeUpload", filesystem.HookValidateCapacity)
   197  		fs.Use("AfterUpload", filesystem.HookChunkUploaded)
   198  		fs.Use("AfterValidateFailed", filesystem.HookChunkUploadFailed)
   199  		if isLastChunk {
   200  			fs.Use("AfterUpload", filesystem.HookPopPlaceholderToFile(""))
   201  			fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key))
   202  		}
   203  	} else {
   204  		if isLastChunk {
   205  			fs.Use("AfterUpload", filesystem.SlaveAfterUpload(session))
   206  			fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key))
   207  		}
   208  	}
   209  
   210  	// 执行上传
   211  	uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c)
   212  	err = fs.Upload(uploadCtx, &fileData)
   213  	if err != nil {
   214  		return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
   215  	}
   216  
   217  	return serializer.Response{}
   218  }
   219  
   220  // UploadSessionService 上传会话服务
   221  type UploadSessionService struct {
   222  	ID string `uri:"sessionId" binding:"required"`
   223  }
   224  
   225  // Delete 删除指定上传会话
   226  func (service *UploadSessionService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
   227  	// 创建文件系统
   228  	fs, err := filesystem.NewFileSystemFromContext(c)
   229  	if err != nil {
   230  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   231  	}
   232  	defer fs.Recycle()
   233  
   234  	// 查找需要删除的上传会话的占位文件
   235  	file, err := model.GetFilesByUploadSession(service.ID, fs.User.ID)
   236  	if err != nil {
   237  		return serializer.Err(serializer.CodeUploadSessionExpired, "", err)
   238  	}
   239  
   240  	// 删除文件
   241  	if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil {
   242  		return serializer.Err(serializer.CodeInternalSetting, "Failed to delete upload session", err)
   243  	}
   244  
   245  	return serializer.Response{}
   246  }
   247  
   248  // SlaveDelete 从机删除指定上传会话
   249  func (service *UploadSessionService) SlaveDelete(ctx context.Context, c *gin.Context) serializer.Response {
   250  	// 创建文件系统
   251  	fs, err := filesystem.NewAnonymousFileSystem()
   252  	if err != nil {
   253  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   254  	}
   255  	defer fs.Recycle()
   256  
   257  	session, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
   258  	if !ok {
   259  		return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
   260  	}
   261  
   262  	if _, err := fs.Handler.Delete(ctx, []string{session.(serializer.UploadSession).SavePath}); err != nil {
   263  		return serializer.Err(serializer.CodeInternalSetting, "Failed to delete temp file", err)
   264  	}
   265  
   266  	cache.Deletes([]string{service.ID}, filesystem.UploadSessionCachePrefix)
   267  	return serializer.Response{}
   268  }
   269  
   270  // DeleteAllUploadSession 删除当前用户的全部上传绘会话
   271  func DeleteAllUploadSession(ctx context.Context, c *gin.Context) serializer.Response {
   272  	// 创建文件系统
   273  	fs, err := filesystem.NewFileSystemFromContext(c)
   274  	if err != nil {
   275  		return serializer.Err(serializer.CodeCreateFSError, "", err)
   276  	}
   277  	defer fs.Recycle()
   278  
   279  	// 查找需要删除的上传会话的占位文件
   280  	files := model.GetUploadPlaceholderFiles(fs.User.ID)
   281  	fileIDs := make([]uint, len(files))
   282  	for i, file := range files {
   283  		fileIDs[i] = file.ID
   284  	}
   285  
   286  	// 删除文件
   287  	if err := fs.Delete(ctx, []uint{}, fileIDs, false, false); err != nil {
   288  		return serializer.Err(serializer.CodeInternalSetting, "Failed to cleanup upload session", err)
   289  	}
   290  
   291  	return serializer.Response{}
   292  }