Compare commits

...

11 Commits
v0.1.0 ... main

Author SHA1 Message Date
jolheiser 6ef23a7c5b Refactor for ff, zerolog, and re-integrate SDK (#14)
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details
Reviewed-on: #14
2022-03-20 04:15:24 +00:00
jolheiser 2d644e2711 Fix sakura (#13)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #13
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Co-committed-by: jolheiser <john.olheiser@gmail.com>
2021-05-09 16:22:19 +00:00
jolheiser 0b3fd9c04c Add sakura for CSS (#12)
continuous-integration/drone/push Build is passing Details
Resolves #7

Reviewed-on: #12
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Co-committed-by: jolheiser <john.olheiser@gmail.com>
2021-05-09 05:02:27 +00:00
John Olheiser cf984234fd Convert to gpm-style service (#11)
This is a **massively** breaking change from `v0.2.0`.
For consumers there will be no difference, `go-get` and `git-import` are both still supported.
The change will be for the admin regarding how package management works.

Prior to merging, `v0.2.0` should be moved to another branch in case myself or another party wants to continue with that style of service.

This version follows a similar implementation to [gpm](https://gitea.com/jolheiser/gpm) (and indeed, some code was copied nearly line-by-line)

-----

Vanity runs as a service, same as before. However, rather than automatic cron-style updates using a third-party API, now the service owner uses their local `vanity` binary with a matching `token` to...
* `vanity add` a new package
* `vanity update` an existing package
* `vanity remove` a package

This allows much finer control over which packages are in the service and should required almost no downtime once the service is started other than to update the service itself.

As well, it allows mixing of git providers.

<small>There's also an SDK, which is nice to have.</small>

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/11
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-04-23 11:21:53 +08:00
John Olheiser c4be5e64b6 Add link to docs for vanity imports (#10)
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/10
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-02-23 22:45:01 +08:00
John Olheiser 2f688b839b Update docker for Go 1.16 (#9)
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/9
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-02-22 04:28:35 +08:00
John Olheiser c3e03c1408 Shiny things (#8)
Resolves #4
Resolves #5
Resolves #6

Change list:

* Adds topics format
   * Groups projects by their topics/tags, any without topics are tossed in `other`
* Adds minimal page for `go-get` or `git-import` URL query parameters
* Changes the version information to explicitly mention Vanity and Go.
* Adds manual update mode as an alternative to the automatic intervals.
* Moved templates to `.tmpl` files and using `//go:embed` to add to binary.

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/8
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-02-22 04:23:06 +08:00
jolheiser e7c3ac2d0f
Fix docker compose
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-09-12 13:04:48 -05:00
jolheiser c6b66c4691
Add license
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-09-12 10:41:21 -05:00
John Olheiser 693e58df7f Vanity overhaul (#3)
More debug output and template fixes

Signed-off-by: jolheiser <john.olheiser@gmail.com>

README cleanup

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Better config options, change package structure

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Add GitLab support

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Initial Overhaul

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/3
2020-09-12 15:23:25 +00:00
John Olheiser 1805220975 Add CI config (#1)
Add CI config

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/1
2020-02-21 14:14:13 +00:00
46 changed files with 1744 additions and 637 deletions

7
.gitignore vendored
View File

@ -1,6 +1,5 @@
# GoLand
.idea
.idea/
# Binaries
/vanity*
!vanity.service
# Vanity
/vanity*

37
.woodpecker.yml 100644
View File

@ -0,0 +1,37 @@
clone:
git:
image: woodpeckerci/plugin-git:next
pipeline:
compliance:
image: golang:1.17
commands:
- go test -race ./...
- go run github.com/rs/zerolog/cmd/lint ./cmd/vanity-cli
- go run github.com/rs/zerolog/cmd/lint ./cmd/vanity-server
- go vet ./...
when:
event: pull_request
build:
image: golang:1.17
commands:
- GOOS="linux" go build ./cmd/vanity-cli
- GOOS="windows" go build ./cmd/vanity-cli
- GOOS="linux" go build ./cmd/vanity-server
- GOOS="windows" go build ./cmd/vanity-server
release-main:
image: jolheiser/drone-gitea-main:latest
settings:
base: https://git.jojodev.com
token:
from_secret: gitea_token
files:
- "vanity-cli"
- "vanity-cli.exe"
- "vanity-server"
- "vanity-server.exe"
when:
event: push
branch: main

7
LICENSE 100644
View File

@ -0,0 +1,7 @@
Copyright 2020 John Olheiser
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,86 +0,0 @@
DIST := dist
GO ?= go
SHASUM ?= shasum -a 256
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
LONG_VERSION ?= $(VERSION)
else
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
else
VERSION ?= master
endif
LONG_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif
LDFLAGS := $(LDFLAGS) -X "go.jolheiser.com/vanity/modules/config.Version=$(LONG_VERSION)"
.PHONY: build
build:
$(GO) build -ldflags '-s -w $(LDFLAGS)'
.PHONY: lint
lint:
@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
export BINARY="golangci-lint"; \
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.23.1; \
fi
golangci-lint run --timeout 5m
.PHONY: fmt
fmt:
$(GO) fmt ./...
.PHONY: test
test:
$(GO) test -race ./...
.PHONY: release
release: release-dirs check-xgo release-windows release-linux release-darwin release-copy release-compress release-check
.PHONY: check-xgo
check-xgo:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
.PHONY: release-dirs
release-dirs:
mkdir -p $(DIST)/binaries $(DIST)/release
.PHONY: release-windows
release-windows:
xgo -dest $(DIST)/binaries -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out vanity-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-linux
release-linux:
xgo -dest $(DIST)/binaries -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out vanity-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-darwin
release-darwin:
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out vanity-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-copy
release-copy:
cd $(DIST); for file in `find /build -type f -name "*"`; do cp $${file} ./release/; done;
.PHONY: release-check
release-check:
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done;
.PHONY: release-compress
release-compress:
@hash gxz > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/ulikunitz/xz/cmd/gxz; \
fi
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done;

View File

@ -1,7 +1,27 @@
# Vanity
A simple web service to serve vanity Go imports. Feel free to check it out using [my instance](https://go.jolheiser.com/).
[![Go Reference](https://pkg.go.dev/badge/go.jolheiser.com/vanity.svg)](https://pkg.go.dev/go.jolheiser.com/vanity)
[![Build Status](https://ci.jojodev.com/api/badges/jolheiser/vanity/status.svg)](https://ci.jojodev.com/jolheiser/vanity)
## Configuration
See [the sample](config.sample.toml).
Vanity also supports [git-import](https://gitea.com/jolheiser/git-import).
A simple web service to serve [vanity Go imports](https://golang.org/cmd/go/#hdr-Remote_import_paths). Feel free to check it out using [my instance](https://go.jolheiser.com/).
Vanity also supports [git-import](https://gitea.com/jolheiser/git-import).
## Docker
```sh
docker run \
--env VANITY_DOMAIN=go.domain.tld \
--env VANITY_TOKEN=<token> \
--publish 80:7777 \
--restart always
jolheiser/vanity:latest
```
## API
Check out the [SDK](go-vanity).
## License
[MIT](LICENSE)

View File

@ -1,79 +0,0 @@
package cmd
import (
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/modules/config"
)
var Add = cli.Command{
Name: "add",
Usage: "Add a package",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite existing package without prompt",
},
},
Action: doAdd,
}
func doAdd(ctx *cli.Context) error {
questions := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "Name"},
Validate: survey.Required,
},
{
Name: "path",
Prompt: &survey.Input{Message: "Path"},
Validate: survey.Required,
},
{
Name: "repo",
Prompt: &survey.Input{Message: "Repository HTTP(S) URL"},
Validate: survey.Required,
},
{
Name: "ssh",
Prompt: &survey.Input{Message: "Repository SSH URL"},
Validate: survey.Required,
},
{
Name: "description",
Prompt: &survey.Input{Message: "Description"},
Validate: survey.Required,
},
}
answers := struct {
Name string
Path string
Repo string
SSH string
Description string
}{}
if err := survey.Ask(questions, &answers); err != nil {
return err
}
pkg := config.Package{
Name: answers.Name,
Path: answers.Path,
Repo: answers.Repo,
SSH: answers.SSH,
Description: answers.Description,
}
config.AddPackages(ctx.Bool("force"), pkg)
if err := config.Save(); err != nil {
return err
}
beaver.Infof("Added `%s` to vanity.", pkg.Name)
return nil
}

View File

@ -1,35 +0,0 @@
package cmd
import (
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/modules/config"
)
var Config = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Configure vanity",
Action: doConfig,
}
func doConfig(ctx *cli.Context) error {
urlQuestion := &survey.Input{
Message: "domain",
Default: "go.domain.tld",
}
var urlAnswer string
if err := survey.AskOne(urlQuestion, &urlAnswer); err != nil {
return err
}
config.Domain = urlAnswer
if err := config.Save(); err != nil {
return err
}
beaver.Info("domain saved!")
return nil
}

View File

@ -1,21 +0,0 @@
package cmd
import (
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/modules/config"
)
var List = cli.Command{
Name: "list",
Aliases: []string{"l"},
Usage: "List packages",
Action: doList,
}
func doList(ctx *cli.Context) error {
for _, pkg := range config.Packages {
beaver.Infof("%s (%s) -> %s | %s", pkg.Name, pkg.Path, pkg.Repo, pkg.SSH)
}
return nil
}

View File

@ -1,56 +0,0 @@
package cmd
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/modules/config"
"strings"
)
var Remove = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a package",
Action: doRemove,
}
func doRemove(ctx *cli.Context) error {
pkgQuestion := &survey.Input{
Message: "Package name",
}
var pkgAnswer string
if err := survey.AskOne(pkgQuestion, &pkgAnswer); err != nil {
return err
}
for idx, p := range config.Packages {
if strings.EqualFold(p.Name, pkgAnswer) {
confirm := &survey.Confirm{
Message: fmt.Sprintf("Are you sure you want to remove %s (%s) ?", p.Name, p.Repo),
Default: false,
}
var answer bool
if err := survey.AskOne(confirm, &answer); err != nil {
return err
}
if answer {
config.Packages = append(config.Packages[:idx], config.Packages[idx+1:]...)
if err := config.Save(); err != nil {
return err
}
beaver.Infof("Removed `%s` from vanity.", p.Name)
break
}
beaver.Infof("Did not remove `%s` from vanity.", p.Name)
break
}
}
return nil
}

View File

@ -1,31 +0,0 @@
package cmd
import (
"fmt"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/modules/router"
"net/http"
)
var Server = cli.Command{
Name: "server",
Usage: "Start the vanity server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "Port to run the vanity server on",
Value: "3333",
},
},
Action: doServer,
}
func doServer(ctx *cli.Context) error {
beaver.Infof("Running vanity server at http://localhost:%s", ctx.String("port"))
if err := http.ListenAndServe(fmt.Sprintf(":%s", ctx.String("port")), router.Init()); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,36 @@
package main
import (
"context"
"net/url"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log"
)
func add(token, server *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
pkg, err := pkgPrompt(sdk.Package{})
if err != nil {
return err
}
client := sdk.New(*token, sdk.WithServer(*server))
if err := client.Add(context.Background(), pkg); err != nil {
return err
}
log.Info().Msgf("Added %q", pkg.Name)
return nil
}
}
func validURL(ans interface{}) error {
if err := survey.Required(ans); err != nil {
return err
}
_, err := url.Parse(ans.(string))
return err
}

View File

@ -0,0 +1,127 @@
package main
import (
"context"
"flag"
"fmt"
"net/url"
"strings"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v3/fftoml"
)
func New() *ffcli.Command {
fs := flag.NewFlagSet("vanity", flag.ExitOnError)
serverFlag := fs.String("server", sdk.DefaultServer, "vanity server to use")
tokenFlag := fs.String("token", "", "vanity auth token to use")
cmd := &ffcli.Command{
Name: "vanity",
ShortUsage: "vanity [add|update|remove]",
ShortHelp: "Vanity CLI",
LongHelp: "Vanity CLI to work with a remote Vanity server",
FlagSet: fs,
Options: []ff.Option{
ff.WithEnvVarPrefix("VANITY"),
ff.WithAllowMissingConfigFile(true),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(fftoml.New().Parse),
},
Subcommands: []*ffcli.Command{
{
Name: "add",
ShortHelp: "add package",
LongHelp: "add a package to the vanity server",
Exec: add(tokenFlag, serverFlag),
},
{
Name: "remove",
ShortHelp: "remove package",
LongHelp: "remove a package from the vanity server",
Exec: remove(tokenFlag, serverFlag),
},
{
Name: "update",
ShortHelp: "update package",
LongHelp: "update a package on the vanity server",
Exec: update(tokenFlag, serverFlag),
},
},
}
return cmd
}
func listPackages(token, server string) ([]sdk.Package, error) {
client := sdk.New(token, sdk.WithServer(server))
info, err := client.Info(context.Background())
if err != nil {
return nil, err
}
return info.Packages, nil
}
func pkgPrompt(def sdk.Package) (sdk.Package, error) {
if def.Branch == "" {
def.Branch = "main"
}
var pkg sdk.Package
questions := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "Name", Default: def.Name},
Validate: survey.Required,
},
{
Name: "description",
Prompt: &survey.Multiline{Message: "Description", Default: def.Description},
Validate: survey.Required,
},
{
Name: "branch",
Prompt: &survey.Input{Message: "Branch", Default: def.Branch},
Validate: survey.Required,
},
{
Name: "weburl",
Prompt: &survey.Input{Message: "Web URL", Default: def.WebURL},
Validate: validURL,
},
}
if err := survey.Ask(questions, &pkg); err != nil {
return pkg, err
}
defHTTP, defSSH := def.CloneHTTP, def.CloneSSH
if def.WebURL != pkg.WebURL {
u, err := url.Parse(pkg.WebURL)
if err != nil {
return pkg, err
}
defHTTP = pkg.WebURL + ".git"
defSSH = fmt.Sprintf("git@%s:%s.git", u.Host, strings.TrimPrefix(u.Path, "/"))
}
questions = []*survey.Question{
{
Name: "clonehttp",
Prompt: &survey.Input{Message: "HTTP(S) CLone URL", Default: defHTTP},
Validate: validURL,
},
{
Name: "clonessh",
Prompt: &survey.Input{Message: "SSH CLone URL", Default: defSSH},
Validate: survey.Required,
},
}
if err := survey.Ask(questions, &pkg); err != nil {
return pkg, err
}
return pkg, nil
}

View File

@ -0,0 +1,18 @@
package main
import (
"context"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
cmd := New()
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
log.Fatal().Err(err).Msg("")
}
}

View File

@ -0,0 +1,48 @@
package main
import (
"context"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log"
)
func remove(token, server *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
pkgs, err := listPackages(*token, *server)
if err != nil {
return err
}
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]sdk.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
pkgQuestion := &survey.Select{
Message: "Select package to remove",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
pkg := sdk.Package{
Name: pkgName,
}
client := sdk.New(*token, sdk.WithServer(*server))
if err := client.Remove(context.Background(), pkg); err != nil {
return err
}
log.Info().Msgf("Removed %q", pkgName)
return nil
}
}

View File

@ -0,0 +1,49 @@
package main
import (
"context"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log"
)
func update(token, server *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
pkgs, err := listPackages(*token, *server)
if err != nil {
return err
}
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]sdk.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
pkgQuestion := &survey.Select{
Message: "Select package to update",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
pkg, err := pkgPrompt(pkgMap[pkgName])
if err != nil {
return err
}
client := sdk.New(*token, sdk.WithServer(*server))
if err := client.Update(context.Background(), pkg); err != nil {
return err
}
log.Info().Msgf("Updated %q", pkgName)
return nil
}
}

View File

@ -0,0 +1,18 @@
package main
import (
"context"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
cmd := New()
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
log.Fatal().Err(err).Msg("")
}
}

View File

@ -0,0 +1,75 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"go.jolheiser.com/vanity/server/database"
"go.jolheiser.com/vanity/server/router"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v3/fftoml"
"github.com/rs/zerolog/log"
)
func New() *ffcli.Command {
fs := flag.NewFlagSet("vanity", flag.ExitOnError)
portFlag := fs.Int("port", 3333, "Port to run the vanity server on")
domainFlag := fs.String("domain", "", "The Go module domain (e.g. go.jolheiser.com)")
tokenFlag := fs.String("token", "", "vanity auth token to use")
dbFlag := fs.String("database", dbPath(), "The path to the database")
cmd := &ffcli.Command{
Name: "vanity",
ShortUsage: "vanity",
ShortHelp: "Vanity Server",
LongHelp: "Vanity Server to serve the Go module vanity domain",
FlagSet: fs,
Options: []ff.Option{
ff.WithEnvVarPrefix("VANITY"),
ff.WithAllowMissingConfigFile(true),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(fftoml.New().Parse),
},
Exec: server(portFlag, domainFlag, tokenFlag, dbFlag),
}
return cmd
}
func server(port *int, domain, token, dbPath *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
if *token == "" || *domain == "" {
return errors.New("vanity server requires --token and --domain")
}
db, err := database.Load(*dbPath)
if err != nil {
log.Fatal().Msgf("could not load database at %s: %v", *dbPath, err)
}
log.Info().Msgf("Running vanity server at http://localhost:%d", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), router.New(*token, *domain, db)); err != nil {
return err
}
return nil
}
}
func dbPath() string {
fn := "vanity.db"
home, err := os.UserHomeDir()
if err != nil {
bin, err := os.Executable()
if err != nil {
return fn
}
return filepath.Join(filepath.Dir(bin), fn)
}
return filepath.Join(home, fn)
}

