sophuwu.site > myweb
added comments
added back up function to copy db
sophuwu sophie@skisiel.com
Wed, 11 Dec 2024 21:01:02 +0100
commit

62eb48714deb9bf3f6e067e23a8e74dd35c8b056

parent

ecc9b487c08e9d7aff62711da93d3ac6b6421b0b

5 files changed, 137 insertions(+), 26 deletions(-)

jump to
M animations.goanimations.go

@@ -3,13 +3,13 @@

import ( "crypto/md5" "encoding/base64" - "encoding/json" "net/http" "sophuwu.site/myweb/template" "strings" "time" ) +// AnimInfo is a struct that holds information about an animation. type AnimInfo struct { ID string `storm:"id"` Title string

@@ -19,10 +19,14 @@ Imgs []string

Vids []string } +// HasReqFields checks if all required fields have a non-empty value. func (a *AnimInfo) HasReqFields() bool { return a.Title != "" && a.Desc != "" && (len(a.Imgs)+len(a.Vids) > 0) && a.Date != "" && a.ID != "" } +// GenAnimID generates an ID for an animation. It will generate a different ID +// each time it is called, even if the input is the same. This allows for +// multiple animations with the same title to be stored without conflict. func GenAnimID(a AnimInfo) AnimInfo { md := md5.New() md.Write([]byte(time.Now().String() + a.Title))

@@ -33,25 +37,21 @@ }

return a } +// GetAnim retrieves AnimInfo from the database with the given ID. +// If the ID is not found, an error is returned. func GetAnim(id string) (AnimInfo, error) { var a AnimInfo err := DB.One("ID", id, &a) return a, err } -func AnimSaveJson(js string) error { - var a AnimInfo - err := json.Unmarshal([]byte(js), &a) - if err != nil { - return err - } - return DB.Save(&a) -} - +// AnimDelete deletes an animation from the database with the given ID. func AnimDelete(id string) error { return DB.DeleteStruct(&AnimInfo{ID: id}) } +// GetAnims retrieves all animations from the database. The animations are +// sorted by date, with the most recent first in []AnimInfo. func GetAnims() ([]AnimInfo, error) { var anims []AnimInfo err := DB.All(&anims)

@@ -68,11 +68,12 @@ }

return anims, nil } +// AnimHandler is a http.HandlerFunc that serves the animations page. +// It retrieves all animations from the database and displays them. func AnimHandler(w http.ResponseWriter, r *http.Request) { anims, err := GetAnims() CheckHttpErr(err, w, r, 500) - var d template.HTMLDataMap - err = DB.Get("pages", "anims", &d) + d, err := GetPageData("anims") if CheckHttpErr(err, w, r, 500) { return }

@@ -82,6 +83,9 @@ err = template.Use(w, r, "anims", d)

CheckHttpErr(err, w, r, 500) } +// AnimManageList is a http.HandlerFunc that serves the animation manager list. +// It retrieves all animations from the database and displays them as a list. +// With each animation, there is a link to edit the details of that animation. func AnimManageList(w http.ResponseWriter, r *http.Request) { anims, err := GetAnims() if CheckHttpErr(err, w, r, 500) {

@@ -99,6 +103,10 @@ CheckHttpErr(err, w, r, 500)

return } +// AnimManager is a http.HandlerFunc that serves the animation manager. It +// allows the user to edit an existing animation or create a new one. +// If the ID is "new", a new animation is created. Otherwise, the animation +// with the given ID is retrieved from the database and displayed for editing. func AnimManager(w http.ResponseWriter, r *http.Request) { if "/manage/animation/" != r.URL.Path { HttpErr(w, r, 404)
M blogs.goblogs.go

@@ -11,6 +11,7 @@ "strings"

"time" ) +// BlogMeta is the metadata for a blog post in the database type BlogMeta struct { ID string `storm:"unique"` Title string `storm:"index"`

@@ -18,21 +19,24 @@ Date string `storm:"index"`

Desc string `storm:"index"` } +// BlogContent is the content of a blog post in the database type BlogContent struct { ID string `storm:"unique"` Content string `storm:"index"` } -func IdGen(title, date string) string { +// BlogIdGen generates a unique id for a blog post +func BlogIdGen(title, date string) string { title = strings.ReplaceAll(title, " ", "-") return filepath.Join(date, url.PathEscape(title)) } +// SaveBlog saves a blog post to the database with arguments func SaveBlog(title, desc, body string, date ...string) error { if len(date) == 0 { date = append(date, time.Now().Format("2006-01-02")) } - id := IdGen(title, date[0]) + id := BlogIdGen(title, date[0]) err := DB.Save(&BlogContent{ ID: id,

@@ -52,6 +56,8 @@ err = DB.Save(&blg)

