diff options
Diffstat (limited to 'pim/lib/cal/tree.go')
| -rw-r--r-- | pim/lib/cal/tree.go | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/pim/lib/cal/tree.go b/pim/lib/cal/tree.go new file mode 100644 index 0000000..cd9e9d2 --- /dev/null +++ b/pim/lib/cal/tree.go @@ -0,0 +1,217 @@ +package cal + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/knusbaum/go9p/fs" + "github.com/knusbaum/go9p/proto" +) + +// slug makes a string safe to use as one path element. +func slug(s string) string { + s = strings.TrimSpace(s) + var b strings.Builder + for _, r := range s { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': + b.WriteRune(r) + case r == '-', r == '.', r == '_': + b.WriteRune(r) + case r == ' ': + b.WriteRune('-') + default: + b.WriteRune('_') + } + } + out := b.String() + if out == "" { + out = "unnamed" + } + if len(out) > 64 { + out = out[:64] + } + return out +} + +// uniqueName returns name, or name-2, name-3 ... if it is already taken. +// Real calendars do collide: two events at the same minute with the same +// summary, or several orphaned overrides sharing one UID. +func uniqueName(parent *fs.StaticDir, name string) string { + kids := parent.Children() + if _, taken := kids[name]; !taken { + return name + } + for i := 2; ; i++ { + try := fmt.Sprintf("%s-%d", name, i) + if _, taken := kids[try]; !taken { + return try + } + } +} + +func (s *Server) file(dir *fs.StaticDir, name, content string) { + st := s.fsys.NewStat(name, s.user, s.user, 0444) + dir.AddChild(fs.NewStaticFile(st, []byte(content))) +} + +func (s *Server) subdir(parent *fs.StaticDir, name string) *fs.StaticDir { + if c, ok := parent.Children()[name]; ok { + if d, ok := c.(*fs.StaticDir); ok { + return d + } + } + st := s.fsys.NewStat(name, s.user, s.user, 0555|proto.DMDIR) + d := fs.NewStaticDir(st) + parent.AddChild(d) + return d +} + +func tfmt(t time.Time, allDay bool) string { + if allDay { + return t.Format("2006-01-02") + } + return t.Format(time.RFC3339) +} + +// buildEvents populates events/uuid/<uid>/ with one directory per event. +func (s *Server) buildEvents(root *fs.StaticDir, evs []*Event) { + d := s.subdir(s.subdir(root, "events"), "uuid") + for _, e := range evs { + name := slug(e.UID) + if !e.RecurID.IsZero() { + // an override with no series of its own to hang under + name += "-" + e.RecurID.Format("20060102T150405") + } + name = uniqueName(d, name) + if s.evdir == nil { + s.evdir = make(map[*Event]string) + } + s.evdir[e] = name + ed := s.subdir(d, name) + s.file(ed, "summary", e.Summary+"\n") + s.file(ed, "start", tfmt(e.Start, e.AllDay)+"\n") + if !e.End.IsZero() { + s.file(ed, "end", tfmt(e.End, e.AllDay)+"\n") + } + if e.Location != "" { + s.file(ed, "location", e.Location+"\n") + } + if e.Description != "" { + s.file(ed, "description", e.Description+"\n") + } + if e.RRule != "" { + s.file(ed, "rrule", e.RRule+"\n") + } + if e.Organizer != "" { + s.file(ed, "organizer", e.Organizer+"\n") + } + if len(e.Attendees) > 0 { + s.file(ed, "attendees", attendeeText(e.Attendees)) + } + s.file(ed, "uid", e.UID+"\n") + s.file(ed, "raw", e.Raw) + } +} + +// buildWhen populates events/date/YYYY/MM/DD/ with one file per occurrence. +func (s *Server) buildWhen(root *fs.StaticDir, insts []Instance) { + w := s.subdir(s.subdir(root, "events"), "date") + for _, in := range insts { + // File by local wall-clock time. Events arrive in a mix of + // zones -- TZID=America/New_York here, UTC there -- and if the + // path keeps each event's own zone then a day's files neither + // sort by time nor land on the right day. All-day events are + // floating and must not be shifted. + st := in.Start + if !in.Ev.AllDay { + st = st.Local() + } + y := s.subdir(w, st.Format("2006")) + m := s.subdir(y, st.Format("01")) + d := s.subdir(m, st.Format("02")) + + // The first field is always four digits so that shell tools can + // compare it numerically; all-day events sort to the top of the + // day and are still marked as such. + name := st.Format("1504") + "-" + slug(in.Ev.Summary) + if in.Ev.AllDay { + name = "0000-allday-" + slug(in.Ev.Summary) + } + // An overridden occurrence belongs to its series' directory. + ev := in.Ev + if ev.master != nil { + ev = ev.master + } + fname := uniqueName(d, name) + s.file(d, fname, instText(in, s.evdir[ev])) + s.index = append(s.index, index{ + // relative to the server's root: it cannot know where it + // has been mounted, and under /mnt/pim/calendars/<name> + // it would guess wrong + path: fmt.Sprintf("events/date/%s/%s", st.Format("2006/01/02"), fname), + in: in, + }) + } +} + +func instText(in Instance, evdir string) string { + var b strings.Builder + fmt.Fprintf(&b, "summary: %s\n", in.Ev.Summary) + fmt.Fprintf(&b, "start: %s\n", tfmt(in.Start, in.Ev.AllDay)) + fmt.Fprintf(&b, "end: %s\n", tfmt(in.End, in.Ev.AllDay)) + if in.Ev.Location != "" { + fmt.Fprintf(&b, "location: %s\n", in.Ev.Location) + } + // Epoch seconds as well as RFC3339: rc has no way to parse the + // latter, but date(1) formats the former. + fmt.Fprintf(&b, "epoch: %d\n", in.Start.Unix()) + fmt.Fprintf(&b, "epochend: %d\n", in.End.Unix()) + fmt.Fprintf(&b, "uid: %s\n", in.Ev.UID) + // 9P2000 has no symlinks, so publish the path instead. A tool must + // never have to reproduce the server's slug rules to find this. + if evdir != "" { + fmt.Fprintf(&b, "event: ../../../../uuid/%s\n", evdir) + } + if in.Ev.RRule != "" { + fmt.Fprintf(&b, "rrule: %s\n", in.Ev.RRule) + } + if in.Ev.Description != "" { + fmt.Fprintf(&b, "\n%s\n", strings.TrimRight(in.Ev.Description, "\n")) + } + return b.String() +} + +// expand returns every instance in [t0,t1), sorted by start time. +func expand(evs []*Event, t0, t1 time.Time) []Instance { + var out []Instance + for _, e := range evs { + out = append(out, e.Instances(t0, t1)...) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Start.Equal(out[j].Start) { + return out[i].Ev.Summary < out[j].Ev.Summary + } + return out[i].Start.Before(out[j].Start) + }) + return out +} + +// attendeeText is one attendee per line: status, name, address. +func attendeeText(as []Attendee) string { + var b strings.Builder + for _, a := range as { + st := a.Partstat + if st == "" { + st = "UNKNOWN" + } + name := a.Name + if name == "" { + name = a.Email + } + fmt.Fprintf(&b, "%-12s\t%s\t%s\n", st, name, a.Email) + } + return b.String() +} |
