summaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/iblog/main.go34
-rw-r--r--cmd/iblog/serve.go110
-rw-r--r--cmd/iblog/user.go68
3 files changed, 212 insertions, 0 deletions
diff --git a/cmd/iblog/main.go b/cmd/iblog/main.go
new file mode 100644
index 0000000..1c94f4d
--- /dev/null
+++ b/cmd/iblog/main.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+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 usage() {
+ fmt.Fprintln(os.Stderr, `iblog
+
+Commands:
+ serve [--port N] [--root PATH] serve site + admin UI (default port 8080)
+ user [--root PATH] add <name> add user to password file
+ user [--root PATH] list list users
+ user [--root PATH] delete <name> remove user
+ user [--root PATH] passwd <name> change user password`)
+}
diff --git a/cmd/iblog/serve.go b/cmd/iblog/serve.go
new file mode 100644
index 0000000..7636142
--- /dev/null
+++ b/cmd/iblog/serve.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "html/template"
+ "os"
+ "path/filepath"
+
+ iblog "iblog"
+ "iblog/internal/db"
+ "iblog/internal/media"
+ "iblog/internal/server"
+
+ "github.com/davidbyttow/govips/v2/vips"
+)
+
+func cmdServe(args []string) {
+ fs := flag.NewFlagSet("serve", flag.ExitOnError)
+ port := fs.String("port", "8080", "port to listen on")
+ root := fs.String("root", ".", "site root directory")
+ _ = fs.Parse(args)
+
+ dataDir := filepath.Join(*root, "data")
+ dbPath := filepath.Join(dataDir, "iblog.db")
+ passwordFile := filepath.Join(dataDir, ".passwords")
+ publicMedia := filepath.Join(dataDir, "media")
+ outputDir := filepath.Join(*root, "public")
+ userComponents := filepath.Join(*root, "components")
+ userStyles := filepath.Join(*root, "styles")
+
+ if err := vips.Startup(nil); err != nil {
+ fmt.Fprintln(os.Stderr, "govips startup:", err)
+ os.Exit(1)
+ }
+
+ defer vips.Shutdown()
+
+ database := mustOpenDB(dataDir, dbPath)
+ defer database.Close()
+
+ if err := os.MkdirAll(publicMedia, 0755); err != nil {
+ fmt.Fprintln(os.Stderr, "media dir:", err)
+ os.Exit(1)
+ }
+
+ tmpl, err := template.New("").ParseFS(iblog.SiteTemplates, "templates/*.html", "templates/**/*.html")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "site template load error:", err)
+ os.Exit(1)
+ }
+
+ adminSrv := server.NewAdminServer(passwordFile, database, outputDir, iblog.AdminAssets, iblog.SiteAssets, tmpl)
+ engine := adminSrv.Engine()
+
+ 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",
+ }
+
+ mediaSrv := media.NewMediaHandler(publicMedia)
+ adminSrv.RegisterUploadRoute(mediaSrv.HandleUpload)
+ engine.GET("/media/*filepath", mediaSrv.HandleServe)
+
+ if info, err := os.Stat(userComponents); err == nil && info.IsDir() {
+ engine.GET("/components/*filepath", server.FileHandler(userComponents, nil))
+ }
+
+ if info, err := os.Stat(userStyles); err == nil && info.IsDir() {
+ engine.GET("/styles/*filepath", server.FileHandler(userStyles, nil))
+ }
+
+ frontpage := server.NewFrontpageHandler(database)
+ engine.GET("/", frontpage.Serve)
+
+ postHandler := server.NewPostHandler(database, tmpl, outputDir)
+ engine.GET("/:slug", postHandler.Serve)
+
+ engine.NoRoute(server.PublicFileHandler(outputDir, publicMIMEs))
+
+ addr := "localhost:" + *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)
+ }
+}
+
+func mustOpenDB(dataDir, dbPath string) *db.DB {
+ if err := os.MkdirAll(dataDir, 0755); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ database, err := db.Open(dbPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "db:", err)
+ os.Exit(1)
+ }
+ return database
+}
diff --git a/cmd/iblog/user.go b/cmd/iblog/user.go
new file mode 100644
index 0000000..f9d422a
--- /dev/null
+++ b/cmd/iblog/user.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ auth "iblog/internal"
+ "os"
+ "path/filepath"
+)
+
+func cmdUser(args []string) {
+ f := flag.NewFlagSet("user", flag.ExitOnError)
+ root := f.String("root", ".", "site root directory")
+ _ = f.Parse(args)
+ remaining := f.Args()
+ if len(remaining) == 0 {
+ fmt.Fprintln(os.Stderr, "usage: iblog user [--root PATH] <add|list|delete|passwd> [username]")
+ os.Exit(1)
+ }
+ passwordFile := filepath.Join(*root, "data", ".passwords")
+ a := auth.NewAuth(passwordFile)
+ switch remaining[0] {
+ case "add":
+ requireArg(remaining, 1, "iblog user add <username>")
+ if err := a.AddUser(remaining[1]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("user added:", remaining[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(remaining, 1, "iblog user delete <username>")
+ if err := a.DeleteUser(remaining[1]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("user deleted:", remaining[1])
+ case "passwd":
+ requireArg(remaining, 1, "iblog user passwd <username>")
+ if err := a.ChangePassword(remaining[1]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("password updated:", remaining[1])
+ default:
+ fmt.Fprintf(os.Stderr, "unknown user subcommand %q\n", remaining[0])
+ os.Exit(1)
+ }
+}
+
+func requireArg(args []string, n int, usage string) {
+ if len(args) <= n {
+ fmt.Fprintln(os.Stderr, "usage:", usage)
+ os.Exit(1)
+ }
+}