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

     1  package sharing
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/cozy/cozy-stack/client/request"
    17  	"github.com/cozy/cozy-stack/model/instance"
    18  	"github.com/cozy/cozy-stack/model/note"
    19  	"github.com/cozy/cozy-stack/model/vfs"
    20  	"github.com/cozy/cozy-stack/pkg/consts"
    21  	"github.com/cozy/cozy-stack/pkg/couchdb"
    22  	"github.com/cozy/cozy-stack/pkg/crypto"
    23  	"github.com/cozy/cozy-stack/pkg/metadata"
    24  	multierror "github.com/hashicorp/go-multierror"
    25  )
    26  
    27  // isTrashed returns true for a file or folder inside the trash
    28  func isTrashed(doc couchdb.JSONDoc) bool {
    29  	if doc.Type != consts.Files {
    30  		return false
    31  	}
    32  	if doc.Get("type") == consts.FileType {
    33  		return doc.Get("trashed") == true
    34  	}
    35  	return strings.HasPrefix(doc.Get("path").(string), vfs.TrashDirName+"/")
    36  }
    37  
    38  // MakeXorKey generates a key for transforming the file identifiers
    39  func MakeXorKey() []byte {
    40  	random := crypto.GenerateRandomBytes(8)
    41  	result := make([]byte, 2*len(random))
    42  	for i, val := range random {
    43  		result[2*i] = val & 0xf
    44  		result[2*i+1] = val >> 4
    45  	}
    46  	return result
    47  }
    48  
    49  // XorID transforms the identifier of a file to a new identifier, in a
    50  // reversible way: it makes a XOR on the hexadecimal characters
    51  func XorID(id string, key []byte) string {
    52  	l := len(key)
    53  	buf := []byte(id)
    54  	for i, c := range buf {
    55  		switch {
    56  		case '0' <= c && c <= '9':
    57  			c = (c - '0') ^ key[i%l]
    58  		case 'a' <= c && c <= 'f':
    59  			c = (c - 'a' + 10) ^ key[i%l]
    60  		case 'A' <= c && c <= 'F':
    61  			c = (c - 'A' + 10) ^ key[i%l]
    62  		default:
    63  			continue
    64  		}
    65  		if c < 10 {
    66  			buf[i] = c + '0'
    67  		} else {
    68  			buf[i] = (c - 10) + 'a'
    69  		}
    70  	}
    71  	return string(buf)
    72  }
    73  
    74  // SortFilesToSent sorts the files slice that will be sent in bulk_docs:
    75  //   - directories must come before files (if a file is created in a new
    76  //     directory, we must create directory before the file)
    77  //   - directories are sorted by increasing depth (if a sub-folder is created
    78  //     in a new directory, we must create the parent before the child)
    79  //   - deleted elements must come at the end, to efficiently cope with moves.
    80  //     For example, if we have A->B->C hierarchy and C is moved elsewhere
    81  //     and B deleted, we must make the move before deleting B and its children.
    82  func (s *Sharing) SortFilesToSent(files []map[string]interface{}) {
    83  	sort.SliceStable(files, func(i, j int) bool {
    84  		a, b := files[i], files[j]
    85  		if removed, ok := a["_deleted"].(bool); ok && removed {
    86  			return false
    87  		}
    88  		if removed, ok := b["_deleted"].(bool); ok && removed {
    89  			return true
    90  		}
    91  		if a["type"] == consts.FileType {
    92  			return false
    93  		}
    94  		if b["type"] == consts.FileType {
    95  			return true
    96  		}
    97  		q, ok := b["path"].(string)
    98  		if !ok {
    99  			return false
   100  		}
   101  		p, ok := a["path"].(string)
   102  		if !ok {
   103  			return true
   104  		}
   105  		return strings.Count(p, "/") < strings.Count(q, "/")
   106  	})
   107  }
   108  
   109  // TransformFileToSent transforms an io.cozy.files document before sending it
   110  // to another cozy instance:
   111  // - its identifier is XORed
   112  // - its dir_id is XORed or removed
   113  // - the referenced_by are XORed or removed
   114  // - the path is removed (directory only)
   115  //
   116  // ruleIndexes is a map of "doctype-docid" -> rule index
   117  func (s *Sharing) TransformFileToSent(doc map[string]interface{}, xorKey []byte, ruleIndex int) {
   118  	if doc["type"] == consts.DirType {
   119  		delete(doc, "path")
   120  		delete(doc, "not_synchronized_on")
   121  	}
   122  	id := doc["_id"].(string)
   123  	doc["_id"] = XorID(id, xorKey)
   124  	dir, ok := doc["dir_id"].(string)
   125  	if !ok {
   126  		return
   127  	}
   128  	rule := s.Rules[ruleIndex]
   129  	var noDirID bool
   130  	if rule.Selector == couchdb.SelectorReferencedBy {
   131  		noDirID = true
   132  		if refs, ok := doc[couchdb.SelectorReferencedBy].([]interface{}); ok {
   133  			kept := make([]interface{}, 0)
   134  			for _, ref := range refs {
   135  				if r, ok := ref.(map[string]interface{}); ok {
   136  					v := r["type"].(string) + "/" + r["id"].(string)
   137  					for _, val := range rule.Values {
   138  						if val == v {
   139  							r["id"] = XorID(r["id"].(string), xorKey)
   140  							kept = append(kept, r)
   141  							break
   142  						}
   143  					}
   144  				}
   145  			}
   146  			doc[couchdb.SelectorReferencedBy] = kept
   147  		}
   148  	} else {
   149  		for _, v := range rule.Values {
   150  			if v == id {
   151  				noDirID = true
   152  			}
   153  		}
   154  		delete(doc, couchdb.SelectorReferencedBy)
   155  	}
   156  	if !noDirID {
   157  		for _, v := range rule.Values {
   158  			if v == dir {
   159  				noDirID = true
   160  				break
   161  			}
   162  		}
   163  	}
   164  	if noDirID {
   165  		delete(doc, "dir_id")
   166  	} else {
   167  		doc["dir_id"] = XorID(dir, xorKey)
   168  	}
   169  }
   170  
   171  // EnsureSharedWithMeDir returns the shared-with-me directory, and create it if
   172  // it doesn't exist
   173  func EnsureSharedWithMeDir(inst *instance.Instance) (*vfs.DirDoc, error) {
   174  	fs := inst.VFS()
   175  	dir, _, err := fs.DirOrFileByID(consts.SharedWithMeDirID)
   176  	if err != nil && !errors.Is(err, os.ErrNotExist) {
   177  		inst.Logger().WithNamespace("sharing").
   178  			Warnf("EnsureSharedWithMeDir failed to find the dir: %s", err)
   179  		return nil, err
   180  	}
   181  
   182  	if dir == nil {
   183  		name := inst.Translate("Tree Shared with me")
   184  		dir, err = vfs.NewDirDocWithPath(name, consts.RootDirID, "/", nil)
   185  		if err != nil {
   186  			inst.Logger().WithNamespace("sharing").
   187  				Warnf("EnsureSharedWithMeDir failed to make the dir: %s", err)
   188  			return nil, err
   189  		}
   190  		dir.DocID = consts.SharedWithMeDirID
   191  		dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
   192  		err = fs.CreateDir(dir)
   193  		if errors.Is(err, os.ErrExist) {
   194  			dir, err = fs.DirByPath(dir.Fullpath)
   195  		}
   196  		if err != nil {
   197  			inst.Logger().WithNamespace("sharing").
   198  				Warnf("EnsureSharedWithMeDir failed to create the dir: %s", err)
   199  			return nil, err
   200  		}
   201  		return dir, nil
   202  	}
   203  
   204  	if dir.RestorePath != "" {
   205  		now := time.Now()
   206  		instanceURL := inst.PageURL("/", nil)
   207  		if dir.CozyMetadata == nil {
   208  			dir.CozyMetadata = vfs.NewCozyMetadata(instanceURL)
   209  		} else {
   210  			dir.CozyMetadata.UpdatedAt = now
   211  		}
   212  		_, err = vfs.RestoreDir(fs, dir)
   213  		if err != nil {
   214  			inst.Logger().WithNamespace("sharing").
   215  				Warnf("EnsureSharedWithMeDir failed to restore the dir: %s", err)
   216  			return nil, err
   217  		}
   218  		children, err := fs.DirBatch(dir, couchdb.NewSkipCursor(0, 0))
   219  		if err != nil {
   220  			inst.Logger().WithNamespace("sharing").
   221  				Warnf("EnsureSharedWithMeDir failed to find children: %s", err)
   222  			return nil, err
   223  		}
   224  		for _, child := range children {
   225  			d, f := child.Refine()
   226  			if d != nil {
   227  				if d.CozyMetadata == nil {
   228  					d.CozyMetadata = vfs.NewCozyMetadata(instanceURL)
   229  				} else {
   230  					d.CozyMetadata.UpdatedAt = now
   231  				}
   232  				_, err = vfs.TrashDir(fs, d)
   233  			} else {
   234  				if f.CozyMetadata == nil {
   235  					f.CozyMetadata = vfs.NewCozyMetadata(instanceURL)
   236  				} else {
   237  					f.CozyMetadata.UpdatedAt = now
   238  				}
   239  				_, err = vfs.TrashFile(fs, f)
   240  			}
   241  			if err != nil {
   242  				inst.Logger().WithNamespace("sharing").
   243  					Warnf("EnsureSharedWithMeDir failed to trash children: %s", err)
   244  				return nil, err
   245  			}
   246  		}
   247  	}
   248  
   249  	return dir, nil
   250  }
   251  
   252  // CreateDirForSharing creates the directory where files for this sharing will
   253  // be put. This directory will be initially inside the Shared with me folder.
   254  func (s *Sharing) CreateDirForSharing(inst *instance.Instance, rule *Rule, parentID string) (*vfs.DirDoc, error) {
   255  	fs := inst.VFS()
   256  	var err error
   257  	var parent *vfs.DirDoc
   258  	if parentID == "" {
   259  		parent, err = EnsureSharedWithMeDir(inst)
   260  	} else {
   261  		parent, err = fs.DirByID(parentID)
   262  	}
   263  	if err != nil {
   264  		inst.Logger().WithNamespace("sharing").
   265  			Warnf("CreateDirForSharing failed to find parent directory: %s", err)
   266  		return nil, err
   267  	}
   268  	dir, err := vfs.NewDirDocWithParent(rule.Title, parent, []string{"from-sharing-" + s.SID})
   269  	if err != nil {
   270  		inst.Logger().WithNamespace("sharing").
   271  			Warnf("CreateDirForSharing failed to make dir: %s", err)
   272  		return nil, err
   273  	}
   274  	parts := strings.Split(rule.Values[0], "/")
   275  	dir.DocID = parts[len(parts)-1]
   276  	dir.AddReferencedBy(couchdb.DocReference{
   277  		ID:   s.SID,
   278  		Type: consts.Sharings,
   279  	})
   280  	dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
   281  	basename := dir.DocName
   282  	for i := 2; i < 20; i++ {
   283  		if err = fs.CreateDir(dir); err == nil {
   284  			return dir, nil
   285  		}
   286  		if couchdb.IsConflictError(err) || errors.Is(err, os.ErrExist) {
   287  			doc, err := fs.DirByID(dir.DocID)
   288  			if err == nil {
   289  				doc.AddReferencedBy(couchdb.DocReference{
   290  					ID:   s.SID,
   291  					Type: consts.Sharings,
   292  				})
   293  				_ = couchdb.UpdateDoc(inst, doc)
   294  				return doc, nil
   295  			}
   296  		}
   297  		dir.DocName = fmt.Sprintf("%s (%d)", basename, i)
   298  		dir.Fullpath = path.Join(parent.Fullpath, dir.DocName)
   299  	}
   300  	inst.Logger().WithNamespace("sharing").
   301  		Errorf("Cannot create the sharing directory: %s", err)
   302  	return nil, err
   303  }
   304  
   305  // AddReferenceForSharingDir adds a reference to the sharing on the sharing directory
   306  func (s *Sharing) AddReferenceForSharingDir(inst *instance.Instance, rule *Rule) error {
   307  	fs := inst.VFS()
   308  	parts := strings.Split(rule.Values[0], "/")
   309  	dir, _, err := fs.DirOrFileByID(parts[len(parts)-1])
   310  	if err != nil {
   311  		inst.Logger().WithNamespace("sharing").
   312  			Warnf("AddReferenceForSharingDir failed to find dir: %s", err)
   313  		return err
   314  	}
   315  	if dir == nil {
   316  		return nil
   317  	}
   318  	for _, ref := range dir.ReferencedBy {
   319  		if ref.Type == consts.Sharings && ref.ID == s.SID {
   320  			return nil
   321  		}
   322  	}
   323  	olddoc := dir.Clone().(*vfs.DirDoc)
   324  	dir.AddReferencedBy(couchdb.DocReference{
   325  		ID:   s.SID,
   326  		Type: consts.Sharings,
   327  	})
   328  	if dir.CozyMetadata == nil {
   329  		dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
   330  	} else {
   331  		dir.CozyMetadata.UpdatedAt = time.Now()
   332  	}
   333  	return fs.UpdateDirDoc(olddoc, dir)
   334  }
   335  
   336  // GetSharingDir returns the directory used by this sharing for putting files
   337  // and folders that have no dir_id.
   338  func (s *Sharing) GetSharingDir(inst *instance.Instance) (*vfs.DirDoc, error) {
   339  	// When we can, find the sharing dir by its ID
   340  	fs := inst.VFS()
   341  	rule := s.FirstFilesRule()
   342  	if rule != nil {
   343  		if rule.Mime != "" {
   344  			inst.Logger().WithNamespace("sharing").
   345  				Warnf("GetSharingDir called for only one file: %s", s.SID)
   346  			return nil, ErrInternalServerError
   347  		}
   348  		dir, _ := fs.DirByID(rule.Values[0])
   349  		if dir != nil {
   350  			return dir, nil
   351  		}
   352  	}
   353  
   354  	// Else, try to find it by a reference
   355  	key := []string{consts.Sharings, s.SID}
   356  	end := []string{key[0], key[1], couchdb.MaxString}
   357  	req := &couchdb.ViewRequest{
   358  		StartKey:    key,
   359  		EndKey:      end,
   360  		IncludeDocs: true,
   361  	}
   362  	var res couchdb.ViewResponse
   363  	err := couchdb.ExecView(inst, couchdb.FilesReferencedByView, req, &res)
   364  	if err != nil {
   365  		inst.Logger().WithNamespace("sharing").
   366  			Warnf("Sharing dir not found: %v (%s)", err, s.SID)
   367  		return nil, ErrInternalServerError
   368  	}
   369  	var parentID string
   370  	if len(res.Rows) > 0 {
   371  		dir, file, err := fs.DirOrFileByID(res.Rows[0].ID)
   372  		if err != nil {
   373  			inst.Logger().WithNamespace("sharing").
   374  				Warnf("GetSharingDir failed to find dir: %s", err)
   375  			return dir, err
   376  		}
   377  		if dir != nil {
   378  			return dir, nil
   379  		}
   380  		// file is a shortcut
   381  		parentID = file.DirID
   382  		if err := fs.DestroyFile(file); err != nil {
   383  			inst.Logger().WithNamespace("sharing").
   384  				Warnf("GetSharingDir failed to delete shortcut: %s", err)
   385  			return nil, err
   386  		}
   387  		s.ShortcutID = ""
   388  		_ = couchdb.UpdateDoc(inst, s)
   389  	}
   390  	if rule == nil {
   391  		inst.Logger().WithNamespace("sharing").
   392  			Errorf("no first rule for: %#v", s)
   393  		return nil, ErrInternalServerError
   394  	}
   395  	// And, we may have to create it in last resort
   396  	return s.CreateDirForSharing(inst, rule, parentID)
   397  }
   398  
   399  // RemoveSharingDir removes the reference on the sharing directory, and adds a
   400  // suffix to its name: the suffix will help make the user understand that the
   401  // sharing has been revoked, and it will avoid conflicts if the user accepts a
   402  // new sharing for the same folder. It should be called when a sharing is
   403  // revoked, on the recipient Cozy.
   404  func (s *Sharing) RemoveSharingDir(inst *instance.Instance) error {
   405  	dir, err := s.GetSharingDir(inst)
   406  	if couchdb.IsNotFoundError(err) {
   407  		return nil
   408  	} else if err != nil {
   409  		return err
   410  	}
   411  	olddoc := dir.Clone().(*vfs.DirDoc)
   412  	dir.RemoveReferencedBy(couchdb.DocReference{
   413  		ID:   s.SID,
   414  		Type: consts.Sharings,
   415  	})
   416  	if dir.CozyMetadata == nil {
   417  		dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
   418  	} else {
   419  		dir.CozyMetadata.UpdatedAt = time.Now()
   420  	}
   421  	suffix := inst.Translate("Tree Revoked sharing suffix")
   422  	parentPath := filepath.Dir(dir.Fullpath)
   423  	basename := fmt.Sprintf("%s (%s)", dir.DocName, suffix)
   424  	dir.DocName = basename
   425  	for i := 2; i < 100; i++ {
   426  		dir.Fullpath = path.Join(parentPath, dir.DocName)
   427  		if err = inst.VFS().UpdateDirDoc(olddoc, dir); err == nil {
   428  			return nil
   429  		}
   430  		dir.DocName = fmt.Sprintf("%s (%d)", basename, i)
   431  	}
   432  	return err
   433  }
   434  
   435  // GetNoLongerSharedDir returns the directory used for files and folders that
   436  // are removed from a sharing, but are still used via a reference. It the
   437  // directory does not exist, it is created.
   438  func (s *Sharing) GetNoLongerSharedDir(inst *instance.Instance) (*vfs.DirDoc, error) {
   439  	fs := inst.VFS()
   440  	dir, _, err := fs.DirOrFileByID(consts.NoLongerSharedDirID)
   441  	if err != nil && !errors.Is(err, os.ErrNotExist) {
   442  		return nil, err
   443  	}
   444  
   445  	if dir == nil {
   446  		parent, errp := EnsureSharedWithMeDir(inst)
   447  		if errp != nil {
   448  			return nil, errp
   449  		}
   450  		if strings.HasPrefix(parent.Fullpath, vfs.TrashDirName) {
   451  			parent, errp = fs.DirByID(consts.RootDirID)
   452  			if errp != nil {
   453  				return nil, errp
   454  			}
   455  		}
   456  		name := inst.Translate("Tree No longer shared")
   457  		dir, err = vfs.NewDirDocWithParent(name, parent, nil)
   458  		if err != nil {
   459  			return nil, err
   460  		}
   461  		dir.DocID = consts.NoLongerSharedDirID
   462  		dir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
   463  		if err = fs.CreateDir(dir); err != nil {
   464  			return nil, err
   465  		}
   466  		return dir, nil
   467  	}
   468  
   469  	if dir.RestorePath != "" {
   470  		now := time.Now()
   471  		instanceURL := inst.PageURL("/", nil)
   472  		if dir.CozyMetadata == nil {
   473  			dir.CozyMetadata = vfs.NewCozyMetadata(instanceURL)
   474  		} else {
   475  			dir.CozyMetadata.UpdatedAt = now
   476  		}
   477  		dir.CozyMetadata.UpdatedAt = now
   478  		_, err = vfs.RestoreDir(fs, dir)
   479  		if err != nil {
   480  			return nil, err
   481  		}
   482  		children, err := fs.DirBatch(dir, couchdb.NewSkipCursor(0, 0))
   483  		if err != nil {
   484  			return nil, err
   485  		}
   486  		for _, child := range children {
   487  			d, f := child.Refine()
   488  			if d != nil {
   489  				if d.CozyMetadata == nil {
   490  					d.CozyMetadata = vfs.NewCozyMetadata(instanceURL)
   491  				} else {
   492  					d.CozyMetadata.UpdatedAt = now
   493  				}
   494  				_, err = vfs.TrashDir(fs, d)
   495  			} else {
   496  				if f.CozyMetadata == nil {
   497  					f.CozyMetadata = vfs.NewCozyMetadata(instanceURL)
   498  				} else {
   499  					f.CozyMetadata.UpdatedAt = now
   500  				}
   501  				_, err = vfs.TrashFile(fs, f)
   502  			}
   503  			if err != nil {
   504  				return nil, err
   505  			}
   506  		}
   507  	}
   508  
   509  	return dir, nil
   510  }
   511  
   512  // GetFolder returns informations about a folder (with XORed IDs)
   513  func (s *Sharing) GetFolder(inst *instance.Instance, m *Member, xoredID string) (map[string]interface{}, error) {
   514  	creds := s.FindCredentials(m)
   515  	if creds == nil {
   516  		return nil, ErrInvalidSharing
   517  	}
   518  	dirID := XorID(xoredID, creds.XorKey)
   519  	ref := &SharedRef{}
   520  	err := couchdb.GetDoc(inst, consts.Shared, consts.Files+"/"+dirID, ref)
   521  	if err != nil {
   522  		return nil, err
   523  	}
   524  	info, ok := ref.Infos[s.SID]
   525  	if !ok || info.Removed {
   526  		return nil, ErrFolderNotFound
   527  	}
   528  	dir, err := inst.VFS().DirByID(dirID)
   529  	if err != nil {
   530  		return nil, err
   531  	}
   532  	doc := dirToJSONDoc(dir, inst.PageURL("/", nil)).M
   533  	s.TransformFileToSent(doc, creds.XorKey, info.Rule)
   534  	return doc, nil
   535  }
   536  
   537  // ApplyBulkFiles takes a list of documents for the io.cozy.files doctype and
   538  // will apply changes to the VFS according to those documents.
   539  func (s *Sharing) ApplyBulkFiles(inst *instance.Instance, docs DocsList) error {
   540  	type retryOp struct {
   541  		target map[string]interface{}
   542  		dir    *vfs.DirDoc
   543  		ref    *SharedRef
   544  	}
   545  
   546  	var errm error
   547  	var retries []retryOp
   548  	fs := inst.VFS()
   549  
   550  	for _, target := range docs {
   551  		id, ok := target["_id"].(string)
   552  		if !ok {
   553  			errm = multierror.Append(errm, ErrMissingID)
   554  			continue
   555  		}
   556  		ref := &SharedRef{}
   557  		err := couchdb.GetDoc(inst, consts.Shared, consts.Files+"/"+id, ref)
   558  		if err != nil {
   559  			if !couchdb.IsNotFoundError(err) {
   560  				inst.Logger().WithNamespace("replicator").
   561  					Debugf("Error on finding doc of bulk files: %s", err)
   562  				errm = multierror.Append(errm, err)
   563  				continue
   564  			}
   565  			ref = nil
   566  		}
   567  		var infos SharedInfo
   568  		if ref != nil {
   569  			infos, ok = ref.Infos[s.SID]
   570  			if !ok {
   571  				inst.Logger().WithNamespace("replicator").
   572  					Infof("Operation aborted for %s on sharing %s", id, s.SID)
   573  				errm = multierror.Append(errm, ErrSafety)
   574  				continue
   575  			}
   576  		}
   577  		dir, file, err := fs.DirOrFileByID(id)
   578  		if err != nil && !errors.Is(err, os.ErrNotExist) {
   579  			inst.Logger().WithNamespace("replicator").
   580  				Debugf("Error on finding ref of bulk files: %s", err)
   581  			errm = multierror.Append(errm, err)
   582  			continue
   583  		}
   584  		if _, ok := target["_deleted"]; ok {
   585  			if ref == nil || infos.Removed {
   586  				continue
   587  			}
   588  			if dir == nil && file == nil {
   589  				continue
   590  			}
   591  			if dir != nil {
   592  				err = s.TrashDir(inst, dir)
   593  			} else {
   594  				err = s.TrashFile(inst, file, &s.Rules[infos.Rule])
   595  			}
   596  		} else if target["type"] != consts.DirType {
   597  			// Let the upload worker manages this file
   598  			continue
   599  		} else if ref != nil && infos.Removed && !infos.Dissociated {
   600  			continue
   601  		} else if dir == nil {
   602  			err = s.CreateDir(inst, target, delayResolution)
   603  			if errors.Is(err, os.ErrExist) {
   604  				retries = append(retries, retryOp{
   605  					target: target,
   606  				})
   607  				err = nil
   608  			}
   609  		} else if ref == nil || infos.Dissociated {
   610  			// If it is a file: let the upload worker manages this file
   611  			// If it is a dir: ignore this (safety rule)
   612  			continue
   613  		} else {
   614  			// XXX we have to clone the dir document as it is modified by the
   615  			// UpdateDir function and retrying the operation won't work with
   616  			// the modified doc
   617  			cloned := dir.Clone().(*vfs.DirDoc)
   618  			err = s.UpdateDir(inst, target, dir, ref, delayResolution)
   619  			if errors.Is(err, os.ErrExist) {
   620  				retries = append(retries, retryOp{
   621  					target: target,
   622  					dir:    cloned,
   623  					ref:    ref,
   624  				})
   625  				err = nil
   626  			}
   627  		}
   628  		if err != nil {
   629  			inst.Logger().WithNamespace("replicator").
   630  				Debugf("Error on apply bulk file: %s (%#v - %#v)", err, target, ref)
   631  			errm = multierror.Append(errm, fmt.Errorf("%s - %w", id, err))
   632  		}
   633  	}
   634  
   635  	for _, op := range retries {
   636  		var err error
   637  		if op.dir == nil {
   638  			err = s.CreateDir(inst, op.target, resolveResolution)
   639  		} else {
   640  			err = s.UpdateDir(inst, op.target, op.dir, op.ref, resolveResolution)
   641  		}
   642  		if err != nil {
   643  			inst.Logger().WithNamespace("replicator").
   644  				Debugf("Error on apply bulk file: %s (%#v - %#v)", err, op.target, op.ref)
   645  			errm = multierror.Append(errm, err)
   646  		}
   647  	}
   648  
   649  	return errm
   650  }
   651  
   652  func (s *Sharing) GetNotes(inst *instance.Instance) ([]*vfs.FileDoc, error) {
   653  	rule := s.FirstFilesRule()
   654  	if rule != nil {
   655  		if rule.Mime != "" {
   656  			if rule.Mime == consts.NoteMimeType {
   657  				var notes []*vfs.FileDoc
   658  				req := &couchdb.AllDocsRequest{Keys: rule.Values}
   659  				if err := couchdb.GetAllDocs(inst, consts.Files, req, &notes); err != nil {
   660  					return nil, fmt.Errorf("failed to fetch notes shared by themselves: %w", err)
   661  				}
   662  
   663  				return notes, nil
   664  			} else {
   665  				return nil, nil
   666  			}
   667  		}
   668  
   669  		sharingDir, err := s.GetSharingDir(inst)
   670  		if err != nil {
   671  			return nil, fmt.Errorf("failed to get notes sharing dir: %w", err)
   672  		}
   673  
   674  		var notes []*vfs.FileDoc
   675  		fs := inst.VFS()
   676  		iter := fs.DirIterator(sharingDir, nil)
   677  		for {
   678  			_, f, err := iter.Next()
   679  			if errors.Is(err, vfs.ErrIteratorDone) {
   680  				break
   681  			}
   682  			if err != nil {
   683  				return nil, fmt.Errorf("failed to get next shared note: %w", err)
   684  			}
   685  			if f != nil && f.Mime == consts.NoteMimeType {
   686  				notes = append(notes, f)
   687  			}
   688  		}
   689  
   690  		return notes, nil
   691  	}
   692  
   693  	return nil, nil
   694  }
   695  
   696  func (s *Sharing) FixRevokedNotes(inst *instance.Instance) error {
   697  	docs, err := s.GetNotes(inst)
   698  	if err != nil {
   699  		return fmt.Errorf("failed to get revoked sharing notes: %w", err)
   700  	}
   701  
   702  	var errm error
   703  	for _, doc := range docs {
   704  		// If the note came from another cozy via a sharing that is now revoked, we
   705  		// may need to recreate the trigger.
   706  		if err := note.SetupTrigger(inst, doc.ID()); err != nil {
   707  			errm = multierror.Append(errm, fmt.Errorf("failed to setup revoked note trigger: %w", err))
   708  		}
   709  
   710  		if err := note.ImportImages(inst, doc); err != nil {
   711  			errm = multierror.Append(errm, fmt.Errorf("failed to import revoked note images: %w", err))
   712  		}
   713  	}
   714  	return errm
   715  }
   716  
   717  func removeReferencesFromRule(file *vfs.FileDoc, rule *Rule) {
   718  	if rule.Selector != couchdb.SelectorReferencedBy {
   719  		return
   720  	}
   721  	refs := file.ReferencedBy[:0]
   722  	for _, ref := range file.ReferencedBy {
   723  		if !rule.hasReferencedBy(ref) {
   724  			refs = append(refs, ref)
   725  		}
   726  	}
   727  	file.ReferencedBy = refs
   728  }
   729  
   730  func buildReferencedBy(target, file *vfs.FileDoc, rule *Rule) []couchdb.DocReference {
   731  	refs := make([]couchdb.DocReference, 0)
   732  	if file != nil {
   733  		for _, ref := range file.ReferencedBy {
   734  			if !rule.hasReferencedBy(ref) {
   735  				refs = append(refs, ref)
   736  			}
   737  		}
   738  	}
   739  	for _, ref := range target.ReferencedBy {
   740  		if rule.hasReferencedBy(ref) {
   741  			refs = append(refs, ref)
   742  		}
   743  	}
   744  	return refs
   745  }
   746  
   747  func copySafeFieldsToFile(target, file *vfs.FileDoc) {
   748  	file.Tags = target.Tags
   749  	file.Metadata = target.Metadata.RemoveCertifiedMetadata()
   750  	file.CreatedAt = target.CreatedAt
   751  	file.UpdatedAt = target.UpdatedAt
   752  	file.Mime = target.Mime
   753  	file.Class = target.Class
   754  	file.Executable = target.Executable
   755  	file.CozyMetadata = target.CozyMetadata
   756  }
   757  
   758  func copySafeFieldsToDir(target map[string]interface{}, dir *vfs.DirDoc) {
   759  	if tags, ok := target["tags"].([]interface{}); ok {
   760  		dir.Tags = make([]string, 0, len(tags))
   761  		for _, tag := range tags {
   762  			if t, ok := tag.(string); ok {
   763  				dir.Tags = append(dir.Tags, t)
   764  			}
   765  		}
   766  	}
   767  	if created, ok := target["created_at"].(string); ok {
   768  		if at, err := time.Parse(time.RFC3339Nano, created); err == nil {
   769  			dir.CreatedAt = at
   770  		}
   771  	}
   772  	if updated, ok := target["updated_at"].(string); ok {
   773  		if at, err := time.Parse(time.RFC3339Nano, updated); err == nil {
   774  			dir.UpdatedAt = at
   775  		}
   776  	}
   777  
   778  	if meta, ok := target["metadata"].(map[string]interface{}); ok {
   779  		dir.Metadata = vfs.Metadata(meta).RemoveCertifiedMetadata()
   780  	}
   781  
   782  	if meta, ok := target["cozyMetadata"].(map[string]interface{}); ok {
   783  		dir.CozyMetadata = &vfs.FilesCozyMetadata{}
   784  		if version, ok := meta["doctypeVersion"].(string); ok {
   785  			dir.CozyMetadata.DocTypeVersion = version
   786  		}
   787  		if version, ok := meta["metadataVersion"].(float64); ok {
   788  			dir.CozyMetadata.MetadataVersion = int(version)
   789  		}
   790  		if created, ok := meta["createdAt"].(string); ok {
   791  			if at, err := time.Parse(time.RFC3339Nano, created); err == nil {
   792  				dir.CozyMetadata.CreatedAt = at
   793  			}
   794  		}
   795  		if app, ok := meta["createdByApp"].(string); ok {
   796  			dir.CozyMetadata.CreatedByApp = app
   797  		}
   798  		if version, ok := meta["createdByAppVersion"].(string); ok {
   799  			dir.CozyMetadata.CreatedByAppVersion = version
   800  		}
   801  		if instance, ok := meta["createdOn"].(string); ok {
   802  			dir.CozyMetadata.CreatedOn = instance
   803  		}
   804  
   805  		if updated, ok := meta["updatedAt"].(string); ok {
   806  			if at, err := time.Parse(time.RFC3339Nano, updated); err == nil {
   807  				dir.CozyMetadata.UpdatedAt = at
   808  			}
   809  		}
   810  		if updates, ok := meta["updatedByApps"].([]map[string]interface{}); ok {
   811  			for _, update := range updates {
   812  				if slug, ok := update["slug"].(string); ok {
   813  					entry := &metadata.UpdatedByAppEntry{Slug: slug}
   814  					if date, ok := update["date"].(string); ok {
   815  						if at, err := time.Parse(time.RFC3339Nano, date); err == nil {
   816  							entry.Date = at
   817  						}
   818  					}
   819  					if version, ok := update["version"].(string); ok {
   820  						entry.Version = version
   821  					}
   822  					if instance, ok := update["instance"].(string); ok {
   823  						entry.Instance = instance
   824  					}
   825  					dir.CozyMetadata.UpdatedByApps = append(dir.CozyMetadata.UpdatedByApps, entry)
   826  				}
   827  			}
   828  		}
   829  
   830  		// No upload* for directories
   831  		if account, ok := meta["sourceAccount"].(string); ok {
   832  			dir.CozyMetadata.SourceAccount = account
   833  		}
   834  		if id, ok := meta["sourceAccountIdentifier"].(string); ok {
   835  			dir.CozyMetadata.SourceIdentifier = id
   836  		}
   837  	}
   838  }
   839  
   840  // resolveConflictSamePath is used when two files/folders are in conflict
   841  // because they have the same path. To resolve the conflict, we take the
   842  // file/folder from the owner instance as the winner and rename the other.
   843  //
   844  // Note: previously, the rule was that the higher id wins, but the rule has
   845  // been changed. The new rule helps to minimize the number of exchanges needed
   846  // between the cozy instance to converge, and, as such, it helps to avoid
   847  // creating more conflicts.
   848  //
   849  // If the winner is the new file/folder from the other cozy, this function
   850  // rename the local file/folder and let the caller retry its operation.
   851  // If the winner is the local file/folder, this function returns the new name
   852  // and let the caller do its operation with the new name (the caller should
   853  // create a dummy revision to let the other cozy know of the renaming).
   854  func (s *Sharing) resolveConflictSamePath(inst *instance.Instance, visitorID, pth string) (string, error) {
   855  	inst.Logger().WithNamespace("replicator").
   856  		Infof("Resolve conflict for path=%s (docid=%s)", pth, visitorID)
   857  	fs := inst.VFS()
   858  	d, f, err := fs.DirOrFileByPath(pth)
   859  	if err != nil {
   860  		return "", err
   861  	}
   862  	indexer := vfs.NewCouchdbIndexer(inst)
   863  	var dirID string
   864  	if d != nil {
   865  		dirID = d.DirID
   866  	} else {
   867  		dirID = f.DirID
   868  	}
   869  	name := conflictName(indexer, dirID, path.Base(pth), f != nil)
   870  	if s.Owner {
   871  		return name, nil
   872  	}
   873  	if d != nil {
   874  		old := d.Clone().(*vfs.DirDoc)
   875  		d.DocName = name
   876  		d.Fullpath = path.Join(path.Dir(d.Fullpath), d.DocName)
   877  		return "", fs.UpdateDirDoc(old, d)
   878  	}
   879  	old := f.Clone().(*vfs.FileDoc)
   880  	f.DocName = name
   881  	f.ResetFullpath()
   882  	return "", fs.UpdateFileDoc(old, f)
   883  }
   884  
   885  // getDirDocFromInstance fetches informations about a directory from the given
   886  // member of the sharing.
   887  func (s *Sharing) getDirDocFromInstance(inst *instance.Instance, m *Member, creds *Credentials, dirID string) (*vfs.DirDoc, error) {
   888  	if creds == nil || creds.AccessToken == nil {
   889  		return nil, ErrInvalidSharing
   890  	}
   891  	u, err := url.Parse(m.Instance)
   892  	if err != nil {
   893  		return nil, ErrInvalidSharing
   894  	}
   895  	opts := &request.Options{
   896  		Method: http.MethodGet,
   897  		Scheme: u.Scheme,
   898  		Domain: u.Host,
   899  		Path:   "/sharings/" + s.SID + "/io.cozy.files/" + dirID,
   900  		Headers: request.Headers{
   901  			"Accept":        "application/json",
   902  			"Authorization": "Bearer " + creds.AccessToken.AccessToken,
   903  		},
   904  		ParseError: ParseRequestError,
   905  	}
   906  	res, err := request.Req(opts)
   907  	if res != nil && res.StatusCode/100 == 4 {
   908  		res, err = RefreshToken(inst, err, s, m, creds, opts, nil)
   909  	}
   910  	if err != nil {
   911  		if res != nil && res.StatusCode/100 == 5 {
   912  			return nil, ErrInternalServerError
   913  		}
   914  		return nil, err
   915  	}
   916  	defer res.Body.Close()
   917  	var doc *vfs.DirDoc
   918  	if err = json.NewDecoder(res.Body).Decode(&doc); err != nil {
   919  		return nil, err
   920  	}
   921  	return doc, nil
   922  }
   923  
   924  // getDirDocFromNetwork fetches informations about a directory from the other
   925  // cozy instances of this sharing.
   926  func (s *Sharing) getDirDocFromNetwork(inst *instance.Instance, dirID string) (*vfs.DirDoc, error) {
   927  	if !s.Owner {
   928  		return s.getDirDocFromInstance(inst, &s.Members[0], &s.Credentials[0], dirID)
   929  	}
   930  	for i := range s.Credentials {
   931  		doc, err := s.getDirDocFromInstance(inst, &s.Members[i+1], &s.Credentials[i], dirID)
   932  		if err == nil {
   933  			return doc, nil
   934  		}
   935  	}
   936  	return nil, ErrFolderNotFound
   937  }
   938  
   939  // recreateParent is used when a file or folder is added by a cozy, and sent to
   940  // this instance, but its parent directory was trashed and deleted on this
   941  // cozy. To resolve the conflict, this instance will fetch informations from
   942  // the other instance about the parent directory and will recreate it. It can
   943  // be necessary to recurse if there were several levels of directories deleted.
   944  func (s *Sharing) recreateParent(inst *instance.Instance, dirID string) (*vfs.DirDoc, error) {
   945  	inst.Logger().WithNamespace("replicator").
   946  		Debugf("Recreate parent dirID=%s", dirID)
   947  	doc, err := s.getDirDocFromNetwork(inst, dirID)
   948  	if err != nil {
   949  		return nil, fmt.Errorf("recreateParent: %w", err)
   950  	}
   951  	fs := inst.VFS()
   952  	var parent *vfs.DirDoc
   953  	if doc.DirID == "" {
   954  		parent, err = s.GetSharingDir(inst)
   955  	} else {
   956  		parent, err = fs.DirByID(doc.DirID)
   957  		if errors.Is(err, os.ErrNotExist) {
   958  			parent, err = s.recreateParent(inst, doc.DirID)
   959  		}
   960  	}
   961  	if err != nil {
   962  		return nil, err
   963  	}
   964  	doc.DirID = parent.DocID
   965  	doc.Fullpath = path.Join(parent.Fullpath, doc.DocName)
   966  	doc.SetRev("")
   967  	err = fs.CreateDir(doc)
   968  	if err != nil {
   969  		// Maybe the directory has been created concurrently, so let's try
   970  		// again to fetch it from the database
   971  		if errors.Is(err, os.ErrExist) {
   972  			return fs.DirByID(dirID)
   973  		}
   974  		return nil, fmt.Errorf("recreateParent: %w", err)
   975  	}
   976  	return doc, nil
   977  }
   978  
   979  // extractNameAndIndexer takes a target document, extracts the name and creates
   980  // a sharing indexer with _rev and _revisions
   981  func extractNameAndIndexer(inst *instance.Instance, target map[string]interface{}, ref *SharedRef) (string, *sharingIndexer, error) {
   982  	name, ok := target["name"].(string)
   983  	if !ok {
   984  		inst.Logger().WithNamespace("replicator").
   985  			Warnf("Missing name for directory %#v", target)
   986  		return "", nil, ErrInternalServerError
   987  	}
   988  	rev, ok := target["_rev"].(string)
   989  	if !ok {
   990  		inst.Logger().WithNamespace("replicator").
   991  			Warnf("Missing _rev for directory %#v", target)
   992  		return "", nil, ErrInternalServerError
   993  	}
   994  	revs := revsMapToStruct(target["_revisions"])
   995  	if revs == nil {
   996  		inst.Logger().WithNamespace("replicator").
   997  			Warnf("Invalid _revisions for directory %#v", target)
   998  		return "", nil, ErrInternalServerError
   999  	}
  1000  	indexer := newSharingIndexer(inst, &bulkRevs{
  1001  		Rev:       rev,
  1002  		Revisions: *revs,
  1003  	}, ref)
  1004  	return name, indexer, nil
  1005  }
  1006  
  1007  type nameConflictResolution int
  1008  
  1009  const (
  1010  	delayResolution nameConflictResolution = iota
  1011  	resolveResolution
  1012  )
  1013  
  1014  // CreateDir creates a directory on this cozy to reflect a change on another
  1015  // cozy instance of this sharing.
  1016  func (s *Sharing) CreateDir(inst *instance.Instance, target map[string]interface{}, resolution nameConflictResolution) error {
  1017  	inst.Logger().WithNamespace("replicator").
  1018  		Debugf("CreateDir %v (%#v)", target["_id"], target)
  1019  	ref := SharedRef{
  1020  		Infos: make(map[string]SharedInfo),
  1021  	}
  1022  	name, indexer, err := extractNameAndIndexer(inst, target, &ref)
  1023  	if err != nil {
  1024  		return err
  1025  	}
  1026  	fs := inst.VFS().UseSharingIndexer(indexer)
  1027  
  1028  	var parent *vfs.DirDoc
  1029  	if dirID, ok := target["dir_id"].(string); ok {
  1030  		parent, err = fs.DirByID(dirID)
  1031  		if errors.Is(err, os.ErrNotExist) {
  1032  			parent, err = s.recreateParent(inst, dirID)
  1033  		}
  1034  		if err != nil {
  1035  			inst.Logger().WithNamespace("replicator").
  1036  				Debugf("Conflict for parent on creating dir: %s", err)
  1037  			return err
  1038  		}
  1039  	} else {
  1040  		parent, err = s.GetSharingDir(inst)
  1041  		if err != nil {
  1042  			return err
  1043  		}
  1044  	}
  1045  
  1046  	dir, err := vfs.NewDirDocWithParent(name, parent, nil)
  1047  	if err != nil {
  1048  		inst.Logger().WithNamespace("replicator").
  1049  			Warnf("Cannot initialize dir doc: %s", err)
  1050  		return err
  1051  	}
  1052  	dir.SetID(target["_id"].(string))
  1053  	ref.SID = consts.Files + "/" + dir.DocID
  1054  	copySafeFieldsToDir(target, dir)
  1055  	rule, ruleIndex := s.findRuleForNewDirectory(dir)
  1056  	if rule == nil {
  1057  		return ErrSafety
  1058  	}
  1059  	ref.Infos[s.SID] = SharedInfo{Rule: ruleIndex}
  1060  	err = fs.CreateDir(dir)
  1061  	if errors.Is(err, os.ErrExist) && resolution == resolveResolution {
  1062  		name, errr := s.resolveConflictSamePath(inst, dir.DocID, dir.Fullpath)
  1063  		if errr != nil {
  1064  			return errr
  1065  		}
  1066  		if name != "" {
  1067  			indexer.IncrementRevision()
  1068  			dir.DocName = name
  1069  			dir.Fullpath = path.Join(path.Dir(dir.Fullpath), dir.DocName)
  1070  		}
  1071  		err = fs.CreateDir(dir)
  1072  	}
  1073  	if err != nil {
  1074  		inst.Logger().WithNamespace("replicator").
  1075  			Debugf("Cannot create dir: %s", err)
  1076  		return err
  1077  	}
  1078  	return nil
  1079  }
  1080  
  1081  // prepareDirWithAncestors find the parent directory for dir, and recreates it
  1082  // if it is missing.
  1083  func (s *Sharing) prepareDirWithAncestors(inst *instance.Instance, dir *vfs.DirDoc, dirID string) error {
  1084  	if dirID == "" {
  1085  		parent, err := s.GetSharingDir(inst)
  1086  		if err != nil {
  1087  			return err
  1088  		}
  1089  		dir.DirID = parent.DocID
  1090  		dir.Fullpath = path.Join(parent.Fullpath, dir.DocName)
  1091  	} else if dirID != dir.DirID {
  1092  		parent, err := inst.VFS().DirByID(dirID)
  1093  		if errors.Is(err, os.ErrNotExist) {
  1094  			parent, err = s.recreateParent(inst, dirID)
  1095  		}
  1096  		if err != nil {
  1097  			inst.Logger().WithNamespace("replicator").
  1098  				Debugf("Conflict for parent on updating dir: %s", err)
  1099  			return err
  1100  		}
  1101  		dir.DirID = parent.DocID
  1102  		dir.Fullpath = path.Join(parent.Fullpath, dir.DocName)
  1103  	} else {
  1104  		dir.Fullpath = path.Join(path.Dir(dir.Fullpath), dir.DocName)
  1105  	}
  1106  	return nil
  1107  }
  1108  
  1109  // UpdateDir updates a directory on this cozy to reflect a change on another
  1110  // cozy instance of this sharing.
  1111  func (s *Sharing) UpdateDir(
  1112  	inst *instance.Instance,
  1113  	target map[string]interface{},
  1114  	dir *vfs.DirDoc,
  1115  	ref *SharedRef,
  1116  	resolution nameConflictResolution,
  1117  ) error {
  1118  	inst.Logger().WithNamespace("replicator").
  1119  		Debugf("UpdateDir %v (%#v)", target["_id"], target)
  1120  	if strings.HasPrefix(dir.Fullpath+"/", vfs.TrashDirName+"/") {
  1121  		// Don't update a directory in the trash
  1122  		return nil
  1123  	}
  1124  
  1125  	name, indexer, err := extractNameAndIndexer(inst, target, ref)
  1126  	if err != nil {
  1127  		return err
  1128  	}
  1129  
  1130  	chain := revsStructToChain(indexer.bulkRevs.Revisions)
  1131  	conflict := detectConflict(dir.DocRev, chain)
  1132  	switch conflict {
  1133  	case LostConflict:
  1134  		return nil
  1135  	case WonConflict:
  1136  		indexer.WillResolveConflict(dir.DocRev, chain)
  1137  	case NoConflict:
  1138  		// Nothing to do
  1139  	}
  1140  
  1141  	fs := inst.VFS().UseSharingIndexer(indexer)
  1142  	oldDoc := dir.Clone().(*vfs.DirDoc)
  1143  	dir.DocName = name
  1144  	dirID, _ := target["dir_id"].(string)
  1145  	if err = s.prepareDirWithAncestors(inst, dir, dirID); err != nil {
  1146  		return err
  1147  	}
  1148  	copySafeFieldsToDir(target, dir)
  1149  
  1150  	err = fs.UpdateDirDoc(oldDoc, dir)
  1151  	if errors.Is(err, os.ErrExist) && resolution == resolveResolution {
  1152  		name, errb := s.resolveConflictSamePath(inst, dir.DocID, dir.Fullpath)
  1153  		if errb != nil {
  1154  			return errb
  1155  		}
  1156  		if name != "" {
  1157  			indexer.IncrementRevision()
  1158  			dir.DocName = name
  1159  			dir.Fullpath = path.Join(path.Dir(dir.Fullpath), dir.DocName)
  1160  		}
  1161  		err = fs.UpdateDirDoc(oldDoc, dir)
  1162  	}
  1163  	if err != nil {
  1164  		inst.Logger().WithNamespace("replicator").
  1165  			Debugf("Cannot update dir: %s", err)
  1166  		return err
  1167  	}
  1168  	return nil
  1169  }
  1170  
  1171  // TrashDir puts the directory in the trash
  1172  func (s *Sharing) TrashDir(inst *instance.Instance, dir *vfs.DirDoc) error {
  1173  	inst.Logger().WithNamespace("replicator").
  1174  		Debugf("TrashDir %s (%#v)", dir.DocID, dir)
  1175  	if strings.HasPrefix(dir.Fullpath+"/", vfs.TrashDirName+"/") {
  1176  		// nothing to do if the directory is already in the trash
  1177  		return nil
  1178  	}
  1179  
  1180  	newdir := dir.Clone().(*vfs.DirDoc)
  1181  	if newdir.CozyMetadata == nil {
  1182  		newdir.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
  1183  	} else {
  1184  		newdir.CozyMetadata.UpdatedAt = time.Now()
  1185  	}
  1186  
  1187  	newdir.DirID = consts.TrashDirID
  1188  	fs := inst.VFS()
  1189  	exists, err := fs.DirChildExists(newdir.DirID, newdir.DocName)
  1190  	if err != nil {
  1191  		return fmt.Errorf("Sharing.TrashDir: %w", err)
  1192  	}
  1193  	if exists {
  1194  		newdir.DocName = conflictName(fs, newdir.DirID, newdir.DocName, true)
  1195  	}
  1196  	newdir.Fullpath = path.Join(vfs.TrashDirName, newdir.DocName)
  1197  	newdir.RestorePath = path.Dir(dir.Fullpath)
  1198  	if err := s.dissociateDir(inst, dir, newdir); err != nil {
  1199  		return fmt.Errorf("Sharing.TrashDir: %w", err)
  1200  	}
  1201  	return nil
  1202  }
  1203  
  1204  func (s *Sharing) dissociateDir(inst *instance.Instance, olddoc, newdoc *vfs.DirDoc) error {
  1205  	fs := inst.VFS()
  1206  
  1207  	newdoc.SetID("")
  1208  	newdoc.SetRev("")
  1209  	if err := fs.DissociateDir(olddoc, newdoc); err != nil {
  1210  		newdoc.DocName = conflictName(fs, newdoc.DirID, newdoc.DocName, true)
  1211  		newdoc.Fullpath = path.Join(path.Dir(newdoc.Fullpath), newdoc.DocName)
  1212  		if err := fs.DissociateDir(olddoc, newdoc); err != nil {
  1213  			return err
  1214  		}
  1215  	}
  1216  
  1217  	sid := olddoc.DocType() + "/" + olddoc.ID()
  1218  	var ref SharedRef
  1219  	if err := couchdb.GetDoc(inst, consts.Shared, sid, &ref); err == nil {
  1220  		if s.Owner {
  1221  			ref.Revisions.Add(olddoc.Rev())
  1222  			ref.Infos[s.SID] = SharedInfo{
  1223  				Rule:        ref.Infos[s.SID].Rule,
  1224  				Binary:      false,
  1225  				Removed:     true,
  1226  				Dissociated: true,
  1227  			}
  1228  			_ = couchdb.UpdateDoc(inst, &ref)
  1229  		} else {
  1230  			_ = couchdb.DeleteDoc(inst, &ref)
  1231  		}
  1232  	}
  1233  
  1234  	var errm error
  1235  	iter := fs.DirIterator(olddoc, nil)
  1236  	for {
  1237  		d, f, err := iter.Next()
  1238  		if errors.Is(err, vfs.ErrIteratorDone) {
  1239  			break
  1240  		}
  1241  		if err != nil {
  1242  			return err
  1243  		}
  1244  		if f != nil {
  1245  			newf := f.Clone().(*vfs.FileDoc)
  1246  			newf.DirID = newdoc.DocID
  1247  			newf.Trashed = true
  1248  			newf.ResetFullpath()
  1249  			err = s.dissociateFile(inst, f, newf)
  1250  		} else {
  1251  			newd := d.Clone().(*vfs.DirDoc)
  1252  			newd.DirID = newdoc.DocID
  1253  			newd.Fullpath = path.Join(newdoc.Fullpath, newd.DocName)
  1254  			err = s.dissociateDir(inst, d, newd)
  1255  		}
  1256  		if err != nil {
  1257  			errm = multierror.Append(errm, err)
  1258  		}
  1259  	}
  1260  	return errm
  1261  }
  1262  
  1263  // TrashFile puts the file in the trash (except if the file has a reference, in
  1264  // which case, we keep it in a special folder)
  1265  func (s *Sharing) TrashFile(inst *instance.Instance, file *vfs.FileDoc, rule *Rule) error {
  1266  	inst.Logger().WithNamespace("replicator").
  1267  		Debugf("TrashFile %s (%#v)", file.DocID, file)
  1268  	if file.Trashed {
  1269  		// Nothing to do if the file is already in the trash
  1270  		return nil
  1271  	}
  1272  	if file.CozyMetadata == nil {
  1273  		file.CozyMetadata = vfs.NewCozyMetadata(inst.PageURL("/", nil))
  1274  	} else {
  1275  		file.CozyMetadata.UpdatedAt = time.Now()
  1276  	}
  1277  	olddoc := file.Clone().(*vfs.FileDoc)
  1278  	removeReferencesFromRule(file, rule)
  1279  	if s.Owner && rule.Selector == couchdb.SelectorReferencedBy {
  1280  		// Do not move/trash photos removed from an album for the owner
  1281  		if err := s.dissociateFile(inst, olddoc, file); err != nil {
  1282  			return fmt.Errorf("Sharing.TrashFile: %w", err)
  1283  		}
  1284  		return nil
  1285  	}
  1286  	if len(file.ReferencedBy) == 0 {
  1287  		oldpath, err := olddoc.Path(inst.VFS())
  1288  		if err != nil {
  1289  			return err
  1290  		}
  1291  		file.RestorePath = path.Dir(oldpath)
  1292  		file.Trashed = true
  1293  		file.DirID = consts.TrashDirID
  1294  		file.ResetFullpath()
  1295  		if err := s.dissociateFile(inst, olddoc, file); err != nil {
  1296  			return fmt.Errorf("Sharing.TrashFile: %w", err)
  1297  		}
  1298  		return nil
  1299  	}
  1300  	parent, err := s.GetNoLongerSharedDir(inst)
  1301  	if err != nil {
  1302  		return fmt.Errorf("Sharing.TrashFile: %w", err)
  1303  	}
  1304  	file.DirID = parent.DocID
  1305  	file.ResetFullpath()
  1306  	if err := s.dissociateFile(inst, olddoc, file); err != nil {
  1307  		return fmt.Errorf("Sharing.TrashFile: %w", err)
  1308  	}
  1309  	return nil
  1310  }
  1311  
  1312  func (s *Sharing) dissociateFile(inst *instance.Instance, olddoc, newdoc *vfs.FileDoc) error {
  1313  	fs := inst.VFS()
  1314  
  1315  	newdoc.SetID("")
  1316  	newdoc.SetRev("")
  1317  	if err := fs.DissociateFile(olddoc, newdoc); err != nil {
  1318  		newdoc.DocName = conflictName(fs, newdoc.DirID, newdoc.DocName, true)
  1319  		newdoc.ResetFullpath()
  1320  		if err := fs.DissociateFile(olddoc, newdoc); err != nil {
  1321  			return err
  1322  		}
  1323  	}
  1324  
  1325  	sid := olddoc.DocType() + "/" + olddoc.ID()
  1326  	var ref SharedRef
  1327  	if err := couchdb.GetDoc(inst, consts.Shared, sid, &ref); err != nil {
  1328  		if couchdb.IsNotFoundError(err) {
  1329  			return nil
  1330  		}
  1331  		return err
  1332  	}
  1333  	if !s.Owner {
  1334  		return couchdb.DeleteDoc(inst, &ref)
  1335  	}
  1336  	ref.Revisions.Add(olddoc.Rev())
  1337  	ref.Infos[s.SID] = SharedInfo{
  1338  		Rule:        ref.Infos[s.SID].Rule,
  1339  		Binary:      false,
  1340  		Removed:     true,
  1341  		Dissociated: true,
  1342  	}
  1343  	return couchdb.UpdateDoc(inst, &ref)
  1344  }
  1345  
  1346  func dirToJSONDoc(dir *vfs.DirDoc, instanceURL string) couchdb.JSONDoc {
  1347  	doc := couchdb.JSONDoc{
  1348  		Type: consts.Files,
  1349  		M: map[string]interface{}{
  1350  			"type":                       dir.Type,
  1351  			"_id":                        dir.DocID,
  1352  			"_rev":                       dir.DocRev,
  1353  			"name":                       dir.DocName,
  1354  			"created_at":                 dir.CreatedAt,
  1355  			"updated_at":                 dir.UpdatedAt,
  1356  			"tags":                       dir.Tags,
  1357  			"path":                       dir.Fullpath,
  1358  			couchdb.SelectorReferencedBy: dir.ReferencedBy,
  1359  		},
  1360  	}
  1361  	if dir.DirID != "" {
  1362  		doc.M["dir_id"] = dir.DirID
  1363  	}
  1364  	if dir.RestorePath != "" {
  1365  		doc.M["restore_path"] = dir.RestorePath
  1366  	}
  1367  	if len(dir.Metadata) > 0 {
  1368  		doc.M["metadata"] = dir.Metadata.RemoveCertifiedMetadata()
  1369  	}
  1370  	fcm := dir.CozyMetadata
  1371  	if fcm == nil {
  1372  		fcm = vfs.NewCozyMetadata(instanceURL)
  1373  		fcm.CreatedAt = dir.CreatedAt
  1374  		fcm.UpdatedAt = dir.UpdatedAt
  1375  	}
  1376  	doc.M["cozyMetadata"] = fcm.ToJSONDoc()
  1377  	return doc
  1378  }
  1379  
  1380  func fileToJSONDoc(file *vfs.FileDoc, instanceURL string) couchdb.JSONDoc {
  1381  	doc := couchdb.JSONDoc{
  1382  		Type: consts.Files,
  1383  		M: map[string]interface{}{
  1384  			"type":                       file.Type,
  1385  			"_id":                        file.DocID,
  1386  			"_rev":                       file.DocRev,
  1387  			"name":                       file.DocName,
  1388  			"created_at":                 file.CreatedAt,
  1389  			"updated_at":                 file.UpdatedAt,
  1390  			"size":                       file.ByteSize,
  1391  			"md5sum":                     file.MD5Sum,
  1392  			"mime":                       file.Mime,
  1393  			"class":                      file.Class,
  1394  			"executable":                 file.Executable,
  1395  			"trashed":                    file.Trashed,
  1396  			"tags":                       file.Tags,
  1397  			couchdb.SelectorReferencedBy: file.ReferencedBy,
  1398  		},
  1399  	}
  1400  	if file.DirID != "" {
  1401  		doc.M["dir_id"] = file.DirID
  1402  	}
  1403  	if file.RestorePath != "" {
  1404  		doc.M["restore_path"] = file.RestorePath
  1405  	}
  1406  	if len(file.Metadata) > 0 {
  1407  		doc.M["metadata"] = file.Metadata.RemoveCertifiedMetadata()
  1408  	}
  1409  	fcm := file.CozyMetadata
  1410  	if fcm == nil {
  1411  		fcm = vfs.NewCozyMetadata(instanceURL)
  1412  		fcm.CreatedAt = file.CreatedAt
  1413  		fcm.UpdatedAt = file.UpdatedAt
  1414  		uploadedAt := file.CreatedAt
  1415  		fcm.UploadedAt = &uploadedAt
  1416  		fcm.UploadedOn = instanceURL
  1417  	}
  1418  	doc.M["cozyMetadata"] = fcm.ToJSONDoc()
  1419  	return doc
  1420  }