cdn/main.go

229 lines
5.5 KiB
Go

package main
import (
"bytes"
"context"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/a-h/templ"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
)
var (
ctx = context.Background()
previewPort = ":8080"
logger = log.New(os.Stderr)
minifier = minify.New()
)
func closeFile(file *os.File) {
if err := file.Close(); err != nil {
logger.Fatal("failed to close file", "err", err)
}
}
// minifyInPlace replaces the file content with the minified version
func minifyInPlace(mediatype, path string) error {
tempPath := path + ".bak"
err := os.Rename(path, tempPath)
if err != nil {
return err
}
inputFile, err := os.Open(tempPath)
if err != nil {
return err
}
// Open the file again for writing
outputFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer func() {
closeFile(inputFile)
closeFile(outputFile)
if err := os.Remove(tempPath); err != nil {
logger.Fatal("failed to delete temporary input file", "file", path)
}
}()
return minifier.Minify(mediatype, outputFile, inputFile)
}
func genIndexFile(path string, c templ.Component) error {
indexFile, err := os.Create(filepath.Join(path, "index.html"))
if err != nil {
return err
}
defer closeFile(indexFile)
buf := new(bytes.Buffer)
if err := c.Render(ctx, buf); err != nil {
return err
}
return minifier.Minify("text/html", indexFile, buf)
}
func traverse(prefix, path string, d fs.DirEntry) error {
// Generate home page for the root directory.
if prefix == path {
logger.Info("create index.html for root directory")
return genIndexFile(path, indexPage())
}
// Otherwise generate a listing page
if d.IsDir() {
trimedPath := strings.TrimPrefix(strings.TrimPrefix(path, prefix), "/")
logger.Info("create index.html", "dir", trimedPath)
entries, err := os.ReadDir(path)
if err != nil {
return err
}
return genIndexFile(path, listingPage(trimedPath, entries))
}
return nil
}
// preview Previews the website at specified root directory on localhost:<port>
func preview(root, port string) error {
e := echo.New()
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogURI: true,
LogError: true,
LogStatus: true,
LogMethod: true,
LogLatency: true,
LogValuesFunc: func(_ echo.Context, v middleware.RequestLoggerValues) error {
logger.
With("Method", v.Method).
With("URI", v.URI).
With("Status", v.Status).
With("Latency", v.Latency).
Info("request")
if v.Error != nil {
logger.Error("request", "err", v.Error)
}
return nil
},
}))
e.Static("/", root)
return e.Start(port)
}
func main() {
genFlag := flag.Bool("gen", false, "Generate index files")
previewFlag := flag.Bool("preview", false, "Preview the built website")
minifyFlag := flag.Bool("minify", false, "Minify existing CSS files")
flag.Usage = func() {
fmt.Printf(`Usage: go run . [OPTIONS] DIR
Arguments:
DIR
Path to the directory containing the source files
Options:
`)
flag.PrintDefaults()
}
flag.Parse()
// Being fancy ^-^
logger.SetStyles(&log.Styles{
Separator: lipgloss.NewStyle().Foreground(lipgloss.Color("#7b88a1")).Bold(true),
Message: lipgloss.NewStyle().Foreground(lipgloss.Color("#7b88a1")),
Key: lipgloss.NewStyle().Foreground(lipgloss.Color("4")).Bold(true),
Levels: map[log.Level]lipgloss.Style{
log.FatalLevel: lipgloss.NewStyle().
Bold(true).
Background(lipgloss.Color("5")).
Foreground(lipgloss.Color("0")).
Padding(0, 1, 0, 1).
SetString("FATAL"),
log.ErrorLevel: lipgloss.NewStyle().
Bold(true).
Background(lipgloss.Color("1")).
Foreground(lipgloss.Color("0")).
Padding(0, 1, 0, 1).
SetString("ERROR"),
log.InfoLevel: lipgloss.NewStyle().
Bold(true).
Background(lipgloss.Color("6")).
Foreground(lipgloss.Color("0")).
Padding(0, 1, 0, 1).
SetString("INFO"),
},
Keys: map[string]lipgloss.Style{
"err": lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("1")),
},
})
// Load minifiers
minifier.AddFunc("text/html", html.Minify)
minifier.AddFunc("text/css", css.Minify)
// The root directory argument is required
if len(flag.Args()) != 1 {
logger.Fatal("failed to parse arguments", "err", "exactly 1 argument DIR is required")
}
rootDir, err := filepath.Abs(flag.Arg(0))
if err != nil {
logger.Fatal("unable to get root directory", "err", err)
}
// Run minification first, to ensure file size is calculated correctly later
if *minifyFlag {
err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Type().IsRegular() && filepath.Ext(path) == ".css" {
logger.Info("minify CSS", "file", strings.TrimPrefix(path, rootDir+"/"))
return minifyInPlace("text/css", path)
}
return nil
})
if err != nil {
logger.Fatal("failed minifying CSS files", "err", err)
}
}
if *genFlag {
err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
return traverse(rootDir, path, d)
})
if err != nil {
logger.Fatal("failed to traverse root directory", "err", err)
}
}
// Only preview after things are generated
if *previewFlag {
logger.Fatal("failed to preview website", "err", preview(rootDir, previewPort))
}
}