summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--cache.go129
-rw-r--r--main.go61
-rw-r--r--repo_host.go92
4 files changed, 27 insertions, 256 deletions
diff --git a/.gitignore b/.gitignore
index 85b6ff8..d952bdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
-}
diff --git a/main.go b/main.go
index a4192d9..3fbf45f 100644
--- a/main.go
+++ b/main.go
@@ -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
-}