View File

@ -1,12 +0,0 @@
# For each package, an entry like the following
[[package]]
# The name of the package (can be anything)
name = "Go-Vanity"
# The path to the package (this NEEDS to match the import path)
path = "vanity"
# The repository to direct go-import to
repo = "https://gitea.com/jolheiser/vanity.git"
# git-import for SSH (optional)
ssh = "git@gitea.com:jolheiser/vanity.git"
# A description of the project (optional)
description = "The code responsible for hosting this service!"

View File

@ -8,9 +8,12 @@ RestartSec=2s
Type=simple
User=vanity
Group=vanity
ExecStart=/usr/local/bin/vanity server -p 7777
ExecStart=${bin} server
Restart=always
Environment=USER=vanity HOME=/var/lib/vanity
# Required
Environment=VANITY_TOKEN=${VANITY_TOKEN}
Environment=VANITY_DOMAIN=${VANITY_DOMAIN}
[Install]
WantedBy=multi-user.target

11
docker/Dockerfile 100644
View File

@ -0,0 +1,11 @@
FROM golang:1.16-alpine as builder
RUN apk --no-cache add build-base git
COPY . /app
WORKDIR /app
RUN make build
FROM alpine:latest
LABEL maintainer="john.olheiser@gmail.com"
COPY --from=builder /app/vanity vanity
EXPOSE 7777
ENTRYPOINT ["/vanity"]

