package main import "bytes" import "crypto/sha1" import "encoding/hex" import "encoding/json" import "errors" import "fmt" import "io" import "os" import "path/filepath" import "regexp" import "slices" import "sort" import "strconv" import "time" var stripname *regexp.Regexp var NotFound error func init() { stripname, _ = regexp.Compile("[^a-zA-Z0-9_]") NotFound = errors.New("object not found") } var Mtpt string var Jrnls map[string]*Jrnl type Jrnl struct { Id string Passhash Passhash Timezone *Location } type Passhash struct { Hash [20]byte } type Location time.Location type Entry struct { Jrnl *Jrnl Date Date } type EntryVersion struct { Entry *Entry Id int Body string ModTime time.Time } type Date struct { Year, Month, Day int } func (d Date) URL() string { return fmt.Sprintf("%04d/%02d/%02d", d.Year, d.Month, d.Day) } func (d Date) String() string { return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) } func Time2Date(ts time.Time) Date { return Date { ts.Year(), int(ts.Month()), ts.Day() } } func (p *Passhash) UnmarshalJSON(b []byte) error { var s string dec := json.NewDecoder(bytes.NewBuffer(b)) if err := dec.Decode(&s); err != nil { return errors.New("sha1 password hash is not a string") } if len(s) != 40 { return errors.New("sha1 password hash is not 40 chars") } if _, err := hex.Decode(p.Hash[:], []byte(s)); err != nil { return errors.New("sha1 password hash is not hexadecimal") } return nil } func (p *Passhash) Check(pw string) bool { return sha1.Sum([]byte(pw)) == p.Hash } func (l *Location) UnmarshalJSON(b []byte) error { var s string dec := json.NewDecoder(bytes.NewBuffer(b)) if err := dec.Decode(&s); err != nil { return errors.New("timezone is not a string") } loc, err := time.LoadLocation(s) if err != nil { return err } *l = Location(*loc) return nil } func MountDB() error { var rawJrnls []*Jrnl fp, err := os.Open(filepath.Join(Mtpt, "users.json")) if err != nil { return err } dec := json.NewDecoder(fp) if err = dec.Decode(&rawJrnls); err == io.EOF { return errors.New("must have users in users.json!") } else if err != nil { return err } Jrnls = make(map [string]*Jrnl) for _, jrnl := range rawJrnls { jrnl.Id = stripname.ReplaceAllString(jrnl.Id, "") if _, ok := Jrnls[jrnl.Id]; ok { return errors.New("overlapping usernames") } Jrnls[jrnl.Id] = jrnl if err := os.MkdirAll(jrnl.Path(), 0750); err != nil { return err } } return nil } func (j *Jrnl) Path() string { return filepath.Join(Mtpt, j.Id) } func (j *Jrnl) GetYearText(year int) (s string, err error) { ents, err := j.ListYearEntries(year) if err != nil { return s, err } for _, e := range ents { n, err := e.GetLatest() if err != nil { return "", err } v, err := e.GetVersion(n) if err != nil { return "", err } s += v.GetText() } return s, nil } func (j *Jrnl) GetText() (s string, err error) { years, err := j.ListYears() if err != nil { return s, err } for _, year := range years { yBody, err := j.GetYearText(year) if err != nil { return "", err } s += yBody } return s, nil } func (j *Jrnl) GetBackup() (s string, err error) { ents, err := j.ListEntries() if err != nil { return s, err } for _, e := range ents { vers, err := e.GetVersions() if err != nil { return s, err } slices.Reverse(vers) for _, v := range vers { s += v.GetText() } } return s, nil } func (j *Jrnl) PutText(s string) (vers []EntryVersion, err error) { for { if ts, body, ok := NextEntry(&s); ok { v, new, err := j.PutEntry(Time2Date(ts), body) if err != nil { return vers, err } if new { vers = append(vers, v) } } else { break } } return vers, nil } func (j *Jrnl) ListYears() (years []int, err error) { readdir, err := os.ReadDir(j.Path()) if err != nil { return years, err } years = make([]int, 0, len(readdir)) for _, ent := range readdir { if !ent.IsDir() { continue } if n, err := strconv.ParseUint(ent.Name(), 10, 32); err == nil { years = append(years, int(n)) } } sort.Slice(years, func(i, j int) bool { return years[i] > years[j] }) return } func (j *Jrnl) ListYearEntries(year int) (ents []Entry, err error) { ents = make([]Entry, 0, 365) for month := 12; month > 0; month-- { for day := 31; day > 0; day-- { if e, err := j.GetEntry(Date { year, month, day }); err == nil { ents = append(ents, e) } else if err != NotFound { return ents, err } } } return ents, err } func (j *Jrnl) ListEntries() (ents []Entry, err error) { years, err := j.ListYears() if err != nil { return ents, err } ents = make([]Entry, 0, len(years) * 365) for _, year := range years { yEnts, err := j.ListYearEntries(year) if err != nil { return ents, err } ents = append(ents, yEnts...) } return ents, err } func (j *Jrnl) newEntry(date Date) (e Entry) { e.Jrnl = j e.Date = date return } func (j *Jrnl) HasEntry(date Date) (exists bool, err error) { e := j.newEntry(date) if fi, err := os.Stat(e.Path()); fi != nil && fi.IsDir() { return true, nil } else if err.(*os.PathError).Err.Error() == "no such file or directory" { return false, nil } else { return false, err } } func (j *Jrnl) GetEntry(date Date) (e Entry, err error) { e = j.newEntry(date) if fi, err := os.Stat(e.Path()); fi != nil && fi.IsDir() { return e, nil } else if err.(*os.PathError).Err.Error() == "no such file or directory" { return e, NotFound } else { return e, err } } func (j *Jrnl) PutEntry(date Date, text string) (v EntryVersion, new bool, err error) { e := j.newEntry(date) path := e.Path() if err = os.MkdirAll(path, 0750); err != nil { return } n, err := e.GetLatest() if err != nil { return v, new, err } // don't add a duplicate version if n >= 0 { latest, err := e.GetVersion(n) if err != nil { return v, new, err } if latest.Body == text { return latest, false, err } } for { n++ latestpath := filepath.Join(path, "latest") err = os.WriteFile(latestpath, []byte(fmt.Sprint(n)), 0640) if err != nil { return v, new, err } verpath := filepath.Join(path, fmt.Sprint(n)) fp, err := os.OpenFile(verpath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0640) if err != nil && err.(*os.PathError).Err.Error() == "file exists" { continue } else if err != nil { return v, new, err } _, err = fp.WriteString(text) fp.Close() if err != nil { return v, new, err } else { v, err = e.GetVersion(n) return v, true, err } } } func (e *Entry) Path() string { return filepath.Join(e.Jrnl.Path(), e.Date.URL()) } func (e *Entry) GetLatest() (n int, err error) { data, err := os.ReadFile(filepath.Join(e.Path(), "latest")) if err != nil && err.(*os.PathError).Err.Error() == "no such file or directory" { return -1, nil } else if err != nil { return n, err } parsed, err := strconv.ParseUint(string(data), 10, 32) if err != nil { return -1, nil } else { return int(parsed), nil } } func (e *Entry) GetVersion(n int) (v EntryVersion, err error) { v.Entry = e v.Id = n path := v.Path() fi, err := os.Stat(path) if err != nil && err.(*os.PathError).Err.Error() == "no such file or directory" { return v, NotFound } else if err != nil { return v, err } v.ModTime = fi.ModTime().In((*time.Location)(e.Jrnl.Timezone)) body, err := os.ReadFile(path) if err != nil { return v, err } v.Body = string(body) return v, nil } func (e *Entry) GetVersions() (vers []EntryVersion, err error) { n, err := e.GetLatest() if err != nil { return vers, err } vers = make([]EntryVersion, n + 1) for i := 0; i <= n; i++ { v, err := e.GetVersion(i) if err != nil { return vers, err } vers[n - i] = v } return vers, nil } func (v *EntryVersion) Path() string { return filepath.Join(v.Entry.Path(), fmt.Sprint(v.Id)) } func (v *EntryVersion) GetText() string { e := v.Entry return fmt.Sprintf("> %s\n\n%s\n\n\n", e.Date, v.Body) }