diff --git a/forge/forge.go b/forge/forge.go index 962d8a9..86b0a62 100644 --- a/forge/forge.go +++ b/forge/forge.go @@ -1,5 +1,11 @@ package forge +import ( + "fmt" + "net/url" + "strings" +) + type Forge string const ( @@ -18,5 +24,17 @@ type Asset struct { } type Forger interface { + Name() string Latest() (Release, error) } + +func splitURI(uri string) (string, []string, error) { + u, err := url.ParseRequestURI(uri) + if err != nil { + return "", nil, fmt.Errorf("could not parse URI: %w", err) + } + u.Scheme = "https" + paths := strings.FieldsFunc(u.Path, func(r rune) bool { return r == '/' }) + u.Path = "" + return u.String(), paths, nil +} diff --git a/forge/gitea.go b/forge/gitea.go index 84d27c1..47a3779 100644 --- a/forge/gitea.go +++ b/forge/gitea.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "net/http" + "net/url" + "strings" ) var _ Forger = (*Gitea)(nil) @@ -23,6 +25,23 @@ type GiteaRelease struct { } `json:"assets"` } +func NewGitea(uri string) (Gitea, error) { + var g Gitea + base, paths, err := splitURI(uri) + if err != nil { + return g, err + } + + g.BaseURL = base + g.Owner = paths[0] + g.Repo = paths[1] + return g, nil +} + +func (g Gitea) Name() string { + return g.Repo +} + func (g Gitea) Latest() (Release, error) { var release Release diff --git a/meta/meta.go b/meta/meta.go new file mode 100644 index 0000000..331bdb4 --- /dev/null +++ b/meta/meta.go @@ -0,0 +1,83 @@ +package meta + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/adrg/xdg" +) + +type Meta struct { + Packages []Package `json:"packages"` +} + +type Package struct { + Name string `json:"name"` + Version string `json:"version"` +} + +func Add(pkg Package) error { + m, err := Read() + if err != nil { + return err + } + for _, p := range m.Packages { + if strings.EqualFold(pkg.Name, p.Name) { + return errors.New("package already exists locally") + } + } + m.Packages = append(m.Packages, pkg) + return save(m) +} + +func Read() (Meta, error) { + var m Meta + fp, err := metaPath() + if err != nil { + return m, fmt.Errorf("could not get meta file: %w", err) + } + fi, err := os.Open(fp) + if err != nil { + return m, fmt.Errorf("could not open meta file: %w", err) + } + defer fi.Close() + + if err := json.NewDecoder(fi).Decode(&m); err != nil { + return m, fmt.Errorf("could not decode meta: %w", err) + } + return m, nil +} + +func Remove(name string) error { + m, err := Read() + if err != nil { + return err + } + for idx, p := range m.Packages { + if strings.EqualFold(name, p.Name) { + m.Packages = append(m.Packages[:idx], m.Packages[idx+1:]...) + return save(m) + } + } + return fmt.Errorf("could not find package to remove for %q", name) +} + +func metaPath() (string, error) { + return xdg.DataFile("eget/meta.json") +} + +func save(m Meta) error { + fp, err := metaPath() + if err != nil { + return fmt.Errorf("could not get meta path: %w", err) + } + fi, err := os.Create(fp) + if err != nil { + return fmt.Errorf("could not create meta file: %w", err) + } + defer fi.Close() + return json.NewEncoder(fi).Encode(m) +} diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..b86d419 --- /dev/null +++ b/web/web.go @@ -0,0 +1,115 @@ +package web + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "runtime" + + "github.com/adrg/xdg" + "github.com/mholt/archiver/v3" + "go.jolheiser.com/eget/forge" +) + +var ( + amd64Re = regexp.MustCompile(`amd64|x86_64|64-bit`) + linuxRe = regexp.MustCompile(`linux`) + windowsRe = regexp.MustCompile(`windows`) +) + +func Install(f forge.Forger) error { + var re *regexp.Regexp + switch runtime.GOOS { + case "linux": + re = linuxRe + case "windows": + re = windowsRe + default: + return fmt.Errorf("%q is not a supported OS", runtime.GOOS) + } + + latest, err := f.Latest() + if err != nil { + return err + } + + var asset forge.Asset + for _, a := range latest.Assets { + if amd64Re.MatchString(a.DownloadURL) && re.MatchString(a.DownloadURL) { + asset = a + } + } + + if asset.Name == "" { + return errors.New("no release found for this OS") + } + + tmp, err := os.MkdirTemp(os.TempDir(), "eget-*") + if err != nil { + return err + } + defer os.RemoveAll(tmp) + + resp, err := http.Get(asset.DownloadURL) + if err != nil { + return err + } + defer resp.Body.Close() + + tmpDest := filepath.Join(tmp, asset.Name) + fi, err := os.Create(tmpDest) + if err != nil { + return err + } + + if _, err := io.Copy(fi, resp.Body); err != nil { + return err + } + if err := fi.Close(); err != nil { + return err + } + + if err := unpack(tmpDest, f.Name()); err != nil { + return fmt.Errorf("could not unpack download: %w", err) + } + + return nil +} + +func detectRootDir(path string) bool { + infos, err := os.ReadDir(path) + if err != nil { + return false + } + return len(infos) != 1 +} + +func unpack(src, name string) error { + dest := filepath.Join(xdg.DataHome, name) + uaIface, err := archiver.ByExtension(src) + if err != nil { + return err + } + u, ok := uaIface.(archiver.Unarchiver) + if !ok { + d, ok := uaIface.(archiver.Decompressor) + if !ok { + return fmt.Errorf("format specified by source filename is not an archive or compression format: %s (%T)", src, uaIface) + } + var binExt string + if runtime.GOOS == "windows" { + binExt = ".exe" + } + dest = filepath.Join(dest, name+binExt) + c := archiver.FileCompressor{Decompressor: d} + if err := c.DecompressFile(src, dest); err != nil { + return err + } + return os.Chmod(dest, 0o755) + } + return u.Unarchive(src, dest) +}