View File

@ -0,0 +1,11 @@
version: "2"
services:
vanity:
image: jolheiser/vanity:latest
environment:
- VANITY_DOMAIN=go.domain.tld
- VANITY_TOKEN=<token>
restart: always
ports:
- "80:7777"

15
go.mod
View File

@ -1,13 +1,12 @@
module go.jolheiser.com/vanity
go 1.12
go 1.16
require (
github.com/AlecAivazis/survey/v2 v2.0.5
github.com/BurntSushi/toml v0.3.1
github.com/go-chi/chi v4.0.3+incompatible
github.com/mitchellh/go-homedir v1.1.0
github.com/stretchr/testify v1.3.0 // indirect
github.com/urfave/cli/v2 v2.1.1
go.jolheiser.com/beaver v1.0.1
github.com/AlecAivazis/survey/v2 v2.2.8
github.com/go-chi/chi/v5 v5.0.3
github.com/peterbourgon/ff/v3 v3.1.2
github.com/rs/zerolog v1.26.1
go.etcd.io/bbolt v1.3.5
go.jolheiser.com/overlay v0.0.3
)

79
go.sum
View File

@ -1,16 +1,15 @@
github.com/AlecAivazis/survey/v2 v2.0.5 h1:xpZp+Q55wi5C7Iaze+40onHnEkex1jSc34CltJjOoPM=
github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
github.com/AlecAivazis/survey/v2 v2.2.8 h1:TgxCwybKdBckmC+/P9/5h49rw/nAHe/itZL0dgHs+Q0=
github.com/AlecAivazis/survey/v2 v2.2.8/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4=
github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@ -23,37 +22,59 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM=
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
go.jolheiser.com/beaver v1.0.1 h1:gt3aGEr5Bj4ZjDF1g8t8OYOGRCRXGaanGR9CmXUxez8=
go.jolheiser.com/beaver v1.0.1/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.jolheiser.com/overlay v0.0.3 h1:5WoXtnRi1w5KkU9xq+si/wV0GINOMJphfSOvgG2l53I=
go.jolheiser.com/overlay v0.0.3/go.mod h1:xNbssakJ3HjK4RnjuP38q9yQNS4wxXKsyprYIWWr2bg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

