package main import ( "bufio" "context" "errors" "io/fs" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" assets "galo" "galo/internal/app" "galo/internal/auth" "galo/internal/config" "galo/internal/db" "galo/internal/settings" "galo/internal/sse" "galo/internal/storage" "galo/internal/store" ) // version is set at build time via -ldflags "-X main.version=..." var version = "dev" func main() { loadDotEnv(".env") loadDotEnv("../.env") logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) slog.SetDefault(logger) slog.Info("starting galo", "version", version) cfg, err := config.Load() if err != nil { slog.Error("config error", "error", err) os.Exit(1) } switch cfg.Database.Backend { case "sqlite": slog.Info("metadata backend", "backend", "sqlite", "path", cfg.Database.SQLitePath) default: slog.Info("metadata backend", "backend", "postgres", "host", cfg.Postgres.Host, "database", cfg.Postgres.Database) } switch cfg.Storage.Backend { case "fs": slog.Info("storage backend", "backend", "fs", "root", cfg.Storage.FSRoot) default: slog.Info("storage backend", "backend", "s3", "endpoint", cfg.S3.Endpoint, "bucket", cfg.S3.Bucket) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() var ( repos store.Repos ping func(ctx context.Context) error closer func() ) switch cfg.Database.Backend { case "sqlite": migFS, err := fs.Sub(assets.Migrations, "migrations/sqlite") if err != nil { slog.Error("migrations fs", "error", err) os.Exit(1) } if err := db.MigrateSQLite(cfg.Database.SQLitePath, migFS); err != nil { slog.Error("migration failed", "error", err) os.Exit(1) } database, err := db.OpenSQLite(cfg.Database.SQLitePath) if err != nil { slog.Error("database open failed", "error", err) os.Exit(1) } repos = store.NewSQLite(database) ping = database.PingContext closer = func() { _ = database.Close() } default: migFS, err := fs.Sub(assets.Migrations, "migrations") if err != nil { slog.Error("migrations fs", "error", err) os.Exit(1) } if err := db.Migrate(cfg.Postgres.DSN(), migFS); err != nil { slog.Error("migration failed", "error", err) os.Exit(1) } pool, err := db.Connect(ctx, cfg.Postgres.DSN()) if err != nil { slog.Error("database connect failed", "error", err) os.Exit(1) } repos = store.NewPostgres(pool) ping = pool.Ping closer = pool.Close } defer closer() slog.Info("migrations applied", "db_backend", cfg.Database.Backend) secret, err := repos.Settings.Ensure(ctx, settings.JWTSecretKey, settings.GenerateSecret) if err != nil { slog.Error("secret init failed", "error", err) os.Exit(1) } tm := auth.NewTokenManager(secret, cfg.Auth.AccessTTL, cfg.Auth.RefreshTTL) blob, err := storage.Open(ctx, cfg, secret) if err != nil { slog.Error("storage init failed", "error", err) os.Exit(1) } if err := blob.EnsureBucket(ctx); err != nil { slog.Warn("could not ensure bucket", "error", err) } else { slog.Info("storage ready", "location", blob.Bucket()) } hub := sse.NewHub(cfg.SSE.ClientBuffer) server := app.New(cfg, repos, ping, tm, blob, hub) go server.StartWorker(ctx) httpServer := &http.Server{ Addr: cfg.Server.Addr, Handler: server.Router(), ReadHeaderTimeout: 10 * time.Second, } go func() { slog.Info("listening", "addr", cfg.Server.Addr) if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("server error", "error", err) stop() } }() <-ctx.Done() slog.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = httpServer.Shutdown(shutdownCtx) } func loadDotEnv(path string) { f, err := os.Open(path) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, value, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) value = strings.TrimSpace(value) if _, exists := os.LookupEnv(key); !exists { _ = os.Setenv(key, value) } } }