Closes #6

This is, more or less, a fundamental rewrite of gpm.

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/gpm/pulls/7
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
pull/8/head go-gpm/v0.2.0
John Olheiser 2021-02-28 13:04:05 +08:00
parent b23be07d5e
commit ef0d29afa3
31 changed files with 1284 additions and 563 deletions

View File

@ -1,10 +1,14 @@
# To lint, install Earthly and run `earth +lint`
# This ensures the usage of the same version of golangci-lint
FROM golangci/golangci-lint:v1.31
FROM golangci/golangci-lint:v1.37
WORKDIR /gpm
lint:
lint-cli:
COPY . .
RUN golangci-lint run
lint-lib:
COPY ./go-gpm .
RUN golangci-lint run

19
LICENSE 100644
View File

@ -0,0 +1,19 @@
Copyright (c) 2021 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

@ -3,12 +3,39 @@ VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
.PHONY: build
build:
$(GO) build -ldflags '-s -w -X "go.jolheiser.com/gpm/config.Version=$(VERSION)"'
$(GO) build -ldflags '-s -w -X "go.jolheiser.com/gpm/router.Version=$(VERSION)"'
.PHONY: lint
lint:
earth +lint-cli
earth +lint-lib
.PHONY: fmt
fmt:
$(GO) fmt ./...
fmt: fmt-cli fmt-lib
.PHONY: test
test:
test: test-cli test-lib
.PHONY: fmt-cli
fmt-cli:
$(GO) fmt ./...
.PHONY: test-cli
test-cli:
$(GO) test -race ./...
.PHONY: fmt-lib
fmt-lib:
@cd go-gpm && $(GO) fmt ./...
.PHONY: test-lib
test-lib:
@cd go-gpm && $(GO) test -race ./...
.PHONY: docker-build
docker-build:
docker build -f docker/Dockerfile -t jolheiser/gpm .
.PHONY: docker-push
docker-push: docker-build
docker push jolheiser/gpm

View File

@ -17,18 +17,21 @@ Using either a GPM server or local config, I can instead `gpm get cli` which fin
* `remove` - Remove a local package
* `list` - List local packages
* `config` - Change local configuration
* `export` - Export local packages to JSON
* `import` - Import JSON to local packages. Either give a path to a `.json` file, or a URL to a GPM server export endpoint
* e.g. `https://gpm.jolheiser.com/export`
* `get` - Get a list of packages
* e.g. `gpm get beaver survey toml homedir cli` to get all the modules needed for gpm itself (assuming the map resolves to the same packages)
* e.g. `gpm get beaver survey bbolt cli chi` to get all the modules needed for gpm itself (assuming the map resolves to the same packages)
* `server` - Start a gpm server
### Server
If GPM doesn't find a package locally, it can call out to a configurable gpm server to find a package there instead.
gpm will call out to a gpm server to find a package.
This makes it much simpler to have a central library of packages rather than exporting and importing between environments.
Want to run your own server? It's very easy! This CLI comes packaged with the server inside, simply run `gpm server` to start up a GPM server.
Put it behind your favorite reverse proxy and it's ready to go!
Remember to set a `--token`!
Put it behind your favorite reverse proxy, and it's ready to go!
## License
[MIT](LICENSE)

View File

