diff --git a/api.go b/api.go new file mode 100644 index 0000000..282dcd3 --- /dev/null +++ b/api.go @@ -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) +} diff --git a/files.go b/files.go index d20b595..252216d 100644 --- a/files.go +++ b/files.go @@ -1,12 +1,8 @@ package main import ( - "encoding/json" "errors" - "github.com/go-chi/chi/v5" - "io" "log/slog" - "net/http" "os" "path" "path/filepath" @@ -14,20 +10,12 @@ import ( "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 txt file in data directory -func GetFile(filename string, w http.ResponseWriter) { +// ReadFile returns raw contents of a file +func ReadFile(filename string) ([]byte, error) { filename = "data/" + path.Clean(filename) + ".txt" // Does this sanitize the path? if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { - http.Error(w, "file not found", http.StatusNotFound) - return + return nil, err } fileContents, err := os.ReadFile(filename) @@ -35,103 +23,50 @@ func GetFile(filename string, w http.ResponseWriter) { slog.Error("error reading file", "error", err, "file", filename) - http.Error(w, "error reading file", http.StatusInternalServerError) - return + return nil, err } - _, err = w.Write(fileContents) - if err != nil { - http.Error(w, "error sending file", http.StatusInternalServerError) - } + return fileContents, nil } -// 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" f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { slog.Error("error opening/making file", "error", err, "file", filename) - HandleWrite(w.Write([]byte("error opening or creating file"))) - w.WriteHeader(http.StatusInternalServerError) - return + return err } - - if _, err := f.Write(body); err != nil { + if _, err := f.Write(contents); err != nil { slog.Error("error writing to file", "error", err, "file", filename) - HandleWrite(w.Write([]byte("error writing to file"))) - w.WriteHeader(http.StatusInternalServerError) - return + return err } - HandleWrite(w.Write([]byte("wrote to file"))) - w.WriteHeader(http.StatusOK) + return nil } -// ListFiles returns JSON list of filenames in a directory without extensions or path -func ListFiles(directory string, w http.ResponseWriter) { +// ListFiles returns slice of filenames in a directory without extensions or path +func ListFiles(directory string) ([]string, error) { filenames, err := filepath.Glob("data/" + directory + "/*.txt") if err != nil { - http.Error(w, "error searching for files", http.StatusInternalServerError) - return + return nil, err } for i, file := range filenames { file, _ := strings.CutSuffix(filepath.Base(file), filepath.Ext(file)) filenames[i] = file } - filenamesJson, err := json.Marshal(filenames) - HandleWrite(w.Write(filenamesJson)) + return filenames, nil } -// GetDay returns a day specified in URL -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 - } - GetFile("day/"+dayString, w) +// ReadToday runs ReadFile with today's date as filename +func ReadToday() ([]byte, error) { + return ReadFile("day/" + time.Now().Format(time.DateOnly)) } -// GetToday runs GetFile with today's daily txt -func GetToday(w http.ResponseWriter, _ *http.Request) { - GetFile("day/"+time.Now().Format("2006-01-02"), w) -} - -// 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) +// SaveToday runs SaveFile with today's date as filename +func SaveToday(contents []byte) error { + return SaveFile("day/"+time.Now().Format(time.DateOnly), contents) } diff --git a/log.go b/log.go deleted file mode 100644 index 6c1ed4d..0000000 --- a/log.go +++ /dev/null @@ -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")) -} diff --git a/pages/base.html b/pages/base.html new file mode 100644 index 0000000..7c697c2 --- /dev/null +++ b/pages/base.html @@ -0,0 +1,38 @@ +{{define "header"}} +
+

🌺 Hibiscus.txt

+

Today is a place

+
+{{end}} + + +{{define "base"}} + + + + + + + + + Hibiscus.txt + + + {{template "header" .}} +
+ {{template "main" .}} +
+ {{template "footer" .}} + + + +{{end}} + +{{define "footer"}} + +{{end}} \ No newline at end of file diff --git a/pages/day.html b/pages/day.html index a6c403e..3e87b95 100644 --- a/pages/day.html +++ b/pages/day.html @@ -1,33 +1,4 @@ - - - - - - - - - - - Hibiscus.txt - - -
-

🌺 Hibiscus.txt

-

Today is a place

-
-
-

| Go back

- -
- - - - - \ No newline at end of file +{{define "main"}} +

| Go back

+ +{{end}} \ No newline at end of file diff --git a/pages/days.html b/pages/days.html index 5708108..4376661 100644 --- a/pages/days.html +++ b/pages/days.html @@ -1,33 +1,8 @@ - - - - - - - - - - - Hibiscus.txt - - -
-

🌺 Hibiscus.txt

-

Today is a place

-
-
-

Previous days

- -
- - - - - \ No newline at end of file +{{define "main"}} +

{{.Title}}

+ +{{end}} \ No newline at end of file diff --git a/pages/error/500.html b/pages/error/500.html new file mode 100644 index 0000000..a58daf5 --- /dev/null +++ b/pages/error/500.html @@ -0,0 +1,17 @@ + + + + + + + + Error 500 + + +
+

Error 500 - Internal Server Error

+

It's probably not your fault, but something broke

+

Go home?

+
+ + \ No newline at end of file diff --git a/pages/index.html b/pages/index.html index 6823ccc..b404cac 100644 --- a/pages/index.html +++ b/pages/index.html @@ -1,34 +1,7 @@ - - - - - - - - - - - Hibiscus.txt - - -
-

🌺 Hibiscus.txt

-

Today is a place

-
-
-

- - -
- - - - - \ No newline at end of file +{{define "main"}} +
+

+ + +
+{{end}} \ No newline at end of file diff --git a/public/requests.js b/public/requests.js deleted file mode 100644 index 5605632..0000000 --- a/public/requests.js +++ /dev/null @@ -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 = `${formatDate(data[i])}` // 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 -} \ No newline at end of file diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..548afdc --- /dev/null +++ b/routes.go @@ -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 + } +} diff --git a/serve.go b/serve.go index bc21735..de99d36 100644 --- a/serve.go +++ b/serve.go @@ -13,42 +13,26 @@ func Serve() { r := chi.NewRouter() r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes) r.Use(BasicAuth) // Is this good enough? Sure hope so - r.NotFound(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(404) - http.ServeFile(w, r, "./pages/error/404.html") - }) + r.NotFound(NotFound) - // Home page - r.Get("/", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./pages/index.html") - }) - r.Get("/days", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./pages/days.html") - }) - r.Get("/day", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "./pages/day.html") - }) + // Routes ========== + r.Get("/", GetToday) + r.Post("/", PostToday) + r.Get("/day", GetDays) + r.Get("/day/{day}", GetDay) // API ============= apiRouter := chi.NewRouter() - 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.Get("/log", func(w http.ResponseWriter, r *http.Request) { GetFile("log", w) }) - apiRouter.Post("/log", PostLog) - - 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("/day", func(w http.ResponseWriter, r *http.Request) { GetFileList("day", w) }) + apiRouter.Get("/day/{day}", GetDayApi) + apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { GetFileList("notes", w) }) apiRouter.Get("/notes/{note}", GetNote) apiRouter.Post("/notes/{note}", PostNote) - - apiRouter.Get("/today", GetToday) - apiRouter.Post("/today", PostToday) - + apiRouter.Get("/today", GetTodayApi) + apiRouter.Post("/today", PostTodayApi) apiRouter.Get("/export", GetExport) - r.Mount("/api", apiRouter) // Static files