从服务端归档恢复项目源码(服务端 upload-pack 异常,无法 clone)

This commit is contained in:
CodeBuddy
2026-10-26 16:35:28 +08:00
commit 875976a120
318 changed files with 42625 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
package httpapi
import (
"bytes"
"fmt"
"io"
"io/fs"
"log/slog"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// HandlerFunc 一个 action 的处理函数。返回 errResponded(由 c.JSON 产生)表示正常结束;
// 返回其它 error 对应 PHP 顶层 catch → 500 {"error": message}。
type HandlerFunc func(c *Ctx) error
type Router struct {
app *App
handlers map[string]HandlerFunc
adminFS fs.FS // React 后台构建产物(embed)
}
func NewRouter(app *App, adminFS fs.FS) *Router {
return &Router{app: app, handlers: map[string]HandlerFunc{}, adminFS: adminFS}
}
// Handle 注册 action
func (r *Router) Handle(action string, h HandlerFunc) {
if _, dup := r.handlers[action]; dup {
panic("duplicate action: " + action)
}
r.handlers[action] = h
}
// Actions 已注册的 action 列表(用于自检)
func (r *Router) Actions() []string {
out := make([]string, 0, len(r.handlers))
for k := range r.handlers {
out = append(out, k)
}
return out
}
// 与 index.php 中 $publicActions / $adminActions 完全一致
var publicActions = set("register", "login", "wechat_login", "site_info", "site_stats", "admin_login",
"check_invite_code", "register_by_invite", "check_auth")
var adminActions = set("admin_login", "admin_check_auth", "admin_logout", "admin_stats", "admin_stats_detail", "admin_couples", "admin_couple_detail", "admin_users", "admin_user_detail", "admin_change_password", "admin_change_username", "admin_reset_user_password", "admin_update_user", "admin_delete_user", "admin_delete_content", "admin_activity_log", "admin_get_global_bg", "admin_set_global_bg", "admin_clear_global_bg", "admin_couple_bg_list", "admin_clear_couple_bg", "admin_contents", "admin_fix_last_login", "admin_unbind_requests", "admin_force_unbind", "admin_restore_couple", "admin_delete_couple_data", "admin_pets", "admin_pet_detail", "admin_clean_locations", "admin_subscribe_logs")
func set(items ...string) map[string]bool {
m := make(map[string]bool, len(items))
for _, s := range items {
m[s] = true
}
return m
}
var actionInURIRe = regexp.MustCompile(`[?&]action=([^&]+)`)
// Engine 构建 gin 引擎:API 入口、上传文件静态服务、后台 SPA。
func (r *Router) Engine(debugMode bool) *gin.Engine {
if debugMode {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
e := gin.New()
e.Use(gin.Recovery(), accessLog())
e.RedirectTrailingSlash = false
e.MaxMultipartMemory = 32 << 20
// 简洁 API 路径;旧入口保留给已发布客户端和管理后台。
e.Any("/api", r.serveAPI)
e.Any("/api/", r.serveAPI)
e.Any("/api/:action", r.serveAPI)
e.Any("/backend/api/index.php", r.serveAPI)
e.Any("/backend/api/", r.serveAPI)
e.Any("/backend/api", r.serveAPI)
// 上传文件:DB 中路径 uploads/... → URL /backend/uploads/...(gin.Dir 关闭目录列表;缓存 1 个月,对应 .htaccess)
uploadDir := r.app.Cfg.UploadDir()
uploads := e.Group("/backend/uploads", func(c *gin.Context) {
// 只对真实存在的文件加长缓存;404 不能被浏览器缓存(否则文件补上后仍显示不出来)
p := filepath.Join(uploadDir, filepath.FromSlash(strings.TrimPrefix(c.Param("filepath"), "/")))
if st, err := os.Stat(p); err == nil && !st.IsDir() && !strings.Contains(c.Param("filepath"), "..") {
c.Header("Cache-Control", "public, max-age=2592000")
} else {
c.Header("Cache-Control", "no-store")
}
})
uploads.StaticFS("/", gin.Dir(uploadDir, false))
// 管理后台 SPA
e.GET("/backend/admin", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/backend/admin/") })
e.GET("/backend/admin/*filepath", r.serveAdmin)
e.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
return e
}
func (r *Router) serveAPI(g *gin.Context) {
w, req := g.Writer, g.Request
// CORS(与 PHP 一致)
h := w.Header()
// API 响应按用户身份变化,且权限可能随时修改,不能复用旧的 GET 结果。
h.Set("Cache-Control", "no-store")
h.Set("Access-Control-Allow-Origin", "*")
h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if req.Method == http.MethodOptions {
w.WriteHeader(200)
return
}
c := &Ctx{G: g, W: w, R: req, App: r.app, DB: r.app.DB, Method: req.Method}
g.Set("action", "")
// 路径中的 action 优先,兼容旧客户端的查询和表单参数。
action := g.Param("action")
if action == "" {
action = req.URL.Query().Get("action")
}
if action == "" {
action = c.Form("action")
}
if action == "" {
if m := actionInURIRe.FindStringSubmatch(req.RequestURI); m != nil {
if v, err := url.QueryUnescape(m[1]); err == nil {
action = v
}
}
}
// A camelCase path opts into camelCase request/response fields. Snake paths retain legacy payloads.
publicAction := action
c.CamelCase = g.Param("action") != "" && !strings.Contains(action, "_")
action = snakeName(action)
c.Action = action
g.Set("action", publicAction)
defer func() {
if rec := recover(); rec != nil {
slog.Error("panic", "action", action, "err", rec, "stack", string(debug.Stack()))
if !c.responded {
c.Err(500, fmt.Sprint(rec))
}
}
g.Set("uid", c.uid)
}()
if action == "" {
c.JSON(200, M{
"name": "我们past API",
"version": "2.0.0",
"publicEndpoints": []string{"register", "login", "wechatLogin", "siteInfo", "siteStats", "adminLogin", "checkInviteCode", "registerByInvite"},
})
return
}
// 权限检查(与 PHP 的 if/elseif 顺序一致)
var err error
if adminActions[action] && action != "admin_login" {
err = c.requireAdminLogin()
} else if !publicActions[action] && !adminActions[action] {
err = c.requireUserLogin()
}
if err != nil {
if !IsResponded(err) {
c.Err(500, err.Error())
}
return
}
hf, ok := r.handlers[action]
if !ok {
c.Err(400, "未知操作")
return
}
if err := hf(c); err != nil && !IsResponded(err) {
slog.Error("handler error", "action", action, "err", err)
c.Err(500, err.Error())
}
if !c.responded {
// PHP 中每个分支都以 jsonResponse 结束;兜底返回空对象,避免挂起
c.JSON(200, M{})
}
}
// accessLog gin 中间件:结构化访问日志(slog)
func accessLog() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
attrs := []any{"method", c.Request.Method, "path", c.Request.URL.Path, "status", c.Writer.Status(), "ms", time.Since(start).Milliseconds()}
if a, ok := c.Get("action"); ok {
attrs = append(attrs, "action", a)
}
if uid, ok := c.Get("uid"); ok {
attrs = append(attrs, "uid", uid)
}
slog.Info("http", attrs...)
}
}
// serveAdmin 提供 embed 的后台 SPA;未命中的路径回退到 index.html
func (r *Router) serveAdmin(g *gin.Context) {
p := strings.TrimPrefix(path.Clean("/"+g.Param("filepath")), "/")
if p == "" {
p = "index.html"
}
if _, err := fs.Stat(r.adminFS, p); err != nil {
p = "index.html"
}
if strings.HasSuffix(p, ".html") {
g.Header("Cache-Control", "no-cache")
}
// 不用 http.FileServer:它会把 /index.html 301 到 ./
f, err := r.adminFS.Open(p)
if err != nil {
g.Status(404)
return
}
defer f.Close()
st, err := f.Stat()
if err != nil {
g.Status(404)
return
}
rs, ok := f.(io.ReadSeeker)
if !ok {
b, _ := io.ReadAll(f)
rs = bytes.NewReader(b)
}
http.ServeContent(g.Writer, g.Request, p, st.ModTime(), rs)
}