@ -1,10 +1,13 @@
package cmd
import (
"context"
"regexp"
"strings"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -12,26 +15,30 @@ import (
)
var Add = cli.Command{
Name: "add",
Usage: "Add a package",
Name: "add",
Aliases: []string{"a"},
Usage: "Add a package",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite existing package without prompt",
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite existing package without prompt",
Destination: &flags.Force,
},
&cli.BoolFlag{
Name: "local",
Aliases: []string{"l"},
Usage: "local mode",
Destination: &flags.Local,
},
},
Before: localOrToken,
Action: doAdd,
}
var vPattern = regexp.MustCompile(`v\d+$`)
func doAdd(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
func doAdd(_ *cli.Context) error {
goGetQuestion := &survey.Input{
Message: "Package go-get import",
}
@ -58,16 +65,26 @@ func doAdd(ctx *cli.Context) error {
return err
}
pkg := config.Package{
pkg := gpm.Package{
Name: nameAnswer,
Import: goGetAnswer,
}
cfg.AddPackages(ctx.Bool("force"), pkg)
if err := cfg.Save(); err != nil {
return err
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return err
}
if err := db.PutPackage(pkg); err != nil {
return err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
if err := client.Add(context.Background(), pkg); err != nil {
return err
}
}
beaver.Infof("Added `%s` to local gpm.", nameAnswer)
beaver.Infof("Added %s", yellow.Format(nameAnswer))
return nil
}

View File

@ -1,18 +1,104 @@
package cmd
import (
"go.jolheiser.com/gpm/config"
"context"
"errors"
"os"
"path/filepath"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"go.jolheiser.com/gpm/router"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver/color"
)
func NewFlags(cfg *config.Config) []cli.Flag {
return []cli.Flag{
var yellow = color.FgYellow
func New() *cli.App {
app := cli.NewApp()
app.Name = "gpm"
app.Usage = "Go Package Manager"
app.Version = router.Version
app.Commands = []*cli.Command{
&Add,
&Get,
&List,
&Remove,
&Search,
&Server,
&Update,
}
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Usage: "gpm server to use",
Value: cfg.GPMURL,
Name: "server",
Aliases: []string{"s"},
Usage: "gpm server to use",
Value: gpm.DefaultServer,
EnvVars: []string{"GPM_SERVER"},
Destination: &flags.Server,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Usage: "gpm auth token to use",
DefaultText: "${GPM_TOKEN}",
EnvVars: []string{"GPM_TOKEN"},
Destination: &flags.Token,
},
&cli.StringFlag{
Name: "database",
Aliases: []string{"d"},
Usage: "path to gpm database for server",
Value: dbPath(),
DefaultText: "`${HOME}/gpm.db` or `${BINPATH}/gpm.db`",
EnvVars: []string{"GPM_DATABASE"},
Destination: &flags.Database,
},
}
return app
}
func dbPath() string {
fn := "gpm.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)
}
func localOrToken(_ *cli.Context) error {
if flags.Local && flags.Token == "" {
return errors.New("server interaaction requires --token")
}
return nil
}
func listPackages() ([]gpm.Package, error) {
var pkgs []gpm.Package
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return pkgs, err
}
pkgs, err = db.Packages()
if err != nil {
return pkgs, err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
info, err := client.Info(context.Background())
if err != nil {
return pkgs, err
}
pkgs = info.Packages
}
return pkgs, nil
}

View File

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

View File

@ -1,30 +0,0 @@
package cmd
import (
"fmt"
"go.jolheiser.com/gpm/config"
"github.com/urfave/cli/v2"
)
var Export = cli.Command{
Name: "export",
Usage: "Export JSON for local packages",
Action: doExport,
}
func doExport(_ *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
export, err := cfg.Export()
if err != nil {
return err
}
fmt.Println(export)
return nil
}

11
cmd/flags/flags.go 100644
View File

@ -0,0 +1,11 @@
package flags
var (
Server string
Token string
Database string
Local bool
Force bool
Port int
)

View File

@ -1,15 +1,13 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"context"
"os"
"os/exec"
"strings"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -17,27 +15,13 @@ import (
)
var Get = cli.Command{
Name: "get",
Usage: "Get package(s)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "ignore-local",
Usage: "Ignore local packages",
},
&cli.BoolFlag{
Name: "offline",
Usage: "Offline mode, return error instead of querying server",
},
},
Action: doGet,
Name: "get",
Aliases: []string{"g"},
Usage: "Get package(s)",
Action: doGet,
}
func doGet(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
pkgs := ctx.Args().Slice()
if len(pkgs) == 0 {
pkgsQuestion := &survey.Multiline{
@ -52,27 +36,16 @@ func doGet(ctx *cli.Context) error {
pkgs = strings.Split(pkgsAnswer, "\n")
}
local := cfg.Packages.Map()
for _, pkg := range pkgs {
var url string
if u, ok := local[pkg]; ok && !ctx.Bool("ignore-local") {
url = u.Import
} else if !ctx.Bool("offline") {
u, err := queryServer(ctx.String("url"), pkg)
if err != nil {
beaver.Error(err)
continue
}
url = u
}
if url == "" {
beaver.Errorf("no package found for `%s`", pkg)
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
for _, p := range pkgs {
pkg, err := client.Get(context.Background(), p)
if err != nil {
beaver.Error(err)
continue
}
beaver.Infof("getting `%s`...", pkg)
if err := goGet(url); err != nil {
if err := goGet(pkg.Import); err != nil {
beaver.Error(err)
}
}
@ -80,31 +53,6 @@ func doGet(ctx *cli.Context) error {
return nil
}
func queryServer(server, name string) (string, error) {
endpoint := fmt.Sprintf("%s/package/%s", server, name)
resp, err := http.Get(endpoint)
if err != nil {
return "", fmt.Errorf("could not query server at `%s`", endpoint)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("could not find server package for `%s`", name)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
var pkg config.Package
if err := json.Unmarshal(body, &pkg); err != nil {
return "", err
}
return pkg.Import, nil
}
func goGet(url string) error {
cmd := exec.Command("go", "get", url)
cmd.Stdout = os.Stdout

View File

@ -1,79 +0,0 @@
package cmd
import (
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strings"
"go.jolheiser.com/gpm/config"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Import = cli.Command{
Name: "import",
Usage: "Import JSON for local packages",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite any existing packages without prompt",
},
},
Action: doImport,
}
func doImport(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
if ctx.NArg() == 0 {
return errors.New("must point to either a JSON file or gpm server export endpoint")
}
arg := ctx.Args().First()
isJSON := strings.HasSuffix(arg, ".json")
isHTTP := strings.HasPrefix(arg, "http")
if !isJSON && !isHTTP {
return errors.New("must point to either a JSON file or gpm server export endpoint")
}
var data []byte
if isJSON {
data, err = os.ReadFile(arg)
if err != nil {
return err
}
} else if isHTTP {
resp, err := http.Get(arg)
if err != nil {
return err
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
}
var importPkgs []config.Package
if err := json.Unmarshal(data, &importPkgs); err != nil {
return err
}
cfg.AddPackages(ctx.Bool("force"), importPkgs...)
if err := cfg.Save(); err != nil {
return err
}
beaver.Info("Import complete")
return nil
}

View File

@ -1,27 +1,29 @@
package cmd
import (
"go.jolheiser.com/gpm/config"
"fmt"
"os"
"text/tabwriter"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var List = cli.Command{
Name: "list",
Aliases: []string{"l"},
Aliases: []string{"ls", "l"},
Usage: "List local packages",
Action: doList,
}
func doList(_ *cli.Context) error {
cfg, err := config.Load()
pkgs, err := listPackages()
if err != nil {
return err
}
for _, pkg := range cfg.Packages {
beaver.Infof("%s -> %s", pkg.Name, pkg.Import)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
for _, pkg := range pkgs {
s := fmt.Sprintf("%s\t%s\n", pkg.Name, pkg.Import)
_, _ = w.Write([]byte(s))
}
return nil
return w.Flush()
}

View File

@ -1,10 +1,11 @@
package cmd
import (
"fmt"
"strings"
"context"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -15,49 +16,53 @@ var Remove = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove package(s)",
Before: localOrToken,
Action: doRemove,
}
func doRemove(_ *cli.Context) error {
cfg, err := config.Load()
pkgs, err := listPackages()
if err != nil {
return err
}
pkgQuestion := &survey.Input{
Message: "Package name",
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]gpm.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
var pkgAnswer string
if err := survey.AskOne(pkgQuestion, &pkgAnswer); err != nil {
pkgQuestion := &survey.Select{
Message: "Select package to remove",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
for idx, p := range cfg.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.Import),
Default: false,
}
var answer bool
pkg := gpm.Package{
Name: pkgName,
Import: pkgMap[pkgName].Import,
}
if err := survey.AskOne(confirm, &answer); err != nil {
return err
}
if answer {
cfg.Packages = append(cfg.Packages[:idx], cfg.Packages[idx+1:]...)
if err := cfg.Save(); err != nil {
return err
}
beaver.Infof("Removed `%s` from local gpm.", p.Name)
break
}
beaver.Infof("Did not remove `%s` from local gpm.", p.Name)
break
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return err
}
if err := db.RemovePackage(pkg.Name); err != nil {
return err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
if err := client.Remove(context.Background(), pkg); err != nil {
return err
}
}
beaver.Infof("Removed %s", yellow.Format(pkgName))
return nil
}

View File

@ -1,12 +1,7 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -17,35 +12,25 @@ var Search = cli.Command{
Name: "search",
Aliases: []string{"s"},
Usage: "Search packages",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "local",
Usage: "Search locally",
},
},
Action: doSearch,
Action: doSearch,
}
func doSearch(ctx *cli.Context) error {
cfg, err := config.Load()
func doSearch(_ *cli.Context) error {
pkgs, err := listPackages()
if err != nil {
return err
}
packageMap := cfg.Packages.Map()
packageSlice := cfg.Packages.Slice()
if !ctx.Bool("local") {
export, err := queryExport(ctx.String("url"))
if err != nil {
return err
}
packageMap = export.Map()
packageSlice = export.Slice()
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]gpm.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
q := &survey.MultiSelect{
Message: "Select packages",
Options: packageSlice,
Options: pkgSlice,
}
var a []string
@ -54,7 +39,7 @@ func doSearch(ctx *cli.Context) error {
}
for _, name := range a {
pkg, ok := packageMap[name]
pkg, ok := pkgMap[name]
if !ok {
beaver.Errorf("could not find package for `%s`", name)
continue
@ -67,23 +52,3 @@ func doSearch(ctx *cli.Context) error {
return nil
}
func queryExport(server string) (config.Packages, error) {
resp, err := http.Get(fmt.Sprintf("%s/export", server))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var importPkgs config.Packages
if err := json.Unmarshal(data, &importPkgs); err != nil {
return nil, err
}
return importPkgs, nil
}

View File

@ -1,10 +1,12 @@
package cmd
import (
"errors"
"fmt"
"net/http"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/router"
"github.com/urfave/cli/v2"
@ -12,27 +14,34 @@ import (
)
var Server = cli.Command{
Name: "server",
Usage: "Start the gpm server",
Name: "server",
Aliases: []string{"web"},
Usage: "Start the gpm server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "Port to run the gpm server on",
Value: "3333",
&cli.IntFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "Port to run the gpm server on",
Value: 3333,
EnvVars: []string{"GPM_PORT"},
Destination: &flags.Port,
},
},
Action: doServer,
}
func doServer(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
func doServer(_ *cli.Context) error {
if flags.Token == "" {
return errors.New("gpm server requires --token")
}
beaver.Infof("Running gpm server at http://localhost:%s", ctx.String("port"))
if err := http.ListenAndServe(fmt.Sprintf(":%s", ctx.String("port")), router.New(cfg)); err != nil {
db, err := database.Load(flags.Database)
if err != nil {
beaver.Fatalf("could not load database at %s: %v", flags.Database, err)
}
beaver.Infof("Running gpm server at http://localhost:%d", flags.Port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", flags.Port), router.New(flags.Token, db)); err != nil {
return err
}
return nil

86
cmd/update.go 100644
View File

@ -0,0 +1,86 @@
package cmd
import (
"context"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Update = cli.Command{
Name: "update",
Aliases: []string{"u"},
Usage: "Update a package",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "local",
Aliases: []string{"l"},
Usage: "local mode",
Destination: &flags.Local,
},
},
Before: localOrToken,
Action: doUpdate,
}
func doUpdate(_ *cli.Context) error {
pkgs, err := listPackages()
if err != nil {
return err
}
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]gpm.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
}
importQuestion := &survey.Input{
Message: "New import path",
Default: pkgMap[pkgName].Import,
}
var importPath string
if err := survey.AskOne(importQuestion, &importPath); err != nil {
return err
}
pkg := gpm.Package{
Name: pkgName,
Import: importPath,
}
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return err
}
if err := db.PutPackage(pkg); err != nil {
return err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
if err := client.Update(context.Background(), pkg); err != nil {
return err
}
}
beaver.Infof("Updated %s", yellow.Format(pkgName))
return nil
}

View File

@ -1,146 +0,0 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/pelletier/go-toml"
"go.jolheiser.com/beaver"
)
var Version = "develop"
type Config struct {
path string
GPMURL string `toml:"gpm-url" json:"gpm_url"`
Packages Packages `toml:"package" json:"packages"`
}
type Package struct {
Name string `toml:"name" json:"name"`
Import string `toml:"import" json:"import"`
}
type Packages []Package
func Load() (*Config, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not get user home dir: %v", err)
}
home = path.Join(home, ".gpm")
homeEnv := os.Getenv("GPM_HOME")
if homeEnv != "" {
home = homeEnv
}
configPath := path.Join(home, "gpm.toml")
configEnv := os.Getenv("GPM_CONFIG")
if configEnv != "" {
configPath = configEnv
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(configPath), os.ModePerm); err != nil {
return nil, fmt.Errorf("could not create gpm home: %v", err)
}
if _, err := os.Create(configPath); err != nil {
return nil, fmt.Errorf("could not create gpm config: %v", err)
}
}
var cfg Config
tree, err := toml.LoadFile(configPath)
if err != nil {
return nil, fmt.Errorf("could not decode gpm config: %v", err)
}
if err = tree.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("could not unmarshal config: %v", err)
}
dupe := make(map[string]bool)
for _, pkg := range cfg.Packages {
name := strings.ToLower(pkg.Name)
if ok := dupe[name]; ok {
return nil, fmt.Errorf("duplicate package for %s", pkg.Name)
}
dupe[name] = true
}
cfg.path = configPath
return &cfg, nil
}
func (c *Config) Save() error {
fi, err := os.Create(c.path)
if err != nil {
return err
}
defer fi.Close()
if err := toml.NewEncoder(fi).Encode(c); err != nil {
return err
}
return nil
}
func (c *Config) Export() (string, error) {
data, err := json.Marshal(c.Packages)
return string(data), err
}
func (p Packages) Slice() []string {
pkgs := make([]string, len(p))
for idx, pkg := range p {
pkgs[idx] = fmt.Sprintf("%s (%s)", pkg.Name, pkg.Import)
}
return pkgs
}
func (p Packages) Map() map[string]Package {
pkgs := make(map[string]Package)
for _, pkg := range p {
pkgs[pkg.Name] = pkg
}
return pkgs
}
func (c *Config) AddPackages(force bool, pkgs ...Package) {
for _, pkg := range pkgs {
for idx, p := range c.Packages {
if strings.EqualFold(p.Name, pkg.Name) {
if force {
c.Packages[idx] = pkg
break
}
forceQuestion := &survey.Confirm{
Message: fmt.Sprintf("Package `%s` (%s) already exists. Overwrite with `%s`?", p.Name, p.Import, p.Import),
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
}
c.Packages[idx] = pkg
break
}
}
c.Packages = append(c.Packages, pkg)
}
}

View File

@ -0,0 +1,79 @@
package database
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"go.jolheiser.com/gpm/go-gpm"
"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) (gpm.Package, error) {
var pkg gpm.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))
return nil
})
}
func (d *Database) Packages() (pkgs []gpm.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 gpm.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 gpm.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,87 @@
package database
import (
"os"
"path/filepath"
"testing"
"go.jolheiser.com/gpm/go-gpm"
)
var db *Database
func TestMain(m *testing.M) {
tmp, err := os.MkdirTemp(os.TempDir(), "gpm")
if err != nil {
panic(err)
}
dbPath := filepath.Join(tmp, "gpm.db")
db, err = Load(dbPath)
if err != nil {
panic(err)
}
code := m.Run()
// Cleanup
if err := os.RemoveAll(tmp); err != nil {
panic(err)
}
os.Exit(code)
}
func TestPackage(t *testing.T) {
// Does not exist
_, err := db.Package("test")
if err == nil {
t.Log("test package should not exist")
t.FailNow()
}
// Add
pkg := gpm.Package{
Name: "test",
Import: "gitea.com/test/testing",
}
err = db.PutPackage(pkg)
if err != nil {
t.Logf("could not put test package: %v\n", err)
t.FailNow()
}
// Update
pkg.Import = "gitea.com/testing/test"
err = db.PutPackage(pkg)
if err != nil {
t.Logf("could not put test package: %v\n", err)
t.FailNow()
}
// Check
p, err := db.Package("test")
if err != nil {
t.Logf("should find test package: %v\n", err)
t.FailNow()
}
if p.Import != pkg.Import {
t.Logf("test package did not match update:\n\texpected: %s\n\t got: %s\n", pkg.Import, p.Import)
t.FailNow()
}
// Remove
err = db.RemovePackage("test")
if err != nil {
t.Log("could not remove test package")
t.FailNow()
}
// Check
_, err = db.Package("test")
if err == nil {
t.Log("test package should not exist after being removed")
t.FailNow()
}
}

12
docker/Dockerfile 100644
View File

@ -0,0 +1,12 @@
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/gpm gpm
EXPOSE 3333
ENV GPM_TOKEN=""
ENTRYPOINT exec gpm --token $GPM_TOKEN server

View File

@ -0,0 +1,9 @@
version: "2"
services:
vanity:
image: jolheiser/gpm:latest
environment:
- GPM_TOKEN=<token>
restart: always
ports:
- "80:3333"

33
docs.go 100644
View File

@ -0,0 +1,33 @@
//+build docs
package main
import (
"os"
"strings"
"go.jolheiser.com/gpm/cmd"
)
func main() {
app := cmd.New()
md, err := app.ToMarkdown()
if err != nil {
panic(err)
}
// FIXME Why is this not fixed yet??
md = md[strings.Index(md, "#"):]
fi, err := os.Create("DOCS.md")
if err != nil {
panic(err)
}
if _, err := fi.WriteString(md); err != nil {
panic(err)
}
if err := fi.Close(); err != nil {
panic(err)
}
}

59
go-gpm/client.go 100644
View File

@ -0,0 +1,59 @@
package gpm
import (
"context"
"io"
"net/http"
"strings"
)
const (
DefaultServer = "https://gpm.jolheiser.com"
TokenHeader = "X-GPM-Token"
)
// Client is a gpm client
type Client struct {
token string
server string
http *http.Client
}
// New returns a new 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
}
// ClientOption is an option for a Client
type ClientOption func(*Client)
// WithHTTP sets the http.Client for a Client
func WithHTTP(client *http.Client) ClientOption {
return func(c *Client) {
c.http = client
}
}
// WithServer sets the gpm server for a Client
func WithServer(server string) ClientOption {
return func(c *Client) {
c.server = strings.TrimSuffix(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
}

3
go-gpm/go.mod 100644
View File

@ -0,0 +1,3 @@
module go.jolheiser.com/gpm/go-gpm
go 1.16

207
go-gpm/gpm_test.go 100644
View File

@ -0,0 +1,207 @@
package gpm
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
var (
server *httptest.Server
token = "TestingLibrary"
version = "GPMTest"
packages = []Package{
{
Name: "test1",
Import: "gitea.com/test/testing",
},
}
)
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",
Import: "gitea.com/test/testing",
}
pkg2 := Package{
Name: "test2",
Import: "gitea.com/testing/test",
}
// 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)
// Check package
checkGet(t, client, pkg2)
// 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 checkGet(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
_, err := client.Get(ctx, "test3")
if err == nil {
t.Log("should not be able to get invalid package")
t.Fail()
}
// Check valid package
p, err := client.Get(ctx, "test2")
if err != nil {
t.Logf("should not be able to get invalid package: %v\n", err)
t.Fail()
}
if p != pkg {
t.Log("valid package should match pkg")
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", Import: "gitea.com/invalid"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
pkg.Import = "gitea.com/tester/testing"
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.MethodGet:
resp := Info{
Version: version,
NumPackages: len(packages),
Packages: 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)
}

124
go-gpm/package.go 100644
View File

@ -0,0 +1,124 @@
package gpm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
// Package is a gpm package
type Package struct {
Name string `json:"name"`
Import string `json:"import"`
}
// Info is gpm information, such as version and list of packages
type Info struct {
Version string `json:"version"`
NumPackages int `json:"num_packages"`
Packages []Package `json:"packages"`
}
// Info gets Info from a gpm server
func (c *Client) Info(ctx context.Context) (Info, error) {
var info Info
resp, err := c.crud(ctx, Package{}, http.MethodGet)
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 gpm 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 gpm 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 gpm 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
}
// Get gets a Package from a server
func (c *Client) Get(ctx context.Context, name string) (Package, error) {
var pkg Package
uri := fmt.Sprintf("%s/%s", c.server, name)
req, err := c.newRequest(ctx, http.MethodGet, uri, nil)
if err != nil {
return pkg, err
}
resp, err := c.http.Do(req)
if err != nil {
return pkg, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return pkg, fmt.Errorf("package not found for %s", name)
}
if err := json.NewDecoder(resp.Body).Decode(&pkg); err != nil {
return pkg, err
}
return pkg, 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)
}

