GitRoot
craft your forge, build your project, grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package main
6
7import (
8 "bytes"
9 "context"
10 _ "embed"
11 "fmt"
12 "io"
13 "io/fs"
14 "net/http"
15 "strings"
16
17 "gitroot.dev/server/configuration"
18 "gitroot.dev/server/logger"
19 "gitroot.dev/server/repository"
20)
21
22const index = `<!doctype html>
23<html>
24<head>
25<title>%s</title>
26<link rel="stylesheet" href="/rootstyle.css">
27</head>
28<body>
29<header>
30 <h1>%s</h1>
31</header>
32%s
33<footer>
34 <p>🚀 git clone <code>ssh://%s/%s</code></p>
35 <small>Hosted with ❤️ by Gitroot</small>
36</footer>
37</body>
38</html>`
39
40const defaultIndex = `<p>🚀 git clone <code>ssh://%s/%s</code> to begin!</p>
41<article>📖 Need help to configure your repository?
42<ul>
43 <li><a href="https://gitroot.dev/doc/" target="_blanck">official documentation</a></li>
44 <li><a href="https://gitroot.dev/doc/#web" target="_blanck">web configuration</a></li>
45</ul>
46</article>`
47
48//go:embed resources/styles/simple.min.css
49var simpleStyle []byte
50
51type httpServer struct {
52 logger *logger.Logger
53 conf *configuration.Configuration
54 repoManager *repository.Manager
55}
56
57func NewServerHttp(conf *configuration.Configuration, repoManager *repository.Manager) *httpServer {
58 return &httpServer{
59 logger: logger.NewLoggerCtx(logger.HTTP_SERVER_LOGGER_NAME, context.Background()),
60 conf: conf,
61 repoManager: repoManager,
62 }
63}
64
65func (srv *httpServer) ListenAndServe() error {
66 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
67 // / --> root+index.html
68 // /notRepo --> root+notRepo
69 // /repo1 --> repo1+index.html
70 // /repo1/2.html --> repo1+2.html
71 repoName := srv.conf.ForgeConfigName()
72 file := ""
73 if r.URL.Path != "/" {
74 url := strings.TrimPrefix(r.URL.Path, "/")
75 paths := strings.Split(url, "/")
76 if srv.repoManager.Exists(paths[0]) {
77 repoName = paths[0]
78 file = strings.Join(paths[1:], "/")
79 } else {
80 repoName = srv.conf.ForgeConfigName()
81 file = url
82 }
83 }
84
85 if file == "" {
86 file = "index.html"
87 } else if strings.HasSuffix(file, "/") {
88 file = file + "index.html"
89 }
90
91 srv.logger.Info("serve", logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("file", file))
92
93 fsDataWeb := srv.conf.DataWeb(repoName)
94 _, err := fs.Stat(fsDataWeb, file)
95 if err == nil {
96 http.ServeFileFS(w, r, fsDataWeb, file)
97 return
98 }
99
100 repo, err := srv.repoManager.Open(logger.AddCaller(r.Context(), "http.HandleFunc"), repoName)
101 if err != nil {
102 srv.logger.Error("invalid repo", err, logger.NewLoggerPair("repoName", repoName))
103 srv.error(w, repoName)
104 return
105 }
106 defer repo.Close()
107
108 content, err := repo.Content(file)
109 if err == nil {
110 lc, err := repo.GetLastCommits([]string{file})
111 if err == nil && len(lc) > 0 {
112 http.ServeContent(w, r, file, lc[0].Commit.Committer.When, bytes.NewReader(content))
113 } else {
114 io.Copy(w, bytes.NewReader(content))
115 w.WriteHeader(http.StatusOK)
116 }
117 return
118 }
119
120 if r.URL.Path == "/rootstyle.css" {
121 w.Header().Set("Content-Type", "text/css")
122 w.Write(simpleStyle)
123 return
124 }
125
126 if strings.HasSuffix(file, "index.html") {
127 srv.index(w, repoName)
128 } else {
129 _, err := fs.Stat(fsDataWeb, "404.html")
130 if err == nil {
131 //TODO ServeFileFS don't permit to send 404 header
132 http.ServeFileFS(w, r, fsDataWeb, "404.html")
133 return
134 }
135 srv.notFound(w, repoName)
136 }
137 })
138 srv.logger.Warn("starting HTTP server on", logger.NewLoggerPair("addr", srv.conf.HttpAddr))
139 return http.ListenAndServe(srv.conf.HttpAddr, nil)
140}
141
142func (srv *httpServer) index(w http.ResponseWriter, repoName string) {
143 content := fmt.Sprintf(defaultIndex, srv.conf.SshAddr, repoName)
144 fmt.Fprintf(w, index, repoName, repoName, content, srv.conf.SshAddr, repoName)
145}
146
147func (srv *httpServer) notFound(w http.ResponseWriter, repoName string) {
148 w.WriteHeader(http.StatusNotFound)
149 fmt.Fprintf(w, index, repoName, repoName, "<p>Not found</p>", srv.conf.SshAddr, repoName)
150}
151
152func (srv *httpServer) error(w http.ResponseWriter, repoName string) {
153 w.WriteHeader(http.StatusInternalServerError)
154 fmt.Fprintf(w, index, repoName, repoName, "<p>Error</p>", srv.conf.SshAddr, repoName)
155}