diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | cache.go | 129 | ||||
-rw-r--r-- | main.go | 61 | ||||
-rw-r--r-- | repo_host.go | 92 |
4 files changed, 27 insertions, 256 deletions
@@ -1,2 +1,3 @@ .env goredir +goredir.toml diff --git a/cache.go b/cache.go deleted file mode 100644 index d43fa77..0000000 --- a/cache.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "context" - "log" - "path" - "sync" - "time" -) - -// A PackageCache holds information about Go packages and their upstream -// repositories. -// -// Given a package name, it looks for a similarly named repository in one of -// the upstream hosts and holds on to the result for quicker subsequent access. -type PackageCache struct { - // ExpireAfter is how long to consider a found repository valid for. As - // repositories do not frequently move around, this could be rather long, - // though it also determines how long a user might have to wait for a new - // repository to become available if they previously got a 404. - ExpireAfter time.Duration - - // UpstreamTimeout is how long we should allow HTTP requests to upstream - // hosts to carry on before they are canceled. - UpstreamTimeout time.Duration - - // Hosts are the RepoHosts which should be checked for a repository matching - // the package name. Hosts are checked in order; if two hosts both have a - // repo with the desired name, the repo host which is listed first will - // "win". - Hosts []RepoHost - - // Logger is a logger for diagnostics - Logger *log.Logger - - // CanonicalPrefix is the package prefix to use when naming packages. E.g., - // when looking for a package named "conf", we'll treat the package name as - // CanonicalPrefix + "/conf", e.g. burwell.io/conf. - CanonicalPrefix string - - entries sync.Map // concurrency-safe map[string]entry -} - -type entry struct { - t time.Time - pkg *Package -} - -// A Package represents a Go package's canonical name and its source git -// repository. -type Package struct { - Name string - Repo string -} - -// Get loads package name from the cache, consulting upstream repositories to -// find a matching repo if not present. As this method is meant to be called in -// the critical path of an incoming HTTP request, it can also be cancelled with -// the supplied context. -func (c *PackageCache) Get(ctx context.Context, name string) (Package, bool) { - val, ok := c.entries.Load(name) - if !ok { - return c.load(ctx, name) - } - ent := val.(entry) - - if time.Now().After(ent.t.Add(c.ExpireAfter)) { - return c.load(ctx, name) - } - - if ent.pkg == nil { - return Package{}, false - } - - return *ent.pkg, true -} - -// load looks for a package in the upstream repos, and if found, stores it in -// the cache. If no matching repo is found, a nil entry is stored in the map so -// we don't immediately re-check the upstreams for subsequent requests. -func (c *PackageCache) load(ctx context.Context, name string) (Package, bool) { - pkg, ok := c.find(ctx, name) - if !ok { - c.entries.Store(name, entry{t: time.Now()}) - return Package{}, false - } - c.entries.Store(name, entry{t: time.Now(), pkg: &pkg}) - return pkg, true -} - -// requestContext creates a context for making HTTP requests to upstream hosts. -func (c *PackageCache) requestContext(ctx context.Context) (context.Context, context.CancelFunc) { - if c.UpstreamTimeout == 0 { - return ctx, func() {} - } - return context.WithTimeout(ctx, c.UpstreamTimeout) -} - -// logf logs a message using the configured logger, if any. -func (c *PackageCache) logf(msg string, args ...interface{}) { - if c.Logger == nil { - return - } - c.Logger.Printf(msg, args...) -} - -// find consults the configured upstream hosts to try to find a matching repo -// for package name. -func (c *PackageCache) find(ctx context.Context, name string) (Package, bool) { - c.logf("loading %s", name) - for _, h := range c.Hosts { - reqCtx, cancel := c.requestContext(ctx) - defer cancel() - repo, ok, err := h.HostsRepo(reqCtx, name) - if err != nil { - c.logf("error checking upstream for repo %s: %v", name, err) - return Package{}, false - } - if ok { - c.logf("found repo %s: %s", name, repo) - return Package{ - Name: path.Join(c.CanonicalPrefix, name), - Repo: repo, - }, true - } - } - c.logf("could not find repo %s on any host", name) - return Package{}, false -} @@ -5,7 +5,9 @@ import ( "html/template" "log" "net/http" + "net/url" "os" + "path" "strings" "time" @@ -29,14 +31,8 @@ func (d *duration) UnmarshalText(text []byte) error { } type options struct { - BindAddress string `toml:"bind_address"` - CacheExpiry duration `toml:"cache_expiry"` - UpstreamTimeout duration `toml:"upstream_timeout"` - SourcehutUsername string `toml:"srht_username"` - SourcehutToken string `toml:"srht_token"` - GithubUsername string `toml:"github_username"` - GithubToken string `toml:"github_token"` - CanonicalPrefix string `toml:"canonical_prefix"` + BindAddress string `toml:"bind_address"` + CanonicalPrefix string `toml:"canonical_prefix"` } func run() error { @@ -53,26 +49,11 @@ func run() error { return err } - log.Printf("caches expire after %s", opts.CacheExpiry.Duration) - log.Printf("upstream reqs time out after %s", opts.UpstreamTimeout.Duration) - - cache := &PackageCache{ - CanonicalPrefix: opts.CanonicalPrefix, - ExpireAfter: opts.CacheExpiry.Duration, - UpstreamTimeout: opts.UpstreamTimeout.Duration, - Logger: log.New(os.Stdout, "", log.LstdFlags), - Hosts: []RepoHost{ - Sourcehut{Username: opts.SourcehutUsername, Token: opts.SourcehutToken}, - Github{Username: opts.GithubUsername, Token: opts.GithubToken}, - }, - } - handler := handlePackage(cache) log.Printf("starting server to listen on %s...", opts.BindAddress) - return http.ListenAndServe(opts.BindAddress, handler) + return http.ListenAndServe(opts.BindAddress, handlePackage(&opts)) } -func handlePackage(pkgs *PackageCache) http.HandlerFunc { - tmpl, err := template.New("package").Parse(`<!DOCTYPE html> +var tmpl = template.Must(template.New("package").Parse(`<!DOCTYPE html> <html> <head> <meta name="go-import" content="{{ .Name }} git {{ .Repo }}"> @@ -84,10 +65,16 @@ func handlePackage(pkgs *PackageCache) http.HandlerFunc { <li><a href="{{ .Repo }}">Code</a></li> </ul> </body> -</html>`) - if err != nil { - return nil - } +</html>`)) + +// A Package represents a Go package's canonical name and its source git +// repository. +type Package struct { + Name string + Repo *url.URL +} + +func handlePackage(opts *options) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusBadRequest) @@ -95,13 +82,17 @@ func handlePackage(pkgs *PackageCache) http.HandlerFunc { return } parts := strings.SplitN(r.URL.Path[1:], "/", 2) - pkg, ok := pkgs.Get(r.Context(), parts[0]) - if !ok { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(http.StatusText(http.StatusNotFound))) - return + pkg := &Package{ + Name: path.Join(opts.CanonicalPrefix, parts[0]), + Repo: &url.URL{ + Scheme: "https", + Host: "git.burwell.io", + Path: parts[0], + }, } log.Printf("pkg: %v", pkg) - tmpl.Execute(w, pkg) + if err := tmpl.Execute(w, pkg); err != nil { + log.Printf("could not execute template: %v", err) + } } } diff --git a/repo_host.go b/repo_host.go deleted file mode 100644 index 2fdd4cc..0000000 --- a/repo_host.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" -) - -// A RepoHost hosts repositories, and can be queried as to whether it hosts a -// repository with a specific name. -type RepoHost interface { - HostsRepo(ctx context.Context, name string) (string, bool, error) -} - -// Sourcehut is a RepoHost -// -// https://git.sr.ht -type Sourcehut struct { - Token string // a personal access token - Username string // the username -} - -// HostsRepo implements RepoHost -func (s Sourcehut) HostsRepo(ctx context.Context, name string) (string, bool, error) { - endpoint := "https://git.sr.ht/api/~" + s.Username + "/repos/" + name - type response struct { - Name string `json:"name"` - Visibility string `json:"visibility"` - } - var repo response - if err := makeRequest(ctx, &repo, endpoint, "token "+s.Token); err != nil { - return "", false, err - } - if repo.Visibility != "public" || repo.Name == "" { - return "", false, nil - } - return "https://git.sr.ht/~" + s.Username + "/" + repo.Name, true, nil -} - -// Github is a RepoHost -// -// https://github.com -type Github struct { - Username string - Token string -} - -// HostsRepo implements RepoHost -func (g Github) HostsRepo(ctx context.Context, name string) (string, bool, error) { - endpoint := "https://api.github.com/repos/" + g.Username + "/" + name - type response struct { - URL string `json:"html_url"` - Private bool `json:"private"` - } - var repo response - if err := makeRequest(ctx, &repo, endpoint, "token "+g.Token); err != nil { - return "", false, err - } - if repo.Private || repo.URL == "" { - return "", false, nil - } - return repo.URL, true, nil -} - -func makeRequest(ctx context.Context, out interface{}, url string, auth string) error { - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return err - } - req.Header.Set("Authorization", auth) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - if resp.StatusCode == http.StatusNotFound { - return nil - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("get %s: %s", url, resp.Status) - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - if err := json.Unmarshal(body, out); err != nil { - return err - } - return nil -} |