fixed prior merge conflic causing double'd embed, added readme parser with full markdown, added readme render to HTML, renamed filepath to fpath to use FilePath.Join for case insensitivity, fixed ident in html src (inline css), added readme css with proper accents

This commit is contained in:
claire treise 2026-03-16 20:41:16 +01:00
parent cf03c723d0
commit 245d6e0fa0
4 changed files with 270 additions and 133 deletions

185
main.go
View file

@ -1,6 +1,7 @@
package main
import (
_ "embed"
"fmt"
"html/template"
"io/fs"
@ -8,11 +9,15 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
_ "embed"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
//go:embed templates/dir.html
@ -20,25 +25,25 @@ var dirTemplateSrc string
type (
Directory struct {
Name string
Root bool
Files []*File
Name string
Root bool
Files []*File
Readme template.HTML
}
File struct {
Name string
URI string
IsDir bool
Size string
Name string
URI string
IsDir bool
Size string
ModifiedDate string
}
)
//go:embed templates/dir.html
var dirHTML string
func main() {
if len(os.Args) < 2 { printHelp() }
if len(os.Args) < 2 {
printHelp()
}
host := "127.0.0.1"
port := 8080
@ -47,7 +52,9 @@ func main() {
filesDir := ""
i := 1
for {
if i >= len(os.Args) { break }
if i >= len(os.Args) {
break
}
switch os.Args[i] {
case "-h":
fallthrough
@ -55,7 +62,7 @@ func main() {
printHelp()
case "--host":
if i + 1 >= len(os.Args) {
if i+1 >= len(os.Args) {
fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n")
os.Exit(1)
}
@ -63,7 +70,7 @@ func main() {
host = os.Args[i]
case "--port":
if i + 1 >= len(os.Args) {
if i+1 >= len(os.Args) {
fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n")
os.Exit(1)
}
@ -76,14 +83,18 @@ func main() {
}
case "--root":
if i + 1 >= len(os.Args) {
if i+1 >= len(os.Args) {
fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n")
os.Exit(1)
}
i++
root = os.Args[i]
if !strings.HasPrefix(root, "/") { root = "/" + root }
if !strings.HasSuffix(root, "/") { root += "/" }
if !strings.HasPrefix(root, "/") {
root = "/" + root
}
if !strings.HasSuffix(root, "/") {
root += "/"
}
default:
if len(filesDir) > 0 {
@ -111,7 +122,9 @@ func main() {
}
fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port)
if root != "/" { fmt.Printf("%s", root) }
if root != "/" {
fmt.Printf("%s", root)
}
fmt.Println(".")
http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -121,8 +134,8 @@ func main() {
}
isRoot := r.URL.Path == root
filepath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root))
info, err := os.Stat(filepath)
fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root))
info, err := os.Stat(fpath)
if err != nil {
http.NotFound(w, r)
return
@ -135,7 +148,7 @@ func main() {
return
}
file, err := os.Open(filepath)
file, err := os.Open(fpath)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -144,7 +157,7 @@ func main() {
defer func() {
err := file.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", filepath, err)
fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err)
}
}()
@ -159,30 +172,54 @@ func main() {
_, err = file.WriteTo(w)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", filepath, err)
fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err)
}
return
}
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path + "/", http.StatusFound)
http.Redirect(w, r, r.URL.Path+"/", http.StatusFound)
return
}
data := Directory{
Root: isRoot,
Name: r.URL.Path,
Files: []*File{},
// embeded readme
var readmeHTML template.HTML
entries, err := os.ReadDir(fpath)
if err == nil {
for _, entry := range entries {
if strings.EqualFold(entry.Name(), "readme.md") {
src, err := os.ReadFile(filepath.Join(fpath, entry.Name()))
if err == nil {
mdFlags := html.CommonFlags | html.HrefTargetBlank
mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags})
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs)
md := mdParser.Parse(src)
readmeHTML = template.HTML(markdown.Render(md, mdRenderer))
}
break
}
}
}
fsDir := os.DirFS(filepath)
data := Directory{
Root: isRoot,
Name: r.URL.Path,
Files: []*File{},
Readme: readmeHTML,
}
fsDir := os.DirFS(fpath)
directories, err := fs.ReadDir(fsDir, ".")
for _, dir := range directories {
name := dir.Name()
if slices.Contains(ignoredFiles, name) { continue }
if slices.Contains(ignoredFiles, name) {
continue
}
info, err := dir.Info()
if err != nil { continue }
if err != nil {
continue
}
var uri string
if isRoot {
@ -213,10 +250,10 @@ func main() {
dateStr := info.ModTime().Format("02-Jan-2006 15:04")
data.Files = append(data.Files, &File{
Name: name,
URI: uri,
IsDir: info.IsDir(),
Size: sizeStr,
Name: name,
URI: uri,
IsDir: info.IsDir(),
Size: sizeStr,
ModifiedDate: dateStr,
})
}
@ -232,52 +269,60 @@ func main() {
}
type LoggingResponseWriter struct {
http.ResponseWriter
Status int
http.ResponseWriter
Status int
}
var COL_Reset = "\033[0m"
var COL_Red = "\033[31m"
var COL_Green = "\033[32m"
var COL_Reset = "\033[0m"
var COL_Red = "\033[31m"
var COL_Green = "\033[32m"
var COL_Yellow = "\033[33m"
var COL_Blue = "\033[34m"
var COL_Blue = "\033[34m"
var COL_Purple = "\033[35m"
var COL_Cyan = "\033[36m"
var COL_Gray = "\033[37m"
var COL_White = "\033[97m"
var COL_Cyan = "\033[36m"
var COL_Gray = "\033[37m"
var COL_White = "\033[97m"
func HTTPLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := LoggingResponseWriter{w, http.StatusOK}
lrw := LoggingResponseWriter{w, http.StatusOK}
next.ServeHTTP(&lrw, r)
next.ServeHTTP(&lrw, r)
after := time.Now()
difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000
elapsed := "<1"
if difference >= 1 {
elapsed = strconv.Itoa(difference)
}
after := time.Now()
difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000
elapsed := "<1"
if difference >= 1 {
elapsed = strconv.Itoa(difference)
}
statusColour := COL_Reset
statusColour := COL_Reset
if lrw.Status - 600 <= 0 { statusColour = COL_Red }
if lrw.Status - 500 <= 0 { statusColour = COL_Yellow }
if lrw.Status - 400 <= 0 { statusColour = COL_White }
if lrw.Status - 300 <= 0 { statusColour = COL_Green }
if lrw.Status-600 <= 0 {
statusColour = COL_Red
}
if lrw.Status-500 <= 0 {
statusColour = COL_Yellow
}
if lrw.Status-400 <= 0 {
statusColour = COL_White
}
if lrw.Status-300 <= 0 {
statusColour = COL_Green
}
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
after.Format(time.UnixDate),
r.Method,
r.URL.Path,
statusColour,
lrw.Status,
COL_Reset,
elapsed,
r.Header["User-Agent"][0])
})
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
after.Format(time.UnixDate),
r.Method,
r.URL.Path,
statusColour,
lrw.Status,
COL_Reset,
elapsed,
r.Header["User-Agent"][0])
})
}
func printHelp() {