package cal import ( "fmt" "strings" "sync" "time" "github.com/knusbaum/go9p/fs" "github.com/knusbaum/go9p/proto" ) // The query file works like /net/cs: open it, write a query, read the // answer back on the same fd. // // % echo 'attendee=joe@example.com' >/mnt/pim/query // % cat /mnt/pim/query // // A query is ndb-style attr=value pairs, all of which must match: // // summary= substring of the summary, case-insensitive // attendee= substring of any attendee's name or address // organizer= substring of the organizer // location= substring of the location // uid= substring of the uid // from= YYYY-MM-DD, occurrences on or after this day // to= YYYY-MM-DD, occurrences before this day // // It answers with one path per line, which is what pim/show takes. // Holding the fd across the write and the read is the correct way to // use it, as with cs. But "echo ... >query; cat query" opens twice, and // that is how people will actually use it from rc, so the last answer is // also kept and served to a fid that has none of its own. type queryFile struct { mu sync.Mutex res map[uint64][]byte last []byte } // index is one occurrence and the path it was published at. type index struct { path string in Instance } func (s *Server) addQuery() { q := &queryFile{res: make(map[uint64][]byte)} st := s.fsys.NewStat("query", s.user, s.user, 0666) base := fs.NewStaticFile(st, []byte("")) s.root.AddChild(&fs.WrappedFile{ File: base, WriteF: func(fid uint64, off uint64, data []byte) (uint32, error) { out, err := s.query(string(data)) if err != nil { return 0, err } q.mu.Lock() q.res[fid] = []byte(out) q.last = []byte(out) q.mu.Unlock() return uint32(len(data)), nil }, ReadF: func(fid uint64, off uint64, count uint64) ([]byte, error) { q.mu.Lock() b, ok := q.res[fid] if !ok { b = q.last } q.mu.Unlock() if off >= uint64(len(b)) { return []byte{}, nil } end := off + count if end > uint64(len(b)) { end = uint64(len(b)) } return b[off:end], nil }, CloseF: func(fid uint64) error { q.mu.Lock() delete(q.res, fid) q.mu.Unlock() return nil }, }) } func (s *Server) query(q string) (string, error) { var from, to time.Time terms := map[string]string{} for _, f := range strings.Fields(strings.TrimSpace(q)) { k, v, ok := strings.Cut(f, "=") if !ok { return "", fmt.Errorf("query: %q is not attr=value", f) } switch k { case "summary", "attendee", "organizer", "location", "uid": terms[k] = strings.ToLower(v) case "from", "to": t, err := time.ParseInLocation("2006-01-02", v, time.Local) if err != nil { return "", fmt.Errorf("query: bad date %q", v) } if k == "from" { from = t } else { to = t } default: return "", fmt.Errorf("query: unknown attribute %q", k) } } if len(terms) == 0 && from.IsZero() && to.IsZero() { return "", fmt.Errorf("query: nothing to match") } s.mu.Lock() idx := s.index s.mu.Unlock() var b strings.Builder for _, e := range idx { if !from.IsZero() && e.in.Start.Before(from) { continue } if !to.IsZero() && !e.in.Start.Before(to) { continue } if match(e.in.Ev, terms) { fmt.Fprintf(&b, "%s\n", e.path) } } return b.String(), nil } func match(ev *Event, terms map[string]string) bool { has := func(hay, needle string) bool { return strings.Contains(strings.ToLower(hay), needle) } for k, v := range terms { switch k { case "summary": if !has(ev.Summary, v) { return false } case "location": if !has(ev.Location, v) { return false } case "organizer": if !has(ev.Organizer, v) { return false } case "uid": if !has(ev.UID, v) { return false } case "attendee": found := false for _, a := range ev.Attendees { if has(a.Name, v) || has(a.Email, v) { found = true break } } if !found { return false } } } return true } var _ = proto.DMDIR