package main
import (
"flag"
"fmt"
"html/template"
"os"
"path/filepath"
"github.com/davidbyttow/govips/v2/vips"
"nebbet.no/internal/admin"
"nebbet.no/internal/admin/auth"
"nebbet.no/internal/db"
"nebbet.no/internal/media"
"nebbet.no/internal/server"
)
const (
outputDir = "public"
dataDir = "data"
metaDBPath = "data/meta.db"
searchDBPath = "data/search.db"
passwordFile = ".passwords"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
switch os.Args[1] {
case "serve":
cmdServe(os.Args[2:])
case "user":
cmdUser(os.Args[2:])
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
usage()
os.Exit(1)
}
}
func cmdServe(args []string) {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
port := fs.String("port", "8080", "port to listen on")
_ = fs.Parse(args)
if err := vips.Startup(nil); err != nil {
fmt.Fprintln(os.Stderr, "govips startup:", err)
os.Exit(1)
}
defer vips.Shutdown()
meta, search := mustOpenDBs()
defer meta.Close()
defer search.Close()
adminSrv := admin.NewServer(passwordFile, meta, search, outputDir)
engine := adminSrv.Engine()
tmpl, err := template.ParseGlob(filepath.Join("templates", "*.html"))
if err != nil {
fmt.Fprintln(os.Stderr, "template load error:", err)
os.Exit(1)
}
libMIMEs := server.AllowedMIMEs{
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".wasm": "application/wasm",
}
publicMIMEs := server.AllowedMIMEs{
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".json": "application/json",
".xml": "application/xml",
".txt": "text/plain; charset=utf-8",
".ico": "image/x-icon",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".webp": "image/webp",
}
stylesMIMEs := server.AllowedMIMEs{
".css": "text/css; charset=utf-8",
}
engine.GET("/assets/styles/*filepath", server.FileHandler("assets/styles", stylesMIMEs))
engine.GET("/lib/*filepath", server.FileHandler("assets/lib", libMIMEs))
engine.GET("/assets/components/*filepath", server.FileHandler("assets/components", libMIMEs))
// Image upload (admin-protected) + public image serving with on-the-fly conversion
mediaSrv := media.NewMediaHandler("assets/media")
adminSrv.RegisterUploadRoute(mediaSrv.HandleUpload)
engine.GET("/media/*filepath", mediaSrv.HandleServe)
frontpage := server.NewFrontpageHandler(meta, search, tmpl)
engine.GET("/", frontpage.Serve)
postHandler := server.NewPostHandler(meta, tmpl, outputDir)
engine.GET("/:slug", postHandler.Serve)
engine.NoRoute(server.PublicFileHandler(outputDir, publicMIMEs))
addr := ":" + *port
fmt.Printf("listening on http://localhost%s\n", addr)
fmt.Printf(" admin UI: http://localhost%s/admin/\n", addr)
if err := engine.Run(addr); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// ── user ─────────────────────────────────────────────────────────────────────
func cmdUser(args []string) {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "usage: nebbet user [username]")
os.Exit(1)
}
a := auth.New(passwordFile)
switch args[0] {
case "add":
requireArg(args, 1, "nebbet user add ")
if err := a.AddUser(args[1]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("user added:", args[1])
case "list":
users, err := a.ListUsers()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if len(users) == 0 {
fmt.Println("(no users)")
return
}
for _, u := range users {
fmt.Println(u)
}
case "delete":
requireArg(args, 1, "nebbet user delete ")
if err := a.DeleteUser(args[1]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("user deleted:", args[1])
case "passwd":
requireArg(args, 1, "nebbet user passwd ")
if err := a.ChangePassword(args[1]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("password updated:", args[1])
default:
fmt.Fprintf(os.Stderr, "unknown user subcommand %q\n", args[0])
os.Exit(1)
}
}
func mustOpenDBs() (*db.MetaDB, *db.SearchDB) {
if err := os.MkdirAll(dataDir, 0755); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
meta, err := db.OpenMeta(metaDBPath)
if err != nil {
fmt.Fprintln(os.Stderr, "meta db:", err)
os.Exit(1)
}
search, err := db.OpenSearch(searchDBPath)
if err != nil {
fmt.Fprintln(os.Stderr, "search db:", err)
os.Exit(1)
}
return meta, search
}
func requireArg(args []string, n int, usage string) {
if len(args) <= n {
fmt.Fprintln(os.Stderr, "usage:", usage)
os.Exit(1)
}
}
func usage() {
fmt.Fprintln(os.Stderr, `nebbet.no
Commands:
serve [--port N] serve site + admin UI (default port 8080)
user add add user to password file
user list list users
user delete remove user
user passwd change user password`)
}