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 | |
| parent | 1208c2e7e7e79cfe122f8d5f38160a0611cc9dfe (diff) | |
Add base files
Diffstat (limited to 'cmd')
| -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 +} | 
