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 }