return err } +// GetBlog retrieves a blog post from the database by id and returns the metadata and content +// as BlogMeta and BlogContent respectively. Returns an error if the blog post is not found. func GetBlog(id string) (meta BlogMeta, content BlogContent, err error) { err = DB.One("ID", id, &content) if err != nil {

@@ -61,6 +67,8 @@ err = DB.One("ID", id, &meta)

return } +// GetBlogs retrieves all blog posts from the database and returns them as a slice of BlogMeta. +// Returns an error if the database query fails. func GetBlogs() ([]BlogMeta, error) { var blogs []BlogMeta // err := DB.All(&blogs)

@@ -78,6 +86,7 @@ }

return blogs, err } +// BlogHandler handles requests to the blog page and individual blog posts func BlogHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/blog/") if path == "" {

@@ -85,8 +94,7 @@ blogs, err := GetBlogs()

if CheckHttpErr(err, w, r, 500) { return } - var d template.HTMLDataMap - err = DB.Get("pages", "blogs", &d) + d, err := GetPageData("blogs") if CheckHttpErr(err, w, r, 500) { return }

@@ -108,6 +116,7 @@ err = template.Use(w, r, "blog", data)

CheckHttpErr(err, w, r, 500) } +// BlogManageList handles the /manage/blog/ route for listing all blog posts func BlogManageList(w http.ResponseWriter, r *http.Request) { blogs, err := GetBlogs() if CheckHttpErr(err, w, r, 500) {

@@ -125,6 +134,7 @@ CheckHttpErr(err, w, r, 500)

return } +// BlogManager handles the /manage/blog/ route for managing blog posts func BlogManager(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/manage/blog/" { HttpErr(w, r, 404)
M db.godb.go

@@ -2,12 +2,16 @@ package main

import ( "encoding/json" + "fmt" "github.com/asdine/storm/v3" + "go.etcd.io/bbolt" "log" "net/http" + "path/filepath" "sophuwu.site/myweb/config" "sophuwu.site/myweb/template" "strings" + "time" ) var DB *storm.DB

@@ -27,29 +31,78 @@ log.Println(err)

} } +// AddRequiredData looks for important data types in the db +// and adds a placeholder if they don't exist. This is useful +// for the first run of the server to prevent errors. +func AddRequiredData() { + d, err := GetPageData("index") + if err != nil || d == nil { + _ = SetPageData("index", template.HTMLDataMap{ + "Title": "The title of the page", + "Description": "The description for meta tags", + "AboutText": []string{ + "Paragraph 1", + }, + "ImagePath": "/path/to/pic.jpg", + "Profiles": []Profile{ + {"char in iconfont", "name of url", "URL", "username"}, + }, + }) + } + d, err = GetPageData("blogs") + if err != nil || d == nil { + _ = SetPageData("blogs", template.HTMLDataMap{ + "Title": "The title of the page", + "Description": "The description for meta tags", + }) + } + d, err = GetPageData("anims") + if err != nil || d == nil { + _ = SetPageData("anims", template.HTMLDataMap{ + "Title": "The title of the page", + "Description": "The description for meta tags", + }) + } +} + +// GetPageData returns a map of page metadata and data +// used for index/list pages which don't have a separate +// data source. func GetPageData(page string) (template.HTMLDataMap, error) { var d template.HTMLDataMap err := DB.Get("pages", page, &d) return d, err } +// SetPageData writes a HTMLDataMap to the db for persistent +// storage of page data without its own data source. func SetPageData(page string, data template.HTMLDataMap) error { return DB.Set("pages", page, data) } +// EditIndex handles the /manage/edit/ route for editing the +// index page's data and metadata. func EditIndex(w http.ResponseWriter, r *http.Request) { var err error + page := r.URL.Query().Get("page") + if page == "" { + HttpErr(w, r, 404) + } if r.Method == "GET" && r.URL.Path == "/manage/edit/" { var d template.HTMLDataMap - d, err = GetPageData("index") + d, err = GetPageData(page) + if CheckHttpErr(err, w, r, 404) { + return + } var b []byte b, err = json.MarshalIndent(d, "", " ") if CheckHttpErr(err, w, r, 500) { return } bb := string(b) - data := template.Data("Edit index", "Edit the index page") + data := template.Data("Edit "+page, "Edit the page's data and metadata") data.Set("Data", bb) + data.Set("EditUrl", "/manage/edit/save?page="+page) err = template.Use(w, r, "edit", data) CheckHttpErr(err, w, r, 500) return

@@ -69,7 +122,7 @@ err = json.Unmarshal([]byte(bb), &d)

if CheckHttpErr(err, w, r, 400) { return } - err = SetPageData("index", d) + err = SetPageData(page, d) if CheckHttpErr(err, w, r, 400) { return }

@@ -79,13 +132,20 @@ }

