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

     1  package sharing
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"runtime"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/cozy/cozy-stack/model/instance"
    11  	"github.com/cozy/cozy-stack/model/job"
    12  	"github.com/cozy/cozy-stack/model/vfs"
    13  	"github.com/cozy/cozy-stack/pkg/config/config"
    14  	"github.com/cozy/cozy-stack/pkg/consts"
    15  	"github.com/cozy/cozy-stack/pkg/couchdb"
    16  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    17  )
    18  
    19  // SetupReceiver is used on the receivers' cozy to make sure the cozy can
    20  // receive the shared documents.
    21  func (s *Sharing) SetupReceiver(inst *instance.Instance) error {
    22  	inst.Logger().WithNamespace("sharing").
    23  		Debugf("Setup receiver on %#v", inst)
    24  
    25  	mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID)
    26  	if err := mu.Lock(); err != nil {
    27  		return err
    28  	}
    29  	defer mu.Unlock()
    30  
    31  	if err := couchdb.EnsureDBExist(inst, consts.Shared); err != nil {
    32  		return err
    33  	}
    34  	if err := s.AddTrackTriggers(inst); err != nil {
    35  		return err
    36  	}
    37  	withFiles := s.FirstFilesRule() != nil
    38  	if !s.ReadOnly() {
    39  		if err := s.AddReplicateTrigger(inst); err != nil {
    40  			return err
    41  		}
    42  		if withFiles {
    43  			if err := s.AddUploadTrigger(inst); err != nil {
    44  				return err
    45  			}
    46  		}
    47  	}
    48  	// The sharing directory is created when the stack receives the
    49  	// first file (it allows to not create it if it's just a file that
    50  	// is shared, not a directory or an album). But, for an empty
    51  	// directory, no files are sent, so we need to create it now.
    52  	if withFiles && s.NbFiles == 0 {
    53  		if _, err := s.GetSharingDir(inst); err != nil {
    54  			return err
    55  		}
    56  	}
    57  	return nil
    58  }
    59  
    60  // Setup is used when a member accepts a sharing to prepare the io.cozy.shared
    61  // database and start an initial replication. It is meant to be used in a new
    62  // goroutine and, as such, does not return errors but log them.
    63  func (s *Sharing) Setup(inst *instance.Instance, m *Member) {
    64  	// Don't do the setup for most tests
    65  	if !inst.OnboardingFinished {
    66  		return
    67  	}
    68  	inst.Logger().WithNamespace("sharing").
    69  		Debugf("Setup for member %#v on %s", m, inst.Domain)
    70  
    71  	defer func() {
    72  		if r := recover(); r != nil {
    73  			var err error
    74  			switch r := r.(type) {
    75  			case error:
    76  				err = r
    77  			default:
    78  				err = fmt.Errorf("%v", r)
    79  			}
    80  			stack := make([]byte, 4<<10) // 4 KB
    81  			length := runtime.Stack(stack, false)
    82  			log := inst.Logger().WithNamespace("sharing").WithField("panic", true)
    83  			log.Errorf("PANIC RECOVER %s: %s", err.Error(), stack[:length])
    84  		}
    85  	}()
    86  
    87  	// XXX Wait a bit to avoid pressure on the recipient Cozy
    88  	time.Sleep(1 * time.Second)
    89  
    90  	mu := config.Lock().ReadWrite(inst, "sharings/"+s.SID)
    91  	if err := mu.Lock(); err != nil {
    92  		return
    93  	}
    94  	defer mu.Unlock()
    95  
    96  	if err := couchdb.EnsureDBExist(inst, consts.Shared); err != nil {
    97  		inst.Logger().WithNamespace("sharing").
    98  			Warnf("Can't ensure io.cozy.shared exists (%s): %s", s.SID, err)
    99  	}
   100  	if rule := s.FirstFilesRule(); rule != nil && rule.Selector != couchdb.SelectorReferencedBy {
   101  		if err := s.AddReferenceForSharingDir(inst, rule); err != nil {
   102  			inst.Logger().WithNamespace("sharing").
   103  				Warnf("Error on referenced_by for the sharing dir (%s): %s", s.SID, err)
   104  		}
   105  	}
   106  
   107  	if err := s.AddTrackTriggers(inst); err != nil {
   108  		inst.Logger().WithNamespace("sharing").
   109  			Warnf("Error on setup of track triggers (%s): %s", s.SID, err)
   110  	}
   111  	if s.Triggers.ReplicateID == "" {
   112  		for i, rule := range s.Rules {
   113  			if err := s.InitialIndex(inst, rule, i); err != nil {
   114  				inst.Logger().Warnf("Error on initial copy for %s (%s): %s", rule.Title, s.SID, err)
   115  			}
   116  		}
   117  	}
   118  	if err := s.AddReplicateTrigger(inst); err != nil {
   119  		inst.Logger().WithNamespace("sharing").
   120  			Warnf("Error on setup replicate trigger (%s): %s", s.SID, err)
   121  	}
   122  	if s.FirstFilesRule() != nil {
   123  		if err := s.AddUploadTrigger(inst); err != nil {
   124  			inst.Logger().WithNamespace("sharing").
   125  				Warnf("Error on setup upload trigger (%s): %s", s.SID, err)
   126  		}
   127  	}
   128  	if err := s.InitialReplication(inst, m); err != nil {
   129  		inst.Logger().WithNamespace("sharing").
   130  			Warnf("Error on initial replication (%s): %s", s.SID, err)
   131  		s.retryWorker(inst, "share-replicate", 0)
   132  		if s.FirstFilesRule() != nil {
   133  			s.retryWorker(inst, "share-upload", 1) // 1, so that it will start after share-replicate
   134  		}
   135  	} else if s.FirstFilesRule() != nil {
   136  		if err := s.InitialUpload(inst, m); err != nil {
   137  			inst.Logger().WithNamespace("sharing").
   138  				Warnf("Error on initial upload (%s): %s", s.SID, err)
   139  			s.retryWorker(inst, "share-upload", 0)
   140  		}
   141  	}
   142  
   143  	s.NotifyRecipients(inst, m)
   144  }
   145  
   146  // AddTrackTriggers creates the share-track triggers for each rule of the
   147  // sharing that will update the io.cozy.shared database.
   148  func (s *Sharing) AddTrackTriggers(inst *instance.Instance) error {
   149  	if len(s.Triggers.TrackIDs) > 0 || s.Triggers.TrackID != "" {
   150  		return nil
   151  	}
   152  	sched := job.System()
   153  	for i, rule := range s.Rules {
   154  		args := rule.TriggerArgs()
   155  		if args == "" {
   156  			continue
   157  		}
   158  		msg := &TrackMessage{
   159  			SharingID: s.SID,
   160  			RuleIndex: i,
   161  			DocType:   rule.DocType,
   162  		}
   163  		t, err := job.NewTrigger(inst, job.TriggerInfos{
   164  			Type:       "@event",
   165  			WorkerType: "share-track",
   166  			Arguments:  args,
   167  		}, msg)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		if err = sched.AddTrigger(t); err != nil {
   172  			return err
   173  		}
   174  		s.Triggers.TrackIDs = append(s.Triggers.TrackIDs, t.ID())
   175  	}
   176  	return couchdb.UpdateDoc(inst, s)
   177  }
   178  
   179  // AddReplicateTrigger creates the share-replicate trigger for this sharing:
   180  // it will starts the replicator when some changes are made to the
   181  // io.cozy.shared database.
   182  func (s *Sharing) AddReplicateTrigger(inst *instance.Instance) error {
   183  	if s.Triggers.ReplicateID != "" {
   184  		return nil
   185  	}
   186  	msg := &ReplicateMsg{
   187  		SharingID: s.SID,
   188  		Errors:    0,
   189  	}
   190  	args := consts.Shared + ":CREATED,UPDATED:" + s.SID + ":sharing"
   191  	t, err := job.NewTrigger(inst, job.TriggerInfos{
   192  		Domain:     inst.ContextualDomain(),
   193  		Type:       "@event",
   194  		WorkerType: "share-replicate",
   195  		Arguments:  args,
   196  		Debounce:   "5s",
   197  	}, msg)
   198  	inst.Logger().WithNamespace("sharing").Debugf("Create trigger %#v", t)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	sched := job.System()
   203  	if err = sched.AddTrigger(t); err != nil {
   204  		return err
   205  	}
   206  	s.Triggers.ReplicateID = t.ID()
   207  	return couchdb.UpdateDoc(inst, s)
   208  }
   209  
   210  // InitialIndex lists the shared documents and put a reference in the
   211  // io.cozy.shared database
   212  func (s *Sharing) InitialIndex(inst *instance.Instance, rule Rule, r int) error {
   213  	if rule.Local || len(rule.Values) == 0 {
   214  		return nil
   215  	}
   216  
   217  	mu := config.Lock().ReadWrite(inst, "shared")
   218  	if err := mu.Lock(); err != nil {
   219  		return err
   220  	}
   221  	defer mu.Unlock()
   222  
   223  	docs, err := FindMatchingDocs(inst, rule)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	refs, err := s.buildReferences(inst, rule, r, docs)
   228  	if err != nil {
   229  		return err
   230  	}
   231  	refs = compactSlice(refs)
   232  	if len(refs) == 0 {
   233  		return nil
   234  	}
   235  	olds := make([]interface{}, len(refs))
   236  	return couchdb.BulkUpdateDocs(inst, consts.Shared, refs, olds)
   237  }
   238  
   239  // FindMatchingDocs finds the documents that match the given rule
   240  func FindMatchingDocs(inst *instance.Instance, rule Rule) ([]couchdb.JSONDoc, error) {
   241  	var docs []couchdb.JSONDoc
   242  	if rule.Selector == "" || rule.Selector == "id" {
   243  		if rule.DocType == consts.Files {
   244  			instanceURL := inst.PageURL("/", nil)
   245  			for _, fileID := range rule.Values {
   246  				err := vfs.WalkByID(inst.VFS(), fileID, func(name string, dir *vfs.DirDoc, file *vfs.FileDoc, err error) error {
   247  					if err != nil {
   248  						return err
   249  					}
   250  					if dir != nil {
   251  						if dir.DocID != fileID {
   252  							docs = append(docs, dirToJSONDoc(dir, instanceURL))
   253  						}
   254  					} else if file != nil {
   255  						docs = append(docs, fileToJSONDoc(file, instanceURL))
   256  					}
   257  					return nil
   258  				})
   259  				if err != nil {
   260  					return nil, err
   261  				}
   262  			}
   263  		} else {
   264  			req := &couchdb.AllDocsRequest{
   265  				Keys: rule.Values,
   266  			}
   267  			if err := couchdb.GetAllDocs(inst, rule.DocType, req, &docs); err != nil {
   268  				return nil, err
   269  			}
   270  		}
   271  	} else {
   272  		if rule.Selector == couchdb.SelectorReferencedBy {
   273  			for _, val := range rule.Values {
   274  				req := &couchdb.ViewRequest{
   275  					Key:         strings.SplitN(val, "/", 2),
   276  					IncludeDocs: true,
   277  					Reduce:      false,
   278  				}
   279  				var res couchdb.ViewResponse
   280  				err := couchdb.ExecView(inst, couchdb.FilesReferencedByView, req, &res)
   281  				if err != nil {
   282  					return nil, err
   283  				}
   284  				for _, row := range res.Rows {
   285  					var doc couchdb.JSONDoc
   286  					if err = json.Unmarshal(row.Doc, &doc); err == nil {
   287  						docs = append(docs, doc)
   288  					}
   289  				}
   290  			}
   291  		} else {
   292  			// Create index based on selector to retrieve documents to share
   293  			name := "by-" + rule.Selector
   294  			idx := mango.MakeIndex(rule.DocType, name, mango.IndexDef{Fields: []string{rule.Selector}})
   295  			if err := couchdb.DefineIndex(inst, idx); err != nil {
   296  				return nil, err
   297  			}
   298  			// Request the index for all values
   299  			for _, val := range rule.Values {
   300  				var results []couchdb.JSONDoc
   301  				req := &couchdb.FindRequest{
   302  					UseIndex: name,
   303  					Selector: mango.Equal(rule.Selector, val),
   304  					Limit:    10000,
   305  				}
   306  				if err := couchdb.FindDocs(inst, rule.DocType, req, &results); err != nil {
   307  					return nil, err
   308  				}
   309  				docs = append(docs, results...)
   310  			}
   311  		}
   312  	}
   313  	return docs, nil
   314  }
   315  
   316  // buildReferences build the SharedRef to add/update the given docs in the
   317  // io.cozy.shared database
   318  func (s *Sharing) buildReferences(inst *instance.Instance, rule Rule, r int, docs []couchdb.JSONDoc) ([]interface{}, error) {
   319  	ids := make([]string, len(docs))
   320  	for i, doc := range docs {
   321  		ids[i] = rule.DocType + "/" + doc.ID()
   322  	}
   323  	srefs, err := FindReferences(inst, ids)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  
   328  	refs := make([]interface{}, len(docs))
   329  	for i, doc := range docs {
   330  		rev := doc.Rev()
   331  		info := SharedInfo{
   332  			Rule:   r,
   333  			Binary: rule.DocType == consts.Files && doc.Get("type") == consts.FileType,
   334  		}
   335  		if srefs[i] == nil {
   336  			refs[i] = SharedRef{
   337  				SID:       rule.DocType + "/" + doc.ID(),
   338  				Revisions: &RevsTree{Rev: rev},
   339  				Infos:     map[string]SharedInfo{s.SID: info},
   340  			}
   341  		} else {
   342  			sub, _ := srefs[i].Revisions.Find(rev)
   343  			if sub != nil {
   344  				if _, ok := srefs[i].Infos[s.SID]; ok {
   345  					continue
   346  				}
   347  			} else {
   348  				srefs[i].Revisions.Add(rev)
   349  			}
   350  			srefs[i].Infos[s.SID] = info
   351  			refs[i] = *srefs[i]
   352  		}
   353  	}
   354  
   355  	return refs, nil
   356  }
   357  
   358  // AddUploadTrigger creates the share-upload trigger for this sharing:
   359  // it will starts the synchronization of the binaries when a file is added or
   360  // updated in the io.cozy.shared database.
   361  func (s *Sharing) AddUploadTrigger(inst *instance.Instance) error {
   362  	if s.Triggers.UploadID != "" {
   363  		return nil
   364  	}
   365  	msg := &UploadMsg{
   366  		SharingID: s.SID,
   367  		Errors:    0,
   368  	}
   369  	args := consts.Shared + ":CREATED,UPDATED:" + s.SID + ":sharing"
   370  	t, err := job.NewTrigger(inst, job.TriggerInfos{
   371  		Domain:     inst.ContextualDomain(),
   372  		Type:       "@event",
   373  		WorkerType: "share-upload",
   374  		Arguments:  args,
   375  		Debounce:   "10s",
   376  	}, msg)
   377  	inst.Logger().WithNamespace("sharing").Debugf("Create trigger %#v", t)
   378  	if err != nil {
   379  		return err
   380  	}
   381  	sched := job.System()
   382  	if err = sched.AddTrigger(t); err != nil {
   383  		return err
   384  	}
   385  	s.Triggers.UploadID = t.ID()
   386  	return couchdb.UpdateDoc(inst, s)
   387  }
   388  
   389  // compactSlice returns the given slice without the nil values
   390  // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
   391  func compactSlice(a []interface{}) []interface{} {
   392  	b := a[:0]
   393  	for _, x := range a {
   394  		if x != nil {
   395  			b = append(b, x)
   396  		}
   397  	}
   398  	return b
   399  }