9
go.mod
View File

@ -2,18 +2,21 @@ module go.jolheiser.com/gpm
go 1.15
replace go.jolheiser.com/gpm/go-gpm => ./go-gpm
require (
github.com/AlecAivazis/survey/v2 v2.2.7
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/go-chi/chi v1.5.2
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/pelletier/go-toml v1.8.1
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.3.0
go.jolheiser.com/beaver v1.1.0
go.etcd.io/bbolt v1.3.5
go.jolheiser.com/beaver v1.1.1
go.jolheiser.com/gpm/go-gpm v0.0.0-00010101000000-000000000000
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac // indirect
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.5 // indirect
)

13
go.sum
View File

@ -25,8 +25,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -37,8 +35,10 @@ github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
go.jolheiser.com/beaver v1.1.0 h1:Igz73y+jJQoe8Uteewf14mOMnozGAo2vxjzyqU8v9kA=
go.jolheiser.com/beaver v1.1.0/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.jolheiser.com/beaver v1.1.1 h1:py8Zj3tjT52dUzsvnu97aiLj1fBJjDJiK6kHjKJejMQ=
go.jolheiser.com/beaver v1.1.1/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
@ -51,10 +51,11 @@ golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac h1:9glrpwtNjBYgRpb67AZJKHfzj1stG/8BL5H7In2oTC4=
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