HttpErr(w, r, 405) } +// UrlOpt is a struct for the options on the manage page. type UrlOpt struct{ Name, URL string } +// ManagerHandler handles the /manage/ route for managing +// the website. It displays a list of options for managing +// the website's content. And forwards to the appropriate +// handler for each option. func ManagerHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/manage/" { data := template.Data("Manage", config.URL) data["Options"] = []UrlOpt{ - {"Edit index", "/manage/edit/"}, + {"Edit index", "/manage/edit/?page=index"}, + {"Edit blog list meta", "/manage/edit/?page=blogs"}, + {"Edit animation meta", "/manage/edit/?page=anims"}, {"Upload media", "/manage/media/"}, {"Delete media", "/manage/delete/media/"}, {"Manage blogs", "/manage/blog/"},

@@ -116,5 +176,23 @@ if r.URL.Path == "/manage/blog/" {

BlogManager(w, r) return } + if r.URL.Path == "/manage/backup/" { + BackerUpper(w, r) + return + } HttpErr(w, r, 404) } + +// BackerUpper is a handler to download the database file. +func BackerUpper(w http.ResponseWriter, r *http.Request) { + err := DB.Bolt.View(func(tx *bbolt.Tx) error { + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-data.db.%s.bak"`, filepath.Base(config.URL), time.Now().Format("2006-01-02"))) + w.Header().Set("Content-Length", fmt.Sprint(int(tx.Size()))) + _, err := tx.WriteTo(w) + return err + }) + if CheckHttpErr(err, w, r, 500) { + return + } +}
M main.gomain.go

@@ -15,6 +15,8 @@ "sophuwu.site/myweb/template"

"strings" ) +// CheckHttpErr will check if err is not nil. It will then handle the HTTP +// response and return true if an error occurred. func CheckHttpErr(err error, w http.ResponseWriter, r *http.Request, code int) bool { if err != nil { HttpErr(w, r, code)

@@ -24,6 +26,7 @@ }

return false } +// HttpErrs is a map of HTTP error codes to error messages. var HttpErrs = map[int]string{ 400: "Bad request: the server could not understand your request. Please check the URL and try again.", 401: "Unauthorized: You must log in to access this page.",

@@ -33,6 +36,7 @@ 405: "Method not allowed: the requested method is not allowed on this page.",

500: "Internal server error: the server encountered an error while processing your request. Please try again later.", } +// HttpErr will write an HTTP error response with the given status code. func HttpErr(w http.ResponseWriter, r *http.Request, code int) { w.WriteHeader(code) var ErrTxt string

@@ -50,6 +54,7 @@ log.Printf("error writing error page: %v", err)

} } +// HttpIndex is the handler for the index page. func HttpIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { HttpErr(w, r, 404)

@@ -64,10 +69,9 @@ err = template.Use(w, r, "index", d)

_ = CheckHttpErr(err, w, r, 500) } -func HttpFS(path, fspath string) { - http.Handle(path, http.StripPrefix(path, http.FileServer(http.Dir(fspath)))) -} - +// Profile is a struct that holds information about profiles on +// social media or other external websites. +// Icon is used for a rune to display from the Sophuwu iconfont. type Profile struct { Icon string Website string

@@ -75,6 +79,10 @@ URL string

User string } +// Authenticate is a middleware that checks for basic authentication. +// Passwords are hashed with bcrypt, stored in the userpass file in the +// webhome directory. The file only contains one line, the bcrypt hash. +// The hash is generated hashing the string "user:password" with bcrypt. func Authenticate(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, apass, authOK := r.BasicAuth()

@@ -87,6 +95,7 @@ next.ServeHTTP(w, r)

}) } +// main is the entry point for the web server. func main() { OpenDB() err := template.Init(config.Templates)

@@ -96,7 +105,7 @@ }

http.HandleFunc("/", HttpIndex) http.HandleFunc("/blog/", BlogHandler) - HttpFS("/static/", config.StaticPath) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(config.StaticPath)))) http.HandleFunc("/media/", MediaHandler) http.HandleFunc("/animations/", AnimHandler) http.Handle("/manage/", Authenticate(ManagerHandler))
M webhome/templates/edit.htmlwebhome/templates/edit.html

@@ -85,11 +85,17 @@ </form>

<article id="blog-view"></article> </div> {{ else }} - <form action="/manage/edit/save" method="post" style="width: 100%;"> + {{ if .EditUrl }} + <form action="{{ .EditUrl }}" method="post" style="width: 100%;"> <textarea name="data" rows="30" cols="100" style="width: 100%;">{{ .Data }}</textarea> <br> <input type="submit" value="Save"> </form> + {{ else }} + <p> + No editor found. + </p> + {{ end }} {{ end }} {{ end }} {{ end }}