diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8049888 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/eget* \ No newline at end of file diff --git a/disk/disk.go b/disk/disk.go new file mode 100644 index 0000000..c0909c6 --- /dev/null +++ b/disk/disk.go @@ -0,0 +1,122 @@ +package disk + +import ( + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "go.jolheiser.com/eget/forge" + + "github.com/adrg/xdg" + "github.com/mholt/archiver/v3" +) + +var ( + amd64Re = regexp.MustCompile(`amd64|x86_64|64`) + linuxRe = regexp.MustCompile(`linux|linux64`) + windowsRe = regexp.MustCompile(`windows|win64`) +) + +func Install(asset forge.Asset) error { + 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, asset.Name); err != nil { + return fmt.Errorf("could not unpack download: %w", err) + } + + return nil +} + +func unpack(src, name string) error { + dest := filepath.Join(xdg.DataHome, "eget", name) + if err := os.MkdirAll(dest, os.ModePerm); err != nil { + return fmt.Errorf("could not make all dest dirs: %w", err) + } + var binExt string + if runtime.GOOS == "windows" { + binExt = ".exe" + } + uaIface, err := archiver.ByExtension(src) + if err != nil { + if filepath.Ext(src) == binExt { + return os.Rename(src, filepath.Join(dest, name+binExt)) + } + return err + } + u, ok := uaIface.(archiver.Unarchiver) + if !ok { + dest = filepath.Join(dest, name+binExt) + 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) + } + c := archiver.FileCompressor{Decompressor: d} + if err := c.DecompressFile(src, dest); err != nil { + return err + } + return os.Chmod(dest, 0o755) + } + + tmpDest := filepath.Join(filepath.Dir(src), name) + if err := u.Unarchive(src, tmpDest); err != nil { + return fmt.Errorf("could not unarchive source: %w", err) + } + + tmpSrc := tmpDest + infos, err := os.ReadDir(tmpSrc) + if err != nil { + return fmt.Errorf("could not read unarchived dir: %w", err) + } + if len(infos) == 1 && infos[0].IsDir() { + tmpSrc = filepath.Join(tmpDest, infos[0].Name()) + } + + return moveDir(tmpSrc, dest) +} + +func moveDir(src, dest string) error { + return filepath.WalkDir(src, func(walkPath string, walkEntry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if walkEntry.IsDir() { + return nil + } + + rel := strings.TrimPrefix(walkPath, src) + destPath := filepath.Join(dest, rel) + if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil { + return err + } + return os.Rename(walkPath, destPath) + }) +} diff --git a/forge/forge.go b/forge/forge.go index 86b0a62..40c73e1 100644 --- a/forge/forge.go +++ b/forge/forge.go @@ -1,16 +1,18 @@ package forge import ( + "errors" "fmt" "net/url" + "regexp" + "runtime" "strings" ) -type Forge string - -const ( - ForgeGitea Forge = "gitea" - ForgeGitHub Forge = "github" +var ( + amd64Re = regexp.MustCompile(`amd64|x86_64|64`) + linuxRe = regexp.MustCompile(`linux|linux64`) + windowsRe = regexp.MustCompile(`windows|win64`) ) type Release struct { @@ -29,6 +31,9 @@ type Forger interface { } func splitURI(uri string) (string, []string, error) { + if !strings.HasPrefix(uri, "http") { + uri = "https://" + uri + } u, err := url.ParseRequestURI(uri) if err != nil { return "", nil, fmt.Errorf("could not parse URI: %w", err) @@ -38,3 +43,35 @@ func splitURI(uri string) (string, []string, error) { u.Path = "" return u.String(), paths, nil } + +func Latest(f Forger) (Asset, error) { + var asset Asset + + var re *regexp.Regexp + switch runtime.GOOS { + case "linux": + re = linuxRe + case "windows": + re = windowsRe + default: + return asset, fmt.Errorf("%q is not a supported OS", runtime.GOOS) + } + + release, err := f.Latest() + if err != nil { + return asset, fmt.Errorf("could not get latest release: %w", err) + } + + for _, a := range release.Assets { + if amd64Re.MatchString(a.Name) && re.MatchString(a.Name) { + fmt.Printf("found %q\n", a.Name) + asset = a + } + } + + if asset.Name == "" { + return asset, errors.New("no release found for this OS") + } + + return asset, nil +} diff --git a/forge/gitea.go b/forge/gitea.go index 47a3779..fc443f9 100644 --- a/forge/gitea.go +++ b/forge/gitea.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "net/http" - "net/url" - "strings" ) var _ Forger = (*Gitea)(nil) diff --git a/forge/github.go b/forge/github.go new file mode 100644 index 0000000..38eb0d9 --- /dev/null +++ b/forge/github.go @@ -0,0 +1,63 @@ +package forge + +import ( + "encoding/json" + "fmt" + "net/http" +) + +var _ Forger = (*GitHub)(nil) + +type GitHub struct { + Owner string + Repo string +} + +type GitHubRelease struct { + TagName string `json:"tag_name"` + Assets []struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` + } `json:"assets"` +} + +func NewGitHub(uri string) (GitHub, error) { + var g GitHub + _, paths, err := splitURI(uri) + if err != nil { + return g, err + } + g.Owner = paths[0] + g.Repo = paths[1] + return g, nil +} + +func (g GitHub) Name() string { + return g.Repo +} + +func (g GitHub) Latest() (Release, error) { + var release Release + + var r GitHubRelease + u := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", g.Owner, g.Repo) + res, err := http.Get(u) + if err != nil { + return release, err + } + defer res.Body.Close() + + if err := json.NewDecoder(res.Body).Decode(&r); err != nil { + return release, err + } + + release.Name = r.TagName + for _, a := range r.Assets { + release.Assets = append(release.Assets, Asset{ + Name: a.Name, + DownloadURL: a.BrowserDownloadURL, + }) + } + + return release, nil +} diff --git a/go.mod b/go.mod index b4e3054..5787d5b 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,17 @@ module go.jolheiser.com/eget go 1.19 require ( - github.com/adrg/xdg v0.4.0 // indirect + github.com/adrg/xdg v0.4.0 + github.com/mholt/archiver/v3 v3.5.1 +) + +require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/golang/snappy v0.0.2 // indirect github.com/klauspost/compress v1.11.4 // indirect github.com/klauspost/pgzip v1.2.5 // indirect - github.com/matryer/is v1.4.0 // indirect - github.com/mholt/archiver/v3 v3.5.1 // indirect github.com/nwaples/rardecode v1.1.0 // indirect - github.com/peterbourgon/ff/v3 v3.3.0 // indirect github.com/pierrec/lz4/v4 v4.1.2 // indirect github.com/ulikunitz/xz v0.5.9 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect diff --git a/go.sum b/go.sum index 46866aa..3ddfc3e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= -github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= @@ -17,18 +17,16 @@ github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24= -github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= @@ -39,4 +37,5 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6Dg golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index b9764a5..6f7d6c3 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,40 @@ package main +import ( + "fmt" + "os" + "strings" + + "go.jolheiser.com/eget/disk" + "go.jolheiser.com/eget/forge" +) + var Version = "develop" func main() { + if len(os.Args) < 2 { + fmt.Println("eget ") + return + } + + var f forge.Forger + var err error + uri := os.Args[1] + + f, err = forge.NewGitea(uri) + if strings.HasPrefix(uri, "github") { + f, err = forge.NewGitHub(uri) + } + if err != nil { + panic(err) + } + + asset, err := forge.Latest(f) + if err != nil { + panic(err) + } + + if err := disk.Install(asset); err != nil { + panic(err) + } } diff --git a/web/web.go b/web/web.go deleted file mode 100644 index b86d419..0000000 --- a/web/web.go +++ /dev/null @@ -1,115 +0,0 @@ -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) -}