github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/job/trigger_event.go (about) 1 package job 2 3 import ( 4 "errors" 5 "strings" 6 7 "github.com/cozy/cozy-stack/model/permission" 8 "github.com/cozy/cozy-stack/model/vfs" 9 "github.com/cozy/cozy-stack/pkg/consts" 10 "github.com/cozy/cozy-stack/pkg/couchdb" 11 "github.com/cozy/cozy-stack/pkg/logger" 12 "github.com/cozy/cozy-stack/pkg/realtime" 13 ) 14 15 // EventTrigger implements Trigger for realtime triggered events 16 type EventTrigger struct { 17 *TriggerInfos 18 unscheduled chan struct{} 19 mask []permission.Rule 20 } 21 22 // NewEventTrigger returns a new instance of EventTrigger given the specified 23 // options. 24 func NewEventTrigger(infos *TriggerInfos) (*EventTrigger, error) { 25 args := strings.Split(infos.Arguments, " ") 26 rules := make([]permission.Rule, len(args)) 27 for i, arg := range args { 28 rule, err := permission.UnmarshalRuleString(arg) 29 if err != nil { 30 return nil, err 31 } 32 rules[i] = rule 33 } 34 return &EventTrigger{ 35 TriggerInfos: infos, 36 unscheduled: make(chan struct{}), 37 mask: rules, 38 }, nil 39 } 40 41 // Type implements the Type method of the Trigger interface. 42 func (t *EventTrigger) Type() string { 43 return t.TriggerInfos.Type 44 } 45 46 // Schedule implements the Schedule method of the Trigger interface. 47 func (t *EventTrigger) Schedule() <-chan *JobRequest { 48 ch := make(chan *JobRequest) 49 go func() { 50 sub := realtime.GetHub().Subscriber(t) 51 for _, m := range t.mask { 52 sub.Subscribe(m.Type) 53 } 54 defer func() { 55 sub.Close() 56 close(ch) 57 }() 58 for { 59 select { 60 case e := <-sub.Channel: 61 found := false 62 for _, m := range t.mask { 63 if eventMatchRule(e, &m) { 64 found = true 65 break 66 } 67 } 68 if found { 69 if evt, err := t.Infos().JobRequestWithEvent(e); err == nil { 70 ch <- evt 71 } 72 } 73 case <-t.unscheduled: 74 return 75 } 76 } 77 }() 78 return ch 79 } 80 81 // Unschedule implements the Unschedule method of the Trigger interface. 82 func (t *EventTrigger) Unschedule() { 83 close(t.unscheduled) 84 } 85 86 // Infos implements the Infos method of the Trigger interface. 87 func (t *EventTrigger) Infos() *TriggerInfos { 88 return t.TriggerInfos 89 } 90 91 // CombineRequest implements the CombineRequest method of the Trigger interface. 92 func (t *EventTrigger) CombineRequest() string { 93 return suppressPayload 94 } 95 96 func eventMatchRule(e *realtime.Event, rule *permission.Rule) bool { 97 if e.Doc.DocType() != rule.Type { 98 return false 99 } 100 101 if e.Verb == realtime.EventNotify { 102 return false 103 } 104 105 if !rule.Verbs.Contains(permission.Verb(e.Verb)) { 106 return false 107 } 108 109 if len(rule.Values) == 0 { 110 return true 111 } 112 113 if rule.Selector == "" { 114 if rule.ValuesContain(e.Doc.ID()) { 115 return true 116 } 117 if e.Doc.DocType() == consts.Files { 118 for _, value := range rule.Values { 119 var dir vfs.DirDoc 120 if err := couchdb.GetDoc(e, consts.Files, value, &dir); err != nil { 121 logger.WithDomain(e.Domain). 122 WithNamespace("event-trigger"). 123 Debugf("Cannot find io.cozy.files %s for trigger rule: %s", value, err) 124 continue 125 } 126 // The trigger value was for a file, not a dir, and it should 127 // match only on ID, not on path. 128 if dir.Type != consts.DirType { 129 continue 130 } 131 if testPath(&dir, e.Doc) { 132 return true 133 } 134 if e.OldDoc != nil { 135 if testPath(&dir, e.OldDoc) { 136 return true 137 } 138 } 139 } 140 } 141 return false 142 } 143 144 if len(rule.Values) == 1 && rule.Values[0] == "!=" { 145 // Selector for a changed value 146 if e.Verb != realtime.EventUpdate { 147 return true // We consider that the value has changed on create and delete 148 } 149 if e.OldDoc == nil { 150 return false 151 } 152 if doc, ok := e.Doc.(permission.Fetcher); ok { 153 if old, ok := e.OldDoc.(permission.Fetcher); ok { 154 return rule.ValuesChanged(old, doc) 155 } 156 } 157 } else { 158 // Selector with normal values 159 if v, ok := e.Doc.(permission.Fetcher); ok { 160 if rule.ValuesMatch(v) { 161 return true 162 } 163 // Particular case where the new doc is not valid but the old one was. 164 if e.OldDoc != nil { 165 if vOld, okOld := e.OldDoc.(permission.Fetcher); okOld { 166 return rule.ValuesMatch(vOld) 167 } 168 } 169 } 170 } 171 172 return false 173 } 174 175 // DumpFilePather is a struct made for calling the Path method of a FileDoc and 176 // relying on the cached fullpath of this document (not trying to rebuild it) 177 type DumpFilePather struct{} 178 179 // FilePath only returns an error saying to not call this method 180 func (d DumpFilePather) FilePath(doc *vfs.FileDoc) (string, error) { 181 logger.WithNamespace("event-trigger").Warn("FilePath method of DumpFilePather has been called") 182 return "", errors.New("DumpFilePather FilePath should not have been called") 183 } 184 185 var dumpFilePather = DumpFilePather{} 186 187 func testPath(dir *vfs.DirDoc, doc realtime.Doc) bool { 188 if d, ok := doc.(*vfs.DirDoc); ok { 189 return strings.HasPrefix(d.Fullpath, dir.Fullpath+"/") 190 } 191 if f, ok := doc.(*vfs.FileDoc); ok { 192 if f.Trashed { 193 // XXX When a new file is uploaded, a document may be created in 194 // couchdb with trashed: true. We should ignore it. 195 if strings.HasPrefix(f.DocRev, "1-") { 196 return false 197 } 198 if f.RestorePath != "" { 199 return strings.HasPrefix(f.RestorePath, dir.Fullpath+"/") 200 } 201 } 202 p, err := f.Path(dumpFilePather) 203 if err != nil { 204 return false 205 } 206 return strings.HasPrefix(p, dir.Fullpath+"/") 207 } 208 return false 209 } 210 211 var _ Trigger = &EventTrigger{}