diff options
author | Alex Scerba <alex@scerba.org> | 2024-09-27 18:56:29 -0400 |
---|---|---|
committer | Alex Scerba <alex@scerba.org> | 2024-09-27 18:56:29 -0400 |
commit | 2782bafcfdb69ef7b69a51a717a6bd35095e9369 (patch) | |
tree | 32d69f68fe2dbc6b9cc5a5ac8ff970d79f348156 /cmd/http | |
parent | 1208c2e7e7e79cfe122f8d5f38160a0611cc9dfe (diff) |
Add base files
Diffstat (limited to 'cmd/http')
-rw-r--r-- | cmd/http/errors.go | 23 | ||||
-rw-r--r-- | cmd/http/handle.go | 65 | ||||
-rw-r--r-- | cmd/http/load.go | 106 | ||||
-rw-r--r-- | cmd/http/main.go | 79 | ||||
-rw-r--r-- | cmd/http/middle.go | 32 | ||||
-rw-r--r-- | cmd/http/render.go | 39 |
6 files changed, 344 insertions, 0 deletions
diff --git a/cmd/http/errors.go b/cmd/http/errors.go new file mode 100644 index 0000000..9406a9a --- /dev/null +++ b/cmd/http/errors.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "net/http" + "runtime/debug" +) + +func (app *application) serverError(w http.ResponseWriter, err error) { + trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack()) + app.errorLog.Output(2, trace) + + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +func (app *application) clientError(w http.ResponseWriter, status int) { + app.errorLog.Printf("Clent error: %d\n", status) + http.Error(w, http.StatusText(status), status) +} + +func (app *application) notFound(w http.ResponseWriter) { + app.clientError(w, http.StatusNotFound) +} diff --git a/cmd/http/handle.go b/cmd/http/handle.go new file mode 100644 index 0000000..9c1f62a --- /dev/null +++ b/cmd/http/handle.go @@ -0,0 +1,65 @@ +package main + +import ( + "net/http" + "strings" +) + +func (app *application) home(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + path := strings.Split(r.URL.Path, "/") + if path[1] != "" { + app.notFound(w) + } else { + p, err := app.aggregate("html/projects") + if err != nil { + app.serverError(w, err) + } + + err = renderTemplate(w, "index", p) + if err != nil { + app.serverError(w, err) + } + } +} + +func (app *application) post(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + path := strings.Split(r.URL.Path, "/") + if len(path) > 4 { + app.notFound(w) + } else if len(path) == 4 && path[3] == "" { + http.Redirect(w, r, "/"+path[1]+"/"+path[2], http.StatusFound) + } else { + post, err := app.readFile("html" + strings.TrimSuffix(r.URL.Path, "/") + ".tmpl.html") + if err != nil { + app.notFound(w) + return + } + + var posts []*Post + posts = append(posts, post) + p := &Posts{Contents: posts} + + err = renderTemplate(w, path[1]+"/"+path[2], p) + if err != nil { + app.serverError(w, err) + } + } +} + +func (app *application) about(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + path := strings.Split(r.URL.Path, "/") + if path[1] != "about" { + app.notFound(w) + } else { + err := renderTemplate(w, "about", nil) + if err != nil { + app.serverError(w, err) + } + } +} diff --git a/cmd/http/load.go b/cmd/http/load.go new file mode 100644 index 0000000..d63d7ce --- /dev/null +++ b/cmd/http/load.go @@ -0,0 +1,106 @@ +package main + +import ( + "os" + "regexp" + "sort" + "strings" +) + +// Post struct contains necessary data for a post +type Post struct { + FileName string + Title string + Date string + Tags []string + Image string +} + +// Posts stuct contains a collection of type Post +type Posts struct { + Contents []*Post +} + +// Read all found files and load them into a stuct +func (app *application) aggregate(location string) (p *Posts, err error) { + var posts []*Post + + files, err := os.ReadDir(location) + if err != nil { + return nil, err + } + + // Loop over every file in the directory and read the contents. + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".tmpl.html") { + newPost, err := app.readFile(location + "/" + file.Name()) + if err != nil { + return nil, err + } + + posts = append(posts, newPost) + } + } + + sort.Slice(posts, func(i, j int) bool { + return posts[i].Date > posts[j].Date + }) + + return &Posts{Contents: posts}, nil +} + +func (app *application) readFile(location string) (p *Post, err error) { + fileContent, err := os.ReadFile(location) + if err != nil { + return nil, err + } + + var post *Post = new(Post) + + fileName := strings.TrimSuffix(strings.Split(location, "/")[2], ".tmpl.html") + + // title + title := strings.ReplaceAll(fileName, "_", " ") + + // date + datePattern := regexp.MustCompile(`{{define "uploaded-on"}}(\d{4}-\d{2}-\d{2}){{end}}`) + dateMatching := datePattern.FindStringSubmatch(string(fileContent)) + + var date string + if len(dateMatching) > 1 { + date = dateMatching[1] + } else { + date = "" + } + + // tags + tagsPattern := regexp.MustCompile(`{{define "keywords"}}([\w\s]+){{end}}`) + matchingTags := tagsPattern.FindStringSubmatch(string(fileContent)) + + var tags []string + if len(matchingTags) > 1 { + tags = strings.Fields(matchingTags[1]) + } else { + tags = []string{} + } + + // thumbnail image + imagePattern := regexp.MustCompile(`<img src="(.+)" class="mainImage"( alt="(.+)")* />`) + matchingImage := imagePattern.FindStringSubmatch(string(fileContent)) + + var image string + if len(matchingImage) > 1 { + image = matchingImage[0] + } else { + image = "" + } + + post.FileName = fileName + post.Title = title + post.Date = date + post.Tags = tags + post.Image = image + + return post, nil + +} diff --git a/cmd/http/main.go b/cmd/http/main.go new file mode 100644 index 0000000..661e6fc --- /dev/null +++ b/cmd/http/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" + "strings" +) + +var ( + fullchain = "/etc/letsencrypt/live/alexscerba.com/fullchain.pem" + privkey = "/etc/letsencrypt/live/alexscerba.com/privkey.pem" +) + +type application struct { + errorLog *log.Logger + infoLog *log.Logger +} + +func (app *application) httpsRedirect(w http.ResponseWriter, req *http.Request) { + // remove/add not default ports from req.Host + target := "https://" + req.Host + req.URL.Path + if len(req.URL.RawQuery) > 0 { + target += "?" + req.URL.RawQuery + } + app.infoLog.Printf("redirect to: %s", target) + http.Redirect(w, req, target, + // see comments below and consider the codes 308, 302, or 301 + http.StatusMovedPermanently) +} + +func (app *application) wwwRedirect(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.Host, "www.") { + http.Redirect(w, r, "https://www."+r.Host+r.RequestURI, 302) + return + } + + h.ServeHTTP(w, r) + }) +} + +func main() { + addr := flag.String("addr", ":4000", "HTTP Network Address") + flag.Parse() // required before flag is used + + infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) + errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) + + app := &application{ + errorLog: errorLog, + infoLog: infoLog, + } + + mux := http.NewServeMux() + + fs := http.FileServer(http.Dir("./static")) + mux.Handle("/static/", http.StripPrefix("/static/", fs)) + + mux.HandleFunc("/about", app.about) + mux.HandleFunc("/about/", app.about) + mux.HandleFunc("/projects", app.post) + mux.HandleFunc("/projects/", app.post) + mux.HandleFunc("/", app.home) + + if *addr == ":443" { + www := app.wwwRedirect(mux) + + infoLog.Printf("Starting TLS server on %s...\n", *addr) + go http.ListenAndServe(":80", www) + err := http.ListenAndServeTLS(*addr, fullchain, privkey, gzipHandler(www)) + log.Fatal(err) + } else { + infoLog.Printf("Starting server on %s...\n", *addr) + err := http.ListenAndServe(*addr, gzipHandler(mux)) + log.Fatal(err) + } +} diff --git a/cmd/http/middle.go b/cmd/http/middle.go new file mode 100644 index 0000000..29b49a6 --- /dev/null +++ b/cmd/http/middle.go @@ -0,0 +1,32 @@ +package main + +import ( + "compress/gzip" + "io" + "net/http" + "strings" +) + +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (grw gzipResponseWriter) Write(data []byte) (int, error) { + return grw.Writer.Write(data) +} + +func gzipHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + + w.Header().Set("Content-Encoding", "gzip") + gzipWriter := gzip.NewWriter(w) + defer gzipWriter.Close() + gzippedResponseWriter := gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w} + next.ServeHTTP(gzippedResponseWriter, r) + }) +} diff --git a/cmd/http/render.go b/cmd/http/render.go new file mode 100644 index 0000000..9c01e57 --- /dev/null +++ b/cmd/http/render.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + "strings" + "text/template" +) + +func renderTemplate(w http.ResponseWriter, tmplPath string, p *Posts) (err error) { + t, err := template.ParseFiles("html/master.tmpl.html", "html/"+tmplPath+".tmpl.html") + if err != nil { + return err + } + + splitPath := strings.Split(tmplPath, "/") + + data := make(map[string]interface{}) + + // If were loading the index, set page to 'Index' and pass through all posts. + // Otherwise, set page to 'Projects' and pass through the first post (should only be one + // coming in) + if splitPath[0] == "index" { + data["Page"] = "Index" + data["Posts"] = p + } else if splitPath[0] == "about" { + data["Page"] = "About" + data["Posts"] = nil + } else { + data["Page"] = "Project" + data["Post"] = p.Contents[0] + } + + err = t.Execute(w, data) + if err != nil { + return err + } + + return nil +} |