// Command inventree runs the inventree 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" "inventree/internal/database" "inventree/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("inventree", version) return nil case "database": if len(args) < 2 { return fmt.Errorf("usage: inventree database ") } 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(`inventree - a small IT asset inventory system Usage: inventree [serve] Start the web server (default command) inventree version Print the version inventree database migrate Apply pending database migrations inventree 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/inventree.db") -secure-cookies mark the session cookie Secure (enable when serving over HTTPS)`) } func dbPathFlag(args []string) (dbPath string, rest []string) { dbPath = "data/inventree.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/inventree.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("inventree listening", "addr", addr, "db", dbPath) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } log.Info("server stopped") return nil }