github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/vfs/vfs.go (about) 1 // Package vfs is for storing files on the cozy, including binary ones like 2 // photos and movies. The range of possible operations with this endpoint goes 3 // from simple ones, like uploading a file, to more complex ones, like renaming 4 // a directory. It also ensure that an instance is not exceeding its quota, and 5 // keeps a trash to recover files recently deleted. 6 package vfs 7 8 import ( 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "os" 14 "path" 15 "path/filepath" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/cozy/cozy-stack/pkg/consts" 21 "github.com/cozy/cozy-stack/pkg/couchdb" 22 "github.com/cozy/cozy-stack/pkg/filetype" 23 "github.com/cozy/cozy-stack/pkg/prefixer" 24 ) 25 26 // ForbiddenFilenameChars is the list of forbidden characters in a filename. 27 const ForbiddenFilenameChars = "/\x00\n\r" 28 29 const ( 30 // TrashDirName is the path of the trash directory 31 TrashDirName = "/.cozy_trash" 32 // ThumbsDirName is the path of the directory for thumbnails 33 ThumbsDirName = "/.thumbs" 34 // WebappsDirName is the path of the directory in which apps are stored 35 WebappsDirName = "/.cozy_apps" 36 // KonnectorsDirName is the path of the directory in which konnectors source 37 // are stored 38 KonnectorsDirName = "/.cozy_konnectors" 39 // OrphansDirName is the path of the directory used to store data-files added 40 // in the index from a filesystem-check (fsck) 41 OrphansDirName = "/.cozy_orphans" 42 // VersionsDirName is the path of the directory where old versions of files 43 // are persisted. 44 VersionsDirName = "/.cozy_versions" 45 ) 46 47 const conflictFormat = "%s (%s)" 48 49 // MaxDepth is the maximum amount of recursion allowed for the recursive walk 50 // process. 51 const MaxDepth = 512 52 53 // ErrSkipDir is used in WalkFn as an error to skip the current 54 // directory. It is not returned by any function of the package. 55 var ErrSkipDir = errors.New("skip directories") 56 57 // ErrWalkOverflow is used in the walk process when the maximum amount of 58 // recursivity allowed is reached when browsing the index tree. 59 var ErrWalkOverflow = errors.New("vfs: walk overflow") 60 61 // CreateOptions is used for options on the create file operation 62 type CreateOptions int 63 64 const ( 65 // AllowCreationInTrash is an option to allow bypassing the rule that 66 // forbids the creation of file in the trash. 67 AllowCreationInTrash CreateOptions = 1 + iota 68 ) 69 70 // Fs is an interface providing a set of high-level methods to interact with 71 // the file-system binaries and metadata. 72 type Fs interface { 73 prefixer.Prefixer 74 InitFs() error 75 Delete() error 76 77 // Maximum file size 78 MaxFileSize() int64 79 80 // OpenFile return a file handler for reading associated with the given file 81 // document. The file handler implements io.ReadCloser and io.Seeker. 82 OpenFile(doc *FileDoc) (File, error) 83 // OpenFileVersion returns a file handler for reading the content of an old 84 // version of the given file. 85 OpenFileVersion(doc *FileDoc, version *Version) (File, error) 86 // CreateDir is used to create a new directory from its document. 87 CreateDir(doc *DirDoc) error 88 // CreateFile creates a new file or update the content of an existing file. 89 // The first argument contains the document of the new or update version of 90 // the file. The second argument is the optional old document of the old 91 // version of the file. 92 // 93 // Warning: you MUST call the Close() method and check for its error. 94 CreateFile(newdoc, olddoc *FileDoc, opts ...CreateOptions) (File, error) 95 // CopyFile creates a fresh copy of the source file with the given newdoc 96 // attributes (e.g. a new name) 97 CopyFile(olddoc, newdoc *FileDoc) error 98 // DissociateFile creates a copy of the source file with the name and 99 // directory of the destination file doc, and then remove the source file 100 // with all of its version. It is used by the sharings to change the ID 101 // of the document to avoid later conflicts. 102 DissociateFile(src, dst *FileDoc) error 103 // DissociateDir is like DissociateFile but for directories. 104 DissociateDir(src, dst *DirDoc) error 105 106 // DestroyDirContent destroys all directories and files contained in a 107 // directory. 108 DestroyDirContent(doc *DirDoc, push func(TrashJournal) error) error 109 // DestroyDirAndContent destroys all directories and files contained in a 110 // directory and the directory itself. 111 DestroyDirAndContent(doc *DirDoc, push func(TrashJournal) error) error 112 // DestroyFile destroys a file from the trash. 113 DestroyFile(doc *FileDoc) error 114 // EnsureErased remove the files in Swift if they still exist. 115 EnsureErased(journal TrashJournal) error 116 117 // RevertFileVersion restores the content of a file from an old version. 118 // The current version of the content is not lost, but saved as another 119 // version. 120 RevertFileVersion(doc *FileDoc, version *Version) error 121 // CleanOldVersion deletes an old version of a file. 122 CleanOldVersion(fileID string, version *Version) error 123 // ClearOldVersions deletes all the old versions of all files 124 ClearOldVersions() error 125 // ImportFileVersion returns a file handler that can be used to write a 126 // version. 127 ImportFileVersion(version *Version, content io.ReadCloser) error 128 129 // CopyFileFromOtherFS creates or updates a file by copying the content of 130 // a file in another Cozy. It is used for sharings, to optimize I/O when 131 // two instances are on the same stack. 132 CopyFileFromOtherFS(olddoc, newdoc *FileDoc, srcFS Fs, srcDoc *FileDoc) error 133 134 // Fsck return the list of inconsistencies in the VFS 135 Fsck(func(log *FsckLog), bool) (err error) 136 CheckFilesConsistency(func(*FsckLog), bool) error 137 } 138 139 // File is a reader, writer, seeker, closer iterface representing an opened 140 // file for reading or writing. 141 type File interface { 142 io.Reader 143 io.ReaderAt 144 io.Seeker 145 io.Writer 146 io.Closer 147 } 148 149 // FilePather is an interface for computing the fullpath of a filedoc 150 type FilePather interface { 151 FilePath(doc *FileDoc) (string, error) 152 } 153 154 // Indexer is an interface providing a common set of method for indexing layer 155 // of our VFS. 156 // 157 // An indexer is typically responsible for storing and indexing the files and 158 // directories metadata, as well as caching them if necessary. 159 type Indexer interface { 160 InitIndex() error 161 162 FilePather 163 164 // DiskUsage computes the total size of the files contained in the VFS, 165 // including versions. 166 DiskUsage() (int64, error) 167 // FilesUsage computes the total size of the files contained in the VFS, 168 // excluding versions. 169 FilesUsage() (int64, error) 170 // VersionsUsage computes the total size of the old file versions contained 171 // in the VFS, not including latest version. 172 VersionsUsage() (int64, error) 173 // TrashUsage computes the total size of the files contained in the trash. 174 TrashUsage() (int64, error) 175 // DirSize returns the size of a directory, including files in 176 // subdirectories. 177 DirSize(doc *DirDoc) (int64, error) 178 179 // CreateFileDoc creates and add in the index a new file document. 180 CreateFileDoc(doc *FileDoc) error 181 // CreateNamedFileDoc creates and add in the index a new file document with 182 // its id already set. 183 CreateNamedFileDoc(doc *FileDoc) error 184 // UpdateFileDoc is used to update the document of a file. It takes the 185 // new file document that you want to create and the old document, 186 // representing the current revision of the file. 187 UpdateFileDoc(olddoc, newdoc *FileDoc) error 188 // DeleteFileDoc removes from the index the specified file document. 189 DeleteFileDoc(doc *FileDoc) error 190 191 // CreateDirDoc creates and add in the index a new directory document. 192 CreateDirDoc(doc *DirDoc) error 193 // CreateNamedDirDoc creates and add in the index a new directory document 194 // with its id already set. 195 CreateNamedDirDoc(doc *DirDoc) error 196 // UpdateDirDoc is used to update the document of a directory. It takes the 197 // new directory document that you want to create and the old document, 198 // representing the current revision of the directory. 199 UpdateDirDoc(olddoc, newdoc *DirDoc) error 200 // DeleteDirDoc removes from the index the specified directory document. 201 DeleteDirDoc(doc *DirDoc) error 202 // DeleteDirDocAndContent removes from the index the specified directory as 203 // well all its children. It returns the list of the children files that 204 // were removed. 205 DeleteDirDocAndContent(doc *DirDoc, onlyContent bool) ([]*FileDoc, int64, error) 206 207 // MoveDir is an internal call to update the fullpath of the subdirectories 208 // of a renamed/moved directory. It is exported to allow the sharing 209 // indexer to call this method on the couchdb indexer of the VFS. 210 MoveDir(oldpath, newpath string) error 211 212 // DirByID returns the directory document information associated with the 213 // specified identifier. 214 DirByID(fileID string) (*DirDoc, error) 215 // DirByPath returns the directory document information associated with the 216 // specified path. 217 DirByPath(name string) (*DirDoc, error) 218 219 // FileByID returns the file document information associated with the 220 // specified identifier. 221 FileByID(fileID string) (*FileDoc, error) 222 // FileByPath returns the file document information associated with the 223 // specified path. 224 FileByPath(name string) (*FileDoc, error) 225 226 // DirOrFileByID returns the document from its identifier without knowing in 227 // advance its type. One of the returned argument is not nil. 228 DirOrFileByID(fileID string) (*DirDoc, *FileDoc, error) 229 // DirOrFileByPath returns the document from its path without knowing in 230 // advance its type. One of the returned argument is not nil. 231 DirOrFileByPath(name string) (*DirDoc, *FileDoc, error) 232 233 // DirIterator returns an iterator over the children of the specified 234 // directory. 235 DirIterator(doc *DirDoc, opts *IteratorOptions) DirIterator 236 237 // DirBatch returns a batch of documents 238 DirBatch(*DirDoc, couchdb.Cursor) ([]DirOrFileDoc, error) 239 DirLength(*DirDoc) (int, error) 240 DirChildExists(dirID, filename string) (bool, error) 241 BatchDelete([]couchdb.Doc) error 242 243 // CreateVersion adds a version to the CouchDB index. 244 CreateVersion(*Version) error 245 // DeleteVersion removes a version from the CouchDB index. 246 DeleteVersion(*Version) error 247 AllVersions() ([]*Version, error) 248 BatchDeleteVersions([]*Version) error 249 250 ListNotSynchronizedOn(clientID string) ([]DirDoc, error) 251 252 CheckIndexIntegrity(func(*FsckLog), bool) error 253 CheckTreeIntegrity(*Tree, func(*FsckLog), bool) error 254 BuildTree(each ...func(*TreeFile)) (tree *Tree, err error) 255 } 256 257 // DiskThresholder it an interface that can be implemeted to known how many space 258 // is available on the disk. 259 type DiskThresholder interface { 260 // DiskQuota returns the total number of bytes allowed to be stored in the 261 // VFS. If minus or equal to zero, it is considered without limit. 262 DiskQuota() int64 263 } 264 265 // Thumbser defines an interface to define a thumbnail filesystem. 266 type Thumbser interface { 267 ThumbExists(img *FileDoc, format string) (ok bool, err error) 268 CreateThumb(img *FileDoc, format string) (ThumbFiler, error) 269 RemoveThumbs(img *FileDoc, formats []string) error 270 ServeThumbContent(w http.ResponseWriter, req *http.Request, 271 img *FileDoc, format string) error 272 273 CreateNoteThumb(id, mime, format string) (ThumbFiler, error) 274 OpenNoteThumb(id, format string) (io.ReadCloser, error) 275 RemoveNoteThumb(id string, formats []string) error 276 ServeNoteThumbContent(w http.ResponseWriter, req *http.Request, id string) error 277 } 278 279 // ThumbFiler defines a interface to handle the creation of thumbnails. It is 280 // an io.Writer that can be aborted in case of error, or committed in case of 281 // success. 282 type ThumbFiler interface { 283 io.Writer 284 Abort() error 285 Commit() error 286 } 287 288 // VFS is composed of the Indexer and Fs interface. It is the common interface 289 // used throughout the stack to access the VFS. 290 type VFS interface { 291 Indexer 292 DiskThresholder 293 Fs 294 295 // UseSharingIndexer returns a new Fs with an overloaded indexer that can 296 // be used for the special purpose of the sharing. 297 UseSharingIndexer(Indexer) VFS 298 299 // GetIndexer returns the indexer without the overloaded operations from 300 // VFSAfero / VFSSwift. Its result can be used for FilePatherWithCache with 301 // a VFS that is already locked. 302 GetIndexer() Indexer 303 } 304 305 // Prefixer interface describes a prefixer that can also give the context for 306 // the targeted instance. 307 type Prefixer interface { 308 prefixer.Prefixer 309 GetContextName() string 310 } 311 312 // ErrIteratorDone is returned by the Next() method of the iterator when 313 // the iterator is actually done. 314 var ErrIteratorDone = errors.New("No more element in the iterator") 315 316 // IteratorOptions contains the options of the iterator. 317 type IteratorOptions struct { 318 AfterID string 319 ByFetch int 320 } 321 322 // DirIterator is the interface that an iterator over a specific directory 323 // should implement. The Next method will return a ErrIteratorDone when the 324 // iterator is over and does not have element anymore. 325 type DirIterator interface { 326 Next() (*DirDoc, *FileDoc, error) 327 } 328 329 // DocPatch is a struct containing modifiable fields from file and 330 // directory documents. 331 type DocPatch struct { 332 Name *string `json:"name,omitempty"` 333 DirID *string `json:"dir_id,omitempty"` 334 RestorePath *string `json:"restore_path,omitempty"` 335 Tags *[]string `json:"tags,omitempty"` 336 UpdatedAt *time.Time `json:"updated_at,omitempty"` 337 Executable *bool `json:"executable,omitempty"` 338 Encrypted *bool `json:"encrypted,omitempty"` 339 Class *string `json:"class,omitempty"` 340 341 CozyMetadata CozyMetadataPatch `json:"cozyMetadata"` 342 } 343 344 // CozyMetadataPatch is a struct containing the modifiable fields for a 345 // CozyMetadata. 346 type CozyMetadataPatch struct { 347 Favorite *bool `json:"favorite,omitempty"` 348 } 349 350 // DirOrFileDoc is a union struct of FileDoc and DirDoc. It is useful to 351 // unmarshal documents from couch. 352 type DirOrFileDoc struct { 353 *DirDoc 354 355 // fields from FileDoc not contained in DirDoc 356 ByteSize int64 `json:"size,string"` 357 MD5Sum []byte `json:"md5sum,omitempty"` 358 Mime string `json:"mime,omitempty"` 359 Class string `json:"class,omitempty"` 360 Executable bool `json:"executable,omitempty"` 361 Trashed bool `json:"trashed,omitempty"` 362 Encrypted bool `json:"encrypted,omitempty"` 363 InternalID string `json:"internal_vfs_id,omitempty"` 364 } 365 366 // Clone is part of the couchdb.Doc interface 367 func (fd *DirOrFileDoc) Clone() couchdb.Doc { 368 panic("DirOrFileDoc must not be cloned") 369 } 370 371 // Refine returns either a DirDoc or FileDoc pointer depending on the type of 372 // the DirOrFileDoc 373 func (fd *DirOrFileDoc) Refine() (*DirDoc, *FileDoc) { 374 switch fd.Type { 375 case consts.DirType: 376 return fd.DirDoc, nil 377 case consts.FileType: 378 return nil, &FileDoc{ 379 Type: fd.Type, 380 DocID: fd.DocID, 381 DocRev: fd.DocRev, 382 DocName: fd.DocName, 383 DirID: fd.DirID, 384 RestorePath: fd.RestorePath, 385 CreatedAt: fd.CreatedAt, 386 UpdatedAt: fd.UpdatedAt, 387 ByteSize: fd.ByteSize, 388 MD5Sum: fd.MD5Sum, 389 Mime: fd.Mime, 390 Class: fd.Class, 391 Executable: fd.Executable, 392 Trashed: fd.Trashed, 393 Encrypted: fd.Encrypted, 394 Tags: fd.Tags, 395 Metadata: fd.Metadata, 396 ReferencedBy: fd.ReferencedBy, 397 CozyMetadata: fd.CozyMetadata, 398 InternalID: fd.InternalID, 399 } 400 } 401 return nil, nil 402 } 403 404 // Stat returns the FileInfo of the specified file or directory. 405 func Stat(fs VFS, name string) (os.FileInfo, error) { 406 d, f, err := fs.DirOrFileByPath(name) 407 if err != nil { 408 return nil, err 409 } 410 if d != nil { 411 return d, nil 412 } 413 return f, nil 414 } 415 416 // OpenFile returns a file handler of the specified name. It is a 417 // generalized call used to open a file. It opens the 418 // file with the given flag (O_RDONLY, O_WRONLY, O_CREATE, O_EXCL) and 419 // permission. 420 func OpenFile(fs VFS, name string, flag int, perm os.FileMode) (File, error) { 421 if flag&os.O_RDWR != 0 || flag&os.O_APPEND != 0 { 422 return nil, os.ErrInvalid 423 } 424 if flag&os.O_CREATE != 0 && flag&os.O_EXCL == 0 { 425 return nil, os.ErrInvalid 426 } 427 428 name = path.Clean(name) 429 430 if flag == os.O_RDONLY { 431 doc, err := fs.FileByPath(name) 432 if err != nil { 433 return nil, err 434 } 435 return fs.OpenFile(doc) 436 } 437 438 var dirID string 439 olddoc, err := fs.FileByPath(name) 440 if os.IsNotExist(err) && flag&os.O_CREATE != 0 { 441 var parent *DirDoc 442 parent, err = fs.DirByPath(path.Dir(name)) 443 if err != nil { 444 return nil, err 445 } 446 dirID = parent.ID() 447 } 448 if err != nil { 449 return nil, err 450 } 451 452 if olddoc != nil { 453 dirID = olddoc.DirID 454 } 455 456 if dirID == "" { 457 return nil, os.ErrInvalid 458 } 459 460 filename := path.Base(name) 461 exec := false 462 trashed := false 463 encrypted := false 464 mime, class := ExtractMimeAndClassFromFilename(filename) 465 newdoc, err := NewFileDoc(filename, dirID, -1, nil, mime, class, time.Now(), exec, trashed, encrypted, []string{}) 466 if err != nil { 467 return nil, err 468 } 469 return fs.CreateFile(newdoc, olddoc) 470 } 471 472 // Create creates a new file with specified and returns a File handler 473 // that can be used for writing. 474 func Create(fs VFS, name string) (File, error) { 475 return OpenFile(fs, name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 476 } 477 478 // Mkdir creates a new directory with the specified name 479 func Mkdir(fs VFS, name string, tags []string) (*DirDoc, error) { 480 name = path.Clean(name) 481 if name == "/" { 482 return nil, ErrParentDoesNotExist 483 } 484 485 dirname, dirpath := path.Base(name), path.Dir(name) 486 parent, err := fs.DirByPath(dirpath) 487 if err != nil { 488 return nil, err 489 } 490 491 dir, err := NewDirDocWithParent(dirname, parent, tags) 492 if err != nil { 493 return nil, err 494 } 495 496 dir.CozyMetadata = NewCozyMetadata("") 497 if err = fs.CreateDir(dir); err != nil { 498 return nil, err 499 } 500 501 return dir, nil 502 } 503 504 // MkdirAll creates a directory named path, along with any necessary 505 // parents, and returns nil, or else returns an error. 506 func MkdirAll(fs VFS, name string) (*DirDoc, error) { 507 var err error 508 var dirs []string 509 var base, file string 510 var parent *DirDoc 511 512 base = name 513 for { 514 parent, err = fs.DirByPath(base) 515 if os.IsNotExist(err) { 516 base, file = path.Dir(base), path.Base(base) 517 dirs = append(dirs, file) 518 continue 519 } 520 if err != nil { 521 return nil, err 522 } 523 break 524 } 525 526 for i := len(dirs) - 1; i >= 0; i-- { 527 parent, err = NewDirDocWithParent(dirs[i], parent, nil) 528 if err == nil { 529 parent.CozyMetadata = NewCozyMetadata("") 530 err = fs.CreateDir(parent) 531 // XXX MkdirAll has no lock, so we have to consider the risk of a race condition 532 if os.IsExist(err) { 533 parent, err = fs.DirByPath(path.Join(parent.Fullpath, dirs[i])) 534 } 535 } 536 if err != nil { 537 return nil, err 538 } 539 } 540 541 return parent, nil 542 } 543 544 // Remove removes the specified named file or directory. 545 func Remove(fs VFS, name string, push func(TrashJournal) error) error { 546 dir, file, err := fs.DirOrFileByPath(name) 547 if err != nil { 548 return err 549 } 550 if file != nil { 551 return fs.DestroyFile(file) 552 } 553 empty, err := dir.IsEmpty(fs) 554 if err != nil { 555 return err 556 } 557 if !empty { 558 return ErrDirNotEmpty 559 } 560 return fs.DestroyDirAndContent(dir, push) 561 } 562 563 // RemoveAll removes the specified name file or directory and its content. 564 func RemoveAll(fs VFS, name string, push func(TrashJournal) error) error { 565 dir, file, err := fs.DirOrFileByPath(name) 566 if err != nil { 567 return err 568 } 569 if dir != nil { 570 return fs.DestroyDirAndContent(dir, push) 571 } 572 return fs.DestroyFile(file) 573 } 574 575 // Exists returns wether or not the specified path exist in the file system. 576 func Exists(fs VFS, name string) (bool, error) { 577 _, _, err := fs.DirOrFileByPath(name) 578 if os.IsNotExist(err) { 579 return false, nil 580 } 581 if err != nil { 582 return false, err 583 } 584 return true, nil 585 } 586 587 // DirExists returns wether or not the specified path exist in the file system 588 // and is associated with a directory. 589 func DirExists(fs VFS, name string) (bool, error) { 590 _, err := fs.DirByPath(name) 591 if os.IsNotExist(err) { 592 return false, nil 593 } 594 if err != nil { 595 return false, err 596 } 597 return true, nil 598 } 599 600 // WalkFn type works like filepath.WalkFn type function. It receives 601 // as argument the complete name of the file or directory, the type of 602 // the document, the actual directory or file document and a possible 603 // error. 604 type WalkFn func(name string, dir *DirDoc, file *FileDoc, err error) error 605 606 // Walk walks the file tree document rooted at root. It should work 607 // like filepath.Walk. 608 func Walk(fs Indexer, root string, walkFn WalkFn) error { 609 dir, file, err := fs.DirOrFileByPath(root) 610 if err != nil { 611 return walkFn(root, dir, file, err) 612 } 613 return walk(fs, root, dir, file, walkFn, 0) 614 } 615 616 // WalkByID walks the file tree document rooted at root. It should work 617 // like filepath.Walk. 618 func WalkByID(fs Indexer, fileID string, walkFn WalkFn) error { 619 dir, file, err := fs.DirOrFileByID(fileID) 620 if err != nil { 621 return walkFn("", dir, file, err) 622 } 623 if dir != nil { 624 return walk(fs, dir.Fullpath, dir, file, walkFn, 0) 625 } 626 root, err := file.Path(fs) 627 if err != nil { 628 return walkFn("", dir, file, err) 629 } 630 return walk(fs, root, dir, file, walkFn, 0) 631 } 632 633 // WalkAlreadyLocked walks the file tree rooted on the given directory. It is 634 // the responsibility of the caller to ensure the VFS is already locked (read). 635 func WalkAlreadyLocked(fs Indexer, dir *DirDoc, walkFn WalkFn) error { 636 return walk(fs, dir.Fullpath, dir, nil, walkFn, 0) 637 } 638 639 func walk(fs Indexer, name string, dir *DirDoc, file *FileDoc, walkFn WalkFn, count int) error { 640 if count >= MaxDepth { 641 return ErrWalkOverflow 642 } 643 err := walkFn(name, dir, file, nil) 644 if err != nil { 645 if dir != nil && errors.Is(err, ErrSkipDir) { 646 return nil 647 } 648 return err 649 } 650 if file != nil { 651 return nil 652 } 653 iter := fs.DirIterator(dir, nil) 654 for { 655 d, f, err := iter.Next() 656 if errors.Is(err, ErrIteratorDone) { 657 break 658 } 659 if err != nil { 660 return walkFn(name, nil, nil, err) 661 } 662 var fullpath string 663 if f != nil { 664 fullpath = path.Join(name, f.DocName) 665 } else { 666 fullpath = path.Join(name, d.DocName) 667 } 668 if err = walk(fs, fullpath, d, f, walkFn, count+1); err != nil { 669 return err 670 } 671 } 672 return nil 673 } 674 675 // ExtractMimeAndClass returns a mime and class value from the 676 // specified content-type. For now it only takes the first segment of 677 // the type as the class and the whole type as mime. 678 func ExtractMimeAndClass(contentType string) (mime, class string) { 679 if contentType == "" { 680 contentType = filetype.DefaultType 681 } 682 683 charsetIndex := strings.Index(contentType, ";") 684 if charsetIndex >= 0 { 685 mime = contentType[:charsetIndex] 686 } else { 687 mime = contentType 688 } 689 690 mime = strings.TrimSpace(mime) 691 switch mime { 692 case filetype.DefaultType: 693 class = "files" 694 case "application/x-apple-diskimage", "application/x-msdownload": 695 class = "binary" 696 case "text/html", "text/css", "text/xml", "application/js", "text/x-c", 697 "text/x-go", "text/x-python", "application/x-ruby": 698 class = "code" 699 case "application/pdf": 700 class = "pdf" 701 case "application/vnd.ms-powerpoint", "application/x-iwork-keynote-sffkey", 702 "application/vnd.oasis.opendocument.presentation", 703 "application/vnd.oasis.opendocument.graphics", 704 "application/vnd.openxmlformats-officedocument.presentationml.presentation": 705 class = "slide" 706 case "application/vnd.ms-excel", "application/x-iwork-numbers-sffnumbers", 707 "application/vnd.oasis.opendocument.spreadsheet", 708 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": 709 class = "spreadsheet" 710 case "application/msword", "application/x-iwork-pages-sffpages", 711 "application/vnd.oasis.opendocument.text", 712 "application/vnd.openxmlformats-officedocument.wordprocessingml.document": 713 class = "text" 714 case "application/x-7z-compressed", "application/x-rar-compressed", 715 "application/zip", "application/gzip", "application/x-tar": 716 class = "zip" 717 case consts.ShortcutMimeType: 718 class = "shortcut" 719 default: 720 slashIndex := strings.Index(mime, "/") 721 if slashIndex >= 0 { 722 class = mime[:slashIndex] 723 } else { 724 class = mime 725 } 726 } 727 728 return mime, class 729 } 730 731 // ExtractMimeAndClassFromFilename is a shortcut of 732 // ExtractMimeAndClass used to generate the mime and class from a 733 // filename. 734 func ExtractMimeAndClassFromFilename(name string) (mime, class string) { 735 ext := path.Ext(name) 736 mimetype := filetype.ByExtension(ext) 737 return ExtractMimeAndClass(mimetype) 738 } 739 740 var cbDiskQuotaAlert func(domain string, exceeded bool) 741 742 // RegisterDiskQuotaAlertCallback allows to register a callback function called 743 // when the instance reaches, a fall behind, 90% of its quota capacity. 744 func RegisterDiskQuotaAlertCallback(cb func(domain string, exceeded bool)) { 745 cbDiskQuotaAlert = cb 746 } 747 748 // PushDiskQuotaAlert can be used to notify when the VFS reaches, or fall 749 // behind, its quota alert of 90% of its total capacity. 750 func PushDiskQuotaAlert(fs VFS, exceeded bool) { 751 if cbDiskQuotaAlert != nil { 752 cbDiskQuotaAlert(fs.DomainName(), exceeded) 753 } 754 } 755 756 // DiskQuotaAfterDestroy is a helper function that can be used after files or 757 // directories have be erased from the disk in order to register that the disk 758 // quota alert has fall behind (or not). 759 func DiskQuotaAfterDestroy(fs VFS, diskUsageBeforeWrite, destroyed int64) { 760 if diskUsageBeforeWrite <= 0 { 761 return 762 } 763 diskQuota := fs.DiskQuota() 764 quotaBytes := int64(9.0 / 10.0 * float64(diskQuota)) 765 if diskUsageBeforeWrite >= quotaBytes && 766 diskUsageBeforeWrite-destroyed < quotaBytes { 767 PushDiskQuotaAlert(fs, false) 768 } 769 } 770 771 // getRestoreDir returns the restoration directory document from a file a 772 // directory path. The specified file path should be part of the trash 773 // directory. 774 func getRestoreDir(fs VFS, name, restorePath string) (*DirDoc, error) { 775 if !strings.HasPrefix(name, TrashDirName) { 776 return nil, ErrFileNotInTrash 777 } 778 779 // If the restore path is not set, it means that the file is part of a 780 // directory hierarchy which has been trashed. The parent directory at the 781 // root of the trash directory is the document which contains the information 782 // of the restore path. 783 // 784 // For instance, when trying the restore the baz file inside 785 // TrashDirName/foo/bar/baz/quz, it should extract the "foo" (root) and 786 // "bar/baz" (rest) parts of the path. 787 if restorePath == "" { 788 name = strings.TrimPrefix(name, TrashDirName+"/") 789 split := strings.Index(name, "/") 790 if split >= 0 { 791 root := name[:split] 792 rest := path.Dir(name[split+1:]) 793 doc, err := fs.DirByPath(TrashDirName + "/" + root) 794 if err != nil { 795 return nil, err 796 } 797 if doc.RestorePath != "" { 798 restorePath = path.Join(doc.RestorePath, doc.DocName, rest) 799 } 800 } 801 } 802 803 // This should not happened but is here in case we could not resolve the 804 // restore path 805 if restorePath == "" { 806 restorePath = "/" 807 } 808 809 // If the restore directory does not exist anymore, we re-create the 810 // directory hierarchy to restore the file in. 811 restoreDir, err := fs.DirByPath(restorePath) 812 if os.IsNotExist(err) { 813 return MkdirAll(fs, restorePath) 814 } 815 return restoreDir, err 816 } 817 818 func normalizeDocPatch(data, patch *DocPatch, cdate time.Time) (*DocPatch, error) { 819 if patch.DirID == nil { 820 patch.DirID = data.DirID 821 } 822 823 if patch.RestorePath == nil { 824 patch.RestorePath = data.RestorePath 825 } 826 827 if patch.Name == nil { 828 patch.Name = data.Name 829 } 830 831 if patch.Tags == nil { 832 patch.Tags = data.Tags 833 } 834 835 if patch.UpdatedAt == nil || patch.UpdatedAt.Unix() < 0 { 836 patch.UpdatedAt = data.UpdatedAt 837 } 838 839 if patch.UpdatedAt.Before(cdate) { 840 return nil, ErrIllegalTime 841 } 842 843 if patch.Executable == nil { 844 patch.Executable = data.Executable 845 } 846 847 if patch.Encrypted == nil { 848 patch.Encrypted = data.Encrypted 849 } 850 851 if patch.CozyMetadata.Favorite == nil { 852 patch.CozyMetadata.Favorite = data.CozyMetadata.Favorite 853 } 854 855 return patch, nil 856 } 857 858 func checkFileName(str string) error { 859 if str == "" || str == "." || str == ".." || strings.ContainsAny(str, ForbiddenFilenameChars) { 860 return ErrIllegalFilename 861 } 862 return nil 863 } 864 865 func checkDepth(fullpath string) error { 866 depth := strings.Count(fullpath, "/") 867 if depth >= MaxDepth { 868 return ErrIllegalPath 869 } 870 return nil 871 } 872 873 func uniqueTags(tags []string) []string { 874 m := make(map[string]struct{}) 875 clone := make([]string, 0) 876 for _, tag := range tags { 877 tag = strings.TrimSpace(tag) 878 if tag == "" { 879 continue 880 } 881 if _, ok := m[tag]; !ok { 882 clone = append(clone, tag) 883 m[tag] = struct{}{} 884 } 885 } 886 return clone 887 } 888 889 // OptionsAllowCreationInTrash returns true if one of the given option says so. 890 func OptionsAllowCreationInTrash(opts []CreateOptions) bool { 891 for _, opt := range opts { 892 if opt == AllowCreationInTrash { 893 return true 894 } 895 } 896 return false 897 } 898 899 func CreateFileDocCopy(doc *FileDoc, newDirID, copyName string) *FileDoc { 900 newdoc := doc.Clone().(*FileDoc) 901 newdoc.DocID = "" 902 newdoc.DocRev = "" 903 if newDirID != "" { 904 newdoc.DirID = newDirID 905 } 906 if copyName != "" { 907 newdoc.DocName = copyName 908 mime, class := ExtractMimeAndClassFromFilename(copyName) 909 newdoc.Mime = mime 910 newdoc.Class = class 911 } 912 newdoc.CozyMetadata = nil 913 newdoc.InternalID = "" 914 newdoc.CreatedAt = time.Now() 915 newdoc.UpdatedAt = newdoc.CreatedAt 916 newdoc.RemoveReferencedBy() 917 newdoc.ResetFullpath() 918 newdoc.Metadata.RemoveCertifiedMetadata() 919 920 return newdoc 921 } 922 923 func CheckAvailableDiskSpace(fs VFS, doc *FileDoc) (newsize, maxsize, capsize int64, err error) { 924 newsize = doc.ByteSize 925 926 maxsize = fs.MaxFileSize() 927 if maxsize > 0 && newsize > maxsize { 928 return 0, 0, 0, ErrMaxFileSize 929 } 930 931 diskQuota := fs.DiskQuota() 932 if diskQuota > 0 { 933 diskUsage, err := fs.DiskUsage() 934 if err != nil { 935 return 0, 0, 0, err 936 } 937 maxsize = diskQuota - diskUsage 938 if newsize > maxsize { 939 return 0, 0, 0, ErrFileTooBig 940 } 941 if quotaBytes := int64(9.0 / 10.0 * float64(diskQuota)); diskUsage <= quotaBytes { 942 capsize = quotaBytes - diskUsage 943 } 944 } 945 946 return newsize, maxsize, capsize, nil 947 } 948 949 // ConflictName generates a new name for a file/folder in conflict with another 950 // that has the same path. A conflicted file `foo` will be renamed foo (2), 951 // then foo (3), etc. 952 func ConflictName(fs VFS, dirID, name string, isFile bool) string { 953 base, ext := name, "" 954 if isFile { 955 ext = filepath.Ext(name) 956 base = strings.TrimSuffix(base, ext) 957 } 958 i := 2 959 if strings.HasSuffix(base, ")") { 960 if idx := strings.LastIndex(base, " ("); idx > 0 { 961 num, err := strconv.Atoi(base[idx+2 : len(base)-1]) 962 if err == nil { 963 i = num + 1 964 base = base[0:idx] 965 } 966 } 967 } 968 969 indexer := fs.GetIndexer() 970 for ; i < 1000; i++ { 971 newname := fmt.Sprintf("%s (%d)%s", base, i, ext) 972 exists, err := indexer.DirChildExists(dirID, newname) 973 if err != nil || !exists { 974 return newname 975 } 976 } 977 return fmt.Sprintf("%s (%d)%s", base, i, ext) 978 }