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{}