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")
}
if jrnl.Timezone == nil {
jrnl.Timezone = (*Location)(time.UTC)
}
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)
}