初始提交: ScriptForge 脚本快速转运行链接服务
Some checks failed
Release / build-and-release (push) Failing after 1m31s
Some checks failed
Release / build-and-release (push) Failing after 1m31s
- Go 后端 (Gin + GORM + SQLite) 提供 API 和纯文本脚本服务 - Vite + React + TypeScript + Tailwind 前端 - 单二进制部署 (Go embed 前端静态文件) - Gitea Actions CI/CD: 打标签自动构建多平台 Release - 支持 bash/zsh/sh/fish/python3/node/ruby/php 8种运行环境 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
129
backend/cmd/server/main.go
Normal file
129
backend/cmd/server/main.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user