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 }