github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/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  }