pull/8/head
jolheiser 2021-02-20 16:15:33 -06:00
parent 098f726431
commit 46071014b9
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
14 changed files with 201 additions and 58 deletions

View File

@ -70,6 +70,23 @@ Overrides are available via config or by setting an environment variable `VANITY
To run Vanity in config-only mode for packages, set `--service` to `off`. To run Vanity in config-only mode for packages, set `--service` to `off`.
## Manual Mode
To run Vanity without automatic updating, use `--manual`.
## Topic Lists
By setting `--topics`, anyone visiting the index page will see packages grouped by their topics.
Regardless of the setting, you can switch beteween list-view and topic-view with the provided button
or changing the URL between `?format=list` and `?format=topics`.
## API
In order to preserve namespaces for packages, Vanity's API uses the URL `/_/{endpoint}`
Vanity currently supports `/_/status` and `/_/update`, to get some status information and update the package cache respectively.
## License ## License
[MIT](LICENSE) [MIT](LICENSE)

View File

@ -6,12 +6,13 @@ import (
) )
type Package struct { type Package struct {
Name string `toml:"name"` Name string `toml:"name"`
Description string `toml:"description"` Description string `toml:"description"`
Branch string `toml:"branch"` Branch string `toml:"branch"`
WebURL string `toml:"web_url"` WebURL string `toml:"web_url"`
CloneHTTP string `toml:"clone_http"` CloneHTTP string `toml:"clone_http"`
CloneSSH string `toml:"clone_ssh"` CloneSSH string `toml:"clone_ssh"`
Topics []string `toml:"topics"`
Private bool `toml:"-"` Private bool `toml:"-"`
Fork bool `toml:"-"` Fork bool `toml:"-"`

View File

@ -36,6 +36,7 @@ var (
Override = make(map[string]string) Override = make(map[string]string)
Interval time.Duration Interval time.Duration
Manual bool Manual bool
Topics bool
Debug bool Debug bool
ConfigPackages []*api.Package ConfigPackages []*api.Package
@ -142,6 +143,12 @@ var Flags = []cli.Flag{
EnvVars: []string{"VANITY_MANUAL"}, EnvVars: []string{"VANITY_MANUAL"},
Destination: &Manual, Destination: &Manual,
}, },
&cli.BoolFlag{
Name: "topics",
Usage: "Group projects by topic by default",
EnvVars: []string{"VANITY_TOPICS"},
Destination: &Topics,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "debug", Name: "debug",
Usage: "Debug logging", Usage: "Debug logging",

1
go.mod
View File

@ -6,6 +6,7 @@ require (
code.gitea.io/sdk/gitea v0.13.2 code.gitea.io/sdk/gitea v0.13.2
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.1.1 // indirect
github.com/google/go-github/v32 v32.1.0 github.com/google/go-github/v32 v32.1.0
github.com/pelletier/go-toml v1.8.1 github.com/pelletier/go-toml v1.8.1
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0

2
go.sum
View File

@ -53,6 +53,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=

71
router/api.go 100644
View File

@ -0,0 +1,71 @@
package router
import (
"encoding/json"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
"net/http"
"runtime"
"time"
)
func apiRoutes() *chi.Mux {
r := chi.NewRouter()
r.Use(cors.AllowAll().Handler)
r.Get("/status", doAPIStatus)
r.Get("/update", doAPIUpdate)
return r
}
func doAPIStatus(res http.ResponseWriter, _ *http.Request) {
res.Header().Set("Content-Type", "application/json")
var nextUpdate *string
if !lastUpdate.IsZero() {
nu := lastUpdate.Add(flags.Interval).Format(time.RFC3339)
nextUpdate = &nu
}
resp := map[string]interface{}{
"vanity_version": api.Version,
"go_version": runtime.Version(),
"num_packages": len(cache.Packages),
"next_update": nextUpdate,
}
data, err := json.Marshal(&resp)
if err != nil {
beaver.Errorf("could not marshal API status: %v", err)
data = []byte("{}")
}
if _, err = res.Write(data); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}
func doAPIUpdate(res http.ResponseWriter, _ *http.Request) {
res.Header().Set("Content-Type", "application/json")
resp := map[string]bool{
"updated": false,
}
if canUpdate {
updateCache()
resp["updated"] = true
}
payload, err := json.Marshal(resp)
if err != nil {
beaver.Errorf("could not marshal payload: %v", err)
}
if _, err = res.Write(payload); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}

View File

@ -11,30 +11,46 @@ import (
) )
var cache = &packageCache{ var cache = &packageCache{
packages: make(map[string]*api.Package), Packages: make(map[string]*api.Package),
}
type packageList map[string]*api.Package
func (p packageList) Names() []string {
names := make([]string, len(p))
idx := 0
for name := range p {
names[idx] = name
idx++
}
return names
}
func (p packageList) Topics() map[string][]*api.Package {
topics := make(map[string][]*api.Package, 0)
for _, pkg := range p {
for _, t := range pkg.Topics {
if tt, ok := topics[t]; ok {
topics[t] = append(tt, pkg)
} else {
topics[t] = []*api.Package{pkg}
}
}
}
return topics
} }
type packageCache struct { type packageCache struct {
packages map[string]*api.Package Packages packageList
sync.Mutex sync.Mutex
} }
func (c *packageCache) Update(packages map[string]*api.Package) { func (c *packageCache) Update(packages map[string]*api.Package) {
c.Lock() c.Lock()
c.packages = packages c.Packages = packages
c.Unlock() c.Unlock()
} }
func (c *packageCache) Names() []string {
names := make([]string, len(c.packages))
idx := 0
for name := range c.packages {
names[idx] = name
idx++
}
return names
}
func updateCache() { func updateCache() {
packages, err := svc.Packages() packages, err := svc.Packages()
if err != nil { if err != nil {

View File

@ -9,8 +9,9 @@ import (
) )
var ( var (
svc service.Service svc service.Service
canUpdate bool lastUpdate time.Time
canUpdate bool
) )
func cronStart() { func cronStart() {
@ -21,7 +22,8 @@ func cronStart() {
if !flags.Manual && canUpdate { if !flags.Manual && canUpdate {
beaver.Debug("Running package update...") beaver.Debug("Running package update...")
updateCache() updateCache()
beaver.Debugf("Finished package update: %s", cache.Names()) beaver.Debugf("Finished package update: %s", cache.Packages.Names())
lastUpdate = time.Now()
} }
canUpdate = true canUpdate = true
} }

View File

@ -1,7 +1,6 @@
package router package router
import ( import (
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
@ -31,24 +30,34 @@ func Init() (*chi.Mux, error) {
r.Use(middleware.Timeout(30 * time.Second)) r.Use(middleware.Timeout(30 * time.Second))
r.Get("/", doIndex) r.Get("/", doIndex)
r.Head("/", doUpdate)
r.Get("/*", doVanity) r.Get("/*", doVanity)
r.Mount("/_", apiRoutes())
svc = service.New() svc = service.New()
beaver.Info("Warming up cache...") beaver.Info("Warming up cache...")
updateCache() updateCache()
beaver.Infof("Finished warming up cache: %s", cache.Names()) beaver.Infof("Finished warming up cache: %s", cache.Packages.Names())
go cronStart() go cronStart()
beaver.Infof("Running vanity server at http://localhost:%d", flags.Port) beaver.Infof("Running vanity server at http://localhost:%d", flags.Port)
return r, nil return r, nil
} }
func doIndex(res http.ResponseWriter, _ *http.Request) { func doIndex(res http.ResponseWriter, req *http.Request) {
format := "list"
if flags.Topics {
format = "topics"
}
q := req.URL.Query().Get("format")
if q != "" {
format = q
}
if err := tmpl.Lookup("index.tmpl").Execute(res, map[string]interface{}{ if err := tmpl.Lookup("index.tmpl").Execute(res, map[string]interface{}{
"Packages": cache.packages, "Packages": cache.Packages,
"Index": true, "Index": true,
"Format": format,
}); err != nil { }); err != nil {
beaver.Errorf("could not write response: %v", err) beaver.Errorf("could not write response: %v", err)
} }
@ -56,7 +65,7 @@ func doIndex(res http.ResponseWriter, _ *http.Request) {
func doVanity(res http.ResponseWriter, req *http.Request) { func doVanity(res http.ResponseWriter, req *http.Request) {
key := chi.URLParam(req, "*") key := chi.URLParam(req, "*")
pkg, ok := cache.packages[strings.Split(key, "/")[0]] pkg, ok := cache.Packages[strings.Split(key, "/")[0]]
if !ok { if !ok {
http.NotFound(res, req) http.NotFound(res, req)
return return
@ -66,29 +75,8 @@ func doVanity(res http.ResponseWriter, req *http.Request) {
"Package": pkg, "Package": pkg,
"Module": pkg.Module(flags.Domain), "Module": pkg.Module(flags.Domain),
"GoSource": fmt.Sprintf("%s %s %s %s", pkg.Module(flags.Domain), pkg.CloneHTTP, svc.GoDir(pkg), svc.GoFile(pkg)), "GoSource": fmt.Sprintf("%s %s %s %s", pkg.Module(flags.Domain), pkg.CloneHTTP, svc.GoDir(pkg), svc.GoFile(pkg)),
"Index": false, "Index": false,
}); err != nil { }); err != nil {
beaver.Errorf("could not write response: %v", err) beaver.Errorf("could not write response: %v", err)
} }
} }
func doUpdate(res http.ResponseWriter, _ *http.Request) {
res.Header().Set("Content-Type", "application/json")
resp := map[string]bool{
"updated": false,
}
if canUpdate {
updateCache()
resp["updated"] = true
}
payload, err := json.Marshal(resp)
if err != nil {
beaver.Errorf("could not marshal payload: %v", err)
}
if _, err = res.Write(payload); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}

View File

@ -12,8 +12,8 @@
updateImports.addEventListener('click', () => { updateImports.addEventListener('click', () => {
updateImports.disabled = true; updateImports.disabled = true;
updateImports.innerHTML = 'Updating...'; updateImports.innerHTML = 'Updating...';
fetch('{{if .Index}}.{{else}}../{{end}}', { fetch('{{if .Index}}.{{else}}../{{end}}_/update', {
method: 'HEAD' method: 'GET'
}).then(() => { }).then(() => {
location.reload(); location.reload();
}).catch(() => { }).catch(() => {

View File

@ -1,8 +1,29 @@
{{template "head.tmpl" .}} {{template "head.tmpl" .}}
<h3>Imports:</h3> <h3>Imports:</h3>
<ul> {{if eq .Format "list"}}
{{range $path, $package := .Packages}} <ul>
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li> {{range $path, $package := .Packages}}
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li>
{{end}}
</ul>
{{else}}
{{range $topic, $packages := .Packages.Topics}}
<details>
<summary>{{$topic}}</summary>
<ul>
{{range $package := $packages}}
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li>
{{end}}
</ul>
</details>
{{end}} {{end}}
</ul> {{end}}
<br/>
<form method="get">
{{if eq .Format "list"}}
<button type="submit" name="format" value="topics">See Topics</button>
{{else}}
<button type="submit" name="format" value="list"> See List</button>
{{end}}
</form>
{{template "foot.tmpl" .}} {{template "foot.tmpl" .}}

View File

@ -28,6 +28,12 @@ type Gitea struct {
func (g Gitea) Packages() (map[string]*api.Package, error) { func (g Gitea) Packages() (map[string]*api.Package, error) {
packages := make(map[string]*api.Package) packages := make(map[string]*api.Package)
topicOpts := gitea.ListRepoTopicsOptions{
ListOptions: gitea.ListOptions{
Page: 1,
PageSize: 50,
},
}
page := 0 page := 0
for { for {
opts := gitea.ListReposOptions{ opts := gitea.ListReposOptions{
@ -43,7 +49,7 @@ func (g Gitea) Packages() (map[string]*api.Package, error) {
} }
for _, repo := range repos { for _, repo := range repos {
packages[repo.Name] = &api.Package{ pkg := &api.Package{
Name: repo.Name, Name: repo.Name,
Description: repo.Description, Description: repo.Description,
Branch: repo.DefaultBranch, Branch: repo.DefaultBranch,
@ -55,6 +61,15 @@ func (g Gitea) Packages() (map[string]*api.Package, error) {
Mirror: repo.Mirror, Mirror: repo.Mirror,
Archive: repo.Archived, Archive: repo.Archived,
} }
// Get tags
topics, _, err := g.client.ListRepoTopics(flags.Namespace, repo.Name, topicOpts)
if err != nil {
beaver.Warnf("could not get topics for %s: %v", repo.Name, err)
}
pkg.Topics = topics
packages[repo.Name] = pkg
} }
page++ page++

View File

@ -57,6 +57,7 @@ func (g GitHub) Packages() (map[string]*api.Package, error) {
Fork: repo.GetFork(), Fork: repo.GetFork(),
Mirror: false, Mirror: false,
Archive: repo.GetArchived(), Archive: repo.GetArchived(),
Topics: repo.Topics,
} }
} }

View File

@ -55,6 +55,7 @@ func (g GitLab) Packages() (map[string]*api.Package, error) {
Fork: repo.ForkedFromProject != nil, Fork: repo.ForkedFromProject != nil,
Mirror: repo.Mirror, Mirror: repo.Mirror,
Archive: repo.Archived, Archive: repo.Archived,
Topics: repo.TagList,
} }
} }