Replace JS with Go templates for stability
This commit is contained in:
parent
0094534b31
commit
f0198a4a72
11 changed files with 353 additions and 374 deletions
109
api.go
Normal file
109
api.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleWrite checks for error in ResponseWriter.Write output
|
||||||
|
func HandleWrite(_ int, err error) {
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error writing response", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFile returns raw contents of a file
|
||||||
|
func GetFile(filename string, w http.ResponseWriter) {
|
||||||
|
fileContents, err := ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
http.Error(w, "file not found", http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "error reading found", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
HandleWrite(w.Write(fileContents))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostFile writes request's body contents to a file
|
||||||
|
func PostFile(filename string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
HandleWrite(w.Write([]byte("error reading body")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = SaveFile(filename, body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
HandleWrite(w.Write([]byte("error saving file")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
HandleWrite(w.Write([]byte("wrote to file")))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileList returns JSON list of filenames in a directory without extensions or path
|
||||||
|
func GetFileList(directory string, w http.ResponseWriter) {
|
||||||
|
filenames, err := ListFiles(directory)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error searching for files", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filenamesJson, err := json.Marshal(filenames)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "error marshaling json", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
HandleWrite(w.Write(filenamesJson))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDayApi returns a contents of a daily file specified in URL
|
||||||
|
func GetDayApi(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dayString := chi.URLParam(r, "day")
|
||||||
|
if dayString == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
HandleWrite(w.Write([]byte("day not specified")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GetFile("day/"+dayString, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTodayApi runs GetFile with today's date as filename
|
||||||
|
func GetTodayApi(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
GetFile("day/"+time.Now().Format(time.DateOnly), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostTodayApi runs PostFile with today's date as filename
|
||||||
|
func PostTodayApi(w http.ResponseWriter, r *http.Request) {
|
||||||
|
PostFile("day/"+time.Now().Format(time.DateOnly), w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNote returns contents of a note specified in URL
|
||||||
|
func GetNote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
noteString := chi.URLParam(r, "note")
|
||||||
|
if noteString == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
HandleWrite(w.Write([]byte("note not specified")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GetFile("notes/"+noteString, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostNote writes request's body contents to a note specified in URL
|
||||||
|
func PostNote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
noteString := chi.URLParam(r, "note")
|
||||||
|
if noteString == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
HandleWrite(w.Write([]byte("note not specified")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PostFile("notes/"+noteString, w, r)
|
||||||
|
}
|
107
files.go
107
files.go
|
@ -1,12 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -14,20 +10,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleWrite checks for error in ResponseWriter.Write output
|
// ReadFile returns raw contents of a file
|
||||||
func HandleWrite(_ int, err error) {
|
func ReadFile(filename string) ([]byte, error) {
|
||||||
if err != nil {
|
|
||||||
slog.Error("error writing response", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFile returns raw contents of a txt file in data directory
|
|
||||||
func GetFile(filename string, w http.ResponseWriter) {
|
|
||||||
filename = "data/" + path.Clean(filename) + ".txt" // Does this sanitize the path?
|
filename = "data/" + path.Clean(filename) + ".txt" // Does this sanitize the path?
|
||||||
|
|
||||||
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||||
http.Error(w, "file not found", http.StatusNotFound)
|
return nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileContents, err := os.ReadFile(filename)
|
fileContents, err := os.ReadFile(filename)
|
||||||
|
@ -35,103 +23,50 @@ func GetFile(filename string, w http.ResponseWriter) {
|
||||||
slog.Error("error reading file",
|
slog.Error("error reading file",
|
||||||
"error", err,
|
"error", err,
|
||||||
"file", filename)
|
"file", filename)
|
||||||
http.Error(w, "error reading file", http.StatusInternalServerError)
|
return nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = w.Write(fileContents)
|
return fileContents, nil
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "error sending file", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostFile Writes request's contents to a txt file in data directory
|
|
||||||
// TODO: Save to trash to prevent malicious/accidental overrides?
|
|
||||||
func PostFile(filename string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
HandleWrite(w.Write([]byte("error reading body")))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveFile Writes request's contents to a file
|
||||||
|
func SaveFile(filename string, contents []byte) error {
|
||||||
filename = "data/" + filename + ".txt"
|
filename = "data/" + filename + ".txt"
|
||||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error opening/making file",
|
slog.Error("error opening/making file",
|
||||||
"error", err,
|
"error", err,
|
||||||
"file", filename)
|
"file", filename)
|
||||||
HandleWrite(w.Write([]byte("error opening or creating file")))
|
return err
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if _, err := f.Write(contents); err != nil {
|
||||||
if _, err := f.Write(body); err != nil {
|
|
||||||
slog.Error("error writing to file",
|
slog.Error("error writing to file",
|
||||||
"error", err,
|
"error", err,
|
||||||
"file", filename)
|
"file", filename)
|
||||||
HandleWrite(w.Write([]byte("error writing to file")))
|
return err
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
HandleWrite(w.Write([]byte("wrote to file")))
|
return nil
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListFiles returns JSON list of filenames in a directory without extensions or path
|
// ListFiles returns slice of filenames in a directory without extensions or path
|
||||||
func ListFiles(directory string, w http.ResponseWriter) {
|
func ListFiles(directory string) ([]string, error) {
|
||||||
filenames, err := filepath.Glob("data/" + directory + "/*.txt")
|
filenames, err := filepath.Glob("data/" + directory + "/*.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "error searching for files", http.StatusInternalServerError)
|
return nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
for i, file := range filenames {
|
for i, file := range filenames {
|
||||||
file, _ := strings.CutSuffix(filepath.Base(file), filepath.Ext(file))
|
file, _ := strings.CutSuffix(filepath.Base(file), filepath.Ext(file))
|
||||||
filenames[i] = file
|
filenames[i] = file
|
||||||
}
|
}
|
||||||
filenamesJson, err := json.Marshal(filenames)
|
return filenames, nil
|
||||||
HandleWrite(w.Write(filenamesJson))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDay returns a day specified in URL
|
// ReadToday runs ReadFile with today's date as filename
|
||||||
func GetDay(w http.ResponseWriter, r *http.Request) {
|
func ReadToday() ([]byte, error) {
|
||||||
dayString := chi.URLParam(r, "day")
|
return ReadFile("day/" + time.Now().Format(time.DateOnly))
|
||||||
if dayString == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
HandleWrite(w.Write([]byte("day not specified")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
GetFile("day/"+dayString, w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToday runs GetFile with today's daily txt
|
// SaveToday runs SaveFile with today's date as filename
|
||||||
func GetToday(w http.ResponseWriter, _ *http.Request) {
|
func SaveToday(contents []byte) error {
|
||||||
GetFile("day/"+time.Now().Format("2006-01-02"), w)
|
return SaveFile("day/"+time.Now().Format(time.DateOnly), contents)
|
||||||
}
|
|
||||||
|
|
||||||
// PostToday runs PostFile with today's daily txt
|
|
||||||
func PostToday(w http.ResponseWriter, r *http.Request) {
|
|
||||||
PostFile("day/"+time.Now().Format("2006-01-02"), w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNote returns a note specified in URL
|
|
||||||
func GetNote(w http.ResponseWriter, r *http.Request) {
|
|
||||||
noteString := chi.URLParam(r, "note")
|
|
||||||
if noteString == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
HandleWrite(w.Write([]byte("note not specified")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
GetFile("notes/"+noteString, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostNote writes request's contents to a note specified in URL
|
|
||||||
func PostNote(w http.ResponseWriter, r *http.Request) {
|
|
||||||
noteString := chi.URLParam(r, "note")
|
|
||||||
if noteString == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
HandleWrite(w.Write([]byte("note not specified")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PostFile("notes/"+noteString, w, r)
|
|
||||||
}
|
}
|
||||||
|
|
57
log.go
57
log.go
|
@ -1,57 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AppendLog adds the input string to the end of the log file with a timestamp
|
|
||||||
func appendLog(input string) error {
|
|
||||||
t := time.Now().Format("2006-01-02 15:04") // yyyy-mm-dd HH:MM
|
|
||||||
filename := "data/log.txt"
|
|
||||||
|
|
||||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("error opening/making file",
|
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
input = strings.Replace(input, "\n", "", -1) // Remove newlines to maintain structure
|
|
||||||
if _, err := f.Write([]byte(t + " | " + input + "\n")); err != nil {
|
|
||||||
slog.Error("error appending to file",
|
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
slog.Error("error closing file",
|
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostLog(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte("error reading body"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = appendLog(string(body))
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte("error appending to log"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("appended to log"))
|
|
||||||
}
|
|
38
pages/base.html
Normal file
38
pages/base.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{{define "header"}}
|
||||||
|
<header>
|
||||||
|
<h1>🌺 Hibiscus.txt</h1>
|
||||||
|
<p id="status">Today is <span id="today-date">a place</span></p>
|
||||||
|
</header>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
{{define "base"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/public/main.css">
|
||||||
|
<script src="/public/date.js"></script>
|
||||||
|
<title>Hibiscus.txt</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "header" .}}
|
||||||
|
<main>
|
||||||
|
{{template "main" .}}
|
||||||
|
</main>
|
||||||
|
{{template "footer" .}}
|
||||||
|
<script defer>
|
||||||
|
updateDate()
|
||||||
|
beginDateUpdater()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "footer"}}
|
||||||
|
<footer>
|
||||||
|
<p><a href="/">today</a> | <a href="/day">previous days</a></p>
|
||||||
|
</footer>
|
||||||
|
{{end}}
|
|
@ -1,33 +1,4 @@
|
||||||
<!DOCTYPE html>
|
{{define "main"}}
|
||||||
<html lang="en">
|
<h2 style="margin-bottom:0;"><label for="day">Viewing {{ .Date }}</label> | <a href="/day">Go back</a></h2>
|
||||||
<head>
|
<textarea id="day" cols="40" rows="15" readonly>{{ .Day }}</textarea>
|
||||||
<meta charset="UTF-8">
|
{{end}}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
|
||||||
<link rel="stylesheet" href="/public/main.css">
|
|
||||||
<script src="/public/date.js"></script>
|
|
||||||
<script src="/public/requests.js"></script>
|
|
||||||
|
|
||||||
<title>Hibiscus.txt</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>🌺 Hibiscus.txt</h1>
|
|
||||||
<p id="status">Today is <span id="today-date">a place</span></p>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<h2 style="margin-bottom:0;"><label for="day">Viewing <span id="daytag"></span></label> | <a href="/days">Go back</a></h2>
|
|
||||||
<textarea id="day" cols="40" rows="15"></textarea>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p><a href="/">home </a> | <a href="/days">previous days</a> | <a href="/api/readme">readme.txt</a></p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script defer>
|
|
||||||
updateDate()
|
|
||||||
beginDateUpdater()
|
|
||||||
|
|
||||||
loadDay()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,33 +1,8 @@
|
||||||
<!DOCTYPE html>
|
{{define "main"}}
|
||||||
<html lang="en">
|
<h2>{{.Title}}</h2>
|
||||||
<head>
|
<ul id="days">
|
||||||
<meta charset="UTF-8">
|
{{range .Entries}}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<li><a href="/day/{{.Link}}">{{.Name}}</a></li>
|
||||||
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
{{end}}
|
||||||
<link rel="stylesheet" href="/public/main.css">
|
</ul>
|
||||||
<script src="/public/date.js"></script>
|
{{end}}
|
||||||
<script src="/public/requests.js"></script>
|
|
||||||
|
|
||||||
<title>Hibiscus.txt</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>🌺 Hibiscus.txt</h1>
|
|
||||||
<p id="status">Today is <span id="today-date">a place</span></p>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<h2>Previous days</h2>
|
|
||||||
<ul id="days"></ul>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p><a href="/">home </a> | <a href="/days">previous days</a> | <a href="/api/readme">readme.txt</a></p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script defer>
|
|
||||||
updateDate()
|
|
||||||
beginDateUpdater()
|
|
||||||
|
|
||||||
loadPrevious()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
17
pages/error/500.html
Normal file
17
pages/error/500.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/public/main.css">
|
||||||
|
<title>Error 500</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Error 500 - Internal Server Error</h1>
|
||||||
|
<p>It's probably not your fault, but something broke</p>
|
||||||
|
<h3><a href="/">Go home?</a></h3>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,34 +1,7 @@
|
||||||
<!DOCTYPE html>
|
{{define "main"}}
|
||||||
<html lang="en">
|
<form method="POST">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
|
||||||
<link rel="stylesheet" href="/public/main.css">
|
|
||||||
<script src="/public/date.js"></script>
|
|
||||||
<script src="/public/requests.js"></script>
|
|
||||||
|
|
||||||
<title>Hibiscus.txt</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>🌺 Hibiscus.txt</h1>
|
|
||||||
<p id="status">Today is <span id="today-date">a place</span></p>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<h2 style="margin-bottom:0;"><label for="day">Your day so far:</label></h2>
|
<h2 style="margin-bottom:0;"><label for="day">Your day so far:</label></h2>
|
||||||
<textarea id="day" cols="40" rows="15"></textarea>
|
<textarea id="day" cols="40" rows="15" name="day">{{ .Day }}</textarea>
|
||||||
<button onclick="saveToday()">Save</button>
|
<button type="submit">Save</button>
|
||||||
</main>
|
</form>
|
||||||
<footer>
|
{{end}}
|
||||||
<p><a href="/">home </a> | <a href="/days">previous days</a> | <a href="/api/readme">readme.txt</a></p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script defer>
|
|
||||||
updateDate()
|
|
||||||
beginDateUpdater()
|
|
||||||
|
|
||||||
loadToday()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,104 +0,0 @@
|
||||||
async function postData(url = "", data = "") {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
credentials: "same-origin",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/plain",
|
|
||||||
},
|
|
||||||
redirect: "follow",
|
|
||||||
referrerPolicy: "no-referrer",
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getData(url = "", data = "") {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
credentials: "same-origin",
|
|
||||||
redirect: "follow",
|
|
||||||
referrerPolicy: "no-referrer"
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
return response.text();
|
|
||||||
} else {
|
|
||||||
console.log(response.text())
|
|
||||||
return response.status
|
|
||||||
// return "Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveLog() {
|
|
||||||
let logField = document.getElementById("log")
|
|
||||||
postData("/api/log", logField.value).then((data) => {
|
|
||||||
if (data.ok) {
|
|
||||||
logField.value = ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveToday() {
|
|
||||||
let logField = document.getElementById("day")
|
|
||||||
postData("/api/today", logField.value).then((data) => {
|
|
||||||
if (!data.ok) {
|
|
||||||
alert(`Error saving: ${data.text()}`)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function loadToday() {
|
|
||||||
let dayField = document.getElementById("day")
|
|
||||||
getData("/api/today", dayField.value).then((data) => {
|
|
||||||
if (data === 404) {
|
|
||||||
dayField.value = ""
|
|
||||||
} else if (data === 401) {
|
|
||||||
dayField.readOnly = true
|
|
||||||
dayField.value = "Unauthorized"
|
|
||||||
} else if (data === 500) {
|
|
||||||
dayField.readOnly = true
|
|
||||||
dayField.value = "Internal server error"
|
|
||||||
} else {
|
|
||||||
dayField.value = data
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPrevious() {
|
|
||||||
let daysField = document.getElementById("days")
|
|
||||||
daysField.innerHTML = ""
|
|
||||||
getData("/api/day", "").then((data) => {
|
|
||||||
if (data === 401) {
|
|
||||||
alert("Unauthorized")
|
|
||||||
} else if (data === 500) {
|
|
||||||
alert("Internal server error")
|
|
||||||
} else {
|
|
||||||
data = JSON.parse(data).reverse() // Reverse: latest days first
|
|
||||||
for (let i in data) {
|
|
||||||
let li = document.createElement("li");
|
|
||||||
li.innerHTML = `<a href="/day?d=${data[i]}">${formatDate(data[i])}</a>` // Parse to human-readable
|
|
||||||
daysField.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadDay() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const day = urlParams.get('d');
|
|
||||||
|
|
||||||
let dayTag = document.getElementById("daytag")
|
|
||||||
dayTag.innerText = formatDate(day)
|
|
||||||
|
|
||||||
let dayField = document.getElementById("day")
|
|
||||||
getData("/api/day/" + day, "").then((data) => {
|
|
||||||
if (data === 404) {
|
|
||||||
dayField.value = ""
|
|
||||||
} else if (data === 401) {
|
|
||||||
dayField.value = "Unauthorized"
|
|
||||||
} else if (data === 500) {
|
|
||||||
dayField.value = "Internal server error"
|
|
||||||
} else {
|
|
||||||
dayField.value = data
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dayField.readOnly = true
|
|
||||||
}
|
|
138
routes.go
Normal file
138
routes.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DayData struct {
|
||||||
|
Day string
|
||||||
|
Date string
|
||||||
|
}
|
||||||
|
|
||||||
|
type List struct {
|
||||||
|
Title string
|
||||||
|
Entries []ListEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListEntry struct {
|
||||||
|
Name string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFound returns a user-friendly 404 error page
|
||||||
|
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
http.ServeFile(w, r, "./pages/error/404.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalError returns a user-friendly 500 error page
|
||||||
|
func InternalError(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
http.ServeFile(w, r, "./pages/error/500.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetToday(w http.ResponseWriter, r *http.Request) {
|
||||||
|
day, err := ReadToday()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
day = []byte("")
|
||||||
|
} else {
|
||||||
|
slog.Error("error reading today's file", "error", err)
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{"./pages/base.html", "./pages/index.html"}
|
||||||
|
ts, err := template.ParseFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ts.ExecuteTemplate(w, "base", DayData{Day: string(day)})
|
||||||
|
if err != nil {
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostToday(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := SaveToday([]byte(r.FormValue("day")))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error saving today's file", "error", err)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, r.Header.Get("Referer"), 302)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDays(w http.ResponseWriter, r *http.Request) {
|
||||||
|
day, err := ListFiles("day")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error reading today's file", "error", err)
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var daysFormatted []ListEntry
|
||||||
|
for _, v := range day {
|
||||||
|
dayString := v
|
||||||
|
t, err := time.Parse(time.DateOnly, v)
|
||||||
|
if err == nil {
|
||||||
|
dayString = t.Format("02 Jan 2006")
|
||||||
|
}
|
||||||
|
daysFormatted = append(daysFormatted, ListEntry{Name: dayString, Link: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{"./pages/base.html", "./pages/days.html"}
|
||||||
|
ts, err := template.ParseFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error parsing template files", "error", err)
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ts.ExecuteTemplate(w, "base", List{Title: "Previous days", Entries: daysFormatted})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error executing template", "error", err)
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDay(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dayString := chi.URLParam(r, "day")
|
||||||
|
if dayString == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
HandleWrite(w.Write([]byte("day not specified")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
day, err := ReadFile("day/" + dayString)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error reading day's file", "error", err, "day", dayString)
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{"./pages/base.html", "./pages/day.html"}
|
||||||
|
ts, err := template.ParseFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(time.DateOnly, dayString)
|
||||||
|
if err == nil { // This is low priority so silently fail
|
||||||
|
dayString = t.Format("02 Jan 2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ts.ExecuteTemplate(w, "base", DayData{Day: string(day), Date: dayString})
|
||||||
|
if err != nil {
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
38
serve.go
38
serve.go
|
@ -13,42 +13,26 @@ func Serve() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes)
|
r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes)
|
||||||
r.Use(BasicAuth) // Is this good enough? Sure hope so
|
r.Use(BasicAuth) // Is this good enough? Sure hope so
|
||||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
r.NotFound(NotFound)
|
||||||
w.WriteHeader(404)
|
|
||||||
http.ServeFile(w, r, "./pages/error/404.html")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Home page
|
// Routes ==========
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/", GetToday)
|
||||||
http.ServeFile(w, r, "./pages/index.html")
|
r.Post("/", PostToday)
|
||||||
})
|
r.Get("/day", GetDays)
|
||||||
r.Get("/days", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/day/{day}", GetDay)
|
||||||
http.ServeFile(w, r, "./pages/days.html")
|
|
||||||
})
|
|
||||||
r.Get("/day", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, "./pages/day.html")
|
|
||||||
})
|
|
||||||
|
|
||||||
// API =============
|
// API =============
|
||||||
apiRouter := chi.NewRouter()
|
apiRouter := chi.NewRouter()
|
||||||
|
|
||||||
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w) })
|
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w) })
|
||||||
apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) })
|
apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) })
|
||||||
apiRouter.Get("/log", func(w http.ResponseWriter, r *http.Request) { GetFile("log", w) })
|
apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { GetFileList("day", w) })
|
||||||
apiRouter.Post("/log", PostLog)
|
apiRouter.Get("/day/{day}", GetDayApi)
|
||||||
|
apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { GetFileList("notes", w) })
|
||||||
apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { ListFiles("day", w) })
|
|
||||||
apiRouter.Get("/day/{day}", GetDay)
|
|
||||||
|
|
||||||
apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { ListFiles("notes", w) })
|
|
||||||
apiRouter.Get("/notes/{note}", GetNote)
|
apiRouter.Get("/notes/{note}", GetNote)
|
||||||
apiRouter.Post("/notes/{note}", PostNote)
|
apiRouter.Post("/notes/{note}", PostNote)
|
||||||
|
apiRouter.Get("/today", GetTodayApi)
|
||||||
apiRouter.Get("/today", GetToday)
|
apiRouter.Post("/today", PostTodayApi)
|
||||||
apiRouter.Post("/today", PostToday)
|
|
||||||
|
|
||||||
apiRouter.Get("/export", GetExport)
|
apiRouter.Get("/export", GetExport)
|
||||||
|
|
||||||
r.Mount("/api", apiRouter)
|
r.Mount("/api", apiRouter)
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
|
|
Loading…
Reference in a new issue