github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/service/explorer/file.go (about) 1 package explorer 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "github.com/cloudreve/Cloudreve/v3/pkg/util" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "path" 13 "strconv" 14 "strings" 15 16 model "github.com/cloudreve/Cloudreve/v3/models" 17 "github.com/cloudreve/Cloudreve/v3/pkg/cache" 18 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem" 19 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" 20 "github.com/cloudreve/Cloudreve/v3/pkg/serializer" 21 "github.com/cloudreve/Cloudreve/v3/pkg/wopi" 22 "github.com/gin-gonic/gin" 23 ) 24 25 // SingleFileService 对单文件进行操作的五福,path为文件完整路径 26 type SingleFileService struct { 27 Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"` 28 } 29 30 // FileIDService 通过文件ID对文件进行操作的服务 31 type FileIDService struct { 32 } 33 34 // FileAnonymousGetService 匿名(外链)获取文件服务 35 type FileAnonymousGetService struct { 36 ID uint `uri:"id" binding:"required,min=1"` 37 Name string `uri:"name" binding:"required"` 38 } 39 40 // DownloadService 文件下載服务 41 type DownloadService struct { 42 ID string `uri:"id" binding:"required"` 43 } 44 45 // ArchiveService 文件流式打包下載服务 46 type ArchiveService struct { 47 ID string `uri:"sessionID" binding:"required"` 48 } 49 50 // New 创建新文件 51 func (service *SingleFileService) Create(c *gin.Context) serializer.Response { 52 // 创建文件系统 53 fs, err := filesystem.NewFileSystemFromContext(c) 54 if err != nil { 55 return serializer.Err(serializer.CodeCreateFSError, "", err) 56 } 57 defer fs.Recycle() 58 59 // 上下文 60 ctx, cancel := context.WithCancel(context.Background()) 61 defer cancel() 62 63 // 给文件系统分配钩子 64 fs.Use("BeforeUpload", filesystem.HookValidateFile) 65 fs.Use("AfterUpload", filesystem.GenericAfterUpload) 66 67 // 上传空文件 68 err = fs.Upload(ctx, &fsctx.FileStream{ 69 File: ioutil.NopCloser(strings.NewReader("")), 70 Size: 0, 71 VirtualPath: path.Dir(service.Path), 72 Name: path.Base(service.Path), 73 }) 74 if err != nil { 75 return serializer.Err(serializer.CodeUploadFailed, err.Error(), err) 76 } 77 78 return serializer.Response{ 79 Code: 0, 80 } 81 } 82 83 // List 列出从机上的文件 84 func (service *SlaveListService) List(c *gin.Context) serializer.Response { 85 // 创建文件系统 86 fs, err := filesystem.NewAnonymousFileSystem() 87 if err != nil { 88 return serializer.Err(serializer.CodeCreateFSError, "", err) 89 } 90 defer fs.Recycle() 91 92 objects, err := fs.Handler.List(context.Background(), service.Path, service.Recursive) 93 if err != nil { 94 return serializer.Err(serializer.CodeIOFailed, "Cannot list files", err) 95 } 96 97 res, _ := json.Marshal(objects) 98 return serializer.Response{Data: string(res)} 99 } 100 101 // DownloadArchived 通过预签名 URL 打包下载 102 func (service *ArchiveService) DownloadArchived(ctx context.Context, c *gin.Context) serializer.Response { 103 userRaw, exist := cache.Get("archive_user_" + service.ID) 104 if !exist { 105 return serializer.Err(serializer.CodeNotFound, "Archive session not exist", nil) 106 } 107 user := userRaw.(model.User) 108 109 // 创建文件系统 110 fs, err := filesystem.NewFileSystem(&user) 111 if err != nil { 112 return serializer.Err(serializer.CodeCreateFSError, "", err) 113 } 114 defer fs.Recycle() 115 116 // 查找打包的临时文件 117 archiveSession, exist := cache.Get("archive_" + service.ID) 118 if !exist { 119 return serializer.Err(serializer.CodeNotFound, "Archive session not exist", nil) 120 } 121 122 // 开始打包 123 c.Header("Content-Disposition", "attachment;") 124 c.Header("Content-Type", "application/zip") 125 itemService := archiveSession.(ItemIDService) 126 items := itemService.Raw() 127 ctx = context.WithValue(ctx, fsctx.GinCtx, c) 128 err = fs.Compress(ctx, c.Writer, items.Dirs, items.Items, true) 129 if err != nil { 130 return serializer.Err(serializer.CodeNotSet, "Failed to compress file", err) 131 } 132 133 return serializer.Response{ 134 Code: 0, 135 } 136 } 137 138 // Download 签名的匿名文件下载 139 func (service *FileAnonymousGetService) Download(ctx context.Context, c *gin.Context) serializer.Response { 140 fs, err := filesystem.NewAnonymousFileSystem() 141 if err != nil { 142 return serializer.Err(serializer.CodeCreateFSError, "", err) 143 } 144 defer fs.Recycle() 145 146 // 查找文件 147 err = fs.SetTargetFileByIDs([]uint{service.ID}) 148 if err != nil { 149 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 150 } 151 152 // 获取文件流 153 rs, err := fs.GetDownloadContent(ctx, 0) 154 defer rs.Close() 155 if err != nil { 156 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 157 } 158 159 // 发送文件 160 http.ServeContent(c.Writer, c.Request, service.Name, fs.FileTarget[0].UpdatedAt, rs) 161 162 return serializer.Response{ 163 Code: 0, 164 } 165 } 166 167 // Source 重定向到文件的有效原始链接 168 func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Context) serializer.Response { 169 fs, err := filesystem.NewAnonymousFileSystem() 170 if err != nil { 171 return serializer.Err(serializer.CodeCreateFSError, "", err) 172 } 173 defer fs.Recycle() 174 175 // 查找文件 176 err = fs.SetTargetFileByIDs([]uint{service.ID}) 177 if err != nil { 178 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 179 } 180 181 // 获取文件流 182 ttl := int64(model.GetIntSetting("preview_timeout", 60)) 183 res, err := fs.SignURL(ctx, &fs.FileTarget[0], ttl, false) 184 if err != nil { 185 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 186 } 187 188 c.Header("Cache-Control", fmt.Sprintf("max-age=%d", ttl)) 189 return serializer.Response{ 190 Code: -302, 191 Data: res, 192 } 193 } 194 195 // CreateDocPreviewSession 创建DOC文件预览会话,返回预览地址 196 func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context, editable bool) serializer.Response { 197 // 创建文件系统 198 fs, err := filesystem.NewFileSystemFromContext(c) 199 if err != nil { 200 return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) 201 } 202 defer fs.Recycle() 203 204 // 获取对象id 205 objectID, _ := c.Get("object_id") 206 207 // 如果上下文中已有File对象,则重设目标 208 if file, ok := ctx.Value(fsctx.FileModelCtx).(*model.File); ok { 209 fs.SetTargetFile(&[]model.File{*file}) 210 objectID = uint(0) 211 } 212 213 // 如果上下文中已有Folder对象,则重设根目录 214 if folder, ok := ctx.Value(fsctx.FolderModelCtx).(*model.Folder); ok { 215 fs.Root = folder 216 path := ctx.Value(fsctx.PathCtx).(string) 217 err := fs.ResetFileIfNotExist(ctx, path) 218 if err != nil { 219 return serializer.Err(serializer.CodeNotFound, err.Error(), err) 220 } 221 objectID = uint(0) 222 } 223 224 // 获取文件临时下载地址 225 downloadURL, err := fs.GetDownloadURL(ctx, objectID.(uint), "doc_preview_timeout") 226 if err != nil { 227 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 228 } 229 230 // For newer version of Cloudreve - Local Policy 231 // When do not use a cdn, the downloadURL withouts hosts, like "/api/v3/file/download/xxx" 232 if strings.HasPrefix(downloadURL, "/") { 233 downloadURI, err := url.Parse(downloadURL) 234 if err != nil { 235 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 236 } 237 downloadURL = model.GetSiteURL().ResolveReference(downloadURI).String() 238 } 239 240 var resp serializer.DocPreviewSession 241 242 // Use WOPI preview if available 243 if model.IsTrueVal(model.GetSettingByName("wopi_enabled")) && wopi.Default != nil { 244 maxSize := model.GetIntSetting("maxEditSize", 0) 245 if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) { 246 return serializer.Err(serializer.CodeFileTooLarge, "", nil) 247 } 248 249 action := wopi.ActionPreview 250 if editable { 251 action = wopi.ActionEdit 252 } 253 254 session, err := wopi.Default.NewSession(fs.FileTarget[0].UserID, &fs.FileTarget[0], action) 255 if err != nil { 256 return serializer.Err(serializer.CodeInternalSetting, "Failed to create WOPI session", err) 257 } 258 259 resp.URL = session.ActionURL.String() 260 resp.AccessTokenTTL = session.AccessTokenTTL 261 resp.AccessToken = session.AccessToken 262 return serializer.Response{ 263 Code: 0, 264 Data: resp, 265 } 266 } 267 268 // 生成最终的预览器地址 269 srcB64 := base64.StdEncoding.EncodeToString([]byte(downloadURL)) 270 srcEncoded := url.QueryEscape(downloadURL) 271 srcB64Encoded := url.QueryEscape(srcB64) 272 resp.URL = util.Replace(map[string]string{ 273 "{$src}": srcEncoded, 274 "{$srcB64}": srcB64Encoded, 275 "{$name}": url.QueryEscape(fs.FileTarget[0].Name), 276 }, model.GetSettingByName("office_preview_service")) 277 278 return serializer.Response{ 279 Code: 0, 280 Data: resp, 281 } 282 } 283 284 // CreateDownloadSession 创建下载会话,获取下载URL 285 func (service *FileIDService) CreateDownloadSession(ctx context.Context, c *gin.Context) serializer.Response { 286 // 创建文件系统 287 fs, err := filesystem.NewFileSystemFromContext(c) 288 if err != nil { 289 return serializer.Err(serializer.CodeCreateFSError, "", err) 290 } 291 defer fs.Recycle() 292 293 // 获取对象id 294 objectID, _ := c.Get("object_id") 295 296 // 获取下载地址 297 downloadURL, err := fs.GetDownloadURL(ctx, objectID.(uint), "download_timeout") 298 if err != nil { 299 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 300 } 301 302 return serializer.Response{ 303 Code: 0, 304 Data: downloadURL, 305 } 306 } 307 308 // Download 通过签名URL的文件下载,无需登录 309 func (service *DownloadService) Download(ctx context.Context, c *gin.Context) serializer.Response { 310 // 创建文件系统 311 fs, err := filesystem.NewFileSystemFromContext(c) 312 if err != nil { 313 return serializer.Err(serializer.CodeCreateFSError, "", err) 314 } 315 defer fs.Recycle() 316 317 // 查找打包的临时文件 318 file, exist := cache.Get("download_" + service.ID) 319 if !exist { 320 return serializer.Err(serializer.CodeNotFound, "Download session not exist", nil) 321 } 322 fs.FileTarget = []model.File{file.(model.File)} 323 324 // 开始处理下载 325 ctx = context.WithValue(ctx, fsctx.GinCtx, c) 326 rs, err := fs.GetDownloadContent(ctx, 0) 327 if err != nil { 328 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 329 } 330 defer rs.Close() 331 332 // 设置文件名 333 c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(fs.FileTarget[0].Name)+"\"") 334 335 if fs.User.Group.OptionsSerialized.OneTimeDownload { 336 // 清理资源,删除临时文件 337 _ = cache.Deletes([]string{service.ID}, "download_") 338 } 339 340 // 发送文件 341 http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, rs) 342 343 return serializer.Response{ 344 Code: 0, 345 } 346 } 347 348 // PreviewContent 预览文件,需要登录会话, isText - 是否为文本文件,文本文件会 349 // 强制经由服务端中转 350 func (service *FileIDService) PreviewContent(ctx context.Context, c *gin.Context, isText bool) serializer.Response { 351 // 创建文件系统 352 fs, err := filesystem.NewFileSystemFromContext(c) 353 if err != nil { 354 return serializer.Err(serializer.CodeCreateFSError, "", err) 355 } 356 defer fs.Recycle() 357 358 // 获取对象id 359 objectID, _ := c.Get("object_id") 360 361 // 如果上下文中已有File对象,则重设目标 362 if file, ok := ctx.Value(fsctx.FileModelCtx).(*model.File); ok { 363 fs.SetTargetFile(&[]model.File{*file}) 364 objectID = uint(0) 365 } 366 367 // 如果上下文中已有Folder对象,则重设根目录 368 if folder, ok := ctx.Value(fsctx.FolderModelCtx).(*model.Folder); ok { 369 fs.Root = folder 370 path := ctx.Value(fsctx.PathCtx).(string) 371 err := fs.ResetFileIfNotExist(ctx, path) 372 if err != nil { 373 return serializer.Err(serializer.CodeFileNotFound, err.Error(), err) 374 } 375 objectID = uint(0) 376 } 377 378 // 获取文件预览响应 379 resp, err := fs.Preview(ctx, objectID.(uint), isText) 380 if err != nil { 381 return serializer.Err(serializer.CodeNotSet, err.Error(), err) 382 } 383 384 // 重定向到文件源 385 if resp.Redirect { 386 c.Header("Cache-Control", fmt.Sprintf("max-age=%d", resp.MaxAge)) 387 return serializer.Response{ 388 Code: -301, 389 Data: resp.URL, 390 } 391 } 392 393 // 直接返回文件内容 394 defer resp.Content.Close() 395 396 if isText { 397 c.Header("Cache-Control", "no-cache") 398 } 399 400 http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content) 401 402 return serializer.Response{ 403 Code: 0, 404 } 405 } 406 407 // PutContent 更新文件内容 408 func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) serializer.Response { 409 // 创建上下文 410 ctx, cancel := context.WithCancel(context.Background()) 411 defer cancel() 412 413 // 取得文件大小 414 fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64) 415 if err != nil { 416 417 return serializer.ParamErr("Invalid content-length value", err) 418 } 419 420 fileData := fsctx.FileStream{ 421 MimeType: c.Request.Header.Get("Content-Type"), 422 File: c.Request.Body, 423 Size: fileSize, 424 Mode: fsctx.Overwrite, 425 } 426 427 // 创建文件系统 428 fs, err := filesystem.NewFileSystemFromContext(c) 429 if err != nil { 430 return serializer.Err(serializer.CodeCreateFSError, "", err) 431 } 432 uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c) 433 434 // 取得现有文件 435 fileID, _ := c.Get("object_id") 436 originFile, _ := model.GetFilesByIDs([]uint{fileID.(uint)}, fs.User.ID) 437 if len(originFile) == 0 { 438 return serializer.Err(serializer.CodeFileNotFound, "", nil) 439 } 440 fileData.Name = originFile[0].Name 441 442 // 检查此文件是否有软链接 443 fileList, err := model.RemoveFilesWithSoftLinks([]model.File{originFile[0]}) 444 if err == nil && len(fileList) == 0 { 445 // 如果包含软连接,应重新生成新文件副本,并更新source_name 446 originFile[0].SourceName = fs.GenerateSavePath(uploadCtx, &fileData) 447 fileData.Mode &= ^fsctx.Overwrite 448 fs.Use("AfterUpload", filesystem.HookUpdateSourceName) 449 fs.Use("AfterUploadCanceled", filesystem.HookUpdateSourceName) 450 fs.Use("AfterUploadCanceled", filesystem.HookCleanFileContent) 451 fs.Use("AfterUploadCanceled", filesystem.HookClearFileSize) 452 fs.Use("AfterValidateFailed", filesystem.HookUpdateSourceName) 453 fs.Use("AfterValidateFailed", filesystem.HookCleanFileContent) 454 fs.Use("AfterValidateFailed", filesystem.HookClearFileSize) 455 } 456 457 // 给文件系统分配钩子 458 fs.Use("BeforeUpload", filesystem.HookResetPolicy) 459 fs.Use("BeforeUpload", filesystem.HookValidateFile) 460 fs.Use("BeforeUpload", filesystem.HookValidateCapacityDiff) 461 fs.Use("AfterUpload", filesystem.GenericAfterUpdate) 462 463 // 执行上传 464 uploadCtx = context.WithValue(uploadCtx, fsctx.FileModelCtx, originFile[0]) 465 err = fs.Upload(uploadCtx, &fileData) 466 if err != nil { 467 return serializer.Err(serializer.CodeUploadFailed, err.Error(), err) 468 } 469 470 return serializer.Response{ 471 Code: 0, 472 } 473 } 474 475 // Sources 批量获取对象的外链 476 func (s *ItemIDService) Sources(ctx context.Context, c *gin.Context) serializer.Response { 477 fs, err := filesystem.NewFileSystemFromContext(c) 478 if err != nil { 479 return serializer.Err(serializer.CodeCreateFSError, "", err) 480 } 481 defer fs.Recycle() 482 483 if len(s.Raw().Items) > fs.User.Group.OptionsSerialized.SourceBatchSize { 484 return serializer.Err(serializer.CodeBatchSourceSize, "", err) 485 } 486 487 res := make([]serializer.Sources, 0, len(s.Raw().Items)) 488 files, err := model.GetFilesByIDs(s.Raw().Items, fs.User.ID) 489 if err != nil || len(files) == 0 { 490 return serializer.Err(serializer.CodeFileNotFound, "", err) 491 } 492 493 getSourceFunc := func(file model.File) (string, error) { 494 fs.FileTarget = []model.File{file} 495 return fs.GetSource(ctx, file.ID) 496 } 497 498 // Create redirected source link if needed 499 if fs.User.Group.OptionsSerialized.RedirectedSource { 500 getSourceFunc = func(file model.File) (string, error) { 501 source, err := file.CreateOrGetSourceLink() 502 if err != nil { 503 return "", err 504 } 505 506 sourceLinkURL, err := source.Link() 507 if err != nil { 508 return "", err 509 } 510 511 return sourceLinkURL, nil 512 } 513 } 514 515 for _, file := range files { 516 sourceURL, err := getSourceFunc(file) 517 current := serializer.Sources{ 518 URL: sourceURL, 519 Name: file.Name, 520 Parent: file.FolderID, 521 } 522 523 if err != nil { 524 current.Error = err.Error() 525 } 526 527 res = append(res, current) 528 } 529 530 return serializer.Response{ 531 Code: 0, 532 Data: res, 533 } 534 }