package main import ( "io/fs" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" "gitea.kmux.cn/zhilv/scriptforge/internal/assets" "gitea.kmux.cn/zhilv/scriptforge/internal/handler" "gitea.kmux.cn/zhilv/scriptforge/internal/middleware" "gitea.kmux.cn/zhilv/scriptforge/internal/model" "gitea.kmux.cn/zhilv/scriptforge/internal/service" ) func main() { gin.SetMode(gin.ReleaseMode) db := initDB() syncDB(db) svc := service.NewScriptService(db) go cleanupLoop(svc) r := gin.New() r.Use(gin.Recovery(), middleware.Logger()) api := r.Group("/api") { api.Use(middleware.RateLimit(60)) api.POST("/scripts", middleware.RateLimit(10), handler.CreateScript(svc)) api.GET("/scripts/:id", handler.GetScript(svc)) api.DELETE("/scripts/:id", handler.DeleteScript(svc)) } r.GET("/raw/:id", handler.GetRawScript(svc)) r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) serveStaticOrSPA(r) port := getPort() log.Printf("Listening on :%s", port) go func() { if err := r.Run(":" + port); err != nil { log.Fatalf("server error: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down...") } func initDB() *gorm.DB { dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "scriptforge.db" } db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) if err != nil { log.Fatalf("failed to connect database: %v", err) } return db } func syncDB(db *gorm.DB) { if err := db.AutoMigrate(&model.Script{}); err != nil { log.Fatalf("failed to migrate database: %v", err) } } func cleanupLoop(svc *service.ScriptService) { for { time.Sleep(1 * time.Hour) if n, err := svc.CleanupExpired(); err != nil { log.Printf("cleanup error: %v", err) } else if n > 0 { log.Printf("cleaned up %d expired scripts", n) } } } func getPort() string { if p := os.Getenv("PORT"); p != "" { return p } return "8080" } func serveStaticOrSPA(r *gin.Engine) { staticFS, err := fs.Sub(assets.FS, "dist") if err != nil { log.Printf("no embedded static files, API-only mode: %v", err) return } r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/raw") { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } file, err := staticFS.Open(path) if err != nil { c.FileFromFS("index.html", http.FS(staticFS)) return } file.Close() c.FileFromFS(path, http.FS(staticFS)) }) r.GET("/", func(c *gin.Context) { c.FileFromFS("index.html", http.FS(staticFS)) }) }