123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- /*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
- package main
- import (
- "bytes"
- "context"
- "fmt"
- "html/template"
- "log"
- "net"
- "net/http"
- "os"
- "regexp"
- "slices"
- "sort"
- "strconv"
- "strings"
- "time"
- dp "idio.link/go/depager/v2"
- )
- var goGetTmpl *template.Template
- func init() {
- const respTmpl = `<!doctype html>` +
- `<html>` +
- `<head>` +
- `<meta name="go-import"` +
- ` content="{{.Module}} git {{.ImportURI}}">` +
- `<meta name="go-source"` +
- ` content="{{.Module}}` +
- ` {{print .SourceURI}}` +
- ` {{.SourceURI}}/src/{{.Branch}}{/dir}` +
- ` {{.SourceURI}}/src/{{.Branch}}{/dir}/{file}#L{line}">` +
- `</head>` +
- `<body>` +
- `go get https://{{.Module}}` +
- `</body>` +
- `</html>`
- goGetTmpl =
- template.Must(template.New("resp").Parse(respTmpl))
- }
- func NewServer(ctx context.Context) *Server {
- accessToken, ok := os.LookupEnv("GOGS_ACCESS_TOKEN")
- if !ok {
- panic("Missing environment variable GOGS_ACCESS_TOKEN.")
- }
- s := &Server{
- ctx: ctx,
- client: NewGogsClient(ctx, accessToken),
- shutdownTimeout: 5 * time.Second,
- }
- s.svr = &http.Server{
- ReadTimeout: 10 * time.Second,
- WriteTimeout: 10 * time.Second,
- BaseContext: func(_ net.Listener) context.Context {
- return s.ctx
- },
- }
- return s
- }
- type Server struct {
- ctx context.Context
- svr *http.Server
- client *GogsClient
- projects Cached[map[string]GitRepo]
- shutdownTimeout time.Duration
- }
- func (s *Server) ListenAndServe(port int) error {
- s.svr.Addr = fmt.Sprintf(":%d", port)
- s.paths()
- return s.svr.ListenAndServe()
- }
- func (s *Server) Shutdown() error {
- ctx, cancel :=
- context.WithTimeout(s.ctx, s.shutdownTimeout)
- defer cancel()
- return s.svr.Shutdown(ctx)
- }
- func (s *Server) paths() {
- http.HandleFunc("/go/", s.handleGoGet())
- }
- func (s *Server) updateProjectCache(group string) error {
- // TODO Push caches down into Gogs client.
- if s.projects.Expired() {
- projects := map[string]GitRepo{}
- pager := FetchOrgRepos(s.client, group)
- for p := range pager.Iter() {
- projects[p.Path()] = p
- }
- if err := pager.LastErr(); err != nil {
- log.Printf("request go-get: unable to fetch gogs projects: %v", err)
- return err
- }
- s.projects = NewCached(&projects, 30*time.Minute)
- }
- return nil
- }
- func (s *Server) handleGoGet() http.HandlerFunc {
- gpr := new(GoProxyRequest)
- return func(w http.ResponseWriter, req *http.Request) {
- parseGoProxyPath(req, gpr)
- switch {
- // /@latest
- case gpr.Op == GoProxyRequestLatest:
- handleLatest(w, gpr, s.client)
- // /@v/list
- case gpr.Op == GoProxyRequestList:
- handleList(w, gpr, s.client)
- // /@v/$version.{.info,.mod,.zip}
- case gpr.Op == GoProxyRequestInfo ||
- gpr.Op == GoProxyRequestMod ||
- gpr.Op == GoProxyRequestZip:
- handleVersion(w, gpr, s.client)
- // go-get=1
- case req.URL.Query().Get("go-get") == "1":
- err := s.updateProjectCache(gpr.Host)
- if err != nil {
- code := http.StatusServiceUnavailable
- http.Error(w, http.StatusText(code), code)
- }
- repo, ok := (*s.projects.Value())[gpr.RepoPath]
- if !ok {
- code := http.StatusNotFound
- http.Error(w, http.StatusText(code), code)
- log.Printf("request go-get: unable to find git project for module %#v", gpr.Module)
- }
- handleGoGet(w, gpr, repo)
- default:
- log.Printf("malformed request: %s", req.URL.String())
- code := http.StatusBadRequest
- http.Error(w, http.StatusText(code), code)
- }
- }
- }
- func parseVersion(str string) ([]uint, error) {
- str = strings.TrimPrefix(str, "v")
- parts := strings.Split(str, ".")
- ver := make([]uint, 3)
- for i := 0; i < len(parts) && i < 3; i++ {
- v, err := strconv.ParseUint(parts[i], 10, 32)
- if err != nil {
- return nil, err
- }
- ver[i] = uint(v)
- }
- return ver, nil
- }
- func handleLatest(
- w http.ResponseWriter,
- gpr *GoProxyRequest,
- client *GogsClient,
- ) {
- latest := ""
- latestTag := make([]uint, 3)
- pager := ListTags(client, gpr.Host, gpr.Repo)
- for t := range pager.Iter() {
- tag, err := parseVersion(t.Name)
- if err != nil {
- log.Printf("error while parsing tag %#v: %v", t, err)
- continue
- }
- if slices.Compare(latestTag, tag) == -1 {
- latestTag = tag
- latest = t.Name
- }
- }
- if pager.LastErr() != nil {
- log.Printf("request /@latest: %v", pager.LastErr())
- code := http.StatusServiceUnavailable
- http.Error(w, http.StatusText(code), code)
- return
- }
- w.Write([]byte(latest + "\n"))
- }
- func handleVersion(
- w http.ResponseWriter,
- gpr *GoProxyRequest,
- client *GogsClient,
- ) {
- // TODO Dispatch to .info, .mod, or .zip handler
- switch gpr.Op {
- case GoProxyRequestInfo:
- handleVersionInfo(w, gpr, client)
- default:
- log.Printf("request %#v: not implemented", gpr.URI)
- code := http.StatusNotFound
- http.Error(w, http.StatusText(code), code)
- }
- }
- func findTag(
- pager dp.Pager[*GogsTag],
- tagName string,
- ) (tag *GogsTag, err error) {
- for t := range pager.Iter() {
- if t.Name == tagName {
- tag = t
- break
- }
- }
- if err = pager.LastErr(); err != nil {
- err = fmt.Errorf("get tag %#v: %w", tagName, err)
- return
- }
- return
- }
- func handleVersionInfo(
- w http.ResponseWriter,
- gpr *GoProxyRequest,
- client *GogsClient,
- ) {
- pager := ListTags(client, gpr.Host, gpr.Repo)
- tag, err := findTag(pager, gpr.Version)
- if err != nil {
- log.Printf("request %#v: no tag matching version %#v: %v", gpr.URI, gpr.Version, err)
- code := http.StatusServiceUnavailable
- http.Error(w, http.StatusText(code), code)
- return
- }
- json := fmt.Sprintf(
- `{"version": %#v, "timestamp": %#v}`,
- tag.Name,
- tag.Commit.Timestamp,
- )
- w.Write([]byte(json + "\n"))
- }
- func handleList(
- w http.ResponseWriter,
- gpr *GoProxyRequest,
- client *GogsClient,
- ) {
- verMajor := regexp.MustCompile(`^v[01]\.`)
- if v := gpr.VersionMajor; v != "" {
- verMajor =
- regexp.MustCompile(fmt.Sprintf("^%s\\.", v))
- }
- pager := ListTags(client, gpr.Host, gpr.Repo)
- tags := make([]*GogsTag, 0, 16)
- for t := range pager.Iter() {
- if !verMajor.MatchString(t.Name) {
- continue
- }
- tags = append(tags, t)
- }
- sort.SliceStable(tags, func(i, j int) bool {
- ti := tags[i].Name
- tj := tags[j].Name
- return strings.Compare(ti, tj) == -1
- })
- for _, t := range tags {
- w.Write([]byte(t.Name + "\n"))
- }
- if pager.LastErr() != nil {
- log.Printf("request %#v: list gogs releases: %v", gpr.URI, pager.LastErr())
- code := http.StatusServiceUnavailable
- http.Error(w, http.StatusText(code), code)
- }
- }
- func handleGoGet(
- w http.ResponseWriter,
- gpr *GoProxyRequest,
- repo GitRepo,
- ) {
- buf := make([]byte, 0, 2048)
- writer := bytes.NewBuffer(buf)
- err := goGetTmpl.Execute(writer, struct {
- Module string // Go module name (without major)
- ImportURI string // Where go get will download from
- SourceURI string // Where browsers can go
- Branch string
- }{gpr.ImportPath,
- repo.GitURI(),
- repo.WebURI(),
- repo.DefaultBranch(),
- })
- if err != nil {
- code := http.StatusInternalServerError
- http.Error(w, http.StatusText(code), code)
- log.Printf("request go-get: execute template: %#v: %v", gpr.Repo, err)
- }
- w.Write(writer.Bytes())
- }
|