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

     1  package sharing
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/cozy/cozy-stack/client/request"
    16  	"github.com/cozy/cozy-stack/model/instance"
    17  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    18  	"github.com/cozy/cozy-stack/model/vfs"
    19  	"github.com/cozy/cozy-stack/pkg/config/config"
    20  	"github.com/cozy/cozy-stack/pkg/consts"
    21  	"github.com/cozy/cozy-stack/pkg/couchdb"
    22  	"github.com/cozy/cozy-stack/pkg/realtime"
    23  	"github.com/labstack/echo/v4"
    24  	"golang.org/x/sync/errgroup"
    25  )
    26  
    27  // UploadMsg is used for jobs on the share-upload worker.
    28  type UploadMsg struct {
    29  	SharingID string `json:"sharing_id"`
    30  	Errors    int    `json:"errors"`
    31  }
    32  
    33  // fileCreatorWithContent is a function that can be used to create a file in
    34  // the given VFS. The content comes from the function closure.
    35  type fileCreatorWithContent func(fs vfs.VFS, newdoc, olddoc *vfs.FileDoc) error
    36  
    37  // Upload starts uploading files for this sharing
    38  func (s *Sharing) Upload(inst *instance.Instance, ctx context.Context, errors int) error {
    39  	mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID+"/upload")
    40  	if err := mu.Lock(); err != nil {
    41  		return err
    42  	}
    43  	defer mu.Unlock()
    44  
    45  	var errm error
    46  	var members []*Member
    47  	if !s.Owner {
    48  		members = append(members, &s.Members[0])
    49  	} else {
    50  		for i, m := range s.Members {
    51  			if i == 0 {
    52  				continue
    53  			}
    54  			if m.Status == MemberStatusReady {
    55  				members = append(members, &s.Members[i])
    56  			}
    57  		}
    58  	}
    59  
    60  	lastTry := errors+1 == MaxRetries
    61  	done := true
    62  	g, _ := errgroup.WithContext(context.Background())
    63  	for i := range members {
    64  		m := members[i]
    65  		g.Go(func() error {
    66  			more, err := s.UploadBatchTo(inst, ctx, m, lastTry)
    67  			if err != nil {
    68  				return err
    69  			}
    70  			if more {
    71  				done = false
    72  			}
    73  			return nil
    74  		})
    75  	}
    76  	err := g.Wait()
    77  
    78  	if err != nil {
    79  		s.retryWorker(inst, "share-upload", errors)
    80  		inst.Logger().WithNamespace("upload").Infof("err=%s\n", err)
    81  	} else if !done {
    82  		s.pushJob(inst, "share-upload")
    83  	}
    84  	return errm
    85  }
    86  
    87  // InitialUpload uploads files to just a member, for the first time
    88  func (s *Sharing) InitialUpload(inst *instance.Instance, m *Member) error {
    89  	mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID+"/upload")
    90  	if err := mu.Lock(); err != nil {
    91  		return err
    92  	}
    93  	defer mu.Unlock()
    94  
    95  	ctx := context.Background()
    96  	more, err := s.UploadBatchTo(inst, ctx, m, false)
    97  	if err != nil {
    98  		return err
    99  	}
   100  	if !more {
   101  		return s.sendInitialEndNotif(inst, m)
   102  	}
   103  
   104  	s.pushJob(inst, "share-upload")
   105  	return nil
   106  }
   107  
   108  // sendInitialEndNotif sends a notification to the recipient that the initial
   109  // sync is finished
   110  func (s *Sharing) sendInitialEndNotif(inst *instance.Instance, m *Member) error {
   111  	u, err := url.Parse(m.Instance)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	c := s.FindCredentials(m)
   116  	if c == nil || c.AccessToken == nil {
   117  		return ErrInvalidSharing
   118  	}
   119  	opts := &request.Options{
   120  		Method: http.MethodDelete,
   121  		Scheme: u.Scheme,
   122  		Domain: u.Host,
   123  		Path:   fmt.Sprintf("/sharings/%s/initial", s.SID),
   124  		Headers: request.Headers{
   125  			echo.HeaderAuthorization: "Bearer " + c.AccessToken.AccessToken,
   126  		},
   127  	}
   128  	res, err := request.Req(opts)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	res.Body.Close()
   133  	return nil
   134  }
   135  
   136  // UploadBatchTo uploads a batch of files to the given member. It returns false
   137  // if there are no more files to upload to this member currently.
   138  func (s *Sharing) UploadBatchTo(
   139  	inst *instance.Instance,
   140  	ctx context.Context,
   141  	m *Member,
   142  	lastTry bool,
   143  ) (bool, error) {
   144  	if m.Instance == "" {
   145  		return false, ErrInvalidURL
   146  	}
   147  	creds := s.FindCredentials(m)
   148  	if creds == nil {
   149  		return false, ErrInvalidSharing
   150  	}
   151  
   152  	lastSeq, err := s.getLastSeqNumber(inst, m, "upload")
   153  	if err != nil {
   154  		return false, err
   155  	}
   156  	inst.Logger().WithNamespace("upload").Debugf("lastSeq = %s", lastSeq)
   157  
   158  	batch := &batchUpload{
   159  		Sharing:     s,
   160  		Instance:    inst,
   161  		CommitedSeq: lastSeq,
   162  	}
   163  	defer func() {
   164  		if batch.CommitedSeq != lastSeq {
   165  			_ = s.UpdateLastSequenceNumber(inst, m, "upload", batch.CommitedSeq)
   166  		}
   167  	}()
   168  
   169  	for i := 0; i < BatchSize; i++ {
   170  		if ctx.Err() == context.Canceled {
   171  			return true, nil
   172  		}
   173  		file, ruleIndex, err := batch.findNextFileToUpload()
   174  		if err != nil {
   175  			return false, err
   176  		}
   177  		if file == nil {
   178  			return false, nil
   179  		}
   180  		if err = s.uploadFile(inst, m, file, ruleIndex); err != nil {
   181  			return false, err
   182  		}
   183  		batch.CommitedSeq = batch.CandidateSeq
   184  	}
   185  	return true, nil
   186  }
   187  
   188  type batchUpload struct {
   189  	Sharing      *Sharing
   190  	Instance     *instance.Instance
   191  	CandidateSeq string // The sequence number for the next file to try to upload
   192  	CommitedSeq  string // The sequence number for the last successfully uploaded file
   193  
   194  	// changes is used to batch calls to the changes feed and improves
   195  	// performances.
   196  	changes []couchdb.Change
   197  }
   198  
   199  // findNextFileToUpload uses the changes feed to find the next file that needs
   200  // to be uploaded. It returns a file document if there is one file to upload,
   201  // and the index of the sharing rule that applies to this file.
   202  func (b *batchUpload) findNextFileToUpload() (map[string]interface{}, int, error) {
   203  	seq := b.CommitedSeq
   204  	for {
   205  		if len(b.changes) == 0 {
   206  			response, err := couchdb.GetChanges(b.Instance, &couchdb.ChangesRequest{
   207  				DocType:     consts.Shared,
   208  				IncludeDocs: true,
   209  				Since:       seq,
   210  				Limit:       BatchSize,
   211  			})
   212  			if err != nil {
   213  				return nil, 0, err
   214  			}
   215  			if len(response.Results) == 0 {
   216  				return nil, 0, nil
   217  			}
   218  			b.changes = response.Results
   219  		}
   220  		change := b.changes[0]
   221  		b.changes = b.changes[1:]
   222  		b.CandidateSeq = change.Seq
   223  		seq = change.Seq
   224  		infos, ok := change.Doc.Get("infos").(map[string]interface{})
   225  		if !ok {
   226  			continue
   227  		}
   228  		info, ok := infos[b.Sharing.SID].(map[string]interface{})
   229  		if !ok {
   230  			continue
   231  		}
   232  		if _, ok = info["binary"]; !ok {
   233  			continue
   234  		}
   235  		if _, ok = info["removed"]; ok {
   236  			continue
   237  		}
   238  		idx, ok := info["rule"].(float64)
   239  		if !ok {
   240  			continue
   241  		}
   242  		rev := extractLastRevision(change.Doc)
   243  		if rev == "" {
   244  			continue
   245  		}
   246  		docID := strings.SplitN(change.DocID, "/", 2)[1]
   247  		ir := couchdb.IDRev{ID: docID, Rev: rev}
   248  		query := []couchdb.IDRev{ir}
   249  		results, err := couchdb.BulkGetDocs(b.Instance, consts.Files, query)
   250  		if err != nil {
   251  			if couchdb.IsDeletedError(err) {
   252  				continue
   253  			}
   254  			return nil, 0, err
   255  		}
   256  		if len(results) == 0 {
   257  			b.Instance.Logger().WithNamespace("upload").
   258  				Warnf("missing results for bulk get %v", query)
   259  			continue
   260  		}
   261  		if results[0]["_deleted"] == true {
   262  			b.Instance.Logger().WithNamespace("upload").
   263  				Warnf("cannot upload _deleted file %v", results[0])
   264  			return nil, 0, ErrInternalServerError
   265  		}
   266  		return results[0], int(idx), nil
   267  	}
   268  }
   269  
   270  // uploadFile uploads one file to the given member. It first try to just send
   271  // the metadata, and if it is not enough, it also send the binary.
   272  func (s *Sharing) uploadFile(inst *instance.Instance, m *Member, file map[string]interface{}, ruleIndex int) error {
   273  	inst.Logger().WithNamespace("upload").Debugf("going to upload %#v", file)
   274  
   275  	// Do not try to send a trashed file, the trash status will be synchronized
   276  	// via the CouchDB replication protocol
   277  	if trashed, _ := file["trashed"].(bool); trashed {
   278  		return nil
   279  	}
   280  
   281  	creds := s.FindCredentials(m)
   282  	if creds == nil {
   283  		return ErrInvalidSharing
   284  	}
   285  	u, err := url.Parse(m.Instance)
   286  	if err != nil {
   287  		return err
   288  	}
   289  	origFileID := file["_id"].(string)
   290  	origFileRev := file["_rev"].(string)
   291  	s.TransformFileToSent(file, creds.XorKey, ruleIndex)
   292  	xoredFileID := file["_id"].(string)
   293  	body, err := json.Marshal(file)
   294  	if err != nil {
   295  		return err
   296  	}
   297  	opts := &request.Options{
   298  		Method:  http.MethodPut,
   299  		Scheme:  u.Scheme,
   300  		Domain:  u.Host,
   301  		Path:    "/sharings/" + s.SID + "/io.cozy.files/" + xoredFileID + "/metadata",
   302  		Queries: url.Values{"from": {inst.ContextualDomain()}},
   303  		Headers: request.Headers{
   304  			echo.HeaderAccept:        echo.MIMEApplicationJSON,
   305  			echo.HeaderContentType:   echo.MIMEApplicationJSON,
   306  			echo.HeaderAuthorization: "Bearer " + creds.AccessToken.AccessToken,
   307  		},
   308  		Body:       bytes.NewReader(body),
   309  		ParseError: ParseRequestError,
   310  	}
   311  	var res *http.Response
   312  	res, err = request.Req(opts)
   313  	if res != nil && res.StatusCode/100 == 4 {
   314  		res, err = RefreshToken(inst, err, s, m, creds, opts, body)
   315  	}
   316  	if err != nil {
   317  		if res != nil && res.StatusCode/100 == 5 {
   318  			inst.Logger().WithNamespace("upload").
   319  				Warnf("%s got response %d", opts.Path, res.StatusCode)
   320  			return ErrInternalServerError
   321  		}
   322  		return err
   323  	}
   324  	defer res.Body.Close()
   325  
   326  	if res.StatusCode == 204 {
   327  		return nil
   328  	}
   329  	var resBody KeyToUpload
   330  	if err = json.NewDecoder(res.Body).Decode(&resBody); err != nil {
   331  		return err
   332  	}
   333  
   334  	fs := inst.VFS()
   335  	fileDoc, err := fs.FileByID(origFileID)
   336  	if err != nil {
   337  		return err
   338  	}
   339  	// If the wrong revision is returned, we should abort and retry later. It
   340  	// can be caused by CouchDB eventual consistency, or by a change between
   341  	// when the changes feed was fetched and when the file is loaded.
   342  	if fileDoc.Rev() != origFileRev {
   343  		return ErrInternalServerError
   344  	}
   345  
   346  	dstInstance, err := lifecycle.GetInstance(m.InstanceHost())
   347  	if err == nil && onSameStack(inst, dstInstance) {
   348  		err := s.optimizedUploadFile(inst, dstInstance, m, fileDoc, file, resBody)
   349  		if err != nil {
   350  			inst.Logger().WithNamespace("upload").
   351  				Warnf("optimizedUploadFile failed to upload %s to %s (%s): %s", origFileID, m.Instance, s.ID(), err)
   352  		}
   353  		return err
   354  	}
   355  
   356  	content, err := fs.OpenFile(fileDoc)
   357  	if err != nil {
   358  		return err
   359  	}
   360  	defer content.Close()
   361  
   362  	opts2 := &request.Options{
   363  		Method:  http.MethodPut,
   364  		Scheme:  u.Scheme,
   365  		Domain:  u.Host,
   366  		Path:    "/sharings/" + s.SID + "/io.cozy.files/" + resBody.Key,
   367  		Queries: url.Values{"from": {inst.ContextualDomain()}},
   368  		Headers: request.Headers{
   369  			echo.HeaderContentType:   fileDoc.Mime,
   370  			echo.HeaderAuthorization: "Bearer " + creds.AccessToken.AccessToken,
   371  		},
   372  		Body:   content,
   373  		Client: http.DefaultClient,
   374  	}
   375  	res2, err := request.Req(opts2)
   376  	if err != nil {
   377  		if res2 != nil && res2.StatusCode/100 == 5 {
   378  			inst.Logger().WithNamespace("upload").
   379  				Warnf("%s got response %d", opts2.Path, res2.StatusCode)
   380  			return ErrInternalServerError
   381  		}
   382  		return err
   383  	}
   384  	res2.Body.Close()
   385  	return nil
   386  }
   387  
   388  func onSameStack(src, dst *instance.Instance) bool {
   389  	var srcPort, dstPort string
   390  	parts := strings.SplitN(src.Domain, ":", 2)
   391  	if len(parts) > 1 {
   392  		srcPort = parts[1]
   393  	}
   394  	parts = strings.SplitN(dst.Domain, ":", 2)
   395  	if len(parts) > 1 {
   396  		dstPort = parts[1]
   397  	}
   398  	return srcPort == dstPort
   399  }
   400  
   401  func (s *Sharing) optimizedUploadFile(
   402  	srcInstance, dstInstance *instance.Instance,
   403  	m *Member,
   404  	srcFile *vfs.FileDoc,
   405  	dstFile map[string]interface{},
   406  	key KeyToUpload,
   407  ) error {
   408  	srcInstance.Logger().WithNamespace("upload").
   409  		Debugf("optimizedUploadFile %s to %s (%s)", srcFile.ID(), m.Instance, s.ID())
   410  
   411  	create := func(fs vfs.VFS, newdoc, olddoc *vfs.FileDoc) error {
   412  		return fs.CopyFileFromOtherFS(newdoc, olddoc, srcInstance.VFS(), srcFile)
   413  	}
   414  
   415  	dstSharing, err := FindSharing(dstInstance, s.ID())
   416  	if err != nil {
   417  		return err
   418  	}
   419  	if !dstSharing.Active {
   420  		return ErrInvalidSharing
   421  	}
   422  	return dstSharing.HandleFileUpload(dstInstance, key.Key, create)
   423  }
   424  
   425  // FileDocWithRevisions is the struct of the payload for synchronizing a file
   426  type FileDocWithRevisions struct {
   427  	*vfs.FileDoc
   428  	Revisions RevsStruct `json:"_revisions"`
   429  }
   430  
   431  // Clone is part of the couchdb.Doc interface
   432  func (f *FileDocWithRevisions) Clone() couchdb.Doc {
   433  	panic("FileDocWithRevisions must not be cloned")
   434  }
   435  
   436  // KeyToUpload contains the key for uploading a file (when syncing metadata is
   437  // not enough)
   438  type KeyToUpload struct {
   439  	Key string `json:"key"`
   440  }
   441  
   442  func (s *Sharing) createUploadKey(inst *instance.Instance, target *FileDocWithRevisions) (*KeyToUpload, error) {
   443  	key, err := getStore().Save(inst, target)
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  	return &KeyToUpload{Key: key}, nil
   448  }
   449  
   450  // SyncFile tries to synchronize a file with just the metadata. If it can't,
   451  // it will return a key to upload the content.
   452  func (s *Sharing) SyncFile(inst *instance.Instance, target *FileDocWithRevisions) (*KeyToUpload, error) {
   453  	inst.Logger().WithNamespace("upload").Debugf("SyncFile %#v", target)
   454  
   455  	if len(target.MD5Sum) == 0 {
   456  		return nil, vfs.ErrInvalidHash
   457  	}
   458  	sid := consts.Files + "/" + target.DocID
   459  	mu := config.Lock().ReadWrite(inst, "shared/"+sid)
   460  	if err := mu.Lock(); err != nil {
   461  		return nil, err
   462  	}
   463  	defer mu.Unlock()
   464  	var ref SharedRef
   465  	current, err := inst.VFS().FileByID(target.DocID)
   466  	if err != nil {
   467  		if errors.Is(err, os.ErrNotExist) {
   468  			// XXX Even if the file does not exist, it may have existed in the
   469  			// past and have been disociated. In that case, we need to check
   470  			// that what we received is not just the echo, or we will recreate
   471  			// a deleted file for no reason.
   472  			err = couchdb.GetDoc(inst, consts.Shared, sid, &ref)
   473  			if err == nil {
   474  				if sub, _ := ref.Revisions.Find(target.DocRev); sub != nil {
   475  					// It's just the echo, there is nothing to do
   476  					return nil, nil
   477  				}
   478  			} else if !couchdb.IsNotFoundError(err) {
   479  				return nil, err
   480  			}
   481  			if rule, _ := s.findRuleForNewFile(target.FileDoc); rule == nil {
   482  				return nil, ErrSafety
   483  			}
   484  			return s.createUploadKey(inst, target)
   485  		}
   486  		return nil, err
   487  	}
   488  
   489  	err = couchdb.GetDoc(inst, consts.Shared, sid, &ref)
   490  	if err != nil {
   491  		if !couchdb.IsNotFoundError(err) {
   492  			return nil, err
   493  		}
   494  		// XXX It happens that the job for creating the io.cozy.shared has
   495  		// been lost (stack restart for example), and we need to do
   496  		// something to avoid having the sharing stuck. The most efficient
   497  		// way to do that is to check that the file is actually in the
   498  		// sharing directory, and if it is the case, to create the missing
   499  		// io.cozy.shared.
   500  		ref, err = s.fixMissingShared(inst, current)
   501  		if err != nil {
   502  			return nil, ErrSafety
   503  		}
   504  	}
   505  	if infos, ok := ref.Infos[s.SID]; !ok || (infos.Removed && !infos.Dissociated) {
   506  		return nil, ErrSafety
   507  	}
   508  	if sub, _ := ref.Revisions.Find(target.DocRev); sub != nil {
   509  		// It's just the echo, there is nothing to do
   510  		return nil, nil
   511  	}
   512  	if !bytes.Equal(target.MD5Sum, current.MD5Sum) {
   513  		return s.createUploadKey(inst, target)
   514  	}
   515  	return nil, s.updateFileMetadata(inst, target, current, &ref)
   516  }
   517  
   518  // prepareFileWithAncestors find the parent directory for file, and recreates it
   519  // if it is missing.
   520  func (s *Sharing) prepareFileWithAncestors(inst *instance.Instance, newdoc *vfs.FileDoc, dirID string) error {
   521  	// Case 1: there is a rule for sharing this file
   522  	if s.hasExplicitRuleForFile(newdoc) {
   523  		return nil
   524  	}
   525  
   526  	// Case 2: the file is in a directory that is shared
   527  	if dirID == "" {
   528  		parent, err := s.GetSharingDir(inst)
   529  		if err != nil {
   530  			return err
   531  		}
   532  		newdoc.DirID = parent.DocID
   533  	} else if dirID != newdoc.DirID {
   534  		parent, err := inst.VFS().DirByID(dirID)
   535  		if errors.Is(err, os.ErrNotExist) {
   536  			parent, err = s.recreateParent(inst, dirID)
   537  		}
   538  		if err != nil {
   539  			inst.Logger().WithNamespace("upload").
   540  				Debugf("Conflict for parent on sync file: %s", err)
   541  			return err
   542  		}
   543  		newdoc.DirID = parent.DocID
   544  	}
   545  	return nil
   546  }
   547  
   548  // updateFileMetadata updates a file document when only some metadata has
   549  // changed, but not the content.
   550  func (s *Sharing) updateFileMetadata(inst *instance.Instance, target *FileDocWithRevisions, newdoc *vfs.FileDoc, ref *SharedRef) error {
   551  	indexer := newSharingIndexer(inst, &bulkRevs{
   552  		Rev:       target.DocRev,
   553  		Revisions: target.Revisions,
   554  	}, ref)
   555  
   556  	chain := revsStructToChain(target.Revisions)
   557  	conflict := detectConflict(newdoc.DocRev, chain)
   558  	switch conflict {
   559  	case LostConflict:
   560  		return nil
   561  	case WonConflict:
   562  		indexer.WillResolveConflict(newdoc.DocRev, chain)
   563  	case NoConflict:
   564  		// Nothing to do
   565  	}
   566  
   567  	fs := inst.VFS().UseSharingIndexer(indexer)
   568  	olddoc := newdoc.Clone().(*vfs.FileDoc)
   569  	newdoc.DocName = target.DocName
   570  	if err := s.prepareFileWithAncestors(inst, newdoc, target.DirID); err != nil {
   571  		return err
   572  	}
   573  	newdoc.ResetFullpath()
   574  	copySafeFieldsToFile(target.FileDoc, newdoc)
   575  	infos := ref.Infos[s.SID]
   576  	rule := &s.Rules[infos.Rule]
   577  	newdoc.ReferencedBy = buildReferencedBy(target.FileDoc, newdoc, rule)
   578  
   579  	err := fs.UpdateFileDoc(olddoc, newdoc)
   580  	if errors.Is(err, os.ErrExist) {
   581  		pth, errp := newdoc.Path(fs)
   582  		if errp != nil {
   583  			return errp
   584  		}
   585  		name, errr := s.resolveConflictSamePath(inst, newdoc.DocID, pth)
   586  		if errr != nil {
   587  			return errr
   588  		}
   589  		if name != "" {
   590  			indexer.IncrementRevision()
   591  			newdoc.DocName = name
   592  			newdoc.ResetFullpath()
   593  		}
   594  		err = fs.UpdateFileDoc(olddoc, newdoc)
   595  	}
   596  	if err != nil {
   597  		inst.Logger().WithNamespace("upload").
   598  			Debugf("Cannot update file: %s", err)
   599  		return err
   600  	}
   601  	return nil
   602  }
   603  
   604  // HandleFileUpload is used to receive a file upload when synchronizing just
   605  // the metadata was not enough.
   606  func (s *Sharing) HandleFileUpload(inst *instance.Instance, key string, create fileCreatorWithContent) error {
   607  	target, err := getStore().Get(inst, key)
   608  	if err != nil {
   609  		return err
   610  	}
   611  	if target == nil {
   612  		return ErrMissingFileMetadata
   613  	}
   614  	inst.Logger().WithNamespace("upload").Debugf("HandleFileUpload %#v %#v", target.FileDoc, target.Revisions)
   615  	sid := consts.Files + "/" + target.DocID
   616  	mu := config.Lock().ReadWrite(inst, "shared/"+sid)
   617  	if err = mu.Lock(); err != nil {
   618  		return err
   619  	}
   620  	defer mu.Unlock()
   621  
   622  	current, err := inst.VFS().FileByID(target.DocID)
   623  	if err != nil && !errors.Is(err, os.ErrNotExist) {
   624  		inst.Logger().WithNamespace("upload").
   625  			Warnf("Upload has failed: %s", err)
   626  		return err
   627  	}
   628  
   629  	if current == nil {
   630  		return s.UploadNewFile(inst, target, create)
   631  	}
   632  	return s.UploadExistingFile(inst, target, current, create)
   633  }
   634  
   635  // UploadNewFile is used to receive a new file.
   636  func (s *Sharing) UploadNewFile(
   637  	inst *instance.Instance,
   638  	target *FileDocWithRevisions,
   639  	create fileCreatorWithContent,
   640  ) error {
   641  	inst.Logger().WithNamespace("upload").Debugf("UploadNewFile")
   642  	ref := SharedRef{
   643  		Infos: make(map[string]SharedInfo),
   644  	}
   645  	indexer := newSharingIndexer(inst, &bulkRevs{
   646  		Rev:       target.Rev(),
   647  		Revisions: target.Revisions,
   648  	}, &ref)
   649  	fs := inst.VFS().UseSharingIndexer(indexer)
   650  
   651  	rule, ruleIndex := s.findRuleForNewFile(target.FileDoc)
   652  	if rule == nil {
   653  		return ErrSafety
   654  	}
   655  
   656  	var err error
   657  	var parent *vfs.DirDoc
   658  	var addReferencedBy bool
   659  	if target.DirID != "" {
   660  		parent, err = fs.DirByID(target.DirID)
   661  		if errors.Is(err, os.ErrNotExist) {
   662  			parent, err = s.recreateParent(inst, target.DirID)
   663  		}
   664  		if err != nil {
   665  			inst.Logger().WithNamespace("upload").
   666  				Infof("Conflict for parent on file upload: %s", err)
   667  		}
   668  	} else if target.DocID == rule.Values[0] {
   669  		parentID := s.cleanShortcutID(inst)
   670  		if parentID != "" {
   671  			parent, err = fs.DirByID(parentID)
   672  		} else {
   673  			parent, err = EnsureSharedWithMeDir(inst)
   674  		}
   675  		addReferencedBy = true
   676  	} else {
   677  		parent, err = s.GetSharingDir(inst)
   678  	}
   679  	if err != nil {
   680  		return err
   681  	}
   682  
   683  	// XXX In some cases, we have to add a fake revision to the created file
   684  	// because it was already known by CouchDB. For example:
   685  	// - Alice has a shared folder with Bob
   686  	// - inside this folder, there is a directory named foo
   687  	// - there is a file named bar inside foo, at the revision 1-123
   688  	// - Alice moves foo outside of the sharing
   689  	// - the sharing replication deletes bar on Bob's instance (revision 2-456)
   690  	// - later, Alice moves again foo inside the sharing (with bar)
   691  	// - we are in this function for bar, and if we try to recreate the file
   692  	//   with revision 1-123, it will still be seen as deleted by CouchDB
   693  	//   (revision 2-456 wins) => so, we create a revision 2-789.
   694  	var fake couchdb.JSONDoc
   695  	if err := couchdb.GetDoc(inst, consts.Files, target.DocID, &fake); couchdb.IsDeletedError(err) {
   696  		indexer.IncrementRevision()
   697  	}
   698  
   699  	newdoc, err := vfs.NewFileDoc(target.DocName, parent.DocID, target.Size(), target.MD5Sum,
   700  		target.Mime, target.Class, target.CreatedAt, target.Executable, false, false, target.Tags)
   701  	if err != nil {
   702  		return err
   703  	}
   704  	newdoc.SetID(target.DocID)
   705  	ref.SID = consts.Files + "/" + newdoc.DocID
   706  	copySafeFieldsToFile(target.FileDoc, newdoc)
   707  
   708  	ref.Infos[s.SID] = SharedInfo{Rule: ruleIndex, Binary: true}
   709  	newdoc.ReferencedBy = buildReferencedBy(target.FileDoc, nil, rule)
   710  	if addReferencedBy {
   711  		ref := couchdb.DocReference{
   712  			ID:   s.SID,
   713  			Type: consts.Sharings,
   714  		}
   715  		newdoc.ReferencedBy = append(newdoc.ReferencedBy, ref)
   716  	}
   717  
   718  	err = create(fs, newdoc, nil)
   719  	if errors.Is(err, os.ErrExist) {
   720  		pth, errp := newdoc.Path(fs)
   721  		if errp != nil {
   722  			return errp
   723  		}
   724  		name, errr := s.resolveConflictSamePath(inst, newdoc.DocID, pth)
   725  		if errr != nil {
   726  			return errr
   727  		}
   728  		if name != "" {
   729  			indexer.IncrementRevision()
   730  			newdoc.DocName = name
   731  			newdoc.ResetFullpath()
   732  		}
   733  		err = create(fs, newdoc, nil)
   734  	}
   735  	if err != nil {
   736  		inst.Logger().WithNamespace("upload").
   737  			Debugf("Cannot create file: %s", err)
   738  		return err
   739  	}
   740  	if s.NbFiles > 0 {
   741  		s.countReceivedFiles(inst)
   742  	}
   743  	return nil
   744  }
   745  
   746  // countReceivedFiles counts the number of files received during the initial
   747  // sync, and pushs an event to the real-time system with this count
   748  func (s *Sharing) countReceivedFiles(inst *instance.Instance) {
   749  	count := 0
   750  	req := &couchdb.ViewRequest{
   751  		Key:         s.SID,
   752  		IncludeDocs: true,
   753  	}
   754  	var res couchdb.ViewResponse
   755  	err := couchdb.ExecView(inst, couchdb.SharedDocsBySharingID, req, &res)
   756  	if err == nil {
   757  		for _, row := range res.Rows {
   758  			var doc SharedRef
   759  			if err = json.Unmarshal(row.Doc, &doc); err != nil {
   760  				continue
   761  			}
   762  			if doc.Infos[s.SID].Binary {
   763  				count++
   764  			}
   765  		}
   766  	}
   767  
   768  	if count >= s.NbFiles {
   769  		if err = s.EndInitial(inst); err != nil {
   770  			inst.Logger().WithNamespace("sharing").
   771  				Errorf("Can't save sharing %v: %s", s, err)
   772  		}
   773  		return
   774  	}
   775  
   776  	doc := couchdb.JSONDoc{
   777  		Type: consts.SharingsInitialSync,
   778  		M: map[string]interface{}{
   779  			"_id":   s.SID,
   780  			"count": count,
   781  		},
   782  	}
   783  	realtime.GetHub().Publish(inst, realtime.EventUpdate, &doc, nil)
   784  }
   785  
   786  // UploadExistingFile is used to receive new content for an existing file.
   787  //
   788  // Note: if file was renamed + its content has changed, we modify the content
   789  // first, then rename it, not trying to do both at the same time. We do it in
   790  // this order because the difficult case is if one operation succeeds and the
   791  // other fails (if the two succeeds, we are fine; if the two fails, we just
   792  // retry), and in that case, it is easier to manage a conflict on dir_id+name
   793  // than on content: a conflict on different content is resolved by a copy of
   794  // the file (which is not what we want), a conflict of name+dir_id, the higher
   795  // revision wins and it should be the good one in our case.
   796  func (s *Sharing) UploadExistingFile(
   797  	inst *instance.Instance,
   798  	target *FileDocWithRevisions,
   799  	newdoc *vfs.FileDoc,
   800  	create fileCreatorWithContent,
   801  ) error {
   802  	inst.Logger().WithNamespace("upload").Debugf("UploadExistingFile")
   803  	var ref SharedRef
   804  	err := couchdb.GetDoc(inst, consts.Shared, consts.Files+"/"+target.DocID, &ref)
   805  	if err != nil {
   806  		if couchdb.IsNotFoundError(err) {
   807  			return ErrSafety
   808  		}
   809  		return err
   810  	}
   811  	indexer := newSharingIndexer(inst, &bulkRevs{
   812  		Rev:       target.Rev(),
   813  		Revisions: target.Revisions,
   814  	}, &ref)
   815  	fs := inst.VFS().UseSharingIndexer(indexer)
   816  	olddoc := newdoc.Clone().(*vfs.FileDoc)
   817  
   818  	infos, ok := ref.Infos[s.SID]
   819  	if !ok || (infos.Removed && !infos.Dissociated) {
   820  		return ErrSafety
   821  	}
   822  	rule := &s.Rules[infos.Rule]
   823  	newdoc.ReferencedBy = buildReferencedBy(target.FileDoc, olddoc, rule)
   824  	copySafeFieldsToFile(target.FileDoc, newdoc)
   825  	newdoc.DocName = target.DocName
   826  	if err := s.prepareFileWithAncestors(inst, newdoc, target.DirID); err != nil {
   827  		return err
   828  	}
   829  	newdoc.ResetFullpath()
   830  	newdoc.ByteSize = target.ByteSize
   831  	newdoc.MD5Sum = target.MD5Sum
   832  
   833  	chain := revsStructToChain(target.Revisions)
   834  	conflict := detectConflict(newdoc.DocRev, chain)
   835  	switch conflict {
   836  	case LostConflict:
   837  		return s.uploadLostConflict(inst, target, newdoc, create)
   838  	case WonConflict:
   839  		if err = s.uploadWonConflict(inst, olddoc); err != nil {
   840  			return err
   841  		}
   842  	case NoConflict:
   843  		// Nothing to do
   844  	}
   845  	indexer.WillResolveConflict(newdoc.DocRev, chain)
   846  
   847  	// Easy case: only the content has changed, not its path
   848  	if newdoc.DocName == olddoc.DocName && newdoc.DirID == olddoc.DirID {
   849  		return create(fs, newdoc, olddoc)
   850  	}
   851  
   852  	stash := indexer.StashRevision(false)
   853  	tmpdoc := newdoc.Clone().(*vfs.FileDoc)
   854  	tmpdoc.DocName = olddoc.DocName
   855  	tmpdoc.DirID = olddoc.DirID
   856  	tmpdoc.ResetFullpath()
   857  	if err := create(fs, tmpdoc, olddoc); err != nil {
   858  		return err
   859  	}
   860  
   861  	indexer.UnstashRevision(stash)
   862  	newdoc.DocRev = tmpdoc.DocRev
   863  	newdoc.InternalID = tmpdoc.InternalID
   864  	err = fs.UpdateFileDoc(tmpdoc, newdoc)
   865  	if errors.Is(err, os.ErrExist) {
   866  		pth, errp := newdoc.Path(fs)
   867  		if errp != nil {
   868  			return errp
   869  		}
   870  		name, errr := s.resolveConflictSamePath(inst, newdoc.DocID, pth)
   871  		if errr != nil {
   872  			return errr
   873  		}
   874  		if name != "" {
   875  			indexer.IncrementRevision()
   876  			newdoc.DocName = name
   877  			newdoc.ResetFullpath()
   878  		}
   879  		err = fs.UpdateFileDoc(tmpdoc, newdoc)
   880  	}
   881  	return err
   882  }
   883  
   884  // uploadLostConflict manages an upload where a file is in conflict, and the
   885  // uploaded file version goes to a new file.
   886  func (s *Sharing) uploadLostConflict(
   887  	inst *instance.Instance,
   888  	target *FileDocWithRevisions,
   889  	newdoc *vfs.FileDoc,
   890  	create fileCreatorWithContent,
   891  ) error {
   892  	rev := target.Rev()
   893  	inst.Logger().WithNamespace("upload").Debugf("uploadLostConflict %s", rev)
   894  	indexer := newSharingIndexer(inst, &bulkRevs{
   895  		Rev:       rev,
   896  		Revisions: revsChainToStruct([]string{rev}),
   897  	}, nil)
   898  	fs := inst.VFS().UseSharingIndexer(indexer)
   899  	newdoc.DocID = conflictID(newdoc.DocID, rev)
   900  	if _, err := fs.FileByID(newdoc.DocID); !errors.Is(err, os.ErrNotExist) {
   901  		return err
   902  	}
   903  	newdoc.DocName = conflictName(indexer, newdoc.DirID, newdoc.DocName, true)
   904  	newdoc.DocRev = ""
   905  	newdoc.ResetFullpath()
   906  	if err := create(fs, newdoc, nil); err != nil {
   907  		inst.Logger().WithNamespace("upload").Debugf("1. loser = %#v", newdoc)
   908  		return err
   909  	}
   910  	return nil
   911  }
   912  
   913  // uploadWonConflict manages an upload where a file is in conflict, and the
   914  // existing file is copied to a new file to let the upload succeed.
   915  func (s *Sharing) uploadWonConflict(inst *instance.Instance, src *vfs.FileDoc) error {
   916  	rev := src.Rev()
   917  	inst.Logger().WithNamespace("upload").Debugf("uploadWonConflict %s", rev)
   918  	indexer := newSharingIndexer(inst, &bulkRevs{
   919  		Rev:       rev,
   920  		Revisions: revsChainToStruct([]string{rev}),
   921  	}, nil)
   922  	fs := inst.VFS().UseSharingIndexer(indexer)
   923  	dst := src.Clone().(*vfs.FileDoc)
   924  	dst.DocID = conflictID(dst.DocID, rev)
   925  	if _, err := fs.FileByID(dst.DocID); !errors.Is(err, os.ErrNotExist) {
   926  		return err
   927  	}
   928  	dst.DocName = conflictName(indexer, dst.DirID, dst.DocName, true)
   929  	dst.ResetFullpath()
   930  	content, err := fs.OpenFile(src)
   931  	if err != nil {
   932  		return err
   933  	}
   934  	defer content.Close()
   935  	file, err := fs.CreateFile(dst, nil)
   936  	if err != nil {
   937  		return err
   938  	}
   939  	inst.Logger().WithNamespace("upload").Debugf("2. loser = %#v", dst)
   940  	return copyFileContent(inst, file, content)
   941  }
   942  
   943  // copyFileContent will copy the body of the HTTP request to the file, and
   944  // close the file descriptor at the end.
   945  func copyFileContent(inst *instance.Instance, file vfs.File, body io.ReadCloser) error {
   946  	_, err := io.Copy(file, body)
   947  	if cerr := file.Close(); cerr != nil && err == nil {
   948  		err = cerr
   949  		inst.Logger().WithNamespace("upload").
   950  			Infof("Cannot close file descriptor: %s", err)
   951  	}
   952  	return err
   953  }