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 }