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}