github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/files/files.go (about) 1 // Package files is the HTTP frontend of the vfs package. It exposes 2 // an HTTP api to manipulate the filesystem and offer all the 3 // possibilities given by the vfs. 4 package files 5 6 import ( 7 "bytes" 8 "encoding/base64" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "math" 14 "net/http" 15 "net/url" 16 "os" 17 "path" 18 "path/filepath" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/cozy/cozy-stack/model/instance" 24 "github.com/cozy/cozy-stack/model/job" 25 "github.com/cozy/cozy-stack/model/note" 26 "github.com/cozy/cozy-stack/model/oauth" 27 "github.com/cozy/cozy-stack/model/permission" 28 "github.com/cozy/cozy-stack/model/sharing" 29 "github.com/cozy/cozy-stack/model/vfs" 30 "github.com/cozy/cozy-stack/pkg/assets/statik" 31 "github.com/cozy/cozy-stack/pkg/config/config" 32 "github.com/cozy/cozy-stack/pkg/consts" 33 "github.com/cozy/cozy-stack/pkg/couchdb" 34 "github.com/cozy/cozy-stack/pkg/jsonapi" 35 "github.com/cozy/cozy-stack/pkg/limits" 36 "github.com/cozy/cozy-stack/pkg/logger" 37 "github.com/cozy/cozy-stack/pkg/metadata" 38 "github.com/cozy/cozy-stack/pkg/utils" 39 "github.com/cozy/cozy-stack/web/middlewares" 40 "github.com/cozy/cozy-stack/worker/thumbnail" 41 "github.com/labstack/echo/v4" 42 "github.com/ncw/swift/v2" 43 ) 44 45 type docPatch struct { 46 docID string 47 docPath string 48 49 Trash bool `json:"move_to_trash,omitempty"` 50 Delete bool `json:"permanent_delete,omitempty"` 51 vfs.DocPatch 52 } 53 54 // TagSeparator is the character separating tags 55 const TagSeparator = "," 56 57 // ErrDocTypeInvalid is used when the document type sent is not 58 // recognized 59 var ErrDocTypeInvalid = errors.New("Invalid document type") 60 61 // SharedDrivesCreationHandler is the handler for POST /files/drives. It 62 // creates the directory where shared and external drives are saved if it 63 // doesn't exist, and return information about this directory. 64 func SharedDrivesCreationHandler(c echo.Context) error { 65 inst := middlewares.GetInstance(c) 66 if err := middlewares.AllowWholeType(c, permission.POST, consts.Files); err != nil { 67 return err 68 } 69 doc, err := inst.EnsureSharedDrivesDir() 70 if err != nil { 71 return wrapVfsError(err) 72 } 73 return jsonapi.Data(c, http.StatusOK, newDir(doc), nil) 74 } 75 76 // CreationHandler handle all POST requests on /files/:file-id 77 // aiming at creating a new document in the FS. Given the Type 78 // parameter of the request, it will either upload a new file or 79 // create a new directory. 80 func CreationHandler(c echo.Context) error { 81 instance := middlewares.GetInstance(c) 82 var doc jsonapi.Object 83 var err error 84 switch c.QueryParam("Type") { 85 case consts.FileType: 86 doc, err = createFileHandler(c, instance.VFS()) 87 case consts.DirType: 88 doc, err = createDirHandler(c, instance.VFS()) 89 default: 90 err = ErrDocTypeInvalid 91 } 92 93 if err != nil { 94 return WrapVfsError(err) 95 } 96 97 return jsonapi.Data(c, http.StatusCreated, doc, nil) 98 } 99 100 func createFileHandler(c echo.Context, fs vfs.VFS) (*file, error) { 101 inst := middlewares.GetInstance(c) 102 dirID := c.Param("file-id") 103 name := c.QueryParam("Name") 104 doc, err := FileDocFromReq(c, name, dirID) 105 if err != nil { 106 return nil, err 107 } 108 109 if created := c.QueryParam("CreatedAt"); created != "" { 110 if at, err2 := time.Parse(time.RFC3339, created); err2 == nil { 111 doc.CreatedAt = at 112 } 113 } 114 if updated := c.QueryParam("UpdatedAt"); updated != "" { 115 if at, err3 := time.Parse(time.RFC3339, updated); err3 == nil { 116 doc.UpdatedAt = at 117 } 118 } 119 doc.CozyMetadata, _ = CozyMetadataFromClaims(c, true) 120 121 err = checkPerm(c, "POST", nil, doc) 122 if err != nil { 123 return nil, err 124 } 125 126 if filepath.Ext(doc.DocName) == ".cozy-note" { 127 err := note.ImportFile(inst, doc, nil, c.Request().Body) 128 if err != nil { 129 inst.Logger().WithNamespace("files"). 130 Infof("Cannot import note: %s", err) 131 return nil, WrapVfsError(err) 132 } 133 return NewFile(doc, inst), nil 134 } 135 136 file, err := fs.CreateFile(doc, nil) 137 if err != nil { 138 return nil, err 139 } 140 141 n, err := io.Copy(file, c.Request().Body) 142 if err != nil { 143 inst.Logger().WithNamespace("files"). 144 Warnf("Error on uploading file (copy): %s (%d bytes written - expected %d)", err, n, doc.ByteSize) 145 } 146 if cerr := file.Close(); cerr != nil && (err == nil || errors.Is(err, io.ErrUnexpectedEOF)) { 147 err = cerr 148 inst.Logger().WithNamespace("files"). 149 Warnf("Error on uploading file (close): %s", err) 150 } 151 if err != nil { 152 return nil, wrapVfsError(err) 153 } 154 return NewFile(doc, inst), nil 155 } 156 157 func createDirHandler(c echo.Context, fs vfs.VFS) (*dir, error) { 158 path := c.QueryParam("Path") 159 tags := utils.SplitTrimString(c.QueryParam("Tags"), TagSeparator) 160 161 var doc *vfs.DirDoc 162 var err error 163 if path != "" { 164 if c.QueryParam("Recursive") == "true" { 165 doc, err = vfs.MkdirAll(fs, path) 166 } else { 167 doc, err = vfs.Mkdir(fs, path, tags) 168 } 169 if err != nil { 170 return nil, err 171 } 172 return newDir(doc), nil 173 } 174 175 dirID := c.Param("file-id") 176 name := c.QueryParam("Name") 177 doc, err = vfs.NewDirDoc(fs, name, dirID, tags) 178 if err != nil { 179 return nil, err 180 } 181 if date := c.Request().Header.Get("Date"); date != "" { 182 if t, err2 := time.Parse(time.RFC1123, date); err2 == nil { 183 doc.CreatedAt = t 184 doc.UpdatedAt = t 185 } 186 } 187 if created := c.QueryParam("CreatedAt"); created != "" { 188 if at, err2 := time.Parse(time.RFC3339, created); err2 == nil { 189 doc.CreatedAt = at 190 } 191 } 192 193 if updated := c.QueryParam("UpdatedAt"); updated != "" { 194 if at, err3 := time.Parse(time.RFC3339, updated); err3 == nil { 195 doc.UpdatedAt = at 196 } 197 } 198 199 if secret := c.QueryParam("MetadataID"); secret != "" { 200 instance := middlewares.GetInstance(c) 201 meta, err := vfs.GetStore().GetMetadata(instance, secret) 202 if err != nil { 203 return nil, err 204 } 205 doc.Metadata = *meta 206 } 207 208 if len(doc.Metadata) > 0 { 209 if _, ok := doc.Metadata[consts.CarbonCopyKey]; ok { 210 if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedCarbonCopy); err != nil { 211 delete(doc.Metadata, consts.CarbonCopyKey) 212 } 213 } 214 if _, ok := doc.Metadata[consts.ElectronicSafeKey]; ok { 215 if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedElectronicSafe); err != nil { 216 delete(doc.Metadata, consts.ElectronicSafeKey) 217 } 218 } 219 } 220 221 doc.CozyMetadata, _ = CozyMetadataFromClaims(c, false) 222 223 err = checkPerm(c, "POST", doc, nil) 224 if err != nil { 225 return nil, err 226 } 227 228 if err = fs.CreateDir(doc); err != nil { 229 return nil, err 230 } 231 232 return newDir(doc), nil 233 } 234 235 // OverwriteFileContentHandler handles PUT requests on /files/:file-id 236 // to overwrite the content of a file given its identifier. 237 func OverwriteFileContentHandler(c echo.Context) error { 238 instance := middlewares.GetInstance(c) 239 240 fileID := c.Param("file-id") 241 if fileID == "" { 242 fileID = c.Param("docid") // Used by sharings.updateDocument 243 } 244 245 olddoc, err := instance.VFS().FileByID(fileID) 246 if err != nil { 247 return WrapVfsError(err) 248 } 249 250 newdoc, err := FileDocFromReq(c, olddoc.DocName, olddoc.DirID) 251 if err != nil { 252 return WrapVfsError(err) 253 } 254 255 if updated := c.QueryParam("UpdatedAt"); updated != "" { 256 if at, err2 := time.Parse(time.RFC3339, updated); err2 == nil { 257 newdoc.UpdatedAt = at 258 } 259 } 260 261 newdoc.ReferencedBy = olddoc.ReferencedBy 262 263 if err := CheckIfMatch(c, olddoc.Rev()); err != nil { 264 return WrapVfsError(err) 265 } 266 267 if olddoc.CozyMetadata != nil { 268 newdoc.CozyMetadata = olddoc.CozyMetadata.Clone() 269 } 270 updateFileCozyMetadata(c, newdoc, true) 271 272 err = checkPerm(c, permission.PUT, nil, olddoc) 273 if err != nil { 274 return err 275 } 276 277 newdoc.SetID(olddoc.ID()) // The ID can be useful to check permissions 278 err = checkPerm(c, permission.PUT, nil, newdoc) 279 if err != nil { 280 return err 281 } 282 283 if filepath.Ext(newdoc.DocName) == ".cozy-note" { 284 err := note.ImportFile(instance, newdoc, olddoc, c.Request().Body) 285 if err != nil { 286 instance.Logger().WithNamespace("files"). 287 Infof("Cannot import note: %s", err) 288 return WrapVfsError(err) 289 } 290 return FileData(c, http.StatusOK, newdoc, true, nil) 291 } 292 293 file, err := instance.VFS().CreateFile(newdoc, olddoc) 294 if err != nil { 295 return WrapVfsError(err) 296 } 297 _, err = io.Copy(file, c.Request().Body) 298 if cerr := file.Close(); cerr != nil && err == nil { 299 err = cerr 300 } 301 if err != nil { 302 return WrapVfsError(err) 303 } 304 return FileData(c, http.StatusOK, newdoc, true, nil) 305 } 306 307 // UploadMetadataHandler accepts a metadata objet and persist it, so that it 308 // can be used in a future file upload. 309 func UploadMetadataHandler(c echo.Context) error { 310 if err := checkPerm(c, permission.POST, nil, &vfs.FileDoc{}); err != nil { 311 return err 312 } 313 314 meta := &vfs.Metadata{} 315 if _, err := jsonapi.Bind(c.Request().Body, meta); err != nil { 316 return err 317 } 318 319 instance := middlewares.GetInstance(c) 320 secret, err := vfs.GetStore().AddMetadata(instance, meta) 321 if err != nil { 322 return WrapVfsError(err) 323 } 324 325 m := apiMetadata{ 326 Metadata: meta, 327 secret: secret, 328 } 329 return jsonapi.Data(c, http.StatusCreated, &m, nil) 330 } 331 332 // FileCopyHandler handles POST requests on /files/:file-id/copy 333 // 334 // It is used to duplicate the given file and its metadata except for 335 // relationships. 336 func FileCopyHandler(c echo.Context) error { 337 inst := middlewares.GetInstance(c) 338 fs := inst.VFS() 339 340 fileID := c.Param("file-id") 341 olddoc, err := inst.VFS().FileByID(fileID) 342 if err != nil { 343 return WrapVfsError(err) 344 } 345 346 newDirID := c.QueryParam("DirID") 347 copyName := c.QueryParam("Name") 348 if copyName == "" { 349 copyName = fileCopyName(inst, olddoc.DocName) 350 } 351 newdoc := vfs.CreateFileDocCopy(olddoc, newDirID, copyName) 352 353 err = checkPerm(c, permission.POST, nil, newdoc) 354 if err != nil { 355 return err 356 } 357 358 exists, err := fs.GetIndexer().DirChildExists(newdoc.DirID, newdoc.DocName) 359 if err != nil { 360 return WrapVfsError(err) 361 } 362 if exists { 363 newdoc.DocName = vfs.ConflictName(fs, newdoc.DirID, newdoc.DocName, true) 364 exists, err = fs.GetIndexer().DirChildExists(newdoc.DirID, newdoc.DocName) 365 if err != nil { 366 return WrapVfsError(err) 367 } 368 if exists { 369 return WrapVfsError(os.ErrExist) 370 } 371 } 372 newdoc.ResetFullpath() 373 updateFileCozyMetadata(c, newdoc, true) 374 375 if olddoc.Mime == consts.NoteMimeType { 376 // We need a special copy for notes because of their images 377 err = note.CopyFile(inst, olddoc, newdoc) 378 } else { 379 err = fs.CopyFile(olddoc, newdoc) 380 } 381 if err != nil { 382 return WrapVfsError(err) 383 } 384 385 return FileData(c, http.StatusCreated, newdoc, false, nil) 386 } 387 388 // ModifyMetadataByIDHandler handles PATCH requests on /files/:file-id 389 // 390 // It can be used to modify the file or directory metadata, as well as 391 // moving and renaming it in the filesystem. 392 func ModifyMetadataByIDHandler(c echo.Context) error { 393 patch, err := getPatch(c, c.Param("file-id"), "") 394 if err != nil { 395 return WrapVfsError(err) 396 } 397 i := middlewares.GetInstance(c) 398 if err = applyPatch(c, i.VFS(), patch); err != nil { 399 return WrapVfsError(err) 400 } 401 return nil 402 } 403 404 // ModifyMetadataByIDInBatchHandler handles PATCH requests on /files/. 405 // 406 // It can be used to modify many files or directories metadata, as well as 407 // moving and renaming it in the filesystem, in batch. 408 func ModifyMetadataByIDInBatchHandler(c echo.Context) error { 409 patches, err := getPatches(c) 410 if err != nil { 411 return WrapVfsError(err) 412 } 413 i := middlewares.GetInstance(c) 414 patchErrors, err := applyPatches(c, i.VFS(), patches) 415 if err != nil { 416 return err 417 } 418 if len(patchErrors) > 0 { 419 return jsonapi.DataErrorList(c, patchErrors...) 420 } 421 return c.NoContent(http.StatusNoContent) 422 } 423 424 // ModifyMetadataByPathHandler handles PATCH requests on /files/metadata 425 // 426 // It can be used to modify the file or directory metadata, as well as 427 // moving and renaming it in the filesystem. 428 func ModifyMetadataByPathHandler(c echo.Context) error { 429 patch, err := getPatch(c, "", c.QueryParam("Path")) 430 if err != nil { 431 return WrapVfsError(err) 432 } 433 i := middlewares.GetInstance(c) 434 if err = applyPatch(c, i.VFS(), patch); err != nil { 435 return WrapVfsError(err) 436 } 437 return nil 438 } 439 440 // ModifyFileVersionMetadata handles PATCH requests on /files/:file-id/:version-id 441 // 442 // It can be used to modify tags on an old version of a file. 443 func ModifyFileVersionMetadata(c echo.Context) error { 444 inst := middlewares.GetInstance(c) 445 fileID := c.Param("file-id") 446 _, file, err := inst.VFS().DirOrFileByID(fileID) 447 if err != nil { 448 return WrapVfsError(err) 449 } 450 if file == nil { 451 return WrapVfsError(vfs.ErrConflict) 452 } 453 if err = checkPerm(c, permission.PATCH, nil, file); err != nil { 454 return WrapVfsError(err) 455 } 456 docID := fileID + "/" + c.Param("version-id") 457 version, err := vfs.FindVersion(inst, docID) 458 if err != nil { 459 return WrapVfsError(err) 460 } 461 var patch vfs.DocPatch 462 if _, err = jsonapi.Bind(c.Request().Body, &patch); err != nil || patch.Tags == nil { 463 return jsonapi.BadJSON() 464 } 465 version.Tags = *patch.Tags 466 version.CozyMetadata.UpdatedAt = time.Now() 467 if err = couchdb.UpdateDoc(inst, version); err != nil { 468 return WrapVfsError(err) 469 } 470 return jsonapi.Data(c, http.StatusOK, version, nil) 471 } 472 473 // DeleteFileVersionMetadata handles DELETE requests on /files/:file-id/:version-id 474 // 475 // It can be used to delete an old version of a file. 476 func DeleteFileVersionMetadata(c echo.Context) error { 477 inst := middlewares.GetInstance(c) 478 fs := inst.VFS() 479 fileID := c.Param("file-id") 480 _, file, err := fs.DirOrFileByID(fileID) 481 if err != nil { 482 return WrapVfsError(err) 483 } 484 if file == nil { 485 return WrapVfsError(vfs.ErrConflict) 486 } 487 if err = checkPerm(c, permission.DELETE, nil, file); err != nil { 488 return WrapVfsError(err) 489 } 490 docID := fileID + "/" + c.Param("version-id") 491 version, err := vfs.FindVersion(inst, docID) 492 if err != nil { 493 return WrapVfsError(err) 494 } 495 if err := fs.CleanOldVersion(fileID, version); err != nil { 496 return WrapVfsError(err) 497 } 498 return c.NoContent(http.StatusNoContent) 499 } 500 501 // CopyVersionHandler handles POST requests on /files/:file-id/versions. 502 // 503 // It can be used to create a new version of a file, with the same content but 504 // new metadata. 505 func CopyVersionHandler(c echo.Context) error { 506 inst := middlewares.GetInstance(c) 507 fs := inst.VFS() 508 fileID := c.Param("file-id") 509 olddoc, err := fs.FileByID(fileID) 510 if err != nil { 511 return WrapVfsError(err) 512 } 513 if olddoc == nil { 514 return WrapVfsError(vfs.ErrConflict) 515 } 516 if err = checkPerm(c, permission.PUT, nil, olddoc); err != nil { 517 return WrapVfsError(err) 518 } 519 520 meta := vfs.Metadata{} 521 if _, err := jsonapi.Bind(c.Request().Body, &meta); err != nil { 522 return err 523 } 524 525 // Manage the special cases of carbonCopy & electronicSafe 526 if len(meta) > 0 { 527 if _, ok := meta[consts.CarbonCopyKey]; ok { 528 if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedCarbonCopy); err != nil { 529 delete(meta, consts.CarbonCopyKey) 530 } 531 } 532 if _, ok := meta[consts.ElectronicSafeKey]; ok { 533 if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedElectronicSafe); err != nil { 534 delete(meta, consts.ElectronicSafeKey) 535 } 536 } 537 } 538 keepCarbonCopy := true 539 keepElectronicSafe := true 540 for key := range meta { 541 switch key { 542 case consts.CarbonCopyKey: 543 keepCarbonCopy = false 544 case consts.ElectronicSafeKey: 545 keepElectronicSafe = false 546 case "qualification": 547 // 548 default: 549 keepCarbonCopy = false 550 keepElectronicSafe = false 551 } 552 } 553 if value, ok := olddoc.Metadata[consts.CarbonCopyKey]; ok && keepCarbonCopy { 554 meta[consts.CarbonCopyKey] = value 555 } 556 if value, ok := olddoc.Metadata[consts.ElectronicSafeKey]; ok && keepElectronicSafe { 557 meta[consts.ElectronicSafeKey] = value 558 } 559 560 // For notes, preserve the prosemirror metadata 561 if olddoc.Mime == consts.NoteMimeType { 562 for _, name := range []string{"title", "content", "schema", "version"} { 563 if meta[name] == nil { 564 meta[name] = olddoc.Metadata[name] 565 } 566 } 567 } 568 569 newdoc := olddoc.Clone().(*vfs.FileDoc) 570 newdoc.Metadata = meta 571 newdoc.Tags = utils.SplitTrimString(c.QueryParam("Tags"), TagSeparator) 572 updateFileCozyMetadata(c, newdoc, true) 573 574 content, err := fs.OpenFile(olddoc) 575 if err != nil { 576 return WrapVfsError(err) 577 } 578 defer content.Close() 579 580 file, err := fs.CreateFile(newdoc, olddoc) 581 if err != nil { 582 return WrapVfsError(err) 583 } 584 585 defer func() { 586 if cerr := file.Close(); cerr != nil && err == nil { 587 err = cerr 588 } 589 if err != nil { 590 err = WrapVfsError(err) 591 return 592 } 593 err = FileData(c, http.StatusOK, newdoc, true, nil) 594 }() 595 596 _, err = io.Copy(file, content) 597 return err 598 } 599 600 // ClearOldVersions is the handler for DELETE /files/versions. 601 // It deletes all the old versions of all files to make space for new files. 602 func ClearOldVersions(c echo.Context) error { 603 if err := middlewares.AllowWholeType(c, permission.DELETE, consts.Files); err != nil { 604 return err 605 } 606 607 fs := middlewares.GetInstance(c).VFS() 608 if err := fs.ClearOldVersions(); err != nil { 609 return WrapVfsError(err) 610 } 611 612 return c.NoContent(204) 613 } 614 615 func getPatch(c echo.Context, docID, docPath string) (*docPatch, error) { 616 var patch docPatch 617 obj, err := jsonapi.Bind(c.Request().Body, &patch) 618 if err != nil { 619 return nil, jsonapi.BadJSON() 620 } 621 patch.docID = docID 622 patch.docPath = docPath 623 patch.RestorePath = nil 624 if rel, ok := obj.GetRelationship("parent"); ok { 625 rid, ok := rel.ResourceIdentifier() 626 if !ok { 627 return nil, jsonapi.BadJSON() 628 } 629 patch.DirID = &rid.ID 630 } 631 return &patch, nil 632 } 633 634 func getPatches(c echo.Context) ([]*docPatch, error) { 635 req := c.Request() 636 objs, err := jsonapi.BindCompound(req.Body) 637 if err != nil { 638 return nil, jsonapi.BadJSON() 639 } 640 patches := make([]*docPatch, len(objs)) 641 for i, obj := range objs { 642 var patch docPatch 643 if obj.Attributes == nil { 644 return nil, jsonapi.BadJSON() 645 } 646 if err = json.Unmarshal(*obj.Attributes, &patch); err != nil { 647 return nil, err 648 } 649 patch.docID = obj.ID 650 patch.docPath = "" 651 patch.RestorePath = nil 652 if rel, ok := obj.GetRelationship("parent"); ok { 653 rid, ok := rel.ResourceIdentifier() 654 if !ok { 655 return nil, jsonapi.BadJSON() 656 } 657 patch.DirID = &rid.ID 658 } 659 patches[i] = &patch 660 } 661 return patches, nil 662 } 663 664 func applyPatch(c echo.Context, fs vfs.VFS, patch *docPatch) (err error) { 665 var file *vfs.FileDoc 666 var dir *vfs.DirDoc 667 if patch.docID != "" { 668 dir, file, err = fs.DirOrFileByID(patch.docID) 669 } else { 670 dir, file, err = fs.DirOrFileByPath(patch.docPath) 671 } 672 if err != nil { 673 return err 674 } 675 676 var rev string 677 if dir != nil { 678 rev = dir.Rev() 679 } else { 680 rev = file.Rev() 681 } 682 683 if err = CheckIfMatch(c, rev); err != nil { 684 return err 685 } 686 687 if err = checkPerm(c, permission.PATCH, dir, file); err != nil { 688 return err 689 } 690 691 if patch.Delete { 692 if dir != nil { 693 inst := middlewares.GetInstance(c) 694 err = fs.DestroyDirAndContent(dir, pushTrashJob(inst)) 695 } else { 696 err = fs.DestroyFile(file) 697 } 698 } else if patch.Trash { 699 if dir != nil { 700 updateDirCozyMetadata(c, dir) 701 dir, err = vfs.TrashDir(fs, dir) 702 } else { 703 updateFileCozyMetadata(c, file, false) 704 file, err = vfs.TrashFile(fs, file) 705 } 706 } else { 707 if dir != nil { 708 updateDirCozyMetadata(c, dir) 709 dir, err = vfs.ModifyDirMetadata(fs, dir, &patch.DocPatch) 710 } else { 711 updateFileCozyMetadata(c, file, false) 712 file, err = vfs.ModifyFileMetadata(fs, file, &patch.DocPatch) 713 } 714 } 715 if err != nil { 716 return err 717 } 718 719 if dir != nil { 720 return dirData(c, http.StatusOK, dir) 721 } 722 return FileData(c, http.StatusOK, file, false, nil) 723 } 724 725 func applyPatches(c echo.Context, fs vfs.VFS, patches []*docPatch) (errors []*jsonapi.Error, err error) { 726 for _, patch := range patches { 727 dir, file, errf := fs.DirOrFileByID(patch.docID) 728 if errf != nil { 729 jsonapiError := wrapVfsErrorJSONAPI(errf) 730 jsonapiError.Source.Parameter = "_id" 731 jsonapiError.Source.Pointer = patch.docID 732 errors = append(errors, jsonapiError) 733 continue 734 } 735 if err = checkPerm(c, permission.PATCH, dir, file); err != nil { 736 return 737 } 738 var errp error 739 if patch.Delete { 740 if dir != nil { 741 inst := middlewares.GetInstance(c) 742 errp = fs.DestroyDirAndContent(dir, pushTrashJob(inst)) 743 } else if file != nil { 744 errp = fs.DestroyFile(file) 745 } 746 } else if patch.Trash { 747 if dir != nil { 748 updateDirCozyMetadata(c, dir) 749 _, errp = vfs.TrashDir(fs, dir) 750 } else if file != nil { 751 updateFileCozyMetadata(c, file, false) 752 _, errp = vfs.TrashFile(fs, file) 753 } 754 } else if dir != nil { 755 updateDirCozyMetadata(c, dir) 756 _, errp = vfs.ModifyDirMetadata(fs, dir, &patch.DocPatch) 757 } else if file != nil { 758 updateFileCozyMetadata(c, file, false) 759 _, errp = vfs.ModifyFileMetadata(fs, file, &patch.DocPatch) 760 } 761 if errp != nil { 762 jsonapiError := wrapVfsErrorJSONAPI(errp) 763 jsonapiError.Source.Parameter = "_id" 764 jsonapiError.Source.Pointer = patch.docID 765 errors = append(errors, jsonapiError) 766 } 767 } 768 769 return 770 } 771 772 // ReadMetadataFromIDHandler handles all GET requests on /files/:file- 773 // id aiming at getting file metadata from its id. 774 func ReadMetadataFromIDHandler(c echo.Context) error { 775 instance := middlewares.GetInstance(c) 776 perm, err := middlewares.GetPermission(c) 777 if err != nil { 778 return err 779 } 780 781 fileID := c.Param("file-id") 782 783 dir, file, err := instance.VFS().DirOrFileByID(fileID) 784 if err != nil { 785 return WrapVfsError(err) 786 } 787 788 if err := checkPerm(c, permission.GET, dir, file); err != nil { 789 return err 790 } 791 792 // Limiting the number of public share link consultations 793 if perm.Type == permission.TypeShareByLink { 794 err = config.GetRateLimiter().CheckRateLimitKey(fileID, limits.SharingPublicLinkType) 795 if limits.IsLimitReachedOrExceeded(err) { 796 return err 797 } 798 } 799 800 if dir != nil { 801 return dirData(c, http.StatusOK, dir) 802 } 803 return FileData(c, http.StatusOK, file, true, nil) 804 } 805 806 // GetChildrenHandler returns a list of children of a folder 807 func GetChildrenHandler(c echo.Context) error { 808 instance := middlewares.GetInstance(c) 809 810 fileID := c.Param("file-id") 811 812 dir, file, err := instance.VFS().DirOrFileByID(fileID) 813 if err != nil { 814 return WrapVfsError(err) 815 } 816 817 if err := checkPerm(c, permission.GET, dir, file); err != nil { 818 return err 819 } 820 821 if file != nil { 822 return jsonapi.Errorf(http.StatusBadRequest, "cant read children of file %v", fileID) 823 } 824 825 return dirDataList(c, http.StatusOK, dir) 826 } 827 828 type apiDiskSize struct { 829 DocID string `json:"id,omitempty"` 830 Size int64 `json:"size,string"` 831 } 832 833 func (d *apiDiskSize) ID() string { return d.DocID } 834 func (d *apiDiskSize) Rev() string { return "" } 835 func (d *apiDiskSize) DocType() string { return consts.DirSizes } 836 func (d *apiDiskSize) Clone() couchdb.Doc { return d } 837 func (d *apiDiskSize) SetID(id string) { d.DocID = id } 838 func (d *apiDiskSize) SetRev(_ string) {} 839 func (d *apiDiskSize) Relationships() jsonapi.RelationshipMap { return nil } 840 func (d *apiDiskSize) Included() []jsonapi.Object { return nil } 841 func (d *apiDiskSize) Links() *jsonapi.LinksList { return nil } 842 843 // GetDirSize returns the size of a directory (the sum of the size of the files 844 // in this directory, including those in subdirectories). 845 func GetDirSize(c echo.Context) error { 846 fs := middlewares.GetInstance(c).VFS() 847 fileID := c.Param("file-id") 848 849 dir, err := fs.DirByID(fileID) 850 if err != nil { 851 return WrapVfsError(err) 852 } 853 if err := checkPerm(c, permission.GET, dir, nil); err != nil { 854 return err 855 } 856 857 size, err := fs.DirSize(dir) 858 if err != nil { 859 return WrapVfsError(err) 860 } 861 862 result := apiDiskSize{DocID: fileID, Size: size} 863 return jsonapi.Data(c, http.StatusOK, &result, nil) 864 } 865 866 // ReadMetadataFromPathHandler handles all GET requests on 867 // /files/metadata aiming at getting file metadata from its path. 868 func ReadMetadataFromPathHandler(c echo.Context) error { 869 var err error 870 871 instance := middlewares.GetInstance(c) 872 873 dir, file, err := instance.VFS().DirOrFileByPath(c.QueryParam("Path")) 874 if err != nil { 875 return WrapVfsError(err) 876 } 877 878 if err := checkPerm(c, permission.GET, dir, file); err != nil { 879 return err 880 } 881 882 if dir != nil { 883 return dirData(c, http.StatusOK, dir) 884 } 885 return FileData(c, http.StatusOK, file, true, nil) 886 } 887 888 // ReadFileContentFromIDHandler handles all GET requests on /files/:file-id 889 // aiming at downloading a file given its ID. It serves the file in inline 890 // mode. 891 func ReadFileContentFromIDHandler(c echo.Context) error { 892 instance := middlewares.GetInstance(c) 893 894 doc, err := instance.VFS().FileByID(c.Param("file-id")) 895 if err != nil { 896 return WrapVfsError(err) 897 } 898 899 err = checkPerm(c, permission.GET, nil, doc) 900 if err != nil { 901 return err 902 } 903 904 disposition := "inline" 905 if c.QueryParam("Dl") == "1" { 906 disposition = "attachment" 907 } 908 err = vfs.ServeFileContent(instance.VFS(), doc, nil, "", disposition, c.Request(), c.Response()) 909 if err != nil { 910 return WrapVfsError(err) 911 } 912 913 return nil 914 } 915 916 // ReadFileContentFromVersion handles the download of an old version of the 917 // file content. 918 func ReadFileContentFromVersion(c echo.Context) error { 919 instance := middlewares.GetInstance(c) 920 921 doc, err := instance.VFS().FileByID(c.Param("file-id")) 922 if err != nil { 923 return WrapVfsError(err) 924 } 925 926 err = checkPerm(c, permission.GET, nil, doc) 927 if err != nil { 928 return err 929 } 930 931 version, err := vfs.FindVersion(instance, doc.DocID+"/"+c.Param("version-id")) 932 if err != nil { 933 return WrapVfsError(err) 934 } 935 936 disposition := "inline" 937 if c.QueryParam("Dl") == "1" { 938 disposition = "attachment" 939 } 940 err = vfs.ServeFileContent(instance.VFS(), doc, version, "", disposition, c.Request(), c.Response()) 941 if err != nil { 942 return WrapVfsError(err) 943 } 944 945 return nil 946 } 947 948 // RevertFileVersion restores an old version of the file content. 949 func RevertFileVersion(c echo.Context) error { 950 inst := middlewares.GetInstance(c) 951 952 doc, err := inst.VFS().FileByID(c.Param("file-id")) 953 if err != nil { 954 return WrapVfsError(err) 955 } 956 957 if err = checkPerm(c, permission.POST, nil, doc); err != nil { 958 return err 959 } 960 961 version, err := vfs.FindVersion(inst, doc.DocID+"/"+c.Param("version-id")) 962 if err != nil { 963 return WrapVfsError(err) 964 } 965 966 if err = inst.VFS().RevertFileVersion(doc, version); err != nil { 967 return WrapVfsError(err) 968 } 969 970 return FileData(c, http.StatusOK, doc, true, nil) 971 } 972 973 // HeadDirOrFile handles HEAD requests on directory or file to check their 974 // existence 975 func HeadDirOrFile(c echo.Context) error { 976 instance := middlewares.GetInstance(c) 977 978 dir, file, err := instance.VFS().DirOrFileByID(c.Param("file-id")) 979 if err != nil { 980 return WrapVfsError(err) 981 } 982 983 if dir != nil { 984 err = checkPerm(c, permission.GET, dir, nil) 985 } else { 986 err = checkPerm(c, permission.GET, nil, file) 987 } 988 if err != nil { 989 return err 990 } 991 992 return nil 993 } 994 995 // IconHandler serves icon for the PDFs. 996 func IconHandler(c echo.Context) error { 997 instance := middlewares.GetInstance(c) 998 999 secret := c.Param("secret") 1000 fileID, err := vfs.GetStore().GetThumb(instance, secret) 1001 if err != nil { 1002 return WrapVfsError(err) 1003 } 1004 if c.Param("file-id") != fileID { 1005 return jsonapi.NewError(http.StatusBadRequest, "Wrong download token") 1006 } 1007 1008 doc, err := instance.VFS().FileByID(fileID) 1009 if err != nil { 1010 return WrapVfsError(err) 1011 } 1012 1013 return vfs.ServePDFIcon(c.Response(), c.Request(), instance.VFS(), doc) 1014 } 1015 1016 // PreviewHandler serves preview images for the PDFs. 1017 func PreviewHandler(c echo.Context) error { 1018 instance := middlewares.GetInstance(c) 1019 1020 secret := c.Param("secret") 1021 fileID, err := vfs.GetStore().GetThumb(instance, secret) 1022 if err != nil { 1023 return WrapVfsError(err) 1024 } 1025 if c.Param("file-id") != fileID { 1026 return jsonapi.NewError(http.StatusBadRequest, "Wrong download token") 1027 } 1028 1029 doc, err := instance.VFS().FileByID(fileID) 1030 if err != nil { 1031 return WrapVfsError(err) 1032 } 1033 1034 return vfs.ServePDFPreview(c.Response(), c.Request(), instance.VFS(), doc) 1035 } 1036 1037 // ThumbnailHandler serves thumbnails of the images/photos 1038 func ThumbnailHandler(c echo.Context) error { 1039 instance := middlewares.GetInstance(c) 1040 1041 secret := c.Param("secret") 1042 fileID, err := vfs.GetStore().GetThumb(instance, secret) 1043 if err != nil { 1044 return WrapVfsError(err) 1045 } 1046 if c.Param("file-id") != fileID { 1047 return jsonapi.NewError(http.StatusBadRequest, "Wrong download token") 1048 } 1049 1050 doc, err := instance.VFS().FileByID(fileID) 1051 if err != nil { 1052 return WrapVfsError(err) 1053 } 1054 1055 fs := instance.ThumbsFS() 1056 format := c.Param("format") 1057 err = fs.ServeThumbContent(c.Response(), c.Request(), doc, format) 1058 if err != nil { 1059 if !errors.Is(err, os.ErrInvalid) { 1060 msg, _ := job.NewMessage(thumbnail.ImageMessage{ 1061 File: doc, 1062 Format: format, 1063 }) 1064 _, _ = job.System().PushJob(instance, &job.JobRequest{ 1065 WorkerType: "thumbnail", 1066 Message: msg, 1067 }) 1068 } 1069 return serveThumbnailPlaceholder(c.Response(), c.Request(), doc, format) 1070 } 1071 return nil 1072 } 1073 1074 func serveThumbnailPlaceholder(res http.ResponseWriter, req *http.Request, doc *vfs.FileDoc, format string) error { 1075 if !utils.IsInArray(format, vfs.ThumbnailFormatNames) { 1076 return echo.NewHTTPError(http.StatusNotFound, "Format does not exist") 1077 } 1078 f := statik.GetAsset("/placeholders/thumbnail-" + format + ".png") 1079 if f == nil { 1080 return os.ErrNotExist 1081 } 1082 etag := f.Etag 1083 if utils.CheckPreconditions(res, req, etag) { 1084 return nil 1085 } 1086 res.Header().Set("Etag", etag) 1087 res.WriteHeader(http.StatusNotFound) 1088 _, err := io.Copy(res, f.Reader()) 1089 return err 1090 } 1091 1092 func sendFileFromPath(c echo.Context, path string, checkPermission bool) error { 1093 instance := middlewares.GetInstance(c) 1094 1095 doc, err := instance.VFS().FileByPath(path) 1096 if err != nil { 1097 return WrapVfsError(err) 1098 } 1099 1100 if checkPermission { 1101 err = middlewares.Allow(c, permission.GET, doc) 1102 if err != nil { 1103 return err 1104 } 1105 } 1106 1107 // Forbid extracting autofilled passwords on an HTML page hosted in the Cozy 1108 if !config.GetConfig().CSPDisabled { 1109 middlewares.AppendCSPRule(c, "form-action", "'none'") 1110 } 1111 1112 disposition := "inline" 1113 if c.QueryParam("Dl") == "1" { 1114 disposition = "attachment" 1115 } else if !checkPermission { 1116 addCSPRuleForDirectLink(c, doc.Class, doc.Mime) 1117 } 1118 err = vfs.ServeFileContent(instance.VFS(), doc, nil, "", disposition, c.Request(), c.Response()) 1119 if err != nil { 1120 return WrapVfsError(err) 1121 } 1122 1123 return nil 1124 } 1125 1126 func addCSPRuleForDirectLink(c echo.Context, class, mime string) { 1127 if config.GetConfig().CSPDisabled { 1128 return 1129 } 1130 // Allow some files to be displayed by the browser in the client-side apps 1131 if mime == "text/plain" || class == "image" || class == "audio" || class == "video" || mime == "application/pdf" { 1132 middlewares.AppendCSPRule(c, "frame-ancestors", "*") 1133 } 1134 } 1135 1136 // ReadFileContentFromPathHandler handles all GET request on /files/download 1137 // aiming at downloading a file given its path. It serves the file in in 1138 // attachment mode. 1139 func ReadFileContentFromPathHandler(c echo.Context) error { 1140 return sendFileFromPath(c, c.QueryParam("Path"), true) 1141 } 1142 1143 // ArchiveDownloadCreateHandler handles requests to /files/archive and stores the 1144 // paremeters with a secret to be used in download handler below.s 1145 func ArchiveDownloadCreateHandler(c echo.Context) error { 1146 archive := &vfs.Archive{} 1147 if _, err := jsonapi.Bind(c.Request().Body, archive); err != nil { 1148 return err 1149 } 1150 if len(archive.Files) == 0 && len(archive.IDs) == 0 { 1151 return c.JSON(http.StatusBadRequest, "Can't create an archive with no files") 1152 } 1153 if strings.Contains(archive.Name, "/") { 1154 return c.JSON(http.StatusBadRequest, "The archive filename can't contain a /") 1155 } 1156 if archive.Name == "" { 1157 archive.Name = "archive" 1158 } 1159 instance := middlewares.GetInstance(c) 1160 1161 entries, err := archive.GetEntries(instance.VFS()) 1162 if err != nil { 1163 return WrapVfsError(err) 1164 } 1165 1166 for _, e := range entries { 1167 err = checkPerm(c, permission.GET, e.Dir, e.File) 1168 if err != nil { 1169 return err 1170 } 1171 } 1172 1173 // if accept header is application/zip, send the archive immediately 1174 if c.Request().Header.Get(echo.HeaderAccept) == "application/zip" { 1175 return archive.Serve(instance.VFS(), c.Response()) 1176 } 1177 1178 secret, err := vfs.GetStore().AddArchive(instance, archive) 1179 if err != nil { 1180 return WrapVfsError(err) 1181 } 1182 archive.Secret = secret 1183 1184 fakeName := url.PathEscape(archive.Name) 1185 1186 links := &jsonapi.LinksList{ 1187 Related: "/files/archive/" + secret + "/" + fakeName + ".zip", 1188 } 1189 1190 return jsonapi.Data(c, http.StatusOK, &apiArchive{archive}, links) 1191 } 1192 1193 // FileDownloadCreateHandler stores the required path into a secret 1194 // usable for download handler below. 1195 func FileDownloadCreateHandler(c echo.Context) error { 1196 instance := middlewares.GetInstance(c) 1197 var doc *vfs.FileDoc 1198 var err error 1199 var path string 1200 var versionID string 1201 1202 if path = c.QueryParam("Path"); path != "" { 1203 if doc, err = instance.VFS().FileByPath(path); err != nil { 1204 return WrapVfsError(err) 1205 } 1206 } else if id := c.QueryParam("Id"); id != "" { 1207 if doc, err = instance.VFS().FileByID(id); err != nil { 1208 return WrapVfsError(err) 1209 } 1210 if path, err = doc.Path(instance.VFS()); err != nil { 1211 return WrapVfsError(err) 1212 } 1213 } else if versionID = c.QueryParam("VersionId"); versionID != "" { 1214 docID := strings.Split(versionID, "/")[0] 1215 if doc, err = instance.VFS().FileByID(docID); err != nil { 1216 return WrapVfsError(err) 1217 } 1218 } 1219 1220 err = checkPerm(c, "GET", nil, doc) 1221 if err != nil { 1222 return err 1223 } 1224 1225 var secret string 1226 if versionID == "" { 1227 secret, err = vfs.GetStore().AddFile(instance, path) 1228 } else { 1229 secret, err = vfs.GetStore().AddVersion(instance, versionID) 1230 secret = "v-" + secret 1231 } 1232 if err != nil { 1233 return WrapVfsError(err) 1234 } 1235 1236 filename := c.QueryParam("Filename") 1237 if filename == "" { 1238 filename = doc.DocName 1239 } 1240 links := &jsonapi.LinksList{ 1241 Related: "/files/downloads/" + secret + "/" + filename, 1242 } 1243 1244 return FileData(c, http.StatusOK, doc, false, links) 1245 } 1246 1247 // ArchiveDownloadHandler handles requests to /files/archive/:secret/whatever.zip 1248 // and creates on the fly zip archive from the parameters linked to secret. 1249 func ArchiveDownloadHandler(c echo.Context) error { 1250 instance := middlewares.GetInstance(c) 1251 secret := c.Param("secret") 1252 archive, err := vfs.GetStore().GetArchive(instance, secret) 1253 if err != nil { 1254 return WrapVfsError(err) 1255 } 1256 if err := archive.Serve(instance.VFS(), c.Response()); err != nil { 1257 return WrapVfsError(err) 1258 } 1259 return nil 1260 } 1261 1262 // FileDownloadHandler send a file that have previously be defined 1263 // through FileDownloadCreateHandler 1264 func FileDownloadHandler(c echo.Context) error { 1265 secret := c.Param("secret") 1266 if strings.HasPrefix(secret, "v-") { 1267 return versionDownloadHandler(c, strings.TrimPrefix(secret, "v-")) 1268 } 1269 instance := middlewares.GetInstance(c) 1270 path, err := vfs.GetStore().GetFile(instance, secret) 1271 if err != nil { 1272 return WrapVfsError(err) 1273 } 1274 return sendFileFromPath(c, path, false) 1275 } 1276 1277 func versionDownloadHandler(c echo.Context, secret string) error { 1278 instance := middlewares.GetInstance(c) 1279 versionID, err := vfs.GetStore().GetVersion(instance, secret) 1280 if err != nil { 1281 return WrapVfsError(err) 1282 } 1283 1284 fileID := strings.Split(versionID, "/")[0] 1285 doc, err := instance.VFS().FileByID(fileID) 1286 if err != nil { 1287 return WrapVfsError(err) 1288 } 1289 version, err := vfs.FindVersion(instance, versionID) 1290 if err != nil { 1291 return WrapVfsError(err) 1292 } 1293 1294 disposition := "inline" 1295 if c.QueryParam("Dl") == "1" { 1296 disposition = "attachment" 1297 } else { 1298 addCSPRuleForDirectLink(c, doc.Class, doc.Mime) 1299 } 1300 1301 filename := c.Param("fake-name") 1302 err = vfs.ServeFileContent(instance.VFS(), doc, version, filename, disposition, c.Request(), c.Response()) 1303 if err != nil { 1304 return WrapVfsError(err) 1305 } 1306 return nil 1307 } 1308 1309 // TrashHandler handles all DELETE requests on /files/:file-id and 1310 // moves the file or directory with the specified file-id to the 1311 // trash. 1312 func TrashHandler(c echo.Context) error { 1313 instance := middlewares.GetInstance(c) 1314 1315 fileID := c.Param("file-id") 1316 dir, file, err := instance.VFS().DirOrFileByID(fileID) 1317 if err != nil { 1318 return WrapVfsError(err) 1319 } 1320 1321 err = checkPerm(c, permission.PATCH, dir, file) 1322 if err != nil { 1323 return err 1324 } 1325 1326 var rev string 1327 if dir != nil { 1328 rev = dir.Rev() 1329 } else { 1330 rev = file.Rev() 1331 } 1332 1333 if err := CheckIfMatch(c, rev); err != nil { 1334 return WrapVfsError(err) 1335 } 1336 1337 ensureCleanOldTrashedTrigger(instance) 1338 1339 if dir != nil { 1340 updateDirCozyMetadata(c, dir) 1341 doc, errt := vfs.TrashDir(instance.VFS(), dir) 1342 if errt != nil { 1343 return WrapVfsError(errt) 1344 } 1345 return dirData(c, http.StatusOK, doc) 1346 } 1347 1348 updateFileCozyMetadata(c, file, false) 1349 doc, errt := vfs.TrashFile(instance.VFS(), file) 1350 if errt != nil { 1351 return WrapVfsError(errt) 1352 } 1353 return FileData(c, http.StatusOK, doc, false, nil) 1354 } 1355 1356 // ReadTrashFilesHandler handle GET requests on /files/trash and return the 1357 // list of trashed files and directories 1358 func ReadTrashFilesHandler(c echo.Context) error { 1359 instance := middlewares.GetInstance(c) 1360 1361 trash, err := instance.VFS().DirByID(consts.TrashDirID) 1362 if err != nil { 1363 return WrapVfsError(err) 1364 } 1365 1366 err = checkPerm(c, permission.GET, trash, nil) 1367 if err != nil { 1368 return err 1369 } 1370 1371 return dirDataList(c, http.StatusOK, trash) 1372 } 1373 1374 // RestoreTrashFileHandler handle POST requests on /files/trash/file-id and 1375 // can be used to restore a file or directory from the trash. 1376 func RestoreTrashFileHandler(c echo.Context) error { 1377 instance := middlewares.GetInstance(c) 1378 1379 fileID := c.Param("file-id") 1380 1381 dir, file, err := instance.VFS().DirOrFileByID(fileID) 1382 if err != nil { 1383 return WrapVfsError(err) 1384 } 1385 1386 err = checkPerm(c, permission.PATCH, dir, file) 1387 if err != nil { 1388 return err 1389 } 1390 1391 if dir != nil { 1392 updateDirCozyMetadata(c, dir) 1393 doc, errt := vfs.RestoreDir(instance.VFS(), dir) 1394 if errt != nil { 1395 return WrapVfsError(errt) 1396 } 1397 return dirData(c, http.StatusOK, doc) 1398 } 1399 1400 updateFileCozyMetadata(c, file, false) 1401 doc, errt := vfs.RestoreFile(instance.VFS(), file) 1402 if errt != nil { 1403 return WrapVfsError(errt) 1404 } 1405 return FileData(c, http.StatusOK, doc, false, nil) 1406 } 1407 1408 // ClearTrashHandler handles DELETE request to clear the trash 1409 func ClearTrashHandler(c echo.Context) error { 1410 inst := middlewares.GetInstance(c) 1411 1412 fs := inst.VFS() 1413 trash, err := fs.DirByID(consts.TrashDirID) 1414 if err != nil { 1415 return WrapVfsError(err) 1416 } 1417 1418 err = checkPerm(c, permission.DELETE, trash, nil) 1419 if err != nil { 1420 return err 1421 } 1422 1423 files, _ := fs.FilesUsage() 1424 versions, _ := fs.VersionsUsage() 1425 quota := fs.DiskQuota() 1426 freeSpace := quota - files - versions 1427 inTrash, _ := fs.TrashUsage() 1428 1429 err = fs.DestroyDirContent(trash, pushTrashJob(inst)) 1430 if err != nil { 1431 return WrapVfsError(err) 1432 } 1433 1434 // As a rule of thumb if the freed space (= inTrash) was more than the free 1435 // space, we want to ping other instances with common sharing to tell them 1436 // to try reuploading files that have may have been blocked because of the 1437 // quota. 1438 if inTrash > freeSpace { 1439 go func() { 1440 i := inst.Clone().(*instance.Instance) 1441 if err := sharing.AskReupload(i); err != nil { 1442 i.Logger().WithNamespace("files"). 1443 Warnf("sharing.AskReupload failed with %s", err) 1444 } 1445 }() 1446 } 1447 1448 return c.NoContent(204) 1449 } 1450 1451 // DestroyFileHandler handles DELETE request to clear one element from the trash 1452 func DestroyFileHandler(c echo.Context) error { 1453 inst := middlewares.GetInstance(c) 1454 1455 fileID := c.Param("file-id") 1456 1457 dir, file, err := inst.VFS().DirOrFileByID(fileID) 1458 if err != nil { 1459 return WrapVfsError(err) 1460 } 1461 1462 err = checkPerm(c, permission.DELETE, dir, file) 1463 if err != nil { 1464 return err 1465 } 1466 1467 var rev string 1468 if dir != nil { 1469 rev = dir.Rev() 1470 } else { 1471 rev = file.Rev() 1472 } 1473 1474 if err = CheckIfMatch(c, rev); err != nil { 1475 return WrapVfsError(err) 1476 } 1477 1478 if dir != nil { 1479 err = inst.VFS().DestroyDirAndContent(dir, pushTrashJob(inst)) 1480 } else { 1481 err = inst.VFS().DestroyFile(file) 1482 } 1483 if err != nil { 1484 return WrapVfsError(err) 1485 } 1486 1487 return c.NoContent(204) 1488 } 1489 1490 // FindFilesMango is the route POST /files/_find 1491 // used to retrieve files and their metadata from a mango query. 1492 func FindFilesMango(c echo.Context) error { 1493 instance := middlewares.GetInstance(c) 1494 var findRequest map[string]interface{} 1495 1496 if err := json.NewDecoder(c.Request().Body).Decode(&findRequest); err != nil { 1497 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 1498 } 1499 1500 if err := middlewares.AllowWholeType(c, permission.GET, consts.Files); err != nil { 1501 return err 1502 } 1503 1504 includePath := true 1505 if reqFields, ok := findRequest["fields"].([]interface{}); ok { 1506 includePath = false 1507 // Those fields are necessary for the JSON-API response 1508 fields := []string{"_id", "_rev", "type", "class", "size", "trashed"} 1509 for _, v := range reqFields { 1510 v := v.(string) 1511 if v == "path" { 1512 // path is not stored in database, but added by the stack, and 1513 // it requires the dir_id 1514 includePath = true 1515 fields = append(fields, "dir_id") 1516 } 1517 fields = append(fields, v) 1518 } 1519 findRequest["fields"] = fields 1520 } 1521 1522 limit, hasLimit := findRequest["limit"].(float64) 1523 if !hasLimit || limit > consts.MaxItemsPerPageForMango { 1524 limit = 100 1525 } 1526 if pageLimit := c.QueryParam("page[limit]"); pageLimit != "" { 1527 if limitInt, err := strconv.Atoi(pageLimit); err == nil { 1528 limit = float64(limitInt) 1529 } 1530 } 1531 findRequest["limit"] = limit 1532 1533 skip := 0 1534 if skipF64, ok := findRequest["skip"].(float64); ok { 1535 skip = int(skipF64) 1536 } 1537 if pageSkip := c.QueryParam("page[skip]"); pageSkip != "" { 1538 if skipInt, err := strconv.Atoi(pageSkip); err == nil { 1539 findRequest["skip"] = skipInt 1540 skip = skipInt 1541 } 1542 } 1543 1544 // XXX page[cursor] should be preferred to cursor, but we still accept 1545 // cursor to keep compatibility with the past 1546 if bookmark := c.QueryParam("cursor"); bookmark != "" { 1547 findRequest["bookmark"] = bookmark 1548 } 1549 if bookmark := c.QueryParam("page[cursor]"); bookmark != "" { 1550 findRequest["bookmark"] = bookmark 1551 } 1552 1553 var results []vfs.DirOrFileDoc 1554 resp, err := couchdb.FindDocsRaw(instance, consts.Files, &findRequest, &results) 1555 if err != nil { 1556 return err 1557 } 1558 1559 // XXX: in theory, we should avoid pagination link for POST requests, but 1560 // it is here and used, so let's keep it for compatibility. 1561 var links jsonapi.LinksList 1562 if resp.Bookmark != "" && len(results) >= int(limit) { 1563 links.Next = "/files/_find?page[cursor]=" + resp.Bookmark 1564 } 1565 1566 var total int 1567 if len(results) >= int(limit) { 1568 total = math.MaxInt32 - 1 // we dont know the actual number 1569 } else { 1570 total = skip + len(results) // let the client know its done. 1571 } 1572 1573 // Create secrets for thumbnail links in batch for performance reasons 1574 var thumbIDs []string 1575 for _, dof := range results { 1576 _, f := dof.Refine() 1577 if f != nil { 1578 if f.Class == "image" || f.Class == "pdf" { 1579 thumbIDs = append(thumbIDs, f.ID()) 1580 } 1581 } 1582 } 1583 var secrets map[string]string 1584 if len(thumbIDs) > 0 { 1585 secrets, _ = vfs.GetStore().AddThumbs(instance, thumbIDs) 1586 } 1587 if secrets == nil { 1588 secrets = make(map[string]string) 1589 } 1590 1591 fp := vfs.NewFilePatherWithCache(instance.VFS()) 1592 out := make([]jsonapi.Object, len(results)) 1593 fields, ok := findRequest["fields"].([]string) 1594 for i, dof := range results { 1595 d, f := dof.Refine() 1596 if d != nil { 1597 if ok { 1598 out[i] = newFindDir(d, fields) 1599 } else { 1600 out[i] = newDir(d) 1601 } 1602 } else { 1603 if ok { 1604 file := newFindFile(f, fields, instance) 1605 if includePath { 1606 file.IncludePath(fp) 1607 } 1608 if secret, ok := secrets[f.ID()]; ok { 1609 file.SetThumbSecret(secret) 1610 } 1611 out[i] = file 1612 } else { 1613 file := NewFile(f, instance) 1614 if includePath { 1615 file.IncludePath(fp) 1616 } 1617 if secret, ok := secrets[f.ID()]; ok { 1618 file.SetThumbSecret(secret) 1619 } 1620 out[i] = file 1621 } 1622 } 1623 } 1624 1625 meta := jsonapi.Meta{ 1626 Count: &total, 1627 ExecutionStats: resp.ExecutionStats, 1628 Warning: resp.Warning, 1629 } 1630 return jsonapi.DataListWithMeta(c, http.StatusOK, meta, out, &links) 1631 } 1632 1633 var allowedChangesParams = map[string]bool{ 1634 // supported by CouchDB 1635 "since": true, 1636 "limit": true, 1637 "include_docs": true, 1638 1639 // custom 1640 "fields": false, 1641 "include_file_path": false, 1642 "skip_deleted": false, 1643 "skip_trashed": false, 1644 } 1645 1646 // ChangesFeed is the handler for GET /files/_changes. It is similar to the 1647 // changes feed of CouchDB with some additional options, like skip_trashed. 1648 func ChangesFeed(c echo.Context) error { 1649 inst := middlewares.GetInstance(c) 1650 if err := middlewares.AllowWholeType(c, permission.GET, consts.Files); err != nil { 1651 return err 1652 } 1653 1654 // Drop a clear error for parameters not supported by stack 1655 filter := &changesFilter{} 1656 for key := range c.QueryParams() { 1657 if byCouch, ok := allowedChangesParams[key]; !ok { 1658 return jsonapi.Errorf(http.StatusBadRequest, "Unsupported query parameter '%s'", key) 1659 } else if !byCouch { 1660 filter.Add(key, c.QueryParam(key)) 1661 } 1662 } 1663 1664 limitString := c.QueryParam("limit") 1665 limit := 0 1666 if limitString != "" { 1667 var err error 1668 if limit, err = strconv.Atoi(limitString); err != nil { 1669 return jsonapi.Errorf(http.StatusBadRequest, "Invalid limit value '%s': %s", limitString, err.Error()) 1670 } 1671 if limit > 10000 { 1672 limit = 10000 1673 } 1674 } 1675 1676 includeDocs := c.QueryParam("include_docs") == "true" 1677 if !includeDocs && (filter.IncludePath || filter.SkipTrashed) { 1678 return jsonapi.Errorf(http.StatusBadRequest, "Invalid options: include_docs should be set to true") 1679 } 1680 1681 // Use the VFS lock for the files to avoid sending the changed feed while 1682 // the VFS is moving a directory. 1683 mu := config.Lock().ReadWrite(inst, "vfs") 1684 if err := mu.Lock(); err != nil { 1685 return err 1686 } 1687 1688 couchReq := &couchdb.ChangesRequest{ 1689 DocType: consts.Files, 1690 Since: c.QueryParam("since"), 1691 Limit: limit, 1692 IncludeDocs: includeDocs, 1693 Filter: "_selector", 1694 } 1695 results, err := couchdb.PostChanges(inst, couchReq, filter) 1696 mu.Unlock() 1697 if err != nil { 1698 return err 1699 } 1700 1701 if client, ok := middlewares.GetOAuthClient(c); ok { 1702 err = vfs.FilterNotSynchronizedDocs(inst.VFS(), client.ID(), results) 1703 if err != nil { 1704 return err 1705 } 1706 } 1707 1708 filter.Reject(results) 1709 c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 1710 c.Response().WriteHeader(http.StatusOK) 1711 if err := filter.Stream(c.Response(), inst, results); err != nil { 1712 inst.Logger().WithNamespace("files").Warnf("error on _changes: %s", err) 1713 return err 1714 } 1715 return nil 1716 } 1717 1718 type changesFilter struct { 1719 Fields []string 1720 IncludePath bool 1721 SkipDeleted bool 1722 SkipTrashed bool 1723 reader io.Reader 1724 } 1725 1726 func (filter *changesFilter) Add(key, value string) { 1727 switch key { 1728 case "fields": 1729 filter.Fields = strings.Split(value, ",") 1730 case "include_file_path": 1731 filter.IncludePath = true 1732 case "skip_deleted": 1733 filter.SkipDeleted = true 1734 case "skip_trashed": 1735 filter.SkipTrashed = true 1736 } 1737 } 1738 1739 func (filter *changesFilter) Reject(results *couchdb.ChangesResponse) { 1740 if !filter.SkipDeleted && !filter.SkipTrashed { 1741 return 1742 } 1743 1744 changes := results.Results[:0] 1745 for _, change := range results.Results { 1746 if filter.SkipDeleted && change.Deleted { 1747 continue 1748 } 1749 if filter.SkipTrashed { 1750 if change.Doc.M["type"] == "file" && change.Doc.M["trashed"] == true { 1751 continue 1752 } 1753 if change.Doc.M["type"] == "directory" { 1754 path, _ := change.Doc.M["path"].(string) 1755 if path == vfs.TrashDirName { 1756 continue 1757 } 1758 if strings.HasPrefix(path, vfs.TrashDirName+"/") { 1759 continue 1760 } 1761 } 1762 } 1763 changes = append(changes, change) 1764 } 1765 results.Results = changes 1766 } 1767 1768 func (filter *changesFilter) Stream( 1769 w io.Writer, 1770 inst *instance.Instance, 1771 results *couchdb.ChangesResponse, 1772 ) error { 1773 first := fmt.Sprintf(`{"last_seq": %q, "pending": %d, "results": [`, results.LastSeq, results.Pending) 1774 if _, err := w.Write([]byte(first)); err != nil { 1775 return err 1776 } 1777 1778 fp := vfs.NewFilePatherWithCache(inst.VFS()) 1779 for i, result := range results.Results { 1780 if filter.IncludePath && result.Doc.M != nil && result.Doc.M["type"] == "file" { 1781 dirID, _ := result.Doc.M["dir_id"].(string) 1782 name, _ := result.Doc.M["name"].(string) 1783 doc := &vfs.FileDoc{DirID: dirID, DocName: name} 1784 if pth, err := fp.FilePath(doc); err == nil { 1785 result.Doc.M["path"] = pth 1786 } 1787 } 1788 buf, err := json.Marshal(&result) 1789 if err != nil { 1790 return err 1791 } 1792 if i != len(results.Results)-1 { 1793 buf = append(buf, ',') 1794 } 1795 if _, err := w.Write(buf); err != nil { 1796 return err 1797 } 1798 } 1799 1800 _, err := w.Write([]byte("]}")) 1801 return err 1802 } 1803 1804 func (filter *changesFilter) Body() []byte { 1805 selector := map[string]interface{}{ 1806 "_id": map[string]interface{}{ 1807 "$not": map[string]interface{}{ 1808 "$regex": "^_design/", 1809 }, 1810 }, 1811 } 1812 payload := map[string]interface{}{ 1813 "selector": selector, 1814 } 1815 1816 // Cf https://github.com/apache/couchdb/discussions/3774#discussioncomment-1416510 1817 if len(filter.Fields) > 0 { 1818 if filter.IncludePath || filter.SkipTrashed { 1819 for _, mandatory := range []string{"type", "name", "dir_id"} { 1820 found := false 1821 for _, f := range filter.Fields { 1822 if f == mandatory { 1823 found = true 1824 } 1825 } 1826 if !found { 1827 filter.Fields = append(filter.Fields, mandatory) 1828 } 1829 } 1830 } 1831 payload["fields"] = filter.Fields 1832 } 1833 1834 body, _ := json.Marshal(payload) 1835 return body 1836 } 1837 1838 func (filter *changesFilter) Read(p []byte) (int, error) { 1839 if filter.reader == nil { 1840 filter.reader = bytes.NewReader(filter.Body()) 1841 } 1842 return filter.reader.Read(p) 1843 } 1844 1845 func (filter *changesFilter) Close() error { 1846 filter.reader = nil 1847 return nil 1848 } 1849 1850 func fsckHandler(c echo.Context) error { 1851 instance := middlewares.GetInstance(c) 1852 cacheStorage := config.GetConfig().CacheStorage 1853 1854 if err := middlewares.AllowWholeType(c, permission.GET, consts.Files); err != nil { 1855 return err 1856 } 1857 1858 noCache, _ := strconv.ParseBool(c.QueryParam("NoCache")) 1859 key := "fsck:" + instance.DBPrefix() 1860 if !noCache { 1861 if r, ok := cacheStorage.GetCompressed(key); ok { 1862 return c.Stream(http.StatusOK, echo.MIMEApplicationJSON, r) 1863 } 1864 } 1865 1866 logs := make([]*vfs.FsckLog, 0) 1867 err := instance.VFS().CheckFilesConsistency(func(log *vfs.FsckLog) { 1868 if log.Type == vfs.ContentMismatch { 1869 logs = append(logs, log) 1870 } 1871 }, false) 1872 if err != nil { 1873 return err 1874 } 1875 1876 logsData, err := json.Marshal(logs) 1877 if err != nil { 1878 return err 1879 } 1880 1881 if !noCache { 1882 expiration := utils.DurationFuzzing(3*30*24*time.Hour, 0.10) 1883 cacheStorage.SetCompressed(key, logsData, expiration) 1884 } 1885 1886 return c.JSONBlob(http.StatusOK, logsData) 1887 } 1888 1889 // Routes sets the routing for the files service 1890 func Routes(router *echo.Group) { 1891 router.HEAD("/download", ReadFileContentFromPathHandler) 1892 router.GET("/download", ReadFileContentFromPathHandler) 1893 router.HEAD("/download/:file-id", ReadFileContentFromIDHandler) 1894 router.GET("/download/:file-id", ReadFileContentFromIDHandler) 1895 1896 router.HEAD("/download/:file-id/:version-id", ReadFileContentFromVersion) 1897 router.GET("/download/:file-id/:version-id", ReadFileContentFromVersion) 1898 router.POST("/revert/:file-id/:version-id", RevertFileVersion) 1899 router.PATCH("/:file-id/:version-id", ModifyFileVersionMetadata) 1900 router.DELETE("/:file-id/:version-id", DeleteFileVersionMetadata) 1901 router.POST("/:file-id/versions", CopyVersionHandler) 1902 router.DELETE("/versions", ClearOldVersions) 1903 1904 router.POST("/_find", FindFilesMango) 1905 router.GET("/_changes", ChangesFeed) 1906 1907 router.HEAD("/:file-id", HeadDirOrFile) 1908 1909 router.GET("/metadata", ReadMetadataFromPathHandler) 1910 router.GET("/:file-id", ReadMetadataFromIDHandler) 1911 router.GET("/:file-id/relationships/contents", GetChildrenHandler) 1912 router.GET("/:file-id/size", GetDirSize) 1913 1914 router.PATCH("/metadata", ModifyMetadataByPathHandler) 1915 router.PATCH("/:file-id", ModifyMetadataByIDHandler) 1916 router.PATCH("/", ModifyMetadataByIDInBatchHandler) 1917 1918 router.POST("/shared-drives", SharedDrivesCreationHandler) 1919 router.POST("/", CreationHandler) 1920 router.POST("/:file-id", CreationHandler) 1921 router.PUT("/:file-id", OverwriteFileContentHandler) 1922 router.POST("/upload/metadata", UploadMetadataHandler) 1923 router.POST("/:file-id/copy", FileCopyHandler) 1924 1925 router.GET("/:file-id/icon/:secret", IconHandler) 1926 router.GET("/:file-id/preview/:secret", PreviewHandler) 1927 router.GET("/:file-id/thumbnails/:secret/:format", ThumbnailHandler) 1928 1929 router.POST("/archive", ArchiveDownloadCreateHandler) 1930 router.GET("/archive/:secret/:fake-name", ArchiveDownloadHandler) 1931 1932 router.POST("/downloads", FileDownloadCreateHandler) 1933 router.GET("/downloads/:secret/:fake-name", FileDownloadHandler) 1934 1935 router.POST("/:file-id/relationships/referenced_by", AddReferencedHandler) 1936 router.DELETE("/:file-id/relationships/referenced_by", RemoveReferencedHandler) 1937 1938 router.POST("/:file-id/relationships/not_synchronized_on", AddNotSynchronizedOn) 1939 router.DELETE("/:file-id/relationships/not_synchronized_on", RemoveNotSynchronizedOn) 1940 1941 router.GET("/trash", ReadTrashFilesHandler) 1942 router.DELETE("/trash", ClearTrashHandler) 1943 1944 router.POST("/trash/:file-id", RestoreTrashFileHandler) 1945 router.DELETE("/trash/:file-id", DestroyFileHandler) 1946 1947 router.DELETE("/:file-id", TrashHandler) 1948 router.GET("/fsck", fsckHandler) 1949 } 1950 1951 // WrapVfsError returns a formatted error from a golang error emitted by the vfs 1952 func WrapVfsError(err error) error { 1953 if errj := wrapVfsError(err); errj != nil { 1954 return errj 1955 } 1956 return err 1957 } 1958 1959 func wrapVfsErrorJSONAPI(err error) *jsonapi.Error { 1960 if errj := wrapVfsError(err); errj != nil { 1961 return errj 1962 } 1963 return jsonapi.InternalServerError(err) 1964 } 1965 1966 func wrapVfsError(err error) *jsonapi.Error { 1967 switch err { 1968 case ErrDocTypeInvalid: 1969 return jsonapi.InvalidAttribute("type", err) 1970 case os.ErrExist: 1971 return jsonapi.Conflict(err) 1972 case os.ErrNotExist, swift.ObjectNotFound: 1973 return jsonapi.NotFound(err) 1974 case vfs.ErrParentDoesNotExist: 1975 return jsonapi.NotFound(err) 1976 case vfs.ErrParentInTrash: 1977 return jsonapi.NotFound(err) 1978 case vfs.ErrForbiddenDocMove: 1979 return jsonapi.PreconditionFailed("dir-id", err) 1980 case vfs.ErrIllegalFilename: 1981 return jsonapi.InvalidParameter("name", err) 1982 case vfs.ErrIllegalPath: 1983 return jsonapi.InvalidParameter("path", err) 1984 case vfs.ErrIllegalMime: 1985 return jsonapi.InvalidParameter("mime", err) 1986 case vfs.ErrIllegalTime: 1987 return jsonapi.InvalidParameter("UpdatedAt", err) 1988 case vfs.ErrInvalidHash: 1989 return jsonapi.PreconditionFailed("Content-MD5", err) 1990 case vfs.ErrContentLengthMismatch: 1991 return jsonapi.PreconditionFailed("Content-Length", err) 1992 case vfs.ErrConflict: 1993 return jsonapi.Conflict(err) 1994 case vfs.ErrFileInTrash, vfs.ErrNonAbsolutePath, 1995 vfs.ErrDirNotEmpty: 1996 return jsonapi.BadRequest(err) 1997 case vfs.ErrFileTooBig, vfs.ErrMaxFileSize: 1998 return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err) 1999 case vfs.ErrWrongToken: 2000 return jsonapi.BadRequest(err) 2001 case vfs.ErrInvalidMetadataID: 2002 return jsonapi.InvalidParameter("MetadataID", err) 2003 } 2004 if _, ok := err.(*jsonapi.Error); !ok { 2005 logger.WithNamespace("files").Warnf("Not wrapped error: %s", err) 2006 } 2007 return nil 2008 } 2009 2010 // FileDocFromReq creates a FileDoc from an incoming request. 2011 func FileDocFromReq(c echo.Context, name, dirID string) (*vfs.FileDoc, error) { 2012 header := c.Request().Header 2013 size := c.Request().ContentLength 2014 if size == -1 { 2015 if param := c.QueryParam("Size"); param != "" { 2016 if s, err := strconv.ParseInt(param, 10, 64); err == nil { 2017 size = s 2018 } 2019 } 2020 } 2021 2022 var err error 2023 var md5Sum []byte 2024 if md5Str := header.Get("Content-MD5"); md5Str != "" { 2025 md5Sum, err = parseMD5Hash(md5Str) 2026 } 2027 if err != nil { 2028 err = jsonapi.InvalidParameter("Content-MD5", err) 2029 return nil, err 2030 } 2031 2032 cdate := time.Now() 2033 if date := header.Get("Date"); date != "" { 2034 if t, err := time.Parse(time.RFC1123, date); err == nil { 2035 cdate = t 2036 } 2037 } 2038 2039 var mime, class string 2040 contentType := header.Get(echo.HeaderContentType) 2041 if contentType == "" || contentType == echo.MIMEOctetStream { 2042 mime, class = vfs.ExtractMimeAndClassFromFilename(name) 2043 } else { 2044 ext := strings.ToLower(path.Ext(name)) 2045 // Force the mime-type for .url files 2046 if ext == ".url" { 2047 contentType = consts.ShortcutMimeType 2048 } 2049 if contentType == "text/xml" && ext == "svg" { 2050 contentType = "image/svg+xml" 2051 } 2052 mime, class = vfs.ExtractMimeAndClass(contentType) 2053 } 2054 2055 tags := strings.Split(c.QueryParam("Tags"), TagSeparator) 2056 executable := c.QueryParam("Executable") == "true" 2057 encrypted := c.QueryParam("Encrypted") == "true" 2058 trashed := false 2059 doc, err := vfs.NewFileDoc( 2060 name, 2061 dirID, 2062 size, 2063 md5Sum, 2064 mime, 2065 class, 2066 cdate, 2067 executable, 2068 trashed, 2069 encrypted, 2070 tags, 2071 ) 2072 if err != nil { 2073 return nil, err 2074 } 2075 2076 // This way to send metadata is deprecated, but is still here to ensure 2077 // compatibility with existing clients. 2078 if meta := c.QueryParam("Metadata"); meta != "" { 2079 if err := json.Unmarshal([]byte(meta), &doc.Metadata); err != nil { 2080 return nil, err 2081 } 2082 } 2083 2084 if secret := c.QueryParam("MetadataID"); secret != "" { 2085 instance := middlewares.GetInstance(c) 2086 meta, err := vfs.GetStore().GetMetadata(instance, secret) 2087 if err != nil { 2088 return nil, err 2089 } 2090 doc.Metadata = *meta 2091 } 2092 2093 if len(doc.Metadata) > 0 { 2094 if _, ok := doc.Metadata[consts.CarbonCopyKey]; ok { 2095 if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedCarbonCopy); err != nil { 2096 delete(doc.Metadata, consts.CarbonCopyKey) 2097 } 2098 } 2099 if _, ok := doc.Metadata[consts.ElectronicSafeKey]; ok { 2100 if err := middlewares.AllowWholeType(c, permission.POST, consts.CertifiedElectronicSafe); err != nil { 2101 delete(doc.Metadata, consts.ElectronicSafeKey) 2102 } 2103 } 2104 } 2105 2106 return doc, nil 2107 } 2108 2109 // CheckIfMatch checks if the revision provided matches the revision number 2110 // given in the request, in the header and/or the query. 2111 func CheckIfMatch(c echo.Context, rev string) error { 2112 ifMatch := c.Request().Header.Get("If-Match") 2113 revQuery := c.QueryParam("rev") 2114 var wantedRev string 2115 if ifMatch != "" { 2116 wantedRev = ifMatch 2117 } 2118 if revQuery != "" && wantedRev == "" { 2119 wantedRev = revQuery 2120 } 2121 return checkIfMatch(rev, wantedRev) 2122 } 2123 2124 func checkIfMatch(rev, wantedRev string) error { 2125 if wantedRev != "" && rev != wantedRev { 2126 return jsonapi.PreconditionFailed("If-Match", fmt.Errorf("Revision does not match")) 2127 } 2128 return nil 2129 } 2130 2131 func checkPerm(c echo.Context, v permission.Verb, d *vfs.DirDoc, f *vfs.FileDoc) error { 2132 if d != nil { 2133 return middlewares.AllowVFS(c, v, d) 2134 } 2135 return middlewares.AllowVFS(c, v, f) 2136 } 2137 2138 func parseMD5Hash(md5B64 string) ([]byte, error) { 2139 // Encoded md5 hash in base64 should at least have 22 caracters in 2140 // base64: 16*3/4 = 21+1/3 2141 // 2142 // The padding may add up to 2 characters (non useful). If we are 2143 // out of these boundaries we know we don't have a good hash and we 2144 // can bail immediately. 2145 if len(md5B64) < 22 || len(md5B64) > 24 { 2146 return nil, fmt.Errorf("Given Content-MD5 is invalid") 2147 } 2148 2149 md5Sum, err := base64.StdEncoding.DecodeString(md5B64) 2150 if err != nil || len(md5Sum) != 16 { 2151 return nil, fmt.Errorf("Given Content-MD5 is invalid") 2152 } 2153 2154 return md5Sum, nil 2155 } 2156 2157 func pushTrashJob(inst *instance.Instance) func(vfs.TrashJournal) error { 2158 return func(journal vfs.TrashJournal) error { 2159 msg, err := job.NewMessage(journal) 2160 if err != nil { 2161 return err 2162 } 2163 _, err = job.System().PushJob(inst, &job.JobRequest{ 2164 WorkerType: "trash-files", 2165 Message: msg, 2166 }) 2167 return err 2168 } 2169 } 2170 2171 func ensureCleanOldTrashedTrigger(inst *instance.Instance) { 2172 // 1. Check if we need a trigger for clean-old-trashed worker 2173 cfg := config.GetConfig().Fs.AutoCleanTrashedAfter 2174 after, ok := cfg[inst.ContextName] 2175 if !ok || after == "" { 2176 return 2177 } 2178 2179 // 2. Check if the trigger already exists 2180 sched := job.System() 2181 infos := job.TriggerInfos{ 2182 Type: "@cron", 2183 WorkerType: "clean-old-trashed", 2184 } 2185 if sched.HasTrigger(inst, infos) { 2186 return 2187 } 2188 2189 // 3. Create the trigger 2190 now := time.Now() 2191 hours := (now.Hour() + 12) % 24 2192 infos.Arguments = fmt.Sprintf("0 %d %d * * *", now.Minute(), hours) 2193 trigger, err := job.NewTrigger(inst, infos, nil) 2194 if err != nil { 2195 inst.Logger().Errorf("Cannot create clean-old-trashed trigger: %s", err) 2196 return 2197 } 2198 if err = sched.AddTrigger(trigger); err != nil { 2199 inst.Logger().Errorf("Cannot create clean-old-trashed trigger: %s", err) 2200 } 2201 } 2202 2203 func instanceURL(c echo.Context) string { 2204 return middlewares.GetInstance(c).PageURL("/", nil) 2205 } 2206 2207 func updateDirCozyMetadata(c echo.Context, dir *vfs.DirDoc) { 2208 fcm, _ := CozyMetadataFromClaims(c, false) 2209 if dir.CozyMetadata == nil { 2210 fcm.CreatedAt = dir.CreatedAt 2211 fcm.CreatedByApp = "" 2212 fcm.CreatedByAppVersion = "" 2213 dir.CozyMetadata = fcm 2214 } else { 2215 dir.CozyMetadata.UpdatedAt = fcm.UpdatedAt 2216 if len(fcm.UpdatedByApps) > 0 { 2217 dir.CozyMetadata.UpdatedByApp(fcm.UpdatedByApps[0]) 2218 } 2219 } 2220 } 2221 2222 func updateFileCozyMetadata(c echo.Context, file *vfs.FileDoc, setUploadFields bool) { 2223 var oldSourceAccount, oldSourceIdentifier string 2224 fcm, slug := CozyMetadataFromClaims(c, setUploadFields) 2225 if file.CozyMetadata == nil { 2226 fcm.CreatedAt = file.CreatedAt 2227 fcm.CreatedByApp = "" 2228 fcm.CreatedByAppVersion = "" 2229 uploadedAt := file.CreatedAt 2230 fcm.UploadedAt = &uploadedAt 2231 file.CozyMetadata = fcm 2232 } else { 2233 oldSourceAccount = file.CozyMetadata.SourceAccount 2234 oldSourceIdentifier = file.CozyMetadata.SourceIdentifier 2235 file.CozyMetadata.UpdatedAt = fcm.UpdatedAt 2236 if len(fcm.UpdatedByApps) > 0 { 2237 file.CozyMetadata.UpdatedByApp(fcm.UpdatedByApps[0]) 2238 } 2239 if setUploadFields { 2240 file.CozyMetadata.UploadedAt = fcm.UploadedAt 2241 file.CozyMetadata.UploadedBy = fcm.UploadedBy 2242 file.CozyMetadata.UploadedOn = fcm.UploadedOn 2243 } 2244 } 2245 2246 if setUploadFields { 2247 if oldSourceAccount == "" && fcm.SourceAccount != "" { 2248 file.CozyMetadata.SourceAccount = fcm.SourceAccount 2249 // To ease the transition to cozyMetadata for io.cozy.files, we fill 2250 // the CreatedByApp for konnectors that updates a file: the stack can 2251 // recognize that by the presence of the SourceAccount. 2252 if file.CozyMetadata.CreatedByApp == "" && slug != "" { 2253 file.CozyMetadata.CreatedByApp = slug 2254 } 2255 } 2256 if oldSourceIdentifier == "" && fcm.SourceIdentifier != "" { 2257 file.CozyMetadata.SourceIdentifier = fcm.SourceIdentifier 2258 } 2259 } 2260 } 2261 2262 // CozyMetadataFromClaims returns a FilesCozyMetadata struct, with the app 2263 // fields filled with information from the permission claims. 2264 func CozyMetadataFromClaims(c echo.Context, setUploadFields bool) (*vfs.FilesCozyMetadata, string) { 2265 fcm := vfs.NewCozyMetadata(instanceURL(c)) 2266 2267 var slug, version string 2268 var client map[string]string 2269 if claims := c.Get("claims"); claims != nil { 2270 cl := claims.(permission.Claims) 2271 switch cl.AudienceString() { 2272 case consts.AppAudience, consts.KonnectorAudience: 2273 slug = cl.Subject 2274 case consts.AccessTokenAudience: 2275 if perms, err := middlewares.GetPermission(c); err == nil { 2276 if cli, ok := perms.Client.(*oauth.Client); ok { 2277 slug = oauth.GetLinkedAppSlug(cli.SoftwareID) 2278 // Special case for cozy-desktop: it is an OAuth app not linked 2279 // to a web app, so it has no slug, but we still want to keep 2280 // in cozyMetadata its changes, so we use a fake slug. 2281 if slug == "" && strings.Contains(cli.SoftwareID, "cozy-desktop") { 2282 slug = "cozy-desktop" 2283 } 2284 // Special case for the flagship app 2285 if slug == "" && cli.Flagship { 2286 slug = "cozy-flagship" 2287 } 2288 version = cli.SoftwareVersion 2289 client = map[string]string{ 2290 "id": cli.ID(), 2291 "kind": cli.ClientKind, 2292 "name": cli.ClientName, 2293 } 2294 } 2295 } 2296 } 2297 } 2298 2299 if slug != "" { 2300 fcm.CreatedByApp = slug 2301 fcm.CreatedByAppVersion = version 2302 fcm.UpdatedByApps = []*metadata.UpdatedByAppEntry{ 2303 { 2304 Slug: slug, 2305 Version: version, 2306 Date: fcm.UpdatedAt, 2307 Instance: fcm.CreatedOn, 2308 }, 2309 } 2310 } 2311 2312 if setUploadFields { 2313 uploadedAt := fcm.CreatedAt 2314 fcm.UploadedAt = &uploadedAt 2315 fcm.UploadedOn = fcm.CreatedOn 2316 if slug != "" { 2317 fcm.UploadedBy = &vfs.UploadedByEntry{ 2318 Slug: slug, 2319 Version: version, 2320 Client: client, 2321 } 2322 } 2323 } 2324 2325 if account := c.QueryParam("SourceAccount"); account != "" { 2326 fcm.SourceAccount = account 2327 } 2328 if id := c.QueryParam("SourceAccountIdentifier"); id != "" { 2329 fcm.SourceIdentifier = id 2330 } 2331 2332 return fcm, slug 2333 } 2334 2335 func fileCopyName(inst *instance.Instance, name string) string { 2336 base, ext := name, "" 2337 ext = filepath.Ext(name) 2338 base = strings.TrimSuffix(base, ext) 2339 suffix := inst.Translate("File copy Suffix") 2340 2341 return fmt.Sprintf("%s (%s)%s", base, suffix, ext) 2342 }