Files
github-auto-sync-golang/server.go
T

332 lines
9.3 KiB
Go

package main
import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type Server struct {
engine *Engine
mux *http.ServeMux
password string
tokenMu sync.Mutex
tokens map[string]time.Time
}
func NewServer(e *Engine, static fs.FS, password string) *Server {
s := &Server{
engine: e,
mux: http.NewServeMux(),
password: password,
tokens: map[string]time.Time{},
}
mux := s.mux
mux.HandleFunc("GET /api/status", s.handleStatus)
mux.HandleFunc("GET /api/events", s.handleEvents)
mux.HandleFunc("POST /api/login", s.handleLogin)
mux.HandleFunc("POST /api/logout", s.handleLogout)
mux.HandleFunc("POST /api/repos", s.handleCreate)
mux.HandleFunc("GET /api/repos/{name}", s.wrapRepo(s.handleGet))
mux.HandleFunc("PUT /api/repos/{name}", s.wrapRepo(s.handleUpdate))
mux.HandleFunc("DELETE /api/repos/{name}", s.wrapRepo(s.handleDelete))
mux.HandleFunc("POST /api/repos/{name}/start", s.wrapRepo(s.startRepo))
mux.HandleFunc("POST /api/repos/{name}/stop", s.wrapRepo(s.stopRepo))
mux.HandleFunc("POST /api/repos/{name}/sync", s.wrapRepo(s.syncRepo))
mux.HandleFunc("POST /api/repos/{name}/action", s.wrapRepo(s.actionRepo))
mux.HandleFunc("POST /api/repos/{name}/force-push", s.wrapRepo(s.forcePushRepo))
mux.HandleFunc("POST /api/repos/{name}/abort-push", s.wrapRepo(s.abortPushRepo))
mux.HandleFunc("GET /api/repos/{name}/conflicts", s.wrapRepo(s.conflictsRepo))
mux.HandleFunc("GET /api/repos/{name}/log", s.wrapRepo(s.logRepo))
mux.HandleFunc("GET /api/repos/{name}/stats", s.wrapRepo(s.statsRepo))
mux.HandleFunc("GET /api/repos/{name}/commits", s.wrapRepo(s.commitsRepo))
mux.HandleFunc("GET /api/repos/{name}/commits/{hash}", s.wrapRepo(s.commitDiffRepo))
mux.Handle("/", http.FileServer(http.FS(static)))
return s
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 访问密码保护:静态资源开放,/api/* 需要会话(登录接口除外)
if s.password != "" && strings.HasPrefix(r.URL.Path, "/api/") &&
r.URL.Path != "/api/login" && !s.validToken(r) {
writeErr(w, 401, "需要登录")
return
}
s.mux.ServeHTTP(w, r)
}
func (s *Server) validToken(r *http.Request) bool {
c, err := r.Cookie("git_sync_token")
if err != nil {
return false
}
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
exp, ok := s.tokens[c.Value]
if !ok {
return false
}
if time.Now().After(exp) {
delete(s.tokens, c.Value)
return false
}
return true
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
if subtle.ConstantTimeCompare([]byte(req.Password), []byte(s.password)) != 1 {
writeErr(w, 401, "密码错误")
return
}
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
writeErr(w, 500, "生成会话失败")
return
}
token := hex.EncodeToString(buf)
s.tokenMu.Lock()
s.tokens[token] = time.Now().Add(30 * 24 * time.Hour)
s.tokenMu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "git_sync_token", Value: token, Path: "/",
HttpOnly: true, MaxAge: 30 * 24 * 3600,
})
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("git_sync_token"); err == nil {
s.tokenMu.Lock()
delete(s.tokens, c.Value)
s.tokenMu.Unlock()
}
http.SetCookie(w, &http.Cookie{Name: "git_sync_token", Value: "", Path: "/", MaxAge: -1})
writeJSON(w, 200, map[string]any{"ok": true})
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
func writeErr(w http.ResponseWriter, code int, msg string) {
writeJSON(w, code, map[string]string{"error": msg})
}
func (s *Server) wrapRepo(h func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if s.engine.get(name) == nil {
writeErr(w, 404, "配置不存在: "+name)
return
}
h(w, r, name)
}
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"repos": s.engine.Snapshots(true)})
}
func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, name string) {
if st := s.engine.get(name); st != nil {
writeJSON(w, 200, st.snapshot(true))
return
}
writeErr(w, 404, "配置不存在")
}
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
var cfg RepoConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeErr(w, 400, "请求体格式错误: "+err.Error())
return
}
if err := s.engine.Create(&cfg); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true, "name": cfg.Name})
}
func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request, name string) {
var cfg RepoConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeErr(w, 400, "请求体格式错误: "+err.Error())
return
}
cfg.Name = name
if err := s.engine.Update(&cfg); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.Delete(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) startRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.Start(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) stopRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.Stop(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) syncRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.SyncNow(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) actionRepo(w http.ResponseWriter, r *http.Request, name string) {
var req struct {
Action string `json:"action"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "请求体格式错误")
return
}
switch req.Action {
case "abort":
if err := s.engine.ConflictAbort(name); err != nil {
writeErr(w, 500, err.Error())
return
}
case "resolve":
if err := s.engine.ConflictResolve(name); err != nil {
writeErr(w, 500, err.Error())
return
}
case "fetch", "pull", "push":
if err := s.engine.ManualGit(name, req.Action); err != nil {
writeErr(w, 500, err.Error())
return
}
default:
writeErr(w, 400, "未知操作: "+req.Action)
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) conflictsRepo(w http.ResponseWriter, r *http.Request, name string) {
writeJSON(w, 200, s.engine.ConflictDiffs(name))
}
func (s *Server) logRepo(w http.ResponseWriter, r *http.Request, name string) {
n := 300
if v, err := strconv.Atoi(r.URL.Query().Get("lines")); err == nil && v > 0 && v <= 5000 {
n = v
}
writeJSON(w, 200, map[string]any{"lines": s.engine.ReadLog(name, n)})
}
func (s *Server) statsRepo(w http.ResponseWriter, r *http.Request, name string) {
writeJSON(w, 200, s.engine.ReadStats(name))
}
func (s *Server) commitsRepo(w http.ResponseWriter, r *http.Request, name string) {
limit := 50
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 200 {
limit = v
}
writeJSON(w, 200, s.engine.ReadCommits(name, limit))
}
func (s *Server) commitDiffRepo(w http.ResponseWriter, r *http.Request, name string) {
diff, err := s.engine.ReadCommitDiff(name, r.PathValue("hash"))
if err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]string{"diff": diff})
}
func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
fl, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
id, ch := s.engine.Bus().Subscribe()
defer s.engine.Bus().Unsubscribe(id)
if snap, err := json.Marshal(map[string]any{"type": "snapshot", "data": s.engine.Snapshots(true)}); err == nil {
fmt.Fprintf(w, "data: %s\n\n", snap)
fl.Flush()
}
tick := time.NewTicker(25 * time.Second)
defer tick.Stop()
ctx := r.Context()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
fmt.Fprint(w, ": ping\n\n")
fl.Flush()
case data, ok := <-ch:
if !ok {
return
}
fmt.Fprintf(w, "data: %s\n\n", data)
fl.Flush()
}
}
}
func (s *Server) forcePushRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.ForcePush(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) abortPushRepo(w http.ResponseWriter, r *http.Request, name string) {
if err := s.engine.AbortPush(name); err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}