package main import ( "embed" "flag" "fmt" "io/fs" "log" "net/http" "os" "strings" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gitm/internal/config" "gitm/internal/database" "gitm/internal/handler" "gitm/internal/middleware" "gitm/internal/sync" ) //go:embed all:web/dist var webFS embed.FS var ( flagAddr = flag.String("addr", "", "Listen address") flagDataDir = flag.String("data", "", "Data directory") flagInit = flag.Bool("init", false, "Initialize database and set password") ) func main() { flag.Parse() cfg := config.Get() if *flagDataDir != "" { cfg.SetDataDir(*flagDataDir) } else { cfg.SetDataDir(cfg.DataDir) } if *flagAddr != "" { cfg.ListenAddr = *flagAddr } if err := cfg.EnsureDirs(); err != nil { log.Fatalf("Failed to create directories: %v", err) } if err := database.Initialize(cfg.DBPath); err != nil { log.Fatalf("Failed to initialize database: %v", err) } defer database.Close() if listenAddr, err := database.GetSetting("listen_addr"); err == nil && listenAddr != "" { cfg.ListenAddr = listenAddr } maxConcurrent := 3 if maxStr, err := database.GetSetting("max_concurrent"); err == nil && maxStr != "" { fmt.Sscanf(maxStr, "%d", &maxConcurrent) } engine := sync.NewEngine(maxConcurrent) scheduler := sync.NewScheduler(engine) if *flagInit { runInitMode() return } if pwd, _ := database.GetSetting("admin_password"); pwd == "" { fmt.Println("Not initialized. Please run with --init flag first.") os.Exit(1) } middleware.SetJWTSecret("gitm-default-secret") scheduler.ReloadAll() scheduler.Start() defer scheduler.Stop() log.Printf("GitM starting on %s", cfg.ListenAddr) log.Fatal(runServer(cfg, engine)) } func runInitMode() { var password string fmt.Print("Enter admin password: ") fmt.Scanln(&password) if password == "" { log.Fatal("Password cannot be empty") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { log.Fatalf("Failed to hash password: %v", err) } database.SetSetting("admin_password", string(hash)) database.SetSetting("listen_addr", ":9000") database.SetSetting("max_concurrent", "3") fmt.Println("Initialized successfully!") } func runServer(cfg *config.Config, engine *sync.Engine) error { gin.SetMode(gin.ReleaseMode) r := gin.Default() // Serve embedded frontend distFS, err := fs.Sub(webFS, "web/dist") if err == nil { fileServer := http.FileServer(http.FS(distFS)) r.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api") { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } // Try to serve static file first name := strings.TrimPrefix(c.Request.URL.Path, "/") if name != "" { f, err := distFS.Open(name) if err == nil { f.Close() fileServer.ServeHTTP(c.Writer, c.Request) return } } // SPA fallback - serve index.html data, _ := webFS.ReadFile("web/dist/index.html") c.Data(http.StatusOK, "text/html; charset=utf-8", data) }) } api := r.Group("/api") { api.POST("/login", handler.HandleLogin) api.POST("/init", handler.HandleInit) protected := api.Group("") protected.Use(middleware.AuthMiddleware()) { protected.GET("/settings", handler.HandleGetSettings) protected.PUT("/settings", handler.HandleUpdateSettings) protected.GET("/servers", handler.HandleListServers) protected.POST("/servers/test", handler.HandleTestConnection) protected.POST("/servers", handler.HandleCreateServer) protected.PUT("/servers/:id", handler.HandleUpdateServer) protected.DELETE("/servers/:id", handler.HandleDeleteServer) protected.GET("/servers/:id/repos", handler.HandleListRepos) protected.POST("/servers/:id/discover", handler.HandleDiscoverRepos) protected.POST("/servers/:id/sync", handler.HandleSyncServer(engine)) protected.GET("/servers/:id/sync/status", handler.HandleGetSyncStatus(engine)) protected.POST("/sync/all", handler.HandleSyncAll(engine)) protected.GET("/sync/logs", handler.HandleGetSyncLogs) protected.GET("/sync/stats", handler.HandleGetStats) } } return r.Run(cfg.ListenAddr) }