仰望星辰工作室

gitcat

gitcat/ main.go 3.1 KB · 134 行 原始文件
1// Command gitcat 是面向小型工作室的自建 Git 服务。
2//
3// gitcat -addr :12747 -data ./data
4//
5// 首次启动后访问 http://<主机>:12747 会进入安装向导,设置站点名称与管理员账号。
6package main
7
8import (
9 "context"
10 "errors"
11 "flag"
12 "fmt"
13 "log"
14 "net/http"
15 "os"
16 "os/exec"
17 "os/signal"
18 "path/filepath"
19 "strings"
20 "syscall"
21 "time"
22
23 "gitcat/internal/server"
24 "gitcat/internal/store"
25)
26
27// version 可在构建时通过 -ldflags "-X main.version=..." 注入。
28var version = "1.0.0"
29
30func main() {
31 log.SetFlags(log.LstdFlags)
32 log.SetPrefix("[gitcat] ")
33
34 addr := flag.String("addr", envOr("GITCAT_ADDR", ":12747"), "HTTP 监听地址")
35 dataDir := flag.String("data", envOr("GITCAT_DATA", "./data"), "数据目录(SQLite 数据库、仓库与头像)")
36 showVersion := flag.Bool("version", false, "打印版本号后退出")
37 flag.Parse()
38
39 if *showVersion {
40 fmt.Println("gitcat", version)
41 return
42 }
43
44 if err := run(*addr, *dataDir); err != nil {
45 log.Fatalf("启动失败: %v", err)
46 }
47}
48
49func run(addr, dataDir string) error {
50 if _, err := exec.LookPath("git"); err != nil {
51 return errors.New("未找到 git 命令,请先安装 git 并确保在 PATH 中")
52 }
53 abs, err := filepath.Abs(dataDir)
54 if err != nil {
55 return err
56 }
57 if err := os.MkdirAll(abs, 0o755); err != nil {
58 return fmt.Errorf("创建数据目录: %w", err)
59 }
60
61 st, err := store.Open(filepath.Join(abs, "gitcat.db"))
62 if err != nil {
63 return fmt.Errorf("打开数据库: %w", err)
64 }
65 defer st.Close()
66
67 srv, err := server.New(server.Config{Addr: addr, DataDir: abs, Version: version}, st)
68 if err != nil {
69 return err
70 }
71
72 httpSrv := &http.Server{
73 Addr: addr,
74 Handler: srv.Handler(),
75 ReadHeaderTimeout: 20 * time.Second,
76 IdleTimeout: 90 * time.Second,
77 }
78
79 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
80 defer stop()
81
82 // 定时清理过期会话。
83 go func() {
84 t := time.NewTicker(time.Hour)
85 defer t.Stop()
86 for {
87 select {
88 case <-ctx.Done():
89 return
90 case <-t.C:
91 if err := st.CleanupSessions(); err != nil {
92 log.Printf("清理过期会话失败: %v", err)
93 }
94 }
95 }
96 }()
97
98 errCh := make(chan error, 1)
99 go func() {
100 log.Printf("gitcat %s 已启动", version)
101 log.Printf("监听地址: %s", displayAddr(addr))
102 log.Printf("数据目录: %s", abs)
103 if !st.Installed() {
104 log.Printf("尚未安装,请打开 %s 完成初始化", displayAddr(addr))
105 }
106 if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
107 errCh <- err
108 }
109 }()
110
111 select {
112 case err := <-errCh:
113 return err
114 case <-ctx.Done():
115 log.Println("收到退出信号,正在关闭…")
116 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
117 defer cancel()
118 return httpSrv.Shutdown(shutdownCtx)
119 }
120}
121
122func displayAddr(addr string) string {
123 if strings.HasPrefix(addr, ":") {
124 return "http://localhost" + addr
125 }
126 return "http://" + addr
127}
128
129func envOr(key, def string) string {
130 if v := strings.TrimSpace(os.Getenv(key)); v != "" {
131 return v
132 }
133 return def
134}