31
main.go
View File

@ -1,31 +0,0 @@
package main
import (
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/cmd"
"go.jolheiser.com/vanity/modules/config"
"os"
)
func main() {
// config loads on init
app := cli.NewApp()
app.Name = "vanity"
app.Usage = "Vanity Go Imports"
app.Version = config.Version
app.Commands = []*cli.Command{
&cmd.Add,
&cmd.Remove,
&cmd.List,
&cmd.Config,
&cmd.Server,
}
app.EnableBashCompletion = true
err := app.Run(os.Args)
if err != nil {
beaver.Error(err)
}
}

View File

@ -1,133 +0,0 @@
package config
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/BurntSushi/toml"
"github.com/mitchellh/go-homedir"
"go.jolheiser.com/beaver"
"os"
"path"
"strings"
)
var (
configPath string
cfg *Config
Version = "develop"
// Config items
Domain string
Packages []Package
)
type Config struct {
Domain string `toml:"domain"`
Packages []Package `toml:"package" json:"packages"`
}
type Package struct {
Name string `toml:"name"`
Path string `toml:"path"`
Repo string `toml:"repo"`
SSH string `toml:"ssh"`
Description string `toml:"description"`
}
func (pkg Package) Module() string {
return fmt.Sprintf("%s/%s", Domain, pkg.Path)
}
// Load on init so that CLI contexts are correctly populated
func init() {
home, err := homedir.Dir()
if err != nil {
beaver.Fatalf("could not locate home directory: %v", err)
}
configPath = fmt.Sprintf("%s/.vanity/config.toml", home)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(configPath), os.ModePerm); err != nil {
beaver.Fatalf("could not create vanity home: %v", err)
}
if _, err := os.Create(configPath); err != nil {
beaver.Fatalf("could not create vanity config: %v", err)
}
}
if _, err := toml.DecodeFile(configPath, &cfg); err != nil {
beaver.Fatalf("could not decode vanity config: %v", err)
}
dupe := make(map[string]bool)
for _, pkg := range cfg.Packages {
name := strings.ToLower(pkg.Name)
if ok := dupe[name]; ok {
beaver.Fatalf("duplicate package for %s", pkg.Name)
}
dupe[name] = true
}
Domain = cfg.Domain
Packages = cfg.Packages
}
func Save() error {
cfg.Domain = Domain
cfg.Packages = Packages
fi, err := os.Create(configPath)
if err != nil {
return err
}
defer fi.Close()
if err := toml.NewEncoder(fi).Encode(cfg); err != nil {
return err
}
return nil
}
func PackageMap() map[string]Package {
pkgs := make(map[string]Package)
for _, pkg := range Packages {
pkgs[pkg.Path] = pkg
}
return pkgs
}
func AddPackages(force bool, pkgs ...Package) {
for _, pkg := range pkgs {
for idx, p := range Packages {
if strings.EqualFold(p.Name, pkg.Name) {
if force {
Packages[idx] = pkg
break
}
forceQuestion := &survey.Confirm{
Message: fmt.Sprintf("Package `%s` (%s) already exists. Overwrite with `%s`?", p.Name, p.Repo, p.Repo),
Default: false,
}
var forceAnswer bool
if err := survey.AskOne(forceQuestion, &forceAnswer); err != nil {
beaver.Error(err)
break
}
if !forceAnswer {
beaver.Errorf("leaving package `%s` as-is", pkg.Name)
break
}
Packages[idx] = pkg
break
}
}
Packages = append(Packages, pkg)
}
}

View File

@ -1,59 +0,0 @@
package router
import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/modules/config"
"go.jolheiser.com/vanity/modules/router/templates"
"html/template"
"net/http"
"runtime"
"strings"
"time"
)
var (
index = template.Must(template.New("index").Parse(templates.Head + templates.Index + templates.Info + templates.Foot))
vanity = template.Must(template.New("vanity").Parse(templates.Head + templates.Vanity + templates.Info + templates.Foot))
cache = config.PackageMap()
)
func Init() *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.RedirectSlashes)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Get("/", doGet)
r.Get("/*", doPackage)
return r
}
func doGet(res http.ResponseWriter, req *http.Request) {
if err := index.Execute(res, map[string]interface{}{
"Packages": config.Packages,
"AppVer": config.Version,
"GoVer": runtime.Version(),
}); err != nil {
beaver.Error(err)
}
}
func doPackage(res http.ResponseWriter, req *http.Request) {
key := chi.URLParam(req, "*")
pkg, ok := cache[strings.Split(key, "/")[0]]
if !ok {
http.NotFound(res, req)
return
}
if err := vanity.Execute(res, map[string]interface{}{
"Package": pkg,
"AppVer": config.Version,
"GoVer": runtime.Version(),
}); err != nil {
beaver.Error(err)
}
}

View File

@ -1,6 +0,0 @@
package templates
var Foot = `
</body>
</html>
`

View File

