diff --git a/cmd/cli/commands/add/add.go b/cmd/cli/commands/add/add.go new file mode 100644 index 0000000..0f96861 --- /dev/null +++ b/cmd/cli/commands/add/add.go @@ -0,0 +1,85 @@ +package add + +import ( + "context" + customflag "downloadhub/cmd/cli/flag" + "downloadhub/pkg/data" + "flag" + "fmt" + "os" + + "github.com/google/subcommands" + "github.com/google/uuid" +) + +type ( + AddCmd struct { + slug string + description string + version string + iconURL string + out string + screenshotURLs customflag.Array + } +) + +func (*AddCmd) Name() string { return "add" } +func (*AddCmd) Synopsis() string { return "add an entry" } +func (*AddCmd) Usage() string { + return `Usage: ./cli add [OPTIONS] NAME + +Options: +` +} + +func (p *AddCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&p.slug, "slug", "", "") + f.StringVar(&p.description, "description", "", "") + f.StringVar(&p.version, "version", "0.0.0", "") + f.StringVar(&p.iconURL, "icon", "", "an url or a path to the icon") + f.StringVar(&p.out, "out", "./config.json", "path to the configuration file") + f.Var(&p.screenshotURLs, "screenshot", "an url or a path to a screenshot file") +} + +func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if len(p.slug) == 0 { + p.slug = uuid.NewString() + } + + if len(p.screenshotURLs) == 0 { + p.screenshotURLs = make(customflag.Array, 0) + } + + if f.NArg() == 0 { + fmt.Fprintln(os.Stderr, "error: name cannot be empty") + return subcommands.ExitUsageError + } + + if f.NArg() > 1 { + fmt.Fprintln(os.Stderr, "error: this command cannot take more than 1 argument") + return subcommands.ExitUsageError + } + + d, err := data.Load(p.out) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + d.Softwares = append(d.Softwares, data.Software{ + Slug: p.slug, + Name: f.Arg(0), + Description: p.description, + Version: p.version, + IconURL: p.iconURL, + ScreenshotURLs: p.screenshotURLs, + DownloadLinks: make([]data.DownloadLink, 0), + }) + + if err := data.Save(d, p.out); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + return subcommands.ExitSuccess +} diff --git a/cmd/cli/commands/edit/edit.go b/cmd/cli/commands/edit/edit.go new file mode 100644 index 0000000..3d505a5 --- /dev/null +++ b/cmd/cli/commands/edit/edit.go @@ -0,0 +1,99 @@ +package edit + +import ( + "context" + "downloadhub/pkg/data" + "flag" + "fmt" + "os" + + "github.com/google/subcommands" +) + +type ( + EditCmd struct { + slug string + name string + description string + version string + iconURL string + out string + } +) + +func (*EditCmd) Name() string { return "edit" } +func (*EditCmd) Synopsis() string { return "edit an entry" } +func (*EditCmd) Usage() string { + return `Usage: ./cli edit [OPTIONS] SLUG + +Options: +` +} + +func (p *EditCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&p.slug, "slug", "", "") + f.StringVar(&p.name, "name", "", "") + f.StringVar(&p.description, "description", "", "") + f.StringVar(&p.version, "version", "0.0.0", "") + f.StringVar(&p.iconURL, "icon", "", "an url or a path to the icon") + f.StringVar(&p.out, "out", "./config.json", "path to the configuration file") +} + +func (p *EditCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if f.NArg() == 0 { + fmt.Fprintln(os.Stderr, "error: slug cannot be empty") + return subcommands.ExitUsageError + } + + if f.NArg() > 1 { + fmt.Fprintln(os.Stderr, "error: this command cannot take more than 1 argument") + return subcommands.ExitUsageError + } + + d, err := data.Load(p.out) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + var found bool + for i, soft := range d.Softwares { + if soft.Slug == f.Arg(0) { + if len(p.slug) > 0 { + soft.Slug = p.slug + } + + if len(p.name) > 0 { + soft.Name = p.name + } + + if len(p.description) > 0 { + soft.Description = p.description + } + + if len(p.version) > 0 { + soft.Version = p.version + } + + if len(p.iconURL) > 0 { + soft.IconURL = p.iconURL + } + + d.Softwares[i] = soft + found = true + break + } + } + + if !found { + fmt.Fprintf(os.Stderr, "error: slug '%s' cannot be found\n", f.Arg(0)) + return subcommands.ExitFailure + } + + if err := data.Save(d, p.out); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + return subcommands.ExitSuccess +} diff --git a/cmd/cli/commands/link/link.go b/cmd/cli/commands/link/link.go new file mode 100644 index 0000000..3afacce --- /dev/null +++ b/cmd/cli/commands/link/link.go @@ -0,0 +1,196 @@ +package link + +import ( + "context" + "downloadhub/pkg/data" + "flag" + "fmt" + "os" + "strings" + + "github.com/google/subcommands" +) + +type ( + LinkCmd struct { + os string + arch string + out string + } +) + +func (*LinkCmd) Name() string { return "link" } +func (*LinkCmd) Synopsis() string { return "add/edit/remove a download link" } +func (*LinkCmd) Usage() string { + return `Usage: ./cli link ACTION LINK SLUG + +Actions: add, edit, rm + +Options: +` +} + +func (p *LinkCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&p.os, "os", "", "set the operating system") + f.StringVar(&p.arch, "architecture", "", "set the instruction set") + f.StringVar(&p.out, "out", "./config.json", "path to the configuration file") +} + +func (p *LinkCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if f.NArg() != 3 { + fmt.Fprintln(os.Stderr, "error: bad usage") + return subcommands.ExitUsageError + } + + switch strings.ToLower(f.Arg(0)) { + case "add": + return p.add(f.Arg(1), f.Arg(2)) + case "edit": + return p.edit(f.Arg(1), f.Arg(2)) + case "rm": + return p.rm(f.Arg(1), f.Arg(2)) + default: + { + fmt.Fprintln(os.Stderr, "error: unknown command") + return subcommands.ExitUsageError + } + } +} + +func (p *LinkCmd) add(link, slug string) subcommands.ExitStatus { + d, err := data.Load(p.out) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + var found bool + for i, soft := range d.Softwares { + if soft.Slug == slug { + if exists(link, soft) { + fmt.Fprintln(os.Stderr, "error: link already exists") + return subcommands.ExitFailure + } + soft.DownloadLinks = append(soft.DownloadLinks, data.DownloadLink{ + OS: p.os, + Arch: p.arch, + URL: link, + }) + d.Softwares[i] = soft + found = true + break + } + } + + if !found { + fmt.Fprintf(os.Stderr, "error: slug '%s' cannot be found\n", slug) + return subcommands.ExitFailure + } + + if err := data.Save(d, p.out); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +func (p *LinkCmd) edit(link, slug string) subcommands.ExitStatus { + d, err := data.Load(p.out) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + var foundSlug, foundLink bool + for i, soft := range d.Softwares { + if soft.Slug == slug { + for y, l := range soft.DownloadLinks { + if l.URL == link { + if len(p.os) > 0 { + l.OS = p.os + } + + if len(p.os) > 0 { + l.Arch = p.arch + } + + soft.DownloadLinks[y] = l + foundLink = true + break + } + } + d.Softwares[i] = soft + foundSlug = true + break + } + } + + if !foundSlug { + fmt.Fprintf(os.Stderr, "error: slug '%s' cannot be found\n", slug) + return subcommands.ExitFailure + } + + if !foundLink { + fmt.Fprintf(os.Stderr, "error: link '%s' cannot be found\n", link) + return subcommands.ExitFailure + } + + if err := data.Save(d, p.out); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +func (p *LinkCmd) rm(link, slug string) subcommands.ExitStatus { + d, err := data.Load(p.out) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + + var foundSlug, foundLink bool + for i, soft := range d.Softwares { + if soft.Slug == slug { + var index int + for y, l := range soft.DownloadLinks { + if l.URL == link { + index = y + foundLink = true + break + } + } + if foundLink { + soft.DownloadLinks = append(soft.DownloadLinks[:index], soft.DownloadLinks[index+1:]...) + } + d.Softwares[i] = soft + foundSlug = true + break + } + } + + if !foundSlug { + fmt.Fprintf(os.Stderr, "error: slug '%s' cannot be found\n", slug) + return subcommands.ExitFailure + } + + if !foundLink { + fmt.Fprintf(os.Stderr, "error: link '%s' cannot be found\n", link) + return subcommands.ExitFailure + } + + if err := data.Save(d, p.out); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} + +func exists(link string, soft data.Software) bool { + for _, l := range soft.DownloadLinks { + if l.URL == link { + return true + } + } + return false +} diff --git a/cmd/cli/commands/version/version.go b/cmd/cli/commands/version/version.go new file mode 100644 index 0000000..102169d --- /dev/null +++ b/cmd/cli/commands/version/version.go @@ -0,0 +1,39 @@ +package version + +import ( + "context" + "downloadhub/pkg/constants" + "flag" + "fmt" + "runtime" + + "github.com/google/subcommands" +) + +type ( + VersionCmd struct { + } +) + +func (*VersionCmd) Name() string { return "version" } +func (*VersionCmd) Synopsis() string { return "show version and system information" } +func (*VersionCmd) Usage() string { + return `Usage: ./cli version + +Print the version of the software + +Options: +` +} + +func (p *VersionCmd) SetFlags(f *flag.FlagSet) { +} + +func (p *VersionCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + fmt.Println("Client: downloadhub cli") + fmt.Println(" Version: " + constants.Version) + fmt.Println(" Go version: " + runtime.Version()) + fmt.Println(" OS/Arch: " + runtime.GOOS + "/" + runtime.GOARCH) + + return subcommands.ExitSuccess +} diff --git a/cmd/cli/flag/flag.go b/cmd/cli/flag/flag.go new file mode 100644 index 0000000..80cd8cf --- /dev/null +++ b/cmd/cli/flag/flag.go @@ -0,0 +1,18 @@ +package flag + +import ( + "strings" +) + +type Array []string + +// String is an implementation of the flag.Value interface +func (i *Array) String() string { + return strings.Join(*i, ", ") +} + +// Set is an implementation of the flag.Value interface +func (i *Array) Set(value string) error { + *i = append(*i, value) + return nil +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..54aee35 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "downloadhub/cmd/cli/commands/add" + "downloadhub/cmd/cli/commands/edit" + "downloadhub/cmd/cli/commands/link" + "downloadhub/cmd/cli/commands/version" + "flag" + "os" + + "github.com/google/subcommands" +) + +func main() { + subcommands.Register(subcommands.HelpCommand(), "help") + subcommands.Register(subcommands.FlagsCommand(), "help") + subcommands.Register(subcommands.CommandsCommand(), "help") + subcommands.Register(&version.VersionCmd{}, "help") + + subcommands.Register(&add.AddCmd{}, "management") + subcommands.Register(&edit.EditCmd{}, "management") + subcommands.Register(&link.LinkCmd{}, "management") + + flag.Parse() + ctx := context.Background() + + os.Exit(int(subcommands.Execute(ctx))) +} diff --git a/api/api.go b/cmd/server/api/api.go similarity index 98% rename from api/api.go rename to cmd/server/api/api.go index 0030d71..040ef6f 100644 --- a/api/api.go +++ b/cmd/server/api/api.go @@ -1,7 +1,7 @@ package api import ( - "downloadhub/data" + "downloadhub/pkg/data" _ "embed" "fmt" "html/template" diff --git a/api/templates/description.html b/cmd/server/api/templates/description.html similarity index 100% rename from api/templates/description.html rename to cmd/server/api/templates/description.html diff --git a/api/templates/index.html b/cmd/server/api/templates/index.html similarity index 100% rename from api/templates/index.html rename to cmd/server/api/templates/index.html diff --git a/main.go b/cmd/server/main.go similarity index 90% rename from main.go rename to cmd/server/main.go index da26221..78dba8d 100644 --- a/main.go +++ b/cmd/server/main.go @@ -1,8 +1,8 @@ package main import ( - "downloadhub/api" - "downloadhub/data" + "downloadhub/cmd/server/api" + "downloadhub/pkg/data" "flag" "fmt" "log/slog" diff --git a/go.mod b/go.mod index ef4213f..e183269 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.22 require ( github.com/go-chi/chi/v5 v5.1.0 + github.com/google/subcommands v1.2.0 github.com/google/uuid v1.6.0 ) diff --git a/go.sum b/go.sum index 67edbd5..9be2b9f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 0000000..17ce1fc --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,5 @@ +package constants + +const ( + Version string = "0.0.1" +) diff --git a/data/data.go b/pkg/data/data.go similarity index 51% rename from data/data.go rename to pkg/data/data.go index 9d82eab..fb8f41a 100644 --- a/data/data.go +++ b/pkg/data/data.go @@ -2,18 +2,18 @@ package data import ( "encoding/json" + "errors" + "fmt" "os" - - "github.com/google/uuid" ) type ( - Service struct { + Data struct { Softwares []Software } Software struct { - UUID string + Slug string `json:"slug"` Name string `json:"name"` Description string `json:"description"` Version string `json:"version"` @@ -33,10 +33,13 @@ type ( } ) -func Load(path string) Service { - f, err := os.OpenFile(path, os.O_RDONLY, 0744) +func Load(path string) (Data, error) { + f, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { - panic("failed to open data file: " + err.Error()) + if errors.Is(err, os.ErrNotExist) { + return Data{}, nil + } + return Data{}, fmt.Errorf("failed to open data file: %w", err) } defer f.Close() @@ -44,18 +47,24 @@ func Load(path string) Service { d := json.NewDecoder(f) err = d.Decode(&s) if err != nil { - panic("failed to parse data file: " + err.Error()) - } - s = generateUUID(s) - return Service{ - Softwares: s.Softwares, + return Data{}, fmt.Errorf("failed to parse data file: %w", err) } + + return Data(s), nil } -func generateUUID(d document) document { - for i, s := range d.Softwares { - s.UUID = uuid.New().String() - d.Softwares[i] = s +func Save(doc Data, path string) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) + if err != nil { + return fmt.Errorf("failed to open data file: %w", err) } - return d + defer f.Close() + + e := json.NewEncoder(f) + err = e.Encode(document(doc)) + if err != nil { + return err + } + + return nil }