package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "filefast/backend/internal/config" "filefast/backend/internal/handler" "filefast/backend/internal/scheduler" "filefast/backend/internal/service" "filefast/backend/internal/storage" "filefast/backend/internal/store" "filefast/backend/internal/ws" ) func main() { cfg := config.Load() logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.LogLevel})) memStore := store.NewMemoryStore(cfg.Runtime) var redisBackplane *storage.RedisClient storageConnected := false redisConnected := false sqliteClient, err := storage.NewSQLiteClient(cfg.SQLite) if err != nil { logger.Error("failed to initialize sqlite client", "path", cfg.SQLite.Path, "error", err) os.Exit(1) } defer sqliteClient.Close() storagePingCtx, storagePingCancel := context.WithTimeout(context.Background(), 5*time.Second) defer storagePingCancel() if err := sqliteClient.Ping(storagePingCtx); err != nil { logger.Warn("sqlite connection failed", "path", cfg.SQLite.Path, "error", err) } else { storageConnected = true logger.Info("sqlite connected", "path", cfg.SQLite.Path) bootstrapCtx, bootstrapCancel := context.WithTimeout(context.Background(), 10*time.Second) defer bootstrapCancel() if err := sqliteClient.EnsureSchema(bootstrapCtx); err != nil { logger.Warn("failed to ensure sqlite schema", "error", err) } else { if err := sqliteClient.EnsureAdminUser(bootstrapCtx, cfg.Admin.Username, cfg.Admin.Password); err != nil { logger.Warn("failed to ensure admin user", "username", cfg.Admin.Username, "error", err) } if err := sqliteClient.ResetOnlineDevices(bootstrapCtx); err != nil { logger.Warn("failed to reset device online states", "error", err) } snapshot, err := sqliteClient.LoadSnapshot(bootstrapCtx, cfg.Runtime) if err != nil { logger.Warn("failed to restore state from sqlite", "error", err) } else { memStore.LoadSnapshot(snapshot) logger.Info( "restored state from sqlite", "devices", len(snapshot.Devices), "rooms", len(snapshot.Rooms), "transfers", len(snapshot.Transfers), "fallback_objects", len(snapshot.FallbackObjects), ) } } memStore.SetPersistence(sqliteClient, 5*time.Second, func(kind, id string, err error) { logger.Warn("failed to persist state", "kind", kind, "id", id, "error", err) }) } redisClient := storage.NewRedisClient(cfg.Redis) defer redisClient.Close() redisPingCtx, redisPingCancel := context.WithTimeout(context.Background(), 5*time.Second) defer redisPingCancel() if err := redisClient.Ping(redisPingCtx); err != nil { logger.Warn("redis connection failed", "addr", cfg.Redis.Addr, "db", cfg.Redis.DB, "error", err) } else { redisConnected = true logger.Info("redis connected", "addr", cfg.Redis.Addr, "db", cfg.Redis.DB) redisBackplane = redisClient } deviceService := service.NewDeviceService(memStore, redisBackplane, redisBackplane) roomService := service.NewRoomService(memStore, cfg.RoomTTL) transferService := service.NewTransferService(memStore) adminService := service.NewAdminService(memStore, cfg.Admin, redisBackplane, sqliteClient) minioClient, err := storage.NewMinIOClient(cfg.MinIO) if err != nil { logger.Error("failed to initialize minio client", "error", err) } hub := ws.NewHub(logger, deviceService, redisBackplane) go hub.Run() cleanupScheduler := scheduler.NewCleanupScheduler(logger, memStore, minioClient) cleanupScheduler.Start() defer cleanupScheduler.Stop() httpHandler := handler.NewHTTPHandler(handler.Dependencies{ Config: cfg, Logger: logger, Store: memStore, DeviceService: deviceService, RoomService: roomService, TransferService: transferService, AdminService: adminService, MinIOClient: minioClient, Hub: hub, StorageReady: storageConnected, RedisReady: redisConnected, }) server := &http.Server{ Addr: cfg.HTTPAddress, Handler: httpHandler.Router(), ReadHeaderTimeout: 5 * time.Second, } go func() { logger.Info("server listening", "addr", cfg.HTTPAddress) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("server crashed", "error", err) os.Exit(1) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Error("server shutdown failed", "error", err) } logger.Info("server stopped") }