@ -1,15 +0,0 @@
package templates
var Head = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{{if .Package}}
<meta name="go-import" content="{{.Package.Module}} git {{.Package.Repo}}"/>
<meta name="git-import" content="{{.Package.Path}} {{.Package.Repo}} {{.Package.SSH}}"/>
{{end}}
<title>Vanity - {{if .Package}}{{.Package.Name}}{{else}}Index{{end}}</title>
</head>
<body>
`

View File

@ -1,12 +0,0 @@
package templates
var Index = `
<h1><a href=".">Index</a></h1>
<hr/>
<h3>Imports:</h3>
<ul>
{{range $path, $package := .Packages}}
<li><a href="{{$package.Path}}">{{$package.Name}}</a></li>
{{end}}
</ul>
`

View File

@ -1,5 +0,0 @@
package templates
var Info = `
Version: {{.AppVer}} | {{.GoVer}}
`

View File

@ -1,9 +0,0 @@
package templates
var Vanity = `
<h1><a href="../">Index</a></h1>
<hr/>
<p><strong>Name: </strong>{{.Package.Name}}</p>
<p><strong>Source: </strong><a href="{{.Package.Repo}}">{{.Package.Repo}}</a></p>
<p><strong>Description: </strong>{{.Package.Description}}</p>
`

53
sdk/client.go 100644
View File

@ -0,0 +1,53 @@
package sdk
import (
"context"
"io"
"net/http"
)
const (
DefaultServer = "https://go.jolheiser.com"
TokenHeader = "X-Vanity-Token"
)
type Client struct {
token string
server string
http *http.Client
}
func New(token string, opts ...ClientOption) *Client {
c := &Client{
token: token,
server: DefaultServer,
http: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}
type ClientOption func(*Client)
func WithHTTP(client *http.Client) ClientOption {
return func(c *Client) {
c.http = client
}
}
func WithServer(server string) ClientOption {
return func(c *Client) {
c.server = server
}
}
func (c *Client) newRequest(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set(TokenHeader, c.token)
return req, nil
}

104
sdk/package.go 100644
View File

@ -0,0 +1,104 @@
package sdk
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
)
type Info struct {
Version string `json:"version"`
NumPackages int `json:"num_packages"`
Packages []Package `json:"packages"`
}
type Package struct {
Name string `json:"name"`
Description string `json:"description"`
Branch string `json:"branch"`
WebURL string `json:"web_url"`
CloneHTTP string `json:"clone_http"`
CloneSSH string `json:"clone_ssh"`
}
func (p Package) Module(domain string) string {
return fmt.Sprintf("%s/%s", strings.TrimSuffix(domain, "/"), strings.ToLower(p.Name))
}
// Info gets Info from a vanity server
func (c *Client) Info(ctx context.Context) (Info, error) {
var info Info
resp, err := c.crud(ctx, Package{}, http.MethodOptions)
if err != nil {
return info, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return info, fmt.Errorf("could not get info: %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return info, err
}
return info, nil
}
// Add adds a new Package to a vanity server
func (c *Client) Add(ctx context.Context, pkg Package) error {
resp, err := c.crud(ctx, pkg, http.MethodPost)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("could not add package: %s", resp.Status)
}
return nil
}
// Update updates a Package on a vanity server
func (c *Client) Update(ctx context.Context, pkg Package) error {
resp, err := c.crud(ctx, pkg, http.MethodPatch)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not update package: %s", resp.Status)
}
return nil
}
// Remove removes a Package from a vanity server
func (c *Client) Remove(ctx context.Context, pkg Package) error {
resp, err := c.crud(ctx, pkg, http.MethodDelete)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not remove package: %s", resp.Status)
}
return nil
}
func (c *Client) crud(ctx context.Context, pkg Package, method string) (*http.Response, error) {
payload, err := json.Marshal(pkg)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, method, c.server, bytes.NewReader(payload))
if err != nil {
return nil, err
}
return c.http.Do(req)
}

45
sdk/source.go 100644
View File

@ -0,0 +1,45 @@
package sdk
import (
"errors"
"fmt"
"strings"
)
type SourceDirFile struct {
Dir string
File string
}
func GiteaSDF(pkg Package) SourceDirFile {
return SourceDirFile{
Dir: fmt.Sprintf("%s/src/branch/%s{/dir}", pkg.WebURL, pkg.Branch),
File: fmt.Sprintf("%s/src/branch/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch),
}
}
func GitHubSDF(pkg Package) SourceDirFile {
return SourceDirFile{
Dir: fmt.Sprintf("%s/tree/%s{/dir}", pkg.WebURL, pkg.Branch),
File: fmt.Sprintf("%s/blob/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch),
}
}
func GitLabSDF(pkg Package) SourceDirFile {
return SourceDirFile{
Dir: fmt.Sprintf("%s/-/tree/%s{/dir}", pkg.WebURL, pkg.Branch),
File: fmt.Sprintf("%s/-/blob/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch),
}
}
func AnalyzeSDF(pkg Package) (SourceDirFile, error) {
switch {
case strings.Contains(pkg.WebURL, "gitea.com"):
return GiteaSDF(pkg), nil
case strings.Contains(pkg.WebURL, "github.com"):
return GitHubSDF(pkg), nil
case strings.Contains(pkg.WebURL, "gitlab.com"):
return GitLabSDF(pkg), nil
}
return SourceDirFile{}, errors.New("could not detect SDF")
}

194
sdk/vanity_test.go 100644
View File

@ -0,0 +1,194 @@
package sdk
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
var (
server *httptest.Server
token = "TestingLibrary"
version = "VanityTest"
packages = []Package{
{
Name: "test1",
Description: "test1",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test1",
CloneHTTP: "https://gitea.com/jolheiser/test1.git",
CloneSSH: "https://gitea.com/jolheiser/test1",
},
}
)
func TestMain(m *testing.M) {
server = httptest.NewServer(http.HandlerFunc(testServer))
os.Exit(m.Run())
}
func TestClient(t *testing.T) {
ctx := context.Background()
client := New("", WithServer(server.URL))
// Info
checkInfo(t, client, 1)
pkg1 := Package{
Name: "test1",
Description: "test1",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test1",
CloneHTTP: "https://gitea.com/jolheiser/test1.git",
CloneSSH: "https://gitea.com/jolheiser/test1",
}
pkg2 := Package{
Name: "test2",
Description: "test2",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test2",
CloneHTTP: "https://gitea.com/jolheiser/test2.git",
CloneSSH: "https://gitea.com/jolheiser/test2",
}
// Add (without token)
if err := client.Add(ctx, pkg1); err == nil {
t.Log("adding without token should fail")
t.Fail()
}
// Add (with token)
client = New(token, WithServer(server.URL))
checkAdd(t, client, pkg1, pkg2)
// Info (after second package)
checkInfo(t, client, 2)
// Update package
checkUpdate(t, client, pkg1)
// Remove
checkRemove(t, client, pkg1)
// Info (final)
checkInfo(t, client, 1)
}
func checkInfo(t *testing.T, client *Client, numPackages int) {
info, err := client.Info(context.Background())
if err != nil {
t.Logf("info should not return error: %v\n", err)
t.Fail()
}
if info.Version != version || info.NumPackages != numPackages {
t.Log("info did not match expected")
t.Fail()
}
}
func checkAdd(t *testing.T, client *Client, pkg1, pkg2 Package) {
ctx := context.Background()
if err := client.Add(ctx, pkg2); err != nil {
t.Logf("pkg2 should be added: %v\n", err)
t.Fail()
}
// Duplicate package
if err := client.Add(ctx, pkg1); err == nil {
t.Log("pkg1 should already exist")
t.Fail()
}
}
func checkUpdate(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
// Update invalid package
if err := client.Update(ctx, Package{Name: "test4"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
if err := client.Update(ctx, pkg); err != nil {
t.Logf("should be able to update valid package: %v\n", err)
t.Fail()
}
}
func checkRemove(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package: %v\n", err)
t.Fail()
}
// Remove (idempotent)
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package idempotently: %v\n", err)
t.Fail()
}
}
func testServer(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
switch r.Method {
case http.MethodOptions:
resp := Info{
Version: version,
NumPackages: len(packages),
}
_ = json.NewEncoder(w).Encode(resp)
case http.MethodPost, http.MethodPatch, http.MethodDelete:
if r.Header.Get(TokenHeader) != token {
w.WriteHeader(http.StatusUnauthorized)
return
}
var pkg Package
if err := json.NewDecoder(r.Body).Decode(&pkg); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodPost:
for _, p := range packages {
if p.Name == pkg.Name {
w.WriteHeader(http.StatusConflict)
return
}
}
packages = append(packages, pkg)
w.WriteHeader(http.StatusCreated)
case http.MethodPatch:
for idx, p := range packages {
if p.Name == pkg.Name {
packages[idx] = pkg
return
}
}
w.WriteHeader(http.StatusNotFound)
case http.MethodDelete:
for idx, p := range packages {
if p.Name == pkg.Name {
packages = append(packages[:idx], packages[idx+1:]...)
}
}
}
return
}
return
default:
name := strings.TrimPrefix(r.URL.Path, "/")
for _, pkg := range packages {
if pkg.Name == name {
_ = json.NewEncoder(w).Encode(pkg)
return
}
}
}
w.WriteHeader(http.StatusNotImplemented)
}

View File

@ -0,0 +1,84 @@
package database
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"go.jolheiser.com/vanity/sdk"
"go.etcd.io/bbolt"
)
var packageBucket = []byte("packages")
type Database struct {
db *bbolt.DB
}
func Load(dbPath string) (*Database, error) {
if err := os.MkdirAll(filepath.Dir(dbPath), os.ModePerm); err != nil {
return nil, err
}
db, err := bbolt.Open(dbPath, os.ModePerm, nil)
if err != nil {
return nil, err
}
return &Database{
db: db,
}, db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(packageBucket)
return err
})
}
func (d *Database) Package(name string) (sdk.Package, error) {
var pkg sdk.Package
data, err := d.PackageJSON(name)
if err != nil {
return pkg, err
}
return pkg, json.NewDecoder(bytes.NewReader(data)).Decode(&pkg)
}
func (d *Database) PackageJSON(name string) (pkg []byte, err error) {
return pkg, d.db.View(func(tx *bbolt.Tx) error {
pkg = tx.Bucket(packageBucket).Get([]byte(name))
if pkg == nil {
return ErrPackageNotFound{
Name: name,
}
}
return nil
})
}
func (d *Database) Packages() (pkgs []sdk.Package, err error) {
return pkgs, d.db.View(func(tx *bbolt.Tx) error {
return tx.Bucket(packageBucket).ForEach(func(key, val []byte) error {
var pkg sdk.Package
if err := json.NewDecoder(bytes.NewReader(val)).Decode(&pkg); err != nil {
return err
}
pkgs = append(pkgs, pkg)
return nil
})
})
}
func (d *Database) PutPackage(pkg sdk.Package) error {
return d.db.Update(func(tx *bbolt.Tx) error {
data, err := json.Marshal(pkg)
if err != nil {
return err
}
return tx.Bucket(packageBucket).Put([]byte(pkg.Name), data)
})
}
func (d *Database) RemovePackage(name string) error {
return d.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(packageBucket).Delete([]byte(name))
})
}

View File

@ -0,0 +1,16 @@
package database
import "fmt"
type ErrPackageNotFound struct {
Name string
}
func (e ErrPackageNotFound) Error() string {
return fmt.Sprintf("package not found: %s", e.Name)
}
func IsErrPackageNotFound(err error) bool {
_, ok := err.(ErrPackageNotFound)
return ok
}

View File

@ -0,0 +1,208 @@
package router
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"go.jolheiser.com/vanity/sdk"
"go.jolheiser.com/vanity/server/database"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
func New(token, domain string, db *database.Database) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
r.Mount("/_/", http.StripPrefix("/_/", static()))
r.Get("/", indexGET(domain, db))
r.Options("/", infoPackages(db))
r.Post("/", addUpdatePackage(db, token))
r.Patch("/", addUpdatePackage(db, token))
r.Delete("/", removePackage(db, token))
r.Get("/*", vanityGET(domain, db))
return r
}
func indexGET(domain string, db *database.Database) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
packages, err := db.Packages()
if err != nil {
log.Error().Msgf("could not load packages: %v", err)
http.Error(res, "could not load packages", http.StatusInternalServerError)
return
}
tpl, err := tmpl(domain, "index.tmpl")
if err != nil {
log.Warn().Msgf("could not load index template: %v", err)
}
if err := tpl.Execute(res, map[string]interface{}{
"Packages": packages,
"Index": true,
}); err != nil {
log.Error().Msgf("could not write response: %v", err)
}
}
}
func vanityGET(domain string, db *database.Database) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
key := chi.URLParam(req, "*")
key = strings.Split(key, "/")[0]
pkg, err := db.Package(key)
if err != nil {
if database.IsErrPackageNotFound(err) {
http.NotFound(res, req)
return
}
http.Error(res, "could not load package", http.StatusInternalServerError)
return
}
sdf, err := sdk.AnalyzeSDF(pkg)
if err != nil {
log.Warn().Msgf("could not get SDF for %s: %v", key, err)
}
ctx := map[string]interface{}{
"Package": pkg,
"Module": pkg.Module(domain),
"GoSource": fmt.Sprintf("%s %s %s %s", pkg.Module(domain), pkg.CloneHTTP, sdf.Dir, sdf.File),
"Index": false,
}
q := req.URL.Query()
if q.Get("go-get") != "" || q.Get("git-import") != "" {
tpl, err := tmpl(domain, "import.tmpl")
if err != nil {
log.Warn().Msgf("could not load import template: %v", err)
}
if err := tpl.Execute(res, ctx); err != nil {
log.Error().Msgf("could not write response: %v", err)
}
return
}
tpl, err := tmpl(domain, "vanity.tmpl")
if err != nil {
log.Warn().Msgf("could not load vanity template: %v", err)
}
if err := tpl.Execute(res, ctx); err != nil {
log.Error().Msgf("could not write response: %v", err)
}
}
}
func infoPackages(db *database.Database) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
packages, err := db.Packages()
if err != nil {
http.Error(res, "could not load package", http.StatusInternalServerError)
return
}
info := sdk.Info{
Version: Version,
NumPackages: len(packages),
Packages: packages,
}
if err := json.NewEncoder(res).Encode(info); err != nil {
http.Error(res, "could not marshal info", http.StatusInternalServerError)
}
}
}
func addUpdatePackage(db *database.Database, token string) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
if req.Header.Get(sdk.TokenHeader) != token {
res.WriteHeader(http.StatusUnauthorized)
return
}
data, err := io.ReadAll(req.Body)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var pkg sdk.Package
if err := json.Unmarshal(data, &pkg); err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
exists, err := db.PackageJSON(pkg.Name)
if err != nil && !database.IsErrPackageNotFound(err) {
res.WriteHeader(http.StatusInternalServerError)
return
}
switch req.Method {
case http.MethodPost:
if exists != nil {
res.WriteHeader(http.StatusConflict)
return
}
case http.MethodPatch:
if exists == nil {
res.WriteHeader(http.StatusNotFound)
return
}
}
if err := db.PutPackage(pkg); err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}
switch req.Method {
case http.MethodPost:
res.WriteHeader(http.StatusCreated)
case http.MethodPatch:
res.WriteHeader(http.StatusOK)
}
}
}
func removePackage(db *database.Database, token string) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
if req.Header.Get(sdk.TokenHeader) != token {
res.WriteHeader(http.StatusUnauthorized)
return
}
data, err := io.ReadAll(req.Body)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var pkg sdk.Package
if err := json.Unmarshal(data, &pkg); err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
if err := db.RemovePackage(pkg.Name); err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}
res.WriteHeader(http.StatusOK)
}
}

View File

@ -0,0 +1,175 @@
package router
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"go.jolheiser.com/vanity/sdk"
"go.jolheiser.com/vanity/server/database"
"github.com/rs/zerolog/log"
)
var (
server *httptest.Server
token = "TestingRouter"
)
// NOTE: The router test is more or less a copy/paste from go-vanity
// However, this ensures that testing is the same with the "real" router and DB
func TestMain(m *testing.M) {
tmp, err := os.MkdirTemp(os.TempDir(), "vanity")
if err != nil {
panic(err)
}
dbPath := filepath.Join(tmp, "vanity.db")
db, err := database.Load(dbPath)
if err != nil {
log.Fatal().Msgf("could not load database at %s: %v", dbPath, err)
}
server = httptest.NewServer(New(token, "", db))
code := m.Run()
// Cleanup
if err := os.RemoveAll(tmp); err != nil {
panic(err)
}
os.Exit(code)
}
func TestRouter(t *testing.T) {
ctx := context.Background()
client := sdk.New("", sdk.WithServer(server.URL))
// Info
checkInfo(t, client, 0)
pkg1 := sdk.Package{
Name: "test1",
Description: "test1",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test1",
CloneHTTP: "https://gitea.com/jolheiser/test1.git",
CloneSSH: "https://gitea.com/jolheiser/test1",
}
pkg2 := sdk.Package{
Name: "test2",
Description: "test2",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test2",
CloneHTTP: "https://gitea.com/jolheiser/test2.git",
CloneSSH: "https://gitea.com/jolheiser/test2",
}
// Add (without token)
if err := client.Add(ctx, pkg1); err == nil {
t.Log("adding without token should fail")
t.Fail()
}
// Add (with token)
client = sdk.New(token, sdk.WithServer(server.URL))
checkAdd(t, client, pkg1, pkg2)
// Info (after second package)
checkInfo(t, client, 2)
// Check invalid package (404)
checkResp(t, "test3", http.StatusNotFound)
// Check valid package (200)
checkResp(t, "test1", http.StatusOK)
// Check valid sub-package (200)
checkResp(t, "test1/foo/bar", http.StatusOK)
// Update package
checkUpdate(t, client, pkg1)
// Remove
checkRemove(t, client, pkg1)
// Info (final)
checkInfo(t, client, 1)
}
func checkInfo(t *testing.T, client *sdk.Client, numPackages int) {
info, err := client.Info(context.Background())
if err != nil {
t.Logf("info should not return error: %v\n", err)
t.Fail()
}
if info.Version != Version || info.NumPackages != numPackages {
t.Log("info did not match expected")
t.Fail()
}
}
func checkAdd(t *testing.T, client *sdk.Client, pkg1, pkg2 sdk.Package) {
ctx := context.Background()
if err := client.Add(ctx, pkg1); err != nil {
t.Logf("pkg1 should be added: %v\n", err)
t.Fail()
}
if err := client.Add(ctx, pkg2); err != nil {
t.Logf("pkg2 should be added: %v\n", err)
t.Fail()
}
// Duplicate package
if err := client.Add(ctx, pkg1); err == nil {
t.Log("pkg1 should already exist")
t.Fail()
}
}
func checkUpdate(t *testing.T, client *sdk.Client, pkg sdk.Package) {
ctx := context.Background()
// Update invalid package
if err := client.Update(ctx, sdk.Package{Name: "test4"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
if err := client.Update(ctx, pkg); err != nil {
t.Logf("should be able to update valid package: %v\n", err)
t.Fail()
}
}
func checkRemove(t *testing.T, client *sdk.Client, pkg sdk.Package) {
ctx := context.Background()
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package: %v\n", err)
t.Fail()
}
// Remove (idempotent)
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package idempotently: %v\n", err)
t.Fail()
}
}
func checkResp(t *testing.T, path string, status int) {
resp, err := http.Get(fmt.Sprintf("%s/%s", server.URL, path))
if err != nil {
t.Logf("could not GET %s: %v", path, err)
t.Fail()
return
}
if resp.StatusCode != status {
t.Logf("incorrect response from %s, expected %d but got %d", path, status, resp.StatusCode)
t.Fail()
}
}

View File

@ -0,0 +1,59 @@
package router
import (
"embed"
"html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"go.jolheiser.com/overlay"
)
var (
//go:embed templates
templateFS embed.FS
ofs = overlay.MustNew(customRoot(), templateFS)
Version string
)
func customRoot() string {
bin, err := os.Executable()
if err != nil {
bin = ""
}
customPath := os.Getenv("VANITY_CUSTOM")
if customPath == "" {
customPath = filepath.Join(bin, "custom")
}
return customPath
}
func tmpl(domain, name string) (*template.Template, error) {
return template.New(name).Funcs(funcMap(domain)).ParseFS(ofs, "templates/base.tmpl", "templates/"+name)
}
func static() http.Handler {
sub, err := fs.Sub(ofs, "templates/static")
if err != nil {
return nil
}
return http.FileServer(http.FS(sub))
}
func funcMap(domain string) template.FuncMap {
return template.FuncMap{
"AppVer": func() string {
return Version
},
"GoVer": func() string {
return runtime.Version()
},
"Domain": func() string {
return strings.TrimSuffix(domain, "/")
},
}
}

View File

@ -0,0 +1,34 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{{if .Package}}
<!-- OGP -->
<meta name="og:title" content="{{.Package.Name}}"/>
<meta name="og:description" content="{{.Package.Description}}"/>
<!-- Go -->
<meta name="go-import" content="{{.Module}} git {{.Package.CloneHTTP}}"/>
<meta name="go-source" content="{{.GoSource}}">
<!-- Git Import -->
<meta name="git-import" content="{{.Package.Name}} {{.Package.CloneHTTP}} {{.Package.CloneSSH}}"/>
{{end}}
<title>Vanity - {{if .Package}}{{.Package.Name}}{{else}}Index{{end}}</title>
<link rel="stylesheet" href="/_/sakura.css">
</head>
<body>
<h1><a href="{{if .Index}}.{{else}}../{{end}}">Index</a></h1>
<hr/>
{{block "content" .}}{{end}}
<hr/>
<strong>Vanity Version:</strong>
{{AppVer}}
<br/><br/>
<strong>Go Version:</strong>
{{GoVer}}
</body>
</html>
{{end}}

View File

@ -0,0 +1,6 @@
{{template "base" .}}
{{define "content"}}
<code>go get {{.Module}}</code>
<br/>
<code>git-import {{.Module}}</code>
{{end}}

View File

@ -0,0 +1,9 @@
{{template "base" .}}
{{define "content"}}
<h3>Imports:</h3>
<ul>
{{range $path, $package := .Packages}}
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li>
{{end}}
</ul>
{{end}}

View File

@ -0,0 +1,202 @@
/* Sakura.css v1.3.1
* ================
* Minimal css theme.
* Project: https://github.com/oxalorg/sakura/
*/
/* Body */
:root {
--color-blossom: #1d7484;
--color-fade: #982c61;
--color-bg: #f9f9f9;
--color-bg-alt: #f1f1f1;
--color-text: #4a4a4a;
}
@media (prefers-color-scheme: dark) {
:root {
--color-blossom: #ffffff;
--color-fade: #c9c9c9;
--color-bg: #222222;
--color-bg-alt: #4a4a4a;
--color-text: #c9c9c9;
}
}
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: var(--color-text);
background-color: var(--color-bg);
padding: 13px; }
@media (max-width: 684px) {
body {
font-size: 1.53rem; } }
@media (max-width: 382px) {
body {
font-size: 1.35rem; } }
h1, h2, h3, h4, h5, h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word; }
h1 {
font-size: 2.35em; }
h2 {
font-size: 2.00em; }
h3 {
font-size: 1.75em; }
h4 {
font-size: 1.5em; }
h5 {
font-size: 1.25em; }
h6 {
font-size: 1em; }
p {
margin-top: 0px;
margin-bottom: 2.5rem; }
small, sub, sup {
font-size: 75%; }
hr {
border-color: var(--color-blossom); }
a {
text-decoration: none;
color: var(--color-blossom); }
a:hover {
color: var(--color-fade);
border-bottom: 2px solid var(--color-text); }
a:visited {
color: var(--color-blossom); }
ul {
padding-left: 1.4em;
margin-top: 0px;
margin-bottom: 2.5rem; }
li {
margin-bottom: 0.4em; }
blockquote {
margin-left: 0px;
margin-right: 0px;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid var(--color-blossom);
margin-bottom: 2.5rem;
background-color: var(--color-bg-alt); }
blockquote p {
margin-bottom: 0; }
img, video {
height: auto;
max-width: 100%;
margin-top: 0px;
margin-bottom: 2.5rem; }
/* Pre and Code */
pre {
background-color: var(--color-bg-alt);
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0px;
margin-bottom: 2.5rem; }
code {
font-size: 0.9em;
padding: 0 0.5em;
background-color: var(--color-bg-alt);
white-space: pre-wrap; }
pre > code {
padding: 0;
background-color: transparent;
white-space: pre; }
/* Tables */
table {
text-align: justify;
width: 100%;
border-collapse: collapse; }
td, th {
padding: 0.5em;
border-bottom: 1px solid var(--color-bg-alt); }
/* Buttons, forms and input */
input, textarea {
border: 1px solid var(--color-text); }
input:focus, textarea:focus {
border: 1px solid var(--color-blossom); }
textarea {
width: 100%; }
.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: var(--color-blossom);
color: var(--color-bg);
border-radius: 1px;
border: 1px solid var(--color-blossom);
cursor: pointer;
box-sizing: border-box; }
.button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] {
cursor: default;
opacity: .5; }
.button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled {
background-color: var(--color-fade);
border-color: var(--color-fade);
color: var(--color-bg);
outline: 0; }
textarea, select, input {
color: var(--color-text);
padding: 6px 10px;
/* The 6px vertically centers text on FF, ignored by Webkit */
margin-bottom: 10px;
background-color: var(--color-bg-alt);
border: 1px solid var(--color-bg-alt );
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
textarea:focus, select:focus, input:focus {
border: 1px solid var(--color-blossom);
outline: 0; }
input[type="checkbox"]:focus {
outline: 1px dotted var(--color-blossom); }
label, legend, fieldset {
display: block;
margin-bottom: .5rem;
font-weight: 600; }

View File

@ -0,0 +1,21 @@
{{template "base" .}}
{{define "content"}}
<p>
<strong>Name:</strong>
{{.Package.Name}}
</p>
<p>
<strong>Source:</strong>
<a href="{{.Package.WebURL}}">{{.Package.WebURL}}</a>
</p>
{{if .Package.Description}}
<p>
<strong>Description:</strong>
{{.Package.Description}}
</p>
{{end}}
<p>
<strong>Documentation:</strong>
<a href="https://pkg.go.dev/{{.Module}}">https://pkg.go.dev/{{.Module}}</a>
</p>
{{end}}

8
tools.go 100644
View File

@ -0,0 +1,8 @@
//go:build tools
// +build tools
package main
import (
_ "github.com/rs/zerolog/cmd/lint"
)