diff --git a/Earthfile b/Earthfile index 3ad4128..6bd3e59 100644 --- a/Earthfile +++ b/Earthfile @@ -1,14 +1,10 @@ # To lint, install Earthly and run `earth +lint` # This ensures the usage of the same version of golangci-lint -FROM golangci/golangci-lint:v1.37 +FROM golangci/golangci-lint:v1.31 WORKDIR /gpm -lint-cli: +lint: COPY . . - RUN golangci-lint run - -lint-lib: - COPY ./go-gpm . RUN golangci-lint run \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 433f7db..0000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -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. diff --git a/Makefile b/Makefile index 5cdd052..64cc957 100644 --- a/Makefile +++ b/Makefile @@ -3,39 +3,12 @@ VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//') .PHONY: build build: - $(GO) build -ldflags '-s -w -X "go.jolheiser.com/gpm/router.Version=$(VERSION)"' - -.PHONY: lint -lint: - earth +lint-cli - earth +lint-lib + $(GO) build -ldflags '-s -w -X "go.jolheiser.com/gpm/config.Version=$(VERSION)"' .PHONY: fmt -fmt: fmt-cli fmt-lib - -.PHONY: test -test: test-cli test-lib - -.PHONY: fmt-cli -fmt-cli: +fmt: $(GO) fmt ./... -.PHONY: test-cli -test-cli: +.PHONY: test +test: $(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 \ No newline at end of file diff --git a/README.md b/README.md index 926d6fc..3b27702 100644 --- a/README.md +++ b/README.md @@ -17,21 +17,18 @@ 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 bbolt cli chi` to get all the modules needed for gpm itself (assuming the map resolves to the same 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) * `server` - Start a gpm server ### Server -gpm will call out to a gpm server to find a package. +If GPM doesn't find a package locally, it can call out to a configurable gpm server to find a package there instead. 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. -Remember to set a `--token`! -Put it behind your favorite reverse proxy, and it's ready to go! - - -## License - -[MIT](LICENSE) \ No newline at end of file +Put it behind your favorite reverse proxy and it's ready to go! diff --git a/cmd/add.go b/cmd/add.go index 8e6cd0a..2ee2e71 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -1,13 +1,10 @@ package cmd import ( - "context" "regexp" "strings" - "go.jolheiser.com/gpm/cmd/flags" - "go.jolheiser.com/gpm/database" - "go.jolheiser.com/gpm/go-gpm" + "go.jolheiser.com/gpm/config" "github.com/AlecAivazis/survey/v2" "github.com/urfave/cli/v2" @@ -15,30 +12,26 @@ import ( ) var Add = cli.Command{ - Name: "add", - Aliases: []string{"a"}, - Usage: "Add a package", + Name: "add", + Usage: "Add a package", Flags: []cli.Flag{ &cli.BoolFlag{ - 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, + Name: "force", + Aliases: []string{"f"}, + Usage: "Overwrite existing package without prompt", }, }, - Before: localOrToken, Action: doAdd, } var vPattern = regexp.MustCompile(`v\d+$`) -func doAdd(_ *cli.Context) error { +func doAdd(ctx *cli.Context) error { + cfg, err := config.Load() + if err != nil { + return err + } + goGetQuestion := &survey.Input{ Message: "Package go-get import", } @@ -65,26 +58,16 @@ func doAdd(_ *cli.Context) error { return err } - pkg := gpm.Package{ + pkg := config.Package{ Name: nameAnswer, Import: goGetAnswer, } + cfg.AddPackages(ctx.Bool("force"), pkg) - 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 - } + if err := cfg.Save(); err != nil { + return err } - beaver.Infof("Added %s", yellow.Format(nameAnswer)) + beaver.Infof("Added `%s` to local gpm.", nameAnswer) return nil } diff --git a/cmd/cmd.go b/cmd/cmd.go index 6c99021..b574758 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,104 +1,18 @@ package cmd import ( - "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" + "go.jolheiser.com/gpm/config" "github.com/urfave/cli/v2" - "go.jolheiser.com/beaver/color" ) -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{ +func NewFlags(cfg *config.Config) []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ - 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, + Name: "url", + Aliases: []string{"u"}, + Usage: "gpm server to use", + Value: cfg.GPMURL, }, } - 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 } diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..04be274 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,41 @@ +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 +} diff --git a/cmd/export.go b/cmd/export.go new file mode 100644 index 0000000..c699a63 --- /dev/null +++ b/cmd/export.go @@ -0,0 +1,30 @@ +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 +} diff --git a/cmd/flags/flags.go b/cmd/flags/flags.go deleted file mode 100644 index e4a4f67..0000000 --- a/cmd/flags/flags.go +++ /dev/null @@ -1,11 +0,0 @@ -package flags - -var ( - Server string - Token string - Database string - - Local bool - Force bool - Port int -) diff --git a/cmd/get.go b/cmd/get.go index ba6e39e..95e9ee6 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -1,13 +1,15 @@ package cmd import ( - "context" + "encoding/json" + "fmt" + "io" + "net/http" "os" "os/exec" "strings" - "go.jolheiser.com/gpm/cmd/flags" - "go.jolheiser.com/gpm/go-gpm" + "go.jolheiser.com/gpm/config" "github.com/AlecAivazis/survey/v2" "github.com/urfave/cli/v2" @@ -15,13 +17,27 @@ import ( ) var Get = cli.Command{ - Name: "get", - Aliases: []string{"g"}, - Usage: "Get package(s)", - Action: doGet, + 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, } 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{ @@ -36,16 +52,27 @@ func doGet(ctx *cli.Context) error { pkgs = strings.Split(pkgsAnswer, "\n") } - 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) + 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) continue } beaver.Infof("getting `%s`...", pkg) - if err := goGet(pkg.Import); err != nil { + if err := goGet(url); err != nil { beaver.Error(err) } } @@ -53,6 +80,31 @@ 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 diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 0000000..5a40e9b --- /dev/null +++ b/cmd/import.go @@ -0,0 +1,79 @@ +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 +} diff --git a/cmd/list.go b/cmd/list.go index 9d53d85..bdf825b 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,29 +1,27 @@ package cmd import ( - "fmt" - "os" - "text/tabwriter" + "go.jolheiser.com/gpm/config" "github.com/urfave/cli/v2" + "go.jolheiser.com/beaver" ) var List = cli.Command{ Name: "list", - Aliases: []string{"ls", "l"}, + Aliases: []string{"l"}, Usage: "List local packages", Action: doList, } func doList(_ *cli.Context) error { - pkgs, err := listPackages() + cfg, err := config.Load() if err != nil { return err } - 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)) + + for _, pkg := range cfg.Packages { + beaver.Infof("%s -> %s", pkg.Name, pkg.Import) } - return w.Flush() + return nil } diff --git a/cmd/remove.go b/cmd/remove.go index ad15807..ea38461 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -1,11 +1,10 @@ package cmd import ( - "context" + "fmt" + "strings" - "go.jolheiser.com/gpm/cmd/flags" - "go.jolheiser.com/gpm/database" - "go.jolheiser.com/gpm/go-gpm" + "go.jolheiser.com/gpm/config" "github.com/AlecAivazis/survey/v2" "github.com/urfave/cli/v2" @@ -16,53 +15,49 @@ var Remove = cli.Command{ Name: "remove", Aliases: []string{"rm"}, Usage: "Remove package(s)", - Before: localOrToken, Action: doRemove, } func doRemove(_ *cli.Context) error { - pkgs, err := listPackages() + cfg, err := config.Load() 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.Input{ + Message: "Package name", } + var pkgAnswer string - pkgQuestion := &survey.Select{ - Message: "Select package to remove", - Options: pkgSlice, - } - - var pkgName string - if err := survey.AskOne(pkgQuestion, &pkgName); err != nil { + if err := survey.AskOne(pkgQuestion, &pkgAnswer); err != nil { return err } - pkg := gpm.Package{ - Name: pkgName, - Import: pkgMap[pkgName].Import, - } + 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 - 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 + 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 } } - beaver.Infof("Removed %s", yellow.Format(pkgName)) return nil } diff --git a/cmd/search.go b/cmd/search.go index 7aef35d..b07d490 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -1,7 +1,12 @@ package cmd import ( - "go.jolheiser.com/gpm/go-gpm" + "encoding/json" + "fmt" + "io" + "net/http" + + "go.jolheiser.com/gpm/config" "github.com/AlecAivazis/survey/v2" "github.com/urfave/cli/v2" @@ -12,25 +17,35 @@ var Search = cli.Command{ Name: "search", Aliases: []string{"s"}, Usage: "Search packages", - Action: doSearch, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "local", + Usage: "Search locally", + }, + }, + Action: doSearch, } -func doSearch(_ *cli.Context) error { - pkgs, err := listPackages() +func doSearch(ctx *cli.Context) error { + cfg, err := config.Load() 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 + 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() } q := &survey.MultiSelect{ Message: "Select packages", - Options: pkgSlice, + Options: packageSlice, } var a []string @@ -39,7 +54,7 @@ func doSearch(_ *cli.Context) error { } for _, name := range a { - pkg, ok := pkgMap[name] + pkg, ok := packageMap[name] if !ok { beaver.Errorf("could not find package for `%s`", name) continue @@ -52,3 +67,23 @@ func doSearch(_ *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 +} diff --git a/cmd/server.go b/cmd/server.go index 9931fbc..f592e32 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,12 +1,10 @@ package cmd import ( - "errors" "fmt" "net/http" - "go.jolheiser.com/gpm/cmd/flags" - "go.jolheiser.com/gpm/database" + "go.jolheiser.com/gpm/config" "go.jolheiser.com/gpm/router" "github.com/urfave/cli/v2" @@ -14,34 +12,27 @@ import ( ) var Server = cli.Command{ - Name: "server", - Aliases: []string{"web"}, - Usage: "Start the gpm server", + Name: "server", + Usage: "Start the gpm server", Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "port", - Aliases: []string{"p"}, - Usage: "Port to run the gpm server on", - Value: 3333, - EnvVars: []string{"GPM_PORT"}, - Destination: &flags.Port, + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Usage: "Port to run the gpm server on", + Value: "3333", }, }, Action: doServer, } -func doServer(_ *cli.Context) error { - if flags.Token == "" { - return errors.New("gpm server requires --token") - } - - db, err := database.Load(flags.Database) +func doServer(ctx *cli.Context) error { + cfg, err := config.Load() if err != nil { - beaver.Fatalf("could not load database at %s: %v", flags.Database, err) + return 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 { + 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 { return err } return nil diff --git a/cmd/update.go b/cmd/update.go deleted file mode 100644 index 690e235..0000000 --- a/cmd/update.go +++ /dev/null @@ -1,86 +0,0 @@ -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 -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a3d6ef5 --- /dev/null +++ b/config/config.go @@ -0,0 +1,146 @@ +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) + } +} diff --git a/database/database.go b/database/database.go deleted file mode 100644 index d7827d0..0000000 --- a/database/database.go +++ /dev/null @@ -1,79 +0,0 @@ -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)) - }) -} diff --git a/database/database_test.go b/database/database_test.go deleted file mode 100644 index b6e1f92..0000000 --- a/database/database_test.go +++ /dev/null @@ -1,87 +0,0 @@ -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() - } -} diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 5431a7b..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -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 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 394e21f..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: "2" -services: - vanity: - image: jolheiser/gpm:latest - environment: - - GPM_TOKEN= - restart: always - ports: - - "80:3333" \ No newline at end of file diff --git a/docs.go b/docs.go deleted file mode 100644 index a2a6fd8..0000000 --- a/docs.go +++ /dev/null @@ -1,33 +0,0 @@ -//+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) - } -} diff --git a/go-gpm/client.go b/go-gpm/client.go deleted file mode 100644 index bfcc334..0000000 --- a/go-gpm/client.go +++ /dev/null @@ -1,59 +0,0 @@ -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 -} diff --git a/go-gpm/go.mod b/go-gpm/go.mod deleted file mode 100644 index a2558d4..0000000 --- a/go-gpm/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module go.jolheiser.com/gpm/go-gpm - -go 1.16 diff --git a/go-gpm/gpm_test.go b/go-gpm/gpm_test.go deleted file mode 100644 index 925a0bc..0000000 --- a/go-gpm/gpm_test.go +++ /dev/null @@ -1,207 +0,0 @@ -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) -} diff --git a/go-gpm/package.go b/go-gpm/package.go deleted file mode 100644 index d93df2e..0000000 --- a/go-gpm/package.go +++ /dev/null @@ -1,124 +0,0 @@ -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) -} diff --git a/go.mod b/go.mod index 0715a09..48d4374 100644 --- a/go.mod +++ b/go.mod @@ -2,21 +2,18 @@ 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.etcd.io/bbolt v1.3.5 - go.jolheiser.com/beaver v1.1.1 - go.jolheiser.com/gpm/go-gpm v0.0.0-00010101000000-000000000000 + go.jolheiser.com/beaver v1.1.0 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect - golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 // indirect + golang.org/x/sys v0.0.0-20210216224549-f992740a1bac // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect ) diff --git a/go.sum b/go.sum index 40a724e..7b557ae 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ 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= @@ -35,10 +37,8 @@ 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.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= +go.jolheiser.com/beaver v1.1.0 h1:Igz73y+jJQoe8Uteewf14mOMnozGAo2vxjzyqU8v9kA= +go.jolheiser.com/beaver v1.1.0/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,11 +51,10 @@ 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-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/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/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= diff --git a/main.go b/main.go index b0166e6..4225082 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,35 @@ import ( "os" "go.jolheiser.com/gpm/cmd" + "go.jolheiser.com/gpm/config" + "github.com/urfave/cli/v2" "go.jolheiser.com/beaver" ) func main() { - if err := cmd.New().Run(os.Args); err != nil { + 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 { beaver.Error(err) } } diff --git a/router/router.go b/router/router.go index a6de33f..3697034 100644 --- a/router/router.go +++ b/router/router.go @@ -2,47 +2,38 @@ package router import ( "encoding/json" - "io" "net/http" "time" - "go.jolheiser.com/gpm/database" - "go.jolheiser.com/gpm/go-gpm" + "go.jolheiser.com/gpm/config" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "go.jolheiser.com/beaver" ) -var Version = "develop" +var cache map[string]config.Package -func New(token string, db *database.Database) *chi.Mux { +func New(cfg *config.Config) *chi.Mux { r := chi.NewRouter() r.Use(middleware.RedirectSlashes) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(30 * time.Second)) - r.Get("/", handleHome(db)) - r.Post("/", addUpdatePackage(db, token)) - r.Patch("/", addUpdatePackage(db, token)) - r.Delete("/", removePackage(db, token)) - r.Get("/{name}", getPackage(db)) + r.Get("/", handleHome(cfg)) + r.Get("/export", handleExport(cfg)) + r.Get("/package/{name}", handlePackage) + + cache = cfg.Packages.Map() return r } -func handleHome(db *database.Database) func(res http.ResponseWriter, _ *http.Request) { +func handleHome(cfg *config.Config) func(res http.ResponseWriter, _ *http.Request) { return func(res http.ResponseWriter, _ *http.Request) { - pkgs, err := db.Packages() - if err != nil { - beaver.Error(err) - return - } - - status, err := json.Marshal(gpm.Info{ - Version: Version, - NumPackages: len(pkgs), - Packages: pkgs, + status, err := json.Marshal(map[string]interface{}{ + "version": config.Version, + "packages": len(cfg.Packages), }) if err != nil { res.WriteHeader(http.StatusInternalServerError) @@ -54,98 +45,32 @@ func handleHome(db *database.Database) func(res http.ResponseWriter, _ *http.Req } } -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) +func handleExport(cfg *config.Config) func(res http.ResponseWriter, _ *http.Request) { + return func(res http.ResponseWriter, _ *http.Request) { + export, err := cfg.Export() if err != nil { - res.WriteHeader(http.StatusNotFound) + 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.Write([]byte("{}")) return } - - _, _ = 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) + _, _ = res.Write(data) + return } + + res.WriteHeader(http.StatusNotFound) + _, _ = res.Write([]byte("{}")) } diff --git a/router/router_test.go b/router/router_test.go deleted file mode 100644 index ff34fb0..0000000 --- a/router/router_test.go +++ /dev/null @@ -1,166 +0,0 @@ -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() - } -}