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 }