- go.mod module path (and every internal/vibetree import) inventree -> vibetree - cmd/inventree/ -> cmd/vibetree/, deploy/inventree.service -> vibetree.service - Default site name (seed data + the siteName template func's fallback) is now "vibetree", shown in the navbar, <title>, and login page - Session cookie renamed inventree_session -> vibetree_session - Default DB path data/inventree.db -> data/vibetree.db, CLI/log text, Dockerfile, docker-compose.yml, README, .gitignore/.dockerignore all updated to match Existing installs: this changes the session cookie name (users will be signed out once) and the default DB filename (point -db at your existing file, or rename it, to keep your data).
206 lines
4.5 KiB
Go
206 lines
4.5 KiB
Go
// Command vibetree runs the vibetree IT asset inventory server, or one of
|
|
// its supporting subcommands (version, database migrate, database backup).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"vibetree/internal/database"
|
|
"vibetree/internal/web"
|
|
)
|
|
|
|
// version is set at build time via -ldflags "-X main.version=...".
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
if err := run(os.Args[1:]); err != nil {
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(args []string) error {
|
|
if len(args) == 0 {
|
|
return runServe(args)
|
|
}
|
|
switch args[0] {
|
|
case "serve":
|
|
return runServe(args[1:])
|
|
case "version":
|
|
fmt.Println("vibetree", version)
|
|
return nil
|
|
case "database":
|
|
if len(args) < 2 {
|
|
return fmt.Errorf("usage: vibetree database <migrate|backup>")
|
|
}
|
|
switch args[1] {
|
|
case "migrate":
|
|
return runDatabaseMigrate(args[2:])
|
|
case "backup":
|
|
return runDatabaseBackup(args[2:])
|
|
default:
|
|
return fmt.Errorf("unknown database subcommand %q", args[1])
|
|
}
|
|
case "help", "-h", "--help":
|
|
printUsage()
|
|
return nil
|
|
default:
|
|
printUsage()
|
|
return fmt.Errorf("unknown command %q", args[0])
|
|
}
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Println(`vibetree - a small IT asset inventory system
|
|
|
|
Usage:
|
|
vibetree [serve] Start the web server (default command)
|
|
vibetree version Print the version
|
|
vibetree database migrate Apply pending database migrations
|
|
vibetree database backup [path] Write a consistent backup snapshot
|
|
|
|
Flags for "serve":
|
|
-addr string listen address (default ":8080")
|
|
-db string path to the SQLite database file (default "data/vibetree.db")
|
|
-secure-cookies mark the session cookie Secure (enable when serving over HTTPS)`)
|
|
}
|
|
|
|
func dbPathFlag(args []string) (dbPath string, rest []string) {
|
|
dbPath = "data/vibetree.db"
|
|
for i := 0; i < len(args); i++ {
|
|
if args[i] == "-db" && i+1 < len(args) {
|
|
dbPath = args[i+1]
|
|
i++
|
|
continue
|
|
}
|
|
rest = append(rest, args[i])
|
|
}
|
|
return
|
|
}
|
|
|
|
func runDatabaseMigrate(args []string) error {
|
|
dbPath, _ := dbPathFlag(args)
|
|
if err := ensureDir(dbPath); err != nil {
|
|
return err
|
|
}
|
|
db, err := database.Open(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
fmt.Println("migrations applied to", dbPath)
|
|
return nil
|
|
}
|
|
|
|
func runDatabaseBackup(args []string) error {
|
|
dbPath, rest := dbPathFlag(args)
|
|
dest := fmt.Sprintf("backup-%s.db", time.Now().Format("20060102-150405"))
|
|
if len(rest) > 0 {
|
|
dest = rest[0]
|
|
}
|
|
if err := ensureDir(dbPath); err != nil {
|
|
return err
|
|
}
|
|
db, err := database.Open(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := ensureDir(dest); err != nil {
|
|
return err
|
|
}
|
|
if _, err := db.Exec(`VACUUM INTO ?`, dest); err != nil {
|
|
return fmt.Errorf("backup failed: %w", err)
|
|
}
|
|
fmt.Println("backup written to", dest)
|
|
return nil
|
|
}
|
|
|
|
func ensureDir(path string) error {
|
|
dir := filepath.Dir(path)
|
|
if dir == "" || dir == "." {
|
|
return nil
|
|
}
|
|
return os.MkdirAll(dir, 0o755)
|
|
}
|
|
|
|
func runServe(args []string) error {
|
|
var (
|
|
addr = ":8080"
|
|
dbPath = "data/vibetree.db"
|
|
secureCookies = false
|
|
)
|
|
for i := 0; i < len(args); i++ {
|
|
switch args[i] {
|
|
case "-addr":
|
|
if i+1 < len(args) {
|
|
addr = args[i+1]
|
|
i++
|
|
}
|
|
case "-db":
|
|
if i+1 < len(args) {
|
|
dbPath = args[i+1]
|
|
i++
|
|
}
|
|
case "-secure-cookies":
|
|
secureCookies = true
|
|
}
|
|
}
|
|
|
|
log := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
|
|
if err := ensureDir(dbPath); err != nil {
|
|
return err
|
|
}
|
|
db, err := database.Open(dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open database: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
app, err := web.NewApp(db, log, secureCookies)
|
|
if err != nil {
|
|
return fmt.Errorf("initialize app: %w", err)
|
|
}
|
|
|
|
stop := make(chan struct{})
|
|
app.StartSessionJanitor(stop)
|
|
defer close(stop)
|
|
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: app.Router(),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
log.Info("shutting down")
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer shutdownCancel()
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
log.Error("graceful shutdown failed", "error", err)
|
|
}
|
|
}()
|
|
|
|
log.Info("vibetree listening", "addr", addr, "db", dbPath)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
return err
|
|
}
|
|
log.Info("server stopped")
|
|
return nil
|
|
}
|