Files
GitM/main.go
panw 5eff309a9f feat: 添加同步进度显示和中文界面支持
refactor: 重构同步引擎以支持进度跟踪
style: 更新前端界面为中文
docs: 更新README为中文文档
2026-04-01 10:43:51 +08:00

151 lines
4.1 KiB
Go

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)
}