M db.go => db.go +128 -43
@@ 10,6 10,7 @@ import "io"
import "os"
import "path/filepath"
import "regexp"
+import "slices"
import "sort"
import "strconv"
import "time"
@@ 38,16 39,29 @@ type Passhash struct {
}
type Location time.Location
type Entry struct {
- jrnl *Jrnl
- Year, Month, Day int
+ Jrnl *Jrnl
+ Date Date
}
type EntryVersion struct {
- entry *Entry
+ 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
@@ 124,8 138,8 @@ func (j *Jrnl) Path() string {
return filepath.Join(Mtpt, j.Id)
}
-func (j *Jrnl) GetText() (s string, err error) {
- ents, err := j.ListEntries()
+func (j *Jrnl) GetYearText(year int) (s string, err error) {
+ ents, err := j.ListYearEntries(year)
if err != nil {
return s, err
}
@@ 141,33 155,73 @@ func (j *Jrnl) GetText() (s string, err error) {
return "", err
}
- s += fmt.Sprintf("> %04d-%02d-%02d\n\n%s\n\n\n", e.Year, e.Month, e.Day, v.Body)
+ 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) error {
+func (j *Jrnl) PutText(s string) (vers []EntryVersion, err error) {
for {
if ts, body, ok := NextEntry(&s); ok {
- _, err := j.PutEntry(ts.Year(), int(ts.Month()), ts.Day(), body)
+ v, new, err := j.PutEntry(Time2Date(ts), body)
if err != nil {
- return err
+ return vers, err
+ }
+ if new {
+ vers = append(vers, v)
}
} else {
break
}
}
- return nil
+ return vers, nil
}
-func (j *Jrnl) ListEntries() (ents []Entry, err error) {
+func (j *Jrnl) ListYears() (years []int, err error) {
readdir, err := os.ReadDir(j.Path())
if err != nil {
- return ents, err
+ return years, err
}
- years := make([]int, 0, len(readdir))
+ 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 {
@@ 177,30 231,58 @@ func (j *Jrnl) ListEntries() (ents []Entry, err error) {
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 {
- for month := 12; month > 0; month-- {
- for day := 31; day > 0; day-- {
- if e, err := j.GetEntry(year, month, day); err == nil {
- ents = append(ents, e)
- } else if err != NotFound {
- return ents, err
- }
- }
+ yEnts, err := j.ListYearEntries(year)
+ if err != nil {
+ return ents, err
}
+ ents = append(ents, yEnts...)
}
return ents, err
}
-func (j *Jrnl) newEntry(year, month, day int) (e Entry) {
- e.jrnl = j
- e.Year = year
- e.Month = month
- e.Day = day
+
+func (j *Jrnl) newEntry(date Date) (e Entry) {
+ e.Jrnl = j
+ e.Date = date
return
}
-func (j *Jrnl) GetEntry(year, month, day int) (e Entry, err error) {
- e = j.newEntry(year, month, day)
+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
@@ 210,8 292,8 @@ func (j *Jrnl) GetEntry(year, month, day int) (e Entry, err error) {
return e, err
}
}
-func (j *Jrnl) PutEntry(year, month, day int, text string) (v EntryVersion, err error) {
- e := j.newEntry(year, month, day)
+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 {
@@ 220,18 302,18 @@ func (j *Jrnl) PutEntry(year, month, day int, text string) (v EntryVersion, err
n, err := e.GetLatest()
if err != nil {
- return
+ return v, new, err
}
// don't add a duplicate version
if n >= 0 {
latest, err := e.GetVersion(n)
if err != nil {
- return v, err
+ return v, new, err
}
if latest.Body == text {
- return latest, err
+ return latest, false, err
}
}
@@ 241,7 323,7 @@ func (j *Jrnl) PutEntry(year, month, day int, text string) (v EntryVersion, err
latestpath := filepath.Join(path, "latest")
err = os.WriteFile(latestpath, []byte(fmt.Sprint(n)), 0640)
if err != nil {
- return
+ return v, new, err
}
verpath := filepath.Join(path, fmt.Sprint(n))
@@ 249,23 331,22 @@ func (j *Jrnl) PutEntry(year, month, day int, text string) (v EntryVersion, err
if err != nil && err.(*os.PathError).Err.Error() == "file exists" {
continue
} else if err != nil {
- return v, err
+ return v, new, err
}
_, err = fp.WriteString(text)
fp.Close()
if err != nil {
- return v, err
+ return v, new, err
} else {
- return e.GetVersion(n)
+ v, err = e.GetVersion(n)
+ return v, true, err
}
}
}
func (e *Entry) Path() string {
- return filepath.Join(
- e.jrnl.Path(),
- fmt.Sprintf("%04d/%02d/%02d", e.Year, e.Month, e.Day) )
+ 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"))
@@ 283,7 364,7 @@ func (e *Entry) GetLatest() (n int, err error) {
}
}
func (e *Entry) GetVersion(n int) (v EntryVersion, err error) {
- v.entry = e
+ v.Entry = e
v.Id = n
path := v.Path()
@@ 294,7 375,7 @@ func (e *Entry) GetVersion(n int) (v EntryVersion, err error) {
} else if err != nil {
return v, err
}
- v.ModTime = fi.ModTime()
+ v.ModTime = fi.ModTime().In((*time.Location)(e.Jrnl.Timezone))
body, err := os.ReadFile(path)
if err != nil {
@@ 323,5 404,9 @@ func (e *Entry) GetVersions() (vers []EntryVersion, err error) {
}
func (v *EntryVersion) Path() string {
- return filepath.Join(v.entry.Path(), fmt.Sprint(v.Id))
+ 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)
}
M jrnl.go => jrnl.go +26 -5
@@ 1,6 1,8 @@
package main
import "fmt"
+import "log"
+import "net/http"
import "os"
func fatal(err error) {
@@ 18,7 20,7 @@ func main() {
usage()
}
-// addr := os.Args[1]
+ addr := os.Args[1]
if len(os.Args) == 3 {
Mtpt = os.Args[2]
} else {
@@ 29,8 31,27 @@ func main() {
fatal(err)
}
- data, err := os.ReadFile("/home/amity/Documents/jrnl2.txt")
- if err != nil { panic(err) }
- Jrnls["amity"].PutText(string(data))
- fmt.Println(Jrnls["amity"].GetText())
+ http.HandleFunc("/{$}", Redirect("/dash", 301))
+ http.HandleFunc("GET /dash", Dash)
+
+ http.HandleFunc("GET /goto", GotoRedirect)
+
+ http.HandleFunc("GET /init", InitGet)
+ http.HandleFunc("POST /init", InitPost)
+
+ http.HandleFunc("GET /save", SaveGet)
+
+ http.HandleFunc("GET /jrnl/{$}", JrnlRedirect)
+ http.HandleFunc("GET /jrnl/{year}", JrnlGet)
+ http.HandleFunc("POST /jrnl/{year}", JrnlPost)
+
+ http.HandleFunc("GET /ntry/{$}", NtriesGet)
+ http.HandleFunc("GET /ntry/{year}/{month}/{day}/{$}", NtryGet)
+ http.HandleFunc("POST /ntry/{year}/{month}/{day}/{$}", NtryPost)
+
+ http.HandleFunc("/", NotFoundGet)
+
+ fmt.Println("initialized!")
+
+ log.Fatal(http.ListenAndServe(addr, nil))
}
M parse.go => parse.go +3 -0
@@ 7,6 7,9 @@ const HeadFormat = "> 2006-01-02"
func NextLine(s string) (line, rest string) {
line, rest, _ = strings.Cut(s, "\n")
+ if line[len(line) - 1] == '\r' {
+ line = line[:len(line) - 1]
+ }
return
}
M plan.txt => plan.txt +15 -10
@@ 1,19 1,24 @@
endpoints:
+ /
+ redirect to /dash
/dash - GET
convenient links
- /jrnl - GET, POST
- the full text of the jrnl. POSTing text will lead to
- the entries in it being created.
- /ntry/ - GET, POST
- list of entry slugs, hyperlinks in html
- /ntry/{slug}/ - GET, PATCH
- entry info. list of versions, text of the most recent
- one. allows one to rename an entry or post a new ver.
- /ntry/{slug}/{ver}/ - GET
- text of a given version
+ /init - GET, POST
+ paste in existing jrnl contents, get back an import log
+ /jrnl/ - GET
+ redirect to /jrnl/currentYear
+ /jrnl/(year} - GET, POST
+ the full text of the jrnl for that year. POSTing text
+ will lead to the entries in it being created.
+ /ntry - GET
+ full log
+ /ntry/{year}/{month}/{day}/ - GET, POST
+ entry info.
/edit - GET
log of edits. each edit has an ID, so polling this will
allow js code to edit the <textarea> in realtime
+ /save - GET
+ complete jrnl text for backup
HTTP basic auth
A template/404.html => template/404.html +4 -0
@@ 0,0 1,4 @@
+{{template "head" .}}
+ <p>Whatever you're looking for, it isn't here.</p>
+{{template "foot" .}}
+
A template/500.html => template/500.html +6 -0
@@ 0,0 1,6 @@
+{{template "head" .}}
+ <p>Something went wrong.</p>
+ <pre>Error Type: {{ .Type }}
+Error Info: {{ .Error }}</pre>
+{{template "foot" .}}
+
A template/base.html => template/base.html +27 -0
@@ 0,0 1,27 @@
+{{define "head"}}
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>{{.Title}}</title>
+ <style>
+ textarea {
+ background-color: #ffe;
+ color: #223
+ }
+ html {
+ background-color: #eef;
+ color: #322
+ }
+ </style>
+</head>
+<body>
+{{.Links}}
+<h1>{{.Title}}</h1>
+{{end}}
+
+{{define "foot"}}
+<p class="footer"><em>powered by <a href="https://git.amehut.dev/~aleteoryx/webjrnl">jrnl v0</a></em></p>
+</body>
+</html>
+{{end}}
A template/dash.html => template/dash.html +32 -0
@@ 0,0 1,32 @@
+{{template "head" .}}
+ <p>o/ {{.Jrnl.Id}}</p>
+ {{ $hasToday := .Jrnl.HasEntry .Today }}
+ {{ $hasYesterday := .Jrnl.HasEntry .Yesterday }}
+
+ <table cellspacing="0"><tr>
+ <td>Today is {{ .Today }}.</td>
+ <td> </td>
+ {{if not $hasToday}}
+ <td><form method="POST" action="jrnl/{{ .Today.Year }}">
+ <input type="hidden" name="text" value="> {{ .Today.URL }} " />
+ <button type="submit">Start today's entry!</button>
+ </form></td>
+ {{end}}
+ {{if not $hasYesterday}}
+ <td><form method="POST" action="jrnl/{{ .Yesterday.Year }}">
+ <input type="hidden" name="text" value="> {{ .Yesterday.URL }} " />
+ <button type="submit">Start yesterday's entry!</button>
+ </form></td>
+ {{end}}
+ </tr></table>
+
+ <hr/>
+
+ <p>
+ {{range $i, $year := .Jrnl.ListYears}}
+ {{if ne $i 0}} • {{end}}
+ <a href="jrnl/{{ $year }}">{{ $year }}</a>
+ {{end}}
+ </p>
+{{template "foot" .}}
+
A template/init.html => template/init.html +9 -0
@@ 0,0 1,9 @@
+{{template "head" .}}
+ <p>Paste jrnl text below.</p>
+ <form method="POST">
+ <textarea name="text" cols="72" rows="24"></textarea>
+ <br/>
+ <button type="submit">Import!</button>
+ </form>
+{{template "foot" .}}
+
A template/jrnl.html => template/jrnl.html +29 -0
@@ 0,0 1,29 @@
+{{template "head" .}}
+ {{$text := .Jrnl.GetYearText .Year}}
+ {{if eq $text ""}}
+ <p>It's empty right now. Write a <code>> date heading</code> to get started.</p>
+ {{else}}
+ <p>Edit an entry, or write a <code>> date heading</code> to add a new one.</p>
+ {{end}}
+ <form method="POST">
+ <textarea name="text" cols="72" rows="24">{{$text}}</textarea>
+ <br/>
+ <button type="submit">Update!</button>
+ </form>
+ {{if not (eq $text "")}}
+ <br>
+ <form method="GET" action="../goto">
+ <input type="hidden" name="pfx" value="ntry/{{.Year}}/" />
+ <label for="dest">View history for:</label>
+ <select name="dest">
+ {{range .Jrnl.ListYearEntries .Year}}
+ <option value="{{printf "%02d/%02d" .Date.Month .Date.Day}}">
+ {{printf "%02d-%02d" .Date.Month .Date.Day}}</option>
+ {{end}}
+ </select>
+
+ <button type="submit">Go To</button>
+ </form>
+ {{end}}
+{{template "foot" .}}
+
A template/ntries.html => template/ntries.html +15 -0
@@ 0,0 1,15 @@
+{{template "head" .}}
+ {{ $pfx := .Pfx }}
+ <p>{{ .Blurb }}</p>
+ {{if ne (len .Ntries) 0}}
+ <ul>
+ {{range $i, $e := .Ntries}}
+ <li>
+ <a href="{{ $pfx }}ntry/{{ $e.Entry.Date.URL }}">{{ $e.Entry.Date }}</a>
+ v{{ $e.Id }}
+ </li>
+ {{end}}
+ </ul>
+ {{end}}
+{{template "foot" .}}
+
A template/ntry.html => template/ntry.html +37 -0
@@ 0,0 1,37 @@
+{{template "head" .}}
+ {{ $latest := .Entry.GetVersion .Entry.GetLatest }}
+ <p>
+ {{if eq $latest.Id 0}}
+ It hasn't been updated.
+ {{else if eq $latest.Id 1}}
+ It's been updated once.
+ {{else}}
+ It's been updated {{ $latest.Id }} times.
+ {{end}}
+ This version is from {{ $latest.ModTime.Format "2006-01-02 03:04:05" }}.
+ </p>
+ <form method="POST">
+ <textarea name="text" cols="72" rows="24">{{ $latest.Body }}</textarea>
+ <br/>
+ <button type="submit">Update!</button>
+ </form>
+ <br>
+ {{if ne $latest.Id 0}}
+ <h3>History:</h3>
+ <ul>
+ {{range .Entry.GetVersions}}
+ {{if ne .Id $latest.Id}}
+ <li>
+ <h4>{{ .ModTime.Format "2006-01-02 03:04:05" }}</h4>
+ <pre>{{ .Body }}</pre>
+ <form method="POST">
+ <input type="hidden" name="text" value="{{ .Body }}" />
+ <button type="submit">Revert</button>
+ </form>
+ </li>
+ {{end}}
+ {{end}}
+ </ul>
+ {{end}}
+{{template "foot" .}}
+
A template/save.html => template/save.html +7 -0
@@ 0,0 1,7 @@
+{{template "head" .}}
+ <p>This is a full backup of your jrnl, including every version of every entry.
+ Modification dates are not preserved.</p>
+ <textarea name="text" cols="72" rows="24">{{ .Jrnl.GetBackup }}</textarea>
+ <form method="GET"><button name="download">Download!</button></form>
+{{template "foot" .}}
+
A web.go => web.go +472 -0
@@ 0,0 1,472 @@
+package main
+
+import "embed"
+import "fmt"
+import "html/template"
+import "net/http"
+import "reflect"
+import "strconv"
+import "strings"
+import "time"
+
+
+const dashLinks =
+ template.HTML(
+ "<p><a href=\"ntry\">entries</a> • " +
+ "<a href=\"init\">import</a> • " +
+ "<a href=\"save\">export</a></p>" )
+
+//go:embed template/*
+var templateFS embed.FS
+var Templates *template.Template
+func init() {
+ var err error
+
+ Templates, err = template.ParseFS(templateFS, "template/*.html")
+ if err != nil {
+ panic(err)
+ }
+}
+
+
+func Req2Date(r *http.Request) (date Date, ok bool) {
+ year, err := strconv.ParseUint(r.PathValue("year"), 10, 32)
+ if err != nil {
+ return date, false
+ }
+ month, err := strconv.ParseUint(r.PathValue("month"), 10, 32)
+ if err != nil {
+ return date, false
+ }
+ day, err := strconv.ParseUint(r.PathValue("day"), 10, 32)
+ if err != nil {
+ return date, false
+ }
+
+ date = Time2Date(time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC))
+ if date.Year != int(year) || date.Month != int(month) || date.Day != int(day) {
+ return date, false
+ }
+
+ return date, true
+}
+
+func NotImplemented(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "Not Implemented!", 400)
+}
+
+
+func Login(r *http.Request) *Jrnl {
+ user, pass, ok := r.BasicAuth()
+ if !ok {
+ return nil
+ }
+
+ jrnl, ok := Jrnls[user]
+ if !ok {
+ return nil
+ }
+
+ if !jrnl.Passhash.Check(pass) {
+ return nil
+ } else {
+ return jrnl
+ }
+}
+func MustLogin(w http.ResponseWriter, r *http.Request) *Jrnl {
+ jrnl := Login(r)
+ if jrnl == nil {
+ w.Header().Add("WWW-Authenticate", "Basic realm=\"jrnl\"")
+ http.Error(w, "Unauthorized!", 401)
+ return nil
+ } else {
+ return jrnl
+ }
+}
+
+type Err500Page struct {
+ Links template.HTML
+ Title string
+ Type reflect.Type
+ Error error
+}
+func Err500(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, err error) {
+ w.WriteHeader(500)
+ ReplyTemplate(
+ w, r, jrnl, "500.html",
+ Err500Page {
+ Links: BuildLinks(r, jrnl),
+ Title: "Jrnl Server Error",
+ Type: reflect.TypeOf(err),
+ Error: err } )
+}
+
+func NotFoundGet(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ NotFoundPage(w, r, jrnl)
+}
+type NotFoundPageData struct {
+ Links template.HTML
+ Title string
+}
+func NotFoundPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl) {
+ w.WriteHeader(404)
+ ReplyTemplate(
+ w, r, jrnl, "404.html",
+ NotFoundPageData {
+ Links: BuildLinks(r, jrnl),
+ Title: "Jrnl Page Not Found" } )
+}
+
+func ReplyTemplate(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, tpl string, ctx interface{}) {
+ var buf strings.Builder
+
+ err := Templates.ExecuteTemplate(&buf, tpl, ctx)
+ if err != nil {
+ msg := fmt.Sprintf("error in template %s: %s", tpl, err)
+ fmt.Println(msg)
+ Err500(w, r, jrnl, err)
+ } else {
+ w.Write([]byte(buf.String()))
+ }
+}
+
+func Redirect(loc string, code int) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ http.Redirect(w, r, loc, code)
+ }
+}
+
+func BuildLinks(r *http.Request, jrnl *Jrnl) template.HTML {
+ if jrnl == nil {
+ return template.HTML("")
+ }
+
+ pfx := strings.Repeat("../", strings.Count(r.URL.Path, "/"))
+ links := fmt.Sprintf("<a href=\"%sdash\">dash</a>", pfx)
+
+ years, _ := jrnl.ListYears()
+ for i, year := range years {
+ if i < 5 {
+ links += " • "
+ links += fmt.Sprintf(
+ "<a href=\"%sjrnl/%04d\">jrnl/%04d</a>",
+ pfx, year, year )
+ } else {
+ break
+ }
+ }
+
+ return template.HTML("<p>" + links + "</p>")
+}
+
+
+func NtriesGet(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ ents, err := jrnl.ListEntries()
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ vers := make([]EntryVersion, len(ents))
+ for i, e := range ents {
+ n, err := e.GetLatest()
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+ v, err := e.GetVersion(n)
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ vers[i] = v
+ }
+
+ NtriesPage(
+ w, r, jrnl,
+ "Jrnl Entry Index",
+ "This is a log of every jrnl entry you've made.",
+ vers )
+}
+
+type NtriesPageData struct {
+ Links template.HTML
+ Title, Blurb string
+ Ntries []EntryVersion
+ Pfx string
+}
+func NtriesPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, Title, Blurb string, ntries []EntryVersion) {
+ pfx := strings.Repeat("../", strings.Count(r.URL.Path, "/"))
+ ReplyTemplate(
+ w, r, jrnl, "ntries.html",
+ NtriesPageData {
+ Links: BuildLinks(r, jrnl),
+ Title: Title, Blurb: Blurb,
+ Ntries: ntries,
+ Pfx: pfx } )
+}
+
+type DashPage struct {
+ Links template.HTML
+ Title string
+ Jrnl *Jrnl
+ Today, Yesterday Date
+}
+func Dash(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ today := time.Now().In((*time.Location)(jrnl.Timezone))
+ yesterday := today.AddDate(0,0,-1)
+
+ ReplyTemplate(
+ w, r, jrnl, "dash.html",
+ DashPage {
+ Links: dashLinks,
+ Title: "Jrnl Dash",
+ Jrnl: jrnl,
+ Today: Time2Date(today),
+ Yesterday: Time2Date(yesterday), } )
+}
+
+
+func GotoRedirect(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ query := r.URL.Query()
+ pfx, _ := query["pfx"]
+ dest, _ := query["dest"]
+
+ path := strings.Join(pfx, "") + strings.Join(dest, "")
+
+ http.Redirect(w, r, path, 301)
+}
+
+
+func JrnlRedirect(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ years, err := jrnl.ListYears()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ } else if len(years) > 0 {
+ http.Redirect(w, r, fmt.Sprintf("%04d", years[0]), 302)
+ } else {
+ http.Redirect(w, r, "../init", 302)
+ }
+}
+
+func JrnlGet(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ year, err := strconv.ParseUint(r.PathValue("year"), 10, 32)
+ if err != nil {
+ NotFoundPage(w, r, jrnl)
+ return
+ }
+
+ JrnlPage(w, r, jrnl, int(year))
+}
+func JrnlPost(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ vers, err := jrnl.PutText(r.FormValue("text"))
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ urlYear, err := strconv.ParseUint(r.PathValue("year"), 10, 32)
+ if err != nil {
+ NotFoundPage(w, r, jrnl)
+ return
+ }
+
+ if len(vers) == 0 || (len(vers) == 1 && vers[0].Entry.Date.Year == int(urlYear)) {
+ JrnlPage(w, r, jrnl, int(urlYear))
+ } else if len(vers) == 1 {
+ http.Redirect(w, r, fmt.Sprintf("%04d", vers[0].Entry.Date.Year), 302)
+ } else {
+ NtriesPage(
+ w, r, jrnl,
+ "Jrnl Update",
+ "The following entries were changed:",
+ vers )
+ }
+}
+
+type JrnlPageData struct {
+ Links template.HTML
+ Title string
+ Jrnl *Jrnl
+ Year int
+}
+func JrnlPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, year int) {
+ ReplyTemplate(
+ w, r, jrnl, "jrnl.html",
+ JrnlPageData {
+ Links: BuildLinks(r, jrnl),
+ Title: fmt.Sprintf("Jrnl for Year %04d", year),
+ Jrnl: jrnl,
+ Year: year} )
+}
+
+
+type InitPageData struct {
+ Links template.HTML
+ Title string
+}
+func InitGet(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ ReplyTemplate(
+ w, r, jrnl, "init.html",
+ InitPageData { BuildLinks(r, jrnl), "Jrnl Import" } )
+}
+func InitPost(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ vers, err := jrnl.PutText(r.FormValue("text"))
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ var blurb string
+ if len(vers) < 1 {
+ blurb = "No entries were imported."
+ } else if len(vers) == 1 {
+ blurb = "The following entry was imported:"
+ } else {
+ blurb = "The following entries were imported:"
+ }
+
+ NtriesPage(w, r, jrnl, "Jrnl Import", blurb, vers)
+}
+
+type SavePageData struct {
+ Links template.HTML
+ Title string
+ Jrnl *Jrnl
+}
+func SaveGet(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ query := r.URL.Query()
+ if _, ok := query["download"]; ok {
+ backup, err := jrnl.GetBackup()
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ w.Header().Add("Content-Type", "text/plain; charset=UTF-8")
+ w.Header().Add("Content-Disposition", "attachment; filename=\"backup.txt\"")
+ w.Write([]byte(backup))
+ } else {
+ ReplyTemplate(
+ w, r, jrnl, "save.html",
+ SavePageData {
+ Links: BuildLinks(r, jrnl),
+ Title: "Jrnl Export",
+ Jrnl: jrnl } )
+ }
+}
+
+func NtryGet(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ date, ok := Req2Date(r)
+ if !ok {
+ NotFoundPage(w, r, jrnl)
+ return
+ }
+
+ NtryPage(w, r, jrnl, date)
+}
+func NtryPost(w http.ResponseWriter, r *http.Request) {
+ jrnl := MustLogin(w, r)
+ if jrnl == nil {
+ return
+ }
+
+ date, ok := Req2Date(r)
+ if !ok {
+ NotFoundPage(w, r, jrnl)
+ return
+ }
+
+ _, _, err := jrnl.PutEntry(date, r.FormValue("text"))
+ if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ NtryPage(w, r, jrnl, date)
+}
+
+type NtryPageData struct {
+ Links template.HTML
+ Title string
+ Entry *Entry
+}
+func NtryPage(w http.ResponseWriter, r *http.Request, jrnl *Jrnl, date Date) {
+
+ ent, err := jrnl.GetEntry(date)
+ if err == NotFound {
+ NotFoundPage(w, r, jrnl)
+ return
+ } else if err != nil {
+ Err500(w, r, jrnl, err)
+ return
+ }
+
+ ReplyTemplate(
+ w, r, jrnl, "ntry.html",
+ NtryPageData {
+ Links: BuildLinks(r, jrnl),
+ Title: fmt.Sprintf("Jrnl Entry for %s", date),
+ Entry: &ent } )
+}