// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package main import ( "bytes" "context" _ "embed" "fmt" "io" "io/fs" "net/http" "strings" "gitroot.dev/server/configuration" "gitroot.dev/server/logger" "gitroot.dev/server/repository" ) const index = ` %s

%s

%s ` const defaultIndex = `

🚀 git clone ssh://%s/%s to begin!

📖 Need help to configure your repository?
` //go:embed resources/styles/simple.min.css var simpleStyle []byte type httpServer struct { logger *logger.Logger conf *configuration.Configuration repoManager *repository.Manager } func NewServerHttp(conf *configuration.Configuration, repoManager *repository.Manager) *httpServer { return &httpServer{ logger: logger.NewLoggerCtx(logger.HTTP_SERVER_LOGGER_NAME, context.Background()), conf: conf, repoManager: repoManager, } } func (srv *httpServer) ListenAndServe() error { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // / --> root+index.html // /notRepo --> root+notRepo // /repo1 --> repo1+index.html // /repo1/2.html --> repo1+2.html repoName := srv.conf.ForgeConfigName() file := "" if r.URL.Path != "/" { url := strings.TrimPrefix(r.URL.Path, "/") paths := strings.Split(url, "/") if srv.repoManager.Exists(paths[0]) { repoName = paths[0] file = strings.Join(paths[1:], "/") } else { repoName = srv.conf.ForgeConfigName() file = url } } if file == "" { file = "index.html" } else if strings.HasSuffix(file, "/") { file = file + "index.html" } srv.logger.Info("serve", logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("file", file)) fsDataWeb := srv.conf.DataWeb(repoName) _, err := fs.Stat(fsDataWeb, file) if err == nil { http.ServeFileFS(w, r, fsDataWeb, file) return } repo, err := srv.repoManager.Open(logger.AddCaller(r.Context(), "http.HandleFunc"), repoName) if err != nil { srv.logger.Error("invalid repo", err, logger.NewLoggerPair("repoName", repoName)) srv.error(w, repoName) return } defer repo.Close() content, err := repo.Content(file) if err == nil { lc, err := repo.GetLastCommits([]string{file}) if err == nil && len(lc) > 0 { http.ServeContent(w, r, file, lc[0].Commit.Committer.When, bytes.NewReader(content)) } else { io.Copy(w, bytes.NewReader(content)) w.WriteHeader(http.StatusOK) } return } if r.URL.Path == "/rootstyle.css" { w.Header().Set("Content-Type", "text/css") w.Write(simpleStyle) return } if strings.HasSuffix(file, "index.html") { srv.index(w, repoName) } else { _, err := fs.Stat(fsDataWeb, "404.html") if err == nil { //TODO ServeFileFS don't permit to send 404 header http.ServeFileFS(w, r, fsDataWeb, "404.html") return } srv.notFound(w, repoName) } }) srv.logger.Warn("starting HTTP server on", logger.NewLoggerPair("addr", srv.conf.HttpAddr)) return http.ListenAndServe(srv.conf.HttpAddr, nil) } func (srv *httpServer) index(w http.ResponseWriter, repoName string) { content := fmt.Sprintf(defaultIndex, srv.conf.SshAddr, repoName) fmt.Fprintf(w, index, repoName, repoName, content, srv.conf.SshAddr, repoName) } func (srv *httpServer) notFound(w http.ResponseWriter, repoName string) { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, index, repoName, repoName, "

Not found

", srv.conf.SshAddr, repoName) } func (srv *httpServer) error(w http.ResponseWriter, repoName string) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, index, repoName, repoName, "

Error

", srv.conf.SshAddr, repoName) }