Files
vibetree/cmd/vibetree/main.go
T
Owen f3a959334f Rename project from inventree to vibetree
- 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).
2026-08-17 17:14:52 -05:00

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
}