package app import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "update_server/internal/apikeys" "update_server/internal/auth" "update_server/internal/config" database "update_server/internal/db" httpserver "update_server/internal/http" "update_server/internal/releases" "update_server/internal/storage" ) type App struct { config config.Config logger *slog.Logger server *http.Server store *database.Store } func New(cfg config.Config, logger *slog.Logger) (*App, error) { if err := os.MkdirAll(cfg.DataDir, 0o750); err != nil { return nil, fmt.Errorf("create data dir: %w", err) } renderer, err := httpserver.NewRenderer(cfg.TemplatesDir) if err != nil { return nil, fmt.Errorf("create renderer: %w", err) } sqliteDB, err := database.Open(context.Background(), cfg.SQLitePath) if err != nil { return nil, fmt.Errorf("open database: %w", err) } if err := database.Migrate(context.Background(), sqliteDB, cfg.MigrationsDir); err != nil { sqliteDB.Close() return nil, fmt.Errorf("apply migrations: %w", err) } store := database.NewStore(sqliteDB) authService := auth.NewService(cfg, logger, store) apiKeyService := apikeys.NewService(store) if err := authService.EnsureBootstrapAdmin(context.Background()); err != nil { sqliteDB.Close() return nil, fmt.Errorf("bootstrap admin auth: %w", err) } artifactStore, err := storage.NewLocal(cfg.ArtifactsDir) if err != nil { sqliteDB.Close() return nil, fmt.Errorf("create artifact storage: %w", err) } releaseService := releases.NewService(store, artifactStore) router := httpserver.NewRouter(cfg, logger, renderer, store, authService, apiKeyService, releaseService) server := &http.Server{ Addr: cfg.HTTPAddr, Handler: router, ReadTimeout: cfg.ReadTimeout, ReadHeaderTimeout: cfg.ReadHeaderTimeout, WriteTimeout: cfg.WriteTimeout, IdleTimeout: cfg.IdleTimeout, MaxHeaderBytes: cfg.MaxHeaderBytes, } return &App{ config: cfg, logger: logger, server: server, store: store, }, nil } func (a *App) Run(ctx context.Context) (runErr error) { defer func() { if a.store == nil { return } if err := a.store.Close(); err != nil { closeErr := fmt.Errorf("close database: %w", err) if runErr != nil { runErr = errors.Join(runErr, closeErr) return } runErr = closeErr } }() runCtx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() serverErr := make(chan error, 1) go func() { a.logger.Info("starting server", "addr", a.config.HTTPAddr, "base_url", a.config.BaseURL, "data_dir", a.config.DataDir, "sqlite_path", a.config.SQLitePath, ) serverErr <- a.server.ListenAndServe() }() select { case err := <-serverErr: if err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil case <-runCtx.Done(): } shutdownCtx, cancel := context.WithTimeout(context.Background(), a.config.ShutdownTimeout) defer cancel() a.logger.Info("shutting down server") if err := a.server.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("shutdown server: %w", err) } err := <-serverErr if err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil }