github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/vfs/couchdb_indexer.go (about)

     1  package vfs
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/cozy/cozy-stack/pkg/consts"
    12  	"github.com/cozy/cozy-stack/pkg/couchdb"
    13  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    14  	"github.com/cozy/cozy-stack/pkg/logger"
    15  	"github.com/cozy/cozy-stack/pkg/prefixer"
    16  )
    17  
    18  type couchdbIndexer struct {
    19  	db prefixer.Prefixer
    20  }
    21  
    22  // NewCouchdbIndexer creates an Indexer instance based on couchdb to store
    23  // files and directories metadata and index them.
    24  func NewCouchdbIndexer(db prefixer.Prefixer) Indexer {
    25  	return &couchdbIndexer{
    26  		db: db,
    27  	}
    28  }
    29  
    30  func (c *couchdbIndexer) InitIndex() error {
    31  	createDate := time.Now()
    32  	err := couchdb.CreateNamedDocWithDB(c.db, &DirDoc{
    33  		DocName:   "",
    34  		Type:      consts.DirType,
    35  		DocID:     consts.RootDirID,
    36  		Fullpath:  "/",
    37  		DirID:     "",
    38  		CreatedAt: createDate,
    39  		UpdatedAt: createDate,
    40  	})
    41  	if err != nil {
    42  		return err
    43  	}
    44  
    45  	err = couchdb.CreateNamedDocWithDB(c.db, &DirDoc{
    46  		DocName:   path.Base(TrashDirName),
    47  		Type:      consts.DirType,
    48  		DocID:     consts.TrashDirID,
    49  		Fullpath:  TrashDirName,
    50  		DirID:     consts.RootDirID,
    51  		CreatedAt: createDate,
    52  		UpdatedAt: createDate,
    53  	})
    54  	if err != nil && !couchdb.IsConflictError(err) {
    55  		return err
    56  	}
    57  	return nil
    58  }
    59  
    60  // DiskUsage returns the total size of the files (current + old versions).
    61  func (c *couchdbIndexer) DiskUsage() (int64, error) {
    62  	used, err := c.FilesUsage()
    63  	if err != nil {
    64  		return 0, err
    65  	}
    66  
    67  	if versions, err := c.VersionsUsage(); err == nil {
    68  		used += versions
    69  	}
    70  
    71  	return used, nil
    72  }
    73  
    74  // FilesUsage returns the files total size (without versions)
    75  func (c *couchdbIndexer) FilesUsage() (int64, error) {
    76  	var doc couchdb.ViewResponse
    77  	err := couchdb.ExecView(c.db, couchdb.DiskUsageView, &couchdb.ViewRequest{
    78  		Reduce: true,
    79  	}, &doc)
    80  	if err != nil {
    81  		return 0, err
    82  	}
    83  	if len(doc.Rows) == 0 {
    84  		return 0, nil
    85  	}
    86  	// Reduce of _sum should give us a number value
    87  	used, ok := doc.Rows[0].Value.(float64)
    88  	if !ok {
    89  		return 0, ErrWrongCouchdbState
    90  	}
    91  
    92  	return int64(used), nil
    93  }
    94  
    95  // VersionsUsage returns the total size for old file versions (not including
    96  // current version).
    97  func (c *couchdbIndexer) VersionsUsage() (int64, error) {
    98  	var doc couchdb.ViewResponse
    99  	err := couchdb.ExecView(c.db, couchdb.OldVersionsDiskUsageView, &couchdb.ViewRequest{
   100  		Reduce: true,
   101  	}, &doc)
   102  	if err != nil {
   103  		return 0, err
   104  	}
   105  	if len(doc.Rows) == 0 {
   106  		return 0, nil
   107  	}
   108  	// Reduce of _sum should give us a number value
   109  	used, ok := doc.Rows[0].Value.(float64)
   110  	if !ok {
   111  		return 0, ErrWrongCouchdbState
   112  	}
   113  
   114  	return int64(used), nil
   115  }
   116  
   117  // TrashUsage returns the space taken by the files in the trash.
   118  func (c *couchdbIndexer) TrashUsage() (int64, error) {
   119  	var size int64
   120  	trash, err := c.DirByID(consts.TrashDirID)
   121  	if err != nil {
   122  		return 0, err
   123  	}
   124  	err = walk(c, trash.Name(), trash, nil, func(_ string, _ *DirDoc, file *FileDoc, err error) error {
   125  		if file != nil {
   126  			size += file.ByteSize
   127  		}
   128  		return err
   129  	}, 0)
   130  	return size, err
   131  }
   132  
   133  func (c *couchdbIndexer) prepareFileDoc(doc *FileDoc) error {
   134  	// Ensure that fullpath is filled because it's used in realtime/@events
   135  	if _, err := doc.Path(c); err != nil {
   136  		return err
   137  	}
   138  	// If a valid datetime is extracted from the EXIF metadata, use it as the
   139  	// created_at of the file. By valid, we mean that we filter out photos
   140  	// taken on camera were the clock was never configured (e.g. 1970-01-01).
   141  	if date, ok := doc.Metadata["datetime"].(time.Time); ok && date.Year() > 1990 {
   142  		doc.CreatedAt = date
   143  		if doc.UpdatedAt.Before(date) {
   144  			doc.UpdatedAt = date
   145  		}
   146  	}
   147  	return nil
   148  }
   149  
   150  func (c *couchdbIndexer) DirSize(doc *DirDoc) (int64, error) {
   151  	start := doc.Fullpath + "/"
   152  	stop := doc.Fullpath + "/\ufff0"
   153  	if doc.DocID == consts.RootDirID {
   154  		start = "/"
   155  		stop = "/\ufff0"
   156  	}
   157  
   158  	// Find the subdirectories
   159  	sel := mango.And(
   160  		mango.Gt("path", start),
   161  		mango.Lt("path", stop),
   162  		mango.Equal("type", consts.DirType),
   163  	)
   164  	req := &couchdb.FindRequest{
   165  		UseIndex: "dir-by-path",
   166  		Selector: sel,
   167  		Fields:   []string{"_id"},
   168  		Limit:    10000,
   169  	}
   170  	var children []couchdb.JSONDoc
   171  	err := couchdb.FindDocs(c.db, consts.Files, req, &children)
   172  	if err != nil {
   173  		return 0, err
   174  	}
   175  	keys := make([]interface{}, len(children)+1)
   176  	keys[0] = doc.DocID
   177  	for i, child := range children {
   178  		keys[i+1] = child.ID()
   179  	}
   180  
   181  	// Get the size for the directory and each of its sub-directory, and sum them
   182  	var resp couchdb.ViewResponse
   183  	err = couchdb.ExecView(c.db, couchdb.DiskUsageView, &couchdb.ViewRequest{
   184  		Keys:   keys,
   185  		Group:  true,
   186  		Reduce: true,
   187  	}, &resp)
   188  	if err != nil {
   189  		return 0, err
   190  	}
   191  	if len(resp.Rows) == 0 {
   192  		return 0, nil
   193  	}
   194  
   195  	// Reduce of _sum should give us a number value
   196  	var size int64
   197  	for _, row := range resp.Rows {
   198  		value, ok := row.Value.(float64)
   199  		if !ok {
   200  			return 0, ErrWrongCouchdbState
   201  		}
   202  		size += int64(value)
   203  	}
   204  	return size, nil
   205  }
   206  
   207  func (c *couchdbIndexer) CreateFileDoc(doc *FileDoc) error {
   208  	if err := c.prepareFileDoc(doc); err != nil {
   209  		return err
   210  	}
   211  	return couchdb.CreateDoc(c.db, doc)
   212  }
   213  
   214  func (c *couchdbIndexer) CreateNamedFileDoc(doc *FileDoc) error {
   215  	if err := c.prepareFileDoc(doc); err != nil {
   216  		return err
   217  	}
   218  	return couchdb.CreateNamedDoc(c.db, doc)
   219  }
   220  
   221  func (c *couchdbIndexer) UpdateFileDoc(olddoc, newdoc *FileDoc) error {
   222  	if err := c.prepareFileDoc(newdoc); err != nil {
   223  		return err
   224  	}
   225  
   226  	fullpath, err := olddoc.Path(c)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	if strings.HasPrefix(fullpath, TrashDirName) {
   231  		c.checkTrashedFileIsShared(newdoc)
   232  	}
   233  
   234  	newdoc.SetID(olddoc.ID())
   235  	newdoc.SetRev(olddoc.Rev())
   236  	return couchdb.UpdateDocWithOld(c.db, newdoc, olddoc)
   237  }
   238  
   239  var DeleteNote = func(db prefixer.Prefixer, noteID string) {}
   240  
   241  func (c *couchdbIndexer) DeleteFileDoc(doc *FileDoc) error {
   242  	// Ensure that fullpath is filled because it's used in realtime/@events
   243  	if _, err := doc.Path(c); err != nil {
   244  		return err
   245  	}
   246  	if doc.Mime == consts.NoteMimeType {
   247  		DeleteNote(c.db, doc.DocID)
   248  	}
   249  	return couchdb.DeleteDoc(c.db, doc)
   250  }
   251  
   252  func (c *couchdbIndexer) CreateDirDoc(doc *DirDoc) error {
   253  	return couchdb.CreateDoc(c.db, doc)
   254  }
   255  
   256  func (c *couchdbIndexer) CreateNamedDirDoc(doc *DirDoc) error {
   257  	return couchdb.CreateNamedDoc(c.db, doc)
   258  }
   259  
   260  func (c *couchdbIndexer) UpdateDirDoc(olddoc, newdoc *DirDoc) error {
   261  	newdoc.SetID(olddoc.ID())
   262  	newdoc.SetRev(olddoc.Rev())
   263  
   264  	oldTrashed := strings.HasPrefix(olddoc.Fullpath, TrashDirName)
   265  	newTrashed := strings.HasPrefix(newdoc.Fullpath, TrashDirName)
   266  
   267  	isRestored := oldTrashed && !newTrashed
   268  	isTrashed := !oldTrashed && newTrashed
   269  
   270  	if isTrashed {
   271  		c.checkTrashedDirIsShared(newdoc)
   272  		if err := c.setTrashedForFilesInsideDir(olddoc, true); err != nil {
   273  			return err
   274  		}
   275  	}
   276  
   277  	if newdoc.Fullpath != olddoc.Fullpath {
   278  		if err := c.MoveDir(olddoc.Fullpath, newdoc.Fullpath); err != nil {
   279  			return err
   280  		}
   281  	}
   282  
   283  	if err := couchdb.UpdateDocWithOld(c.db, newdoc, olddoc); err != nil {
   284  		return err
   285  	}
   286  
   287  	if isRestored {
   288  		if err := c.setTrashedForFilesInsideDir(newdoc, false); err != nil {
   289  			return err
   290  		}
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  func (c *couchdbIndexer) DeleteDirDoc(doc *DirDoc) error {
   297  	return couchdb.DeleteDoc(c.db, doc)
   298  }
   299  
   300  func (c *couchdbIndexer) DeleteDirDocAndContent(doc *DirDoc, onlyContent bool) (files []*FileDoc, n int64, err error) {
   301  	var docs []couchdb.Doc
   302  	if !onlyContent {
   303  		docs = append(docs, doc)
   304  	}
   305  	err = walk(c, doc.Name(), doc, nil, func(name string, dir *DirDoc, file *FileDoc, err error) error {
   306  		if err != nil {
   307  			return err
   308  		}
   309  		if dir != nil {
   310  			if dir.ID() == doc.ID() {
   311  				return nil
   312  			}
   313  			docs = append(docs, dir.Clone())
   314  		} else {
   315  			cloned := file.Clone()
   316  			docs = append(docs, cloned)
   317  			files = append(files, cloned.(*FileDoc))
   318  			n += file.ByteSize
   319  			if file.Mime == consts.NoteMimeType {
   320  				DeleteNote(c.db, file.DocID)
   321  			}
   322  		}
   323  		return err
   324  	}, 0)
   325  	if err == nil {
   326  		err = c.BatchDelete(docs)
   327  	}
   328  	return
   329  }
   330  
   331  func (c *couchdbIndexer) BatchDelete(docs []couchdb.Doc) error {
   332  	remaining := docs
   333  	for len(remaining) > 0 {
   334  		n := 1000
   335  		if len(remaining) < n {
   336  			n = len(remaining)
   337  		}
   338  		toDelete := remaining[:n]
   339  		remaining = remaining[n:]
   340  		if err := couchdb.BulkDeleteDocs(c.db, consts.Files, toDelete); err != nil {
   341  			// If it fails once, try again
   342  			time.Sleep(1 * time.Second)
   343  			if err := couchdb.BulkDeleteDocs(c.db, consts.Files, toDelete); err != nil {
   344  				return err
   345  			}
   346  		}
   347  	}
   348  	return nil
   349  }
   350  
   351  func (c *couchdbIndexer) MoveDir(oldpath, newpath string) error {
   352  	if oldpath+"/" == newpath {
   353  		return nil
   354  	}
   355  
   356  	limit := 256
   357  	var children []*DirDoc
   358  	docs := make([]interface{}, 0, limit)
   359  	olddocs := make([]interface{}, 0, limit)
   360  	isTrashed := strings.HasPrefix(newpath, TrashDirName)
   361  
   362  	// We limit the stack to 128 bulk updates to avoid infinite loops, as we
   363  	// had a case in the past.
   364  	start := oldpath + "/"
   365  	stop := oldpath + "/\ufff0"
   366  	for i := 0; i < 128; i++ {
   367  		// The simple selector mango.StartWith can have some issues when
   368  		// renaming a folder to the same name, but with a different unicode
   369  		// normalization (like NFC to NFD). In that case, CouchDB would always
   370  		// return the same documents with this selector, as it does the
   371  		// comparison on a normalized string.
   372  		sel := mango.And(
   373  			mango.Gt("path", start),
   374  			mango.Lt("path", stop),
   375  		)
   376  		req := &couchdb.FindRequest{
   377  			UseIndex: "dir-by-path",
   378  			Selector: sel,
   379  			Skip:     0,
   380  			Limit:    limit,
   381  		}
   382  		err := couchdb.FindDocs(c.db, consts.Files, req, &children)
   383  		if err != nil {
   384  			return err
   385  		}
   386  		if len(children) == 0 {
   387  			break
   388  		}
   389  		start = children[len(children)-1].Fullpath
   390  		for _, child := range children {
   391  			// XXX In theory, only the directories have a path, but in
   392  			// practice, it's safer to check it as we had already some bugs in
   393  			// the VFS.
   394  			if child.Type != consts.DirType {
   395  				continue
   396  			}
   397  			// XXX We can have documents that are not a child of the moved dir
   398  			// because of the comparison of strings used by CouchDB:
   399  			// /Photos/ < /PHOTOS/AAA < /Photos/bbb < /Photos0
   400  			// So, we need to skip the documents that are not really the children.
   401  			// Cf http://docs.couchdb.org/en/stable/ddocs/views/collation.html#collation-specification
   402  			if !strings.HasPrefix(child.Fullpath, oldpath) {
   403  				continue
   404  			}
   405  			cloned := child.Clone()
   406  			if isTrashed {
   407  				c.checkTrashedDirIsShared(child)
   408  			}
   409  			olddocs = append(olddocs, cloned)
   410  			child.Fullpath = path.Join(newpath, child.Fullpath[len(oldpath)+1:])
   411  			docs = append(docs, child)
   412  		}
   413  		if err = couchdb.BulkUpdateDocs(c.db, consts.Files, docs, olddocs); err != nil {
   414  			return err
   415  		}
   416  		if len(children) < limit {
   417  			break
   418  		}
   419  		children = children[:0]
   420  		docs = docs[:0]
   421  		olddocs = olddocs[:0]
   422  	}
   423  
   424  	return nil
   425  }
   426  
   427  func (c *couchdbIndexer) DirByID(fileID string) (*DirDoc, error) {
   428  	doc := &DirDoc{}
   429  	err := couchdb.GetDoc(c.db, consts.Files, fileID, doc)
   430  	if couchdb.IsNotFoundError(err) {
   431  		return nil, os.ErrNotExist
   432  	}
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	if doc.Type != consts.DirType {
   437  		return nil, os.ErrNotExist
   438  	}
   439  	return doc, nil
   440  }
   441  
   442  func (c *couchdbIndexer) DirByPath(name string) (*DirDoc, error) {
   443  	if !path.IsAbs(name) {
   444  		return nil, ErrNonAbsolutePath
   445  	}
   446  	var docs []*DirDoc
   447  	sel := mango.Equal("path", path.Clean(name))
   448  	req := &couchdb.FindRequest{
   449  		UseIndex: "dir-by-path",
   450  		Selector: sel,
   451  		Limit:    1,
   452  	}
   453  	err := couchdb.FindDocs(c.db, consts.Files, req, &docs)
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	if len(docs) == 0 {
   458  		return nil, os.ErrNotExist
   459  	}
   460  	if docs[0].Type != consts.DirType {
   461  		return nil, os.ErrNotExist
   462  	}
   463  	return docs[0], nil
   464  }
   465  
   466  func (c *couchdbIndexer) FileByID(fileID string) (*FileDoc, error) {
   467  	doc := &FileDoc{}
   468  	err := couchdb.GetDoc(c.db, consts.Files, fileID, doc)
   469  	if couchdb.IsNotFoundError(err) {
   470  		return nil, os.ErrNotExist
   471  	}
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  	if doc.Type != consts.FileType {
   476  		return nil, os.ErrNotExist
   477  	}
   478  	return doc, nil
   479  }
   480  
   481  func (c *couchdbIndexer) FileByPath(name string) (*FileDoc, error) {
   482  	if !path.IsAbs(name) {
   483  		return nil, ErrNonAbsolutePath
   484  	}
   485  	parent, err := c.DirByPath(path.Dir(name))
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	// consts.FilesByParentView keys are [parentID, type, name]
   491  	var res couchdb.ViewResponse
   492  	err = couchdb.ExecView(c.db, couchdb.FilesByParentView, &couchdb.ViewRequest{
   493  		Key:         []string{parent.DocID, consts.FileType, path.Base(name)},
   494  		IncludeDocs: true,
   495  	}, &res)
   496  	if err != nil {
   497  		return nil, err
   498  	}
   499  
   500  	// XXX CouchDB can return documents with the same name but in a different
   501  	// encoding (NFC vs NFD), so we need to check that the name is really the
   502  	// expected one.
   503  	for _, row := range res.Rows {
   504  		var doc FileDoc
   505  		if err = json.Unmarshal(row.Doc, &doc); err != nil {
   506  			return nil, err
   507  		}
   508  		if doc.DocName == path.Base(name) {
   509  			return &doc, nil
   510  		}
   511  	}
   512  	return nil, os.ErrNotExist
   513  }
   514  
   515  func (c *couchdbIndexer) FilePath(doc *FileDoc) (string, error) {
   516  	var parentPath string
   517  	if doc.DirID == consts.RootDirID {
   518  		parentPath = "/"
   519  	} else if doc.DirID == consts.TrashDirID {
   520  		parentPath = TrashDirName
   521  	} else {
   522  		parent, err := c.DirByID(doc.DirID)
   523  		if err != nil {
   524  			return "", ErrParentDoesNotExist
   525  		}
   526  		parentPath = parent.Fullpath
   527  	}
   528  	return path.Join(parentPath, doc.DocName), nil
   529  }
   530  
   531  func (c *couchdbIndexer) DirOrFileByID(fileID string) (*DirDoc, *FileDoc, error) {
   532  	dirOrFile := &DirOrFileDoc{}
   533  	err := couchdb.GetDoc(c.db, consts.Files, fileID, dirOrFile)
   534  	if err != nil {
   535  		if couchdb.IsNotFoundError(err) {
   536  			err = os.ErrNotExist
   537  		}
   538  		return nil, nil, err
   539  	}
   540  	dirDoc, fileDoc := dirOrFile.Refine()
   541  	return dirDoc, fileDoc, nil
   542  }
   543  
   544  func (c *couchdbIndexer) DirOrFileByPath(name string) (*DirDoc, *FileDoc, error) {
   545  	dirDoc, err := c.DirByPath(name)
   546  	if err == nil {
   547  		return dirDoc, nil, nil
   548  	}
   549  	if !os.IsNotExist(err) {
   550  		return nil, nil, err
   551  	}
   552  	fileDoc, err := c.FileByPath(name)
   553  	if err == nil {
   554  		return nil, fileDoc, nil
   555  	}
   556  	return nil, nil, err
   557  }
   558  
   559  func (c *couchdbIndexer) DirIterator(doc *DirDoc, opts *IteratorOptions) DirIterator {
   560  	return NewIterator(c.db, doc, opts)
   561  }
   562  
   563  func (c *couchdbIndexer) DirBatch(doc *DirDoc, cursor couchdb.Cursor) ([]DirOrFileDoc, error) {
   564  	// consts.FilesByParentView keys are [parentID, type, name]
   565  	req := couchdb.ViewRequest{
   566  		StartKey:    []string{doc.DocID, ""},
   567  		EndKey:      []string{doc.DocID, couchdb.MaxString},
   568  		IncludeDocs: true,
   569  	}
   570  	var res couchdb.ViewResponse
   571  	cursor.ApplyTo(&req)
   572  	err := couchdb.ExecView(c.db, couchdb.FilesByParentView, &req, &res)
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  	cursor.UpdateFrom(&res)
   577  
   578  	docs := make([]DirOrFileDoc, len(res.Rows))
   579  	for i, row := range res.Rows {
   580  		var doc DirOrFileDoc
   581  		err := json.Unmarshal(row.Doc, &doc)
   582  		if err != nil {
   583  			return nil, err
   584  		}
   585  		docs[i] = doc
   586  	}
   587  
   588  	return docs, nil
   589  }
   590  
   591  func (c *couchdbIndexer) DirLength(doc *DirDoc) (int, error) {
   592  	req := couchdb.ViewRequest{
   593  		StartKey:   []string{doc.DocID, ""},
   594  		EndKey:     []string{doc.DocID, couchdb.MaxString},
   595  		Reduce:     true,
   596  		GroupLevel: 1,
   597  	}
   598  	var res couchdb.ViewResponse
   599  	err := couchdb.ExecView(c.db, couchdb.FilesByParentView, &req, &res)
   600  	if err != nil {
   601  		return 0, err
   602  	}
   603  
   604  	if len(res.Rows) == 0 {
   605  		return 0, nil
   606  	}
   607  
   608  	// Reduce of _count should give us a number value
   609  	f64, ok := res.Rows[0].Value.(float64)
   610  	if !ok {
   611  		return 0, ErrWrongCouchdbState
   612  	}
   613  	return int(f64), nil
   614  }
   615  
   616  func (c *couchdbIndexer) DirChildExists(dirID, name string) (bool, error) {
   617  	var res couchdb.ViewResponse
   618  
   619  	// consts.FilesByParentView keys are [parentID, type, name]
   620  	err := couchdb.ExecView(c.db, couchdb.FilesByParentView, &couchdb.ViewRequest{
   621  		Keys: []interface{}{
   622  			[]string{dirID, consts.FileType, name},
   623  			[]string{dirID, consts.DirType, name},
   624  		},
   625  		IncludeDocs: true,
   626  	}, &res)
   627  	if err != nil {
   628  		return false, err
   629  	}
   630  
   631  	// XXX CouchDB can return documents with the same name but in a different
   632  	// encoding (NFC vs NFD), so we need to check that the name is really the
   633  	// expected one.
   634  	for _, row := range res.Rows {
   635  		var doc DirOrFileDoc
   636  		if err = json.Unmarshal(row.Doc, &doc); err != nil {
   637  			return false, err
   638  		}
   639  		if doc.DirDoc == nil {
   640  			return false, ErrWrongCouchdbState
   641  		}
   642  		if doc.DocName == name {
   643  			return true, nil
   644  		}
   645  	}
   646  	return false, nil
   647  }
   648  
   649  func (c *couchdbIndexer) setTrashedForFilesInsideDir(doc *DirDoc, trashed bool) error {
   650  	var files, olddocs []interface{}
   651  	dirs := map[string]string{
   652  		doc.DocID: doc.Fullpath,
   653  	}
   654  	err := walk(c, doc.Fullpath, doc, nil, func(name string, dir *DirDoc, file *FileDoc, err error) error {
   655  		if dir != nil {
   656  			dirs[dir.DocID] = dir.Fullpath
   657  		}
   658  		if file != nil && file.Trashed != trashed {
   659  			// Fullpath is used by event triggers and should be pre-filled here
   660  			cloned := file.Clone().(*FileDoc)
   661  			parentPath, ok := dirs[file.DirID]
   662  			if !ok {
   663  				logger.WithDomain(c.db.DomainName()).WithNamespace("vfs").
   664  					Infof("setTrashedForFilesInsideDir: parent not found for %s", file.DocID)
   665  				return nil
   666  			}
   667  			fullpath := path.Join(parentPath, file.DocName)
   668  			fullpath = strings.TrimPrefix(fullpath, TrashDirName)
   669  			trashpath := strings.Replace(fullpath, doc.Fullpath, TrashDirName, 1)
   670  			if trashed {
   671  				c.checkTrashedFileIsShared(cloned)
   672  				cloned.fullpath = fullpath
   673  				file.fullpath = trashpath
   674  			} else {
   675  				cloned.fullpath = trashpath
   676  				file.fullpath = fullpath
   677  			}
   678  			file.Trashed = trashed
   679  			files = append(files, file)
   680  			olddocs = append(olddocs, cloned)
   681  		}
   682  		return err
   683  	}, 0)
   684  	if err != nil {
   685  		return err
   686  	}
   687  	return c.BatchUpdate(files, olddocs)
   688  }
   689  
   690  func (c *couchdbIndexer) BatchUpdate(docs, oldDocs []interface{}) error {
   691  	return couchdb.BulkUpdateDocs(c.db, consts.Files, docs, oldDocs)
   692  }
   693  
   694  func (c *couchdbIndexer) CreateVersion(v *Version) error {
   695  	return couchdb.CreateNamedDocWithDB(c.db, v)
   696  }
   697  
   698  func (c *couchdbIndexer) DeleteVersion(v *Version) error {
   699  	return couchdb.DeleteDoc(c.db, v)
   700  }
   701  
   702  func (c *couchdbIndexer) AllVersions() ([]*Version, error) {
   703  	var versions []*Version
   704  	err := couchdb.ForeachDocs(c.db, consts.FilesVersions, func(_id string, doc json.RawMessage) error {
   705  		var v Version
   706  		if err := json.Unmarshal(doc, &v); err != nil {
   707  			return err
   708  		}
   709  		versions = append(versions, &v)
   710  		return nil
   711  	})
   712  	return versions, err
   713  }
   714  
   715  func (c *couchdbIndexer) BatchDeleteVersions(versions []*Version) error {
   716  	remaining := make([]couchdb.Doc, len(versions))
   717  	for i, v := range versions {
   718  		remaining[i] = v
   719  	}
   720  	for len(remaining) > 0 {
   721  		n := 1000
   722  		if len(remaining) < n {
   723  			n = len(remaining)
   724  		}
   725  		toDelete := remaining[:n]
   726  		remaining = remaining[n:]
   727  		if err := couchdb.BulkDeleteDocs(c.db, consts.FilesVersions, toDelete); err != nil {
   728  			// If it fails once, try again
   729  			if err := couchdb.BulkDeleteDocs(c.db, consts.FilesVersions, toDelete); err != nil {
   730  				return err
   731  			}
   732  		}
   733  	}
   734  	return nil
   735  }
   736  
   737  func (c *couchdbIndexer) ListNotSynchronizedOn(clientID string) ([]DirDoc, error) {
   738  	req := couchdb.ViewRequest{
   739  		StartKey:    []string{consts.OAuthClients, clientID},
   740  		EndKey:      []string{consts.OAuthClients, clientID},
   741  		IncludeDocs: true,
   742  	}
   743  	var docs []DirDoc
   744  
   745  	cursor := couchdb.NewKeyCursor(100, nil, "")
   746  	for cursor.HasMore() {
   747  		cursor.ApplyTo(&req)
   748  		var res couchdb.ViewResponse
   749  		err := couchdb.ExecView(c.db, couchdb.DirNotSynchronizedOnView, &req, &res)
   750  		if err != nil {
   751  			return nil, err
   752  		}
   753  		cursor.UpdateFrom(&res)
   754  
   755  		for _, row := range res.Rows {
   756  			var doc DirDoc
   757  			err := json.Unmarshal(row.Doc, &doc)
   758  			if err != nil {
   759  				return nil, err
   760  			}
   761  			docs = append(docs, doc)
   762  		}
   763  	}
   764  
   765  	return docs, nil
   766  }
   767  
   768  // checkTrashedDirIsShared will look for a dir going to the trash if it was the
   769  // main dir of a sharing. If it is the case, the sharing is revoked and the
   770  // reference to the sharing is removed.
   771  func (c *couchdbIndexer) checkTrashedDirIsShared(doc *DirDoc) {
   772  	refs := doc.ReferencedBy[:0]
   773  	for _, ref := range doc.ReferencedBy {
   774  		if ref.Type == consts.Sharings {
   775  			c.revokeSharing(ref.ID)
   776  		} else {
   777  			refs = append(refs, ref)
   778  		}
   779  	}
   780  	doc.ReferencedBy = refs
   781  }
   782  
   783  // checkTrashedFileIsShared will look for a file going to the trash if it was
   784  // the main file of a sharing. If it is the case, the sharing is revoked and
   785  // the reference to the sharing is removed.
   786  func (c *couchdbIndexer) checkTrashedFileIsShared(doc *FileDoc) {
   787  	// A shortcut is created for a sharing not yet accepted if the owner of the
   788  	// sharing knows the Cozy URL of a recipient. Normally, this shortcut is
   789  	// removed when the sharing is accepted, but it is safer to avoid revoking
   790  	// the sharing is such a shortcut is deleted later.
   791  	if doc.Class == "shortcut" {
   792  		return
   793  	}
   794  
   795  	refs := doc.ReferencedBy[:0]
   796  	for _, ref := range doc.ReferencedBy {
   797  		if ref.Type == consts.Sharings {
   798  			c.revokeSharing(ref.ID)
   799  		} else {
   800  			refs = append(refs, ref)
   801  		}
   802  	}
   803  	doc.ReferencedBy = refs
   804  }
   805  
   806  // RevokeSharingFunc does nothing. It will will be overridden from the sharing
   807  // package.
   808  var RevokeSharingFunc = func(db prefixer.Prefixer, sharingID string) {
   809  	logger.WithNamespace("vfs").WithField("critical", "true").
   810  		Errorf("RevokeSharingFunc called without having been overridden!")
   811  }
   812  
   813  // revokeSharing is called when the main file/dir of a sharing is trashed, in
   814  // order to revoke the sharing.
   815  //
   816  // Note: we don't want to rely on triggers/jobs, as they are asynchronous and
   817  // if they are executed a bit late, the trashing of the files inside the shared
   818  // directory can be replicated to the other members before the sharing is
   819  // revoked.
   820  //
   821  // Technically, it isn't straightforward, as we are in the VFS and we can't
   822  // call functions in the sharing module (it would cause cyclic import error).
   823  // And, we don't have an instance, just a CouchDB indexer. So, it is a bit
   824  // hackish.
   825  func (c *couchdbIndexer) revokeSharing(sharingID string) {
   826  	RevokeSharingFunc(c.db, sharingID)
   827  }
   828  
   829  func (c *couchdbIndexer) CheckIndexIntegrity(accumulate func(*FsckLog), failFast bool) error {
   830  	tree, err := c.BuildTree()
   831  	if err != nil {
   832  		return err
   833  	}
   834  	if err := c.CheckTreeIntegrity(tree, accumulate, failFast); err != nil && !errors.Is(err, ErrFsckFailFast) {
   835  		return err
   836  	}
   837  	return nil
   838  }
   839  
   840  func (c *couchdbIndexer) CheckTreeIntegrity(tree *Tree, accumulate func(*FsckLog), failFast bool) error {
   841  	if err := c.checkNoConflicts(accumulate, failFast); err != nil {
   842  		if errors.Is(err, ErrFsckFailFast) {
   843  			return nil
   844  		}
   845  		return err
   846  	}
   847  
   848  	if tree.Root == nil {
   849  		accumulate(&FsckLog{Type: IndexMissingRoot})
   850  		return nil
   851  	}
   852  
   853  	if _, ok := tree.DirsMap[consts.TrashDirID]; !ok {
   854  		accumulate(&FsckLog{Type: IndexMissingTrash})
   855  		if failFast {
   856  			return nil
   857  		}
   858  	}
   859  
   860  	// cleanDirsMap browse the given root tree recursively into its children
   861  	// directories, removing them from the dirsmap table along the way. In the
   862  	// end, only trees with cycles should stay in the dirsmap.
   863  	if ok := cleanDirsMap(tree.Root, tree.DirsMap, accumulate, failFast); !ok {
   864  		return nil
   865  	}
   866  	for _, entries := range tree.Orphans {
   867  		for _, f := range entries {
   868  			if f.IsDir {
   869  				if ok := cleanDirsMap(f, tree.DirsMap, accumulate, failFast); !ok {
   870  					return nil
   871  				}
   872  			}
   873  		}
   874  	}
   875  
   876  	for _, orphanCycle := range tree.DirsMap {
   877  		orphanCycle.HasCycle = true
   878  		tree.Orphans[orphanCycle.DirID] = append(tree.Orphans[orphanCycle.DirID], orphanCycle)
   879  	}
   880  
   881  	for _, orphansTree := range tree.Orphans {
   882  		for _, orphan := range orphansTree {
   883  			if !orphan.IsDir {
   884  				accumulate(&FsckLog{
   885  					Type:    IndexOrphanTree,
   886  					IsFile:  true,
   887  					FileDoc: orphan,
   888  				})
   889  			} else {
   890  				accumulate(&FsckLog{
   891  					Type:   IndexOrphanTree,
   892  					IsFile: false,
   893  					DirDoc: orphan,
   894  				})
   895  			}
   896  			if failFast {
   897  				return nil
   898  			}
   899  		}
   900  	}
   901  
   902  	return couchdb.ForeachDocs(c.db, consts.FilesVersions, func(_ string, data json.RawMessage) error {
   903  		v := &Version{}
   904  		if erru := json.Unmarshal(data, v); erru != nil {
   905  			return erru
   906  		}
   907  		fileID := strings.SplitN(v.DocID, "/", 2)[0]
   908  		if _, ok := tree.Files[fileID]; !ok {
   909  			accumulate(&FsckLog{
   910  				Type:       FileMissing,
   911  				IsVersion:  true,
   912  				VersionDoc: v,
   913  			})
   914  		}
   915  		return nil
   916  	})
   917  }
   918  
   919  func (c *couchdbIndexer) BuildTree(eaches ...func(*TreeFile)) (t *Tree, err error) {
   920  	t = &Tree{
   921  		Root:    nil,
   922  		Orphans: make(map[string][]*TreeFile, 32), // DirID -> *FileDoc
   923  		DirsMap: make(map[string]*TreeFile, 256),  // DocID -> *FileDoc
   924  		Files:   make(map[string]struct{}, 1024),  // DocID -> ∅
   925  	}
   926  
   927  	// NOTE: the each method is called with objects in no particular order. The
   928  	// only enforcement is that either the Fullpath of the objet is informed or
   929  	// the IsOrphan flag is precised. It may be useful to gather along the way
   930  	// the files without having to browse the whole tree structure.
   931  	var each func(*TreeFile)
   932  	if len(eaches) > 0 {
   933  		each = eaches[0]
   934  	} else {
   935  		each = func(*TreeFile) {}
   936  	}
   937  
   938  	err = couchdb.ForeachDocs(c.db, consts.Files, func(_ string, data json.RawMessage) error {
   939  		var f *TreeFile
   940  		if erru := json.Unmarshal(data, &f); erru != nil {
   941  			return erru
   942  		}
   943  		f.IsDir = f.Type == consts.DirType
   944  		if f.DocID == consts.RootDirID {
   945  			t.Root = f
   946  			each(f)
   947  		} else if parent, ok := t.DirsMap[f.DirID]; ok {
   948  			if f.IsDir {
   949  				parent.DirsChildren = append(parent.DirsChildren, f)
   950  			} else {
   951  				parent.FilesChildren = append(parent.FilesChildren, f)
   952  				parent.FilesChildrenSize += f.ByteSize
   953  				if f.Fullpath == "" {
   954  					f.Fullpath = path.Join(parent.Fullpath, f.DocName)
   955  				} else {
   956  					f.HasPath = true
   957  				}
   958  			}
   959  			each(f)
   960  		} else {
   961  			t.Orphans[f.DirID] = append(t.Orphans[f.DirID], f)
   962  		}
   963  		if f.IsDir {
   964  			if bucket, ok := t.Orphans[f.DocID]; ok {
   965  				for _, child := range bucket {
   966  					if child.IsDir {
   967  						f.DirsChildren = append(f.DirsChildren, child)
   968  					} else {
   969  						f.FilesChildren = append(f.FilesChildren, child)
   970  						f.FilesChildrenSize += child.ByteSize
   971  						if child.Fullpath == "" {
   972  							child.Fullpath = path.Join(f.Fullpath, child.DocName)
   973  						} else {
   974  							child.HasPath = true
   975  						}
   976  					}
   977  					each(child)
   978  				}
   979  				delete(t.Orphans, f.DocID)
   980  			}
   981  			t.DirsMap[f.DocID] = f
   982  		} else {
   983  			t.Files[f.DocID] = struct{}{}
   984  		}
   985  		return nil
   986  	})
   987  	for _, bucket := range t.Orphans {
   988  		for _, child := range bucket {
   989  			child.IsOrphan = true
   990  			each(child)
   991  		}
   992  	}
   993  	return
   994  }
   995  
   996  func cleanDirsMap(
   997  	parent *TreeFile,
   998  	dirsmap map[string]*TreeFile,
   999  	accumulate func(*FsckLog),
  1000  	failFast bool,
  1001  ) bool {
  1002  	delete(dirsmap, parent.DocID)
  1003  	names := make(map[string]struct{})
  1004  	inTrash := strings.HasPrefix(parent.Fullpath, TrashDirName)
  1005  
  1006  	for _, file := range parent.FilesChildren {
  1007  		if inTrash != file.Trashed {
  1008  			if file.Trashed {
  1009  				accumulate(&FsckLog{
  1010  					Type:    TrashedNotInTrash,
  1011  					FileDoc: file,
  1012  				})
  1013  			} else {
  1014  				accumulate(&FsckLog{
  1015  					Type:    NotTrashedInTrash,
  1016  					FileDoc: file,
  1017  				})
  1018  			}
  1019  			if failFast {
  1020  				return false
  1021  			}
  1022  		}
  1023  
  1024  		if file.HasPath {
  1025  			accumulate(&FsckLog{
  1026  				Type:    IndexFileWithPath,
  1027  				FileDoc: file,
  1028  			})
  1029  			if failFast {
  1030  				return false
  1031  			}
  1032  		}
  1033  		if _, ok := names[file.DocName]; ok {
  1034  			accumulate(&FsckLog{
  1035  				Type:    IndexDuplicateName,
  1036  				FileDoc: file,
  1037  			})
  1038  			if failFast {
  1039  				return false
  1040  			}
  1041  		}
  1042  		names[file.DocName] = struct{}{}
  1043  	}
  1044  
  1045  	for _, child := range parent.DirsChildren {
  1046  		if _, ok := names[child.DocName]; ok {
  1047  			accumulate(&FsckLog{
  1048  				Type:   IndexDuplicateName,
  1049  				DirDoc: child,
  1050  			})
  1051  			if failFast {
  1052  				return false
  1053  			}
  1054  		}
  1055  		names[child.DocName] = struct{}{}
  1056  		expected := path.Join(parent.Fullpath, child.DocName)
  1057  		if expected != child.Fullpath {
  1058  			accumulate(&FsckLog{
  1059  				Type:             IndexBadFullpath,
  1060  				DirDoc:           child,
  1061  				ExpectedFullpath: expected,
  1062  			})
  1063  			if failFast {
  1064  				return false
  1065  			}
  1066  		}
  1067  		if ok := cleanDirsMap(child, dirsmap, accumulate, failFast); !ok {
  1068  			return false
  1069  		}
  1070  	}
  1071  	return true
  1072  }
  1073  
  1074  func (c *couchdbIndexer) checkNoConflicts(accumulate func(*FsckLog), failFast bool) error {
  1075  	var docs []DirOrFileDoc
  1076  	req := &couchdb.FindRequest{
  1077  		UseIndex:  "with-conflicts",
  1078  		Selector:  mango.Exists("_conflicts"),
  1079  		Limit:     1000,
  1080  		Conflicts: true,
  1081  	}
  1082  	_, err := couchdb.FindDocsRaw(c.db, consts.Files, req, &docs)
  1083  	if err != nil {
  1084  		return err
  1085  	}
  1086  	for _, doc := range docs {
  1087  		if doc.Type == consts.DirType {
  1088  			accumulate(&FsckLog{
  1089  				Type:   ConflictInIndex,
  1090  				DirDoc: &TreeFile{DirOrFileDoc: doc},
  1091  			})
  1092  		} else {
  1093  			accumulate(&FsckLog{
  1094  				Type:    ConflictInIndex,
  1095  				FileDoc: &TreeFile{DirOrFileDoc: doc},
  1096  				IsFile:  true,
  1097  			})
  1098  		}
  1099  		if failFast {
  1100  			return ErrFsckFailFast
  1101  		}
  1102  	}
  1103  	return nil
  1104  }