25
main.go
View File

@ -4,35 +4,12 @@ import (
"os"
"go.jolheiser.com/gpm/cmd"
"go.jolheiser.com/gpm/config"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
func main() {
cfg, err := config.Load()
if err != nil {
beaver.Fatal(err)
}
app := cli.NewApp()
app.Name = "gpm"
app.Usage = "Go Package Manager"
app.Version = config.Version
app.Commands = []*cli.Command{
&cmd.Add,
&cmd.Remove,
&cmd.List,
&cmd.Get,
&cmd.Import,
&cmd.Export,
&cmd.Config,
&cmd.Server,
&cmd.Search,
}
app.Flags = cmd.NewFlags(cfg)
if err := app.Run(os.Args); err != nil {
if err := cmd.New().Run(os.Args); err != nil {
beaver.Error(err)
}
}

View File

@ -2,38 +2,47 @@ package router
import (
"encoding/json"
"io"
"net/http"
"time"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"go.jolheiser.com/beaver"
)
var cache map[string]config.Package
var Version = "develop"
func New(cfg *config.Config) *chi.Mux {
func New(token string, db *database.Database) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.RedirectSlashes)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Get("/", handleHome(cfg))
r.Get("/export", handleExport(cfg))
r.Get("/package/{name}", handlePackage)
cache = cfg.Packages.Map()
r.Get("/", handleHome(db))
r.Post("/", addUpdatePackage(db, token))
r.Patch("/", addUpdatePackage(db, token))
r.Delete("/", removePackage(db, token))
r.Get("/{name}", getPackage(db))
return r
}
func handleHome(cfg *config.Config) func(res http.ResponseWriter, _ *http.Request) {
func handleHome(db *database.Database) func(res http.ResponseWriter, _ *http.Request) {
return func(res http.ResponseWriter, _ *http.Request) {
status, err := json.Marshal(map[string]interface{}{
"version": config.Version,
"packages": len(cfg.Packages),
pkgs, err := db.Packages()
if err != nil {
beaver.Error(err)
return
}
status, err := json.Marshal(gpm.Info{
Version: Version,
NumPackages: len(pkgs),
Packages: pkgs,
})
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
@ -45,32 +54,98 @@ func handleHome(cfg *config.Config) func(res http.ResponseWriter, _ *http.Reques
}
}
func handleExport(cfg *config.Config) func(res http.ResponseWriter, _ *http.Request) {
return func(res http.ResponseWriter, _ *http.Request) {
export, err := cfg.Export()
func getPackage(db *database.Database) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
name := chi.URLParam(req, "name")
pkg, err := db.PackageJSON(name)
if err != nil {
beaver.Error(err)
return
}
_, _ = res.Write([]byte(export))
}
}
func handlePackage(res http.ResponseWriter, req *http.Request) {
name := chi.URLParam(req, "name")
if pkg, ok := cache[name]; ok {
data, err := json.Marshal(pkg)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
res.WriteHeader(http.StatusNotFound)
_, _ = res.Write([]byte("{}"))
return
}
_, _ = res.Write(data)
return
}
res.WriteHeader(http.StatusNotFound)
_, _ = res.Write([]byte("{}"))
_, _ = res.Write(pkg)
}
}
func addUpdatePackage(db *database.Database, token string) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
if req.Header.Get(gpm.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 gpm.Package
if err := json.Unmarshal(data, &pkg); err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
exists, err := db.PackageJSON(pkg.Name)
if err != nil {
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(gpm.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 gpm.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,166 @@
package router
import (
"context"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"go.jolheiser.com/beaver"
)
var (
server *httptest.Server
token = "TestingRouter"
)
// NOTE: The router test is more or less a copy/paste from go-gpm
// 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(), "gpm")
if err != nil {
panic(err)
}
dbPath := filepath.Join(tmp, "gpm.db")
db, err := database.Load(dbPath)
if err != nil {
beaver.Fatalf("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 := gpm.New("", gpm.WithServer(server.URL))
// Info
checkInfo(t, client, 0)
pkg1 := gpm.Package{
Name: "test1",
Import: "gitea.com/test/testing",
}
pkg2 := gpm.Package{
Name: "test2",
Import: "gitea.com/testing/test",
}
// Add (without token)
if err := client.Add(ctx, pkg1); err == nil {
t.Log("adding without token should fail")
t.Fail()
}
// Add (with token)
client = gpm.New(token, gpm.WithServer(server.URL))
checkAdd(t, client, pkg1, pkg2)
// Info (after second package)
checkInfo(t, client, 2)
// Check package
checkGet(t, client, pkg2)
// Update package
checkUpdate(t, client, pkg1)
// Remove
checkRemove(t, client, pkg1)
// Info (final)
checkInfo(t, client, 1)
}
func checkInfo(t *testing.T, client *gpm.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 checkGet(t *testing.T, client *gpm.Client, pkg gpm.Package) {
ctx := context.Background()
_, err := client.Get(ctx, "test3")
if err == nil {
t.Log("should not be able to get invalid package")
t.Fail()
}
// Check valid package
p, err := client.Get(ctx, "test2")
if err != nil {
t.Logf("should not be able to get invalid package: %v\n", err)
t.Fail()
}
if p != pkg {
t.Log("valid package should match pkg")
t.Fail()
}
}
func checkAdd(t *testing.T, client *gpm.Client, pkg1, pkg2 gpm.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 *gpm.Client, pkg gpm.Package) {
ctx := context.Background()
// Update invalid package
if err := client.Update(ctx, gpm.Package{Name: "test4", Import: "gitea.com/invalid"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
pkg.Import = "gitea.com/tester/testing"
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 *gpm.Client, pkg gpm.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()
}
}