refactoring, adding cli to edit config

This commit is contained in:
2025-08-26 21:59:50 +02:00
parent e36b15e271
commit 2c109b945e
14 changed files with 503 additions and 20 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

18
cmd/cli/flag/flag.go Normal file
View File

@@ -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
}

29
cmd/cli/main.go Normal file
View File

@@ -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)))
}

View File

@@ -1,7 +1,7 @@
package api
import (
"downloadhub/data"
"downloadhub/pkg/data"
_ "embed"
"fmt"
"html/template"

View File

@@ -1,8 +1,8 @@
package main
import (
"downloadhub/api"
"downloadhub/data"
"downloadhub/cmd/server/api"
"downloadhub/pkg/data"
"flag"
"fmt"
"log/slog"

1
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

View File

@@ -0,0 +1,5 @@
package constants
const (
Version string = "0.0.1"
)

View File

@@ -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
}