ScuttleBot

scuttlebot / internal / ergo / fetch.go
Blame History Raw 164 lines
1
package ergo
2
3
import (
4
"archive/tar"
5
"compress/gzip"
6
"encoding/json"
7
"fmt"
8
"io"
9
"net/http"
10
"os"
11
"path/filepath"
12
"runtime"
13
)
14
15
const ergoGitHubAPI = "https://api.github.com/repos/ergochat/ergo/releases/latest"
16
17
// EnsureBinary checks that the ergo binary exists at binaryPath. If it does
18
// not, it downloads the latest release from GitHub into destDir and returns
19
// the path to the installed binary.
20
//
21
// binaryPath is the configured path (may be just "ergo" meaning look in PATH).
22
// destDir is where to install if not found.
23
func EnsureBinary(binaryPath, destDir string) (string, error) {
24
// If it's an absolute path or the caller set a specific path, check it first.
25
if filepath.IsAbs(binaryPath) {
26
if _, err := os.Stat(binaryPath); err == nil {
27
return binaryPath, nil
28
}
29
}
30
31
// Check if ergo is already in our data dir.
32
localPath := filepath.Join(destDir, "ergo")
33
if _, err := os.Stat(localPath); err == nil {
34
return localPath, nil
35
}
36
37
// Download from GitHub releases.
38
version, downloadURL, err := latestReleaseURL()
39
if err != nil {
40
return "", fmt.Errorf("ergo: fetch latest release info: %w", err)
41
}
42
43
fmt.Fprintf(os.Stderr, "ergo binary not found — downloading %s...\n", version)
44
45
if err := os.MkdirAll(destDir, 0o700); err != nil {
46
return "", fmt.Errorf("ergo: create data dir: %w", err)
47
}
48
49
if err := downloadAndExtract(downloadURL, destDir); err != nil {
50
return "", fmt.Errorf("ergo: download: %w", err)
51
}
52
53
fmt.Fprintf(os.Stderr, "ergo %s installed to %s\n", version, localPath)
54
return localPath, nil
55
}
56
57
// latestReleaseURL queries GitHub for the latest ergo release and returns
58
// the version string and the download URL for the current OS/arch.
59
func latestReleaseURL() (string, string, error) {
60
resp, err := http.Get(ergoGitHubAPI) //nolint:gosec // known GitHub API URL
61
if err != nil {
62
return "", "", err
63
}
64
defer resp.Body.Close()
65
66
var release struct {
67
TagName string `json:"tag_name"`
68
Assets []struct {
69
Name string `json:"name"`
70
BrowserDownloadURL string `json:"browser_download_url"`
71
} `json:"assets"`
72
}
73
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
74
return "", "", err
75
}
76
77
suffix := platformSuffix()
78
for _, asset := range release.Assets {
79
if matchesPlatform(asset.Name, suffix) {
80
return release.TagName, asset.BrowserDownloadURL, nil
81
}
82
}
83
84
return "", "", fmt.Errorf("no release asset found for %s/%s (tag %s)", runtime.GOOS, runtime.GOARCH, release.TagName)
85
}
86
87
// platformSuffix returns the OS-arch suffix used in ergo release filenames.
88
// Ergo uses "macos" instead of "darwin" and "x86_64" instead of "amd64".
89
func platformSuffix() string {
90
goos := runtime.GOOS
91
if goos == "darwin" {
92
goos = "macos"
93
}
94
arch := runtime.GOARCH
95
if arch == "amd64" {
96
arch = "x86_64"
97
}
98
return goos + "-" + arch
99
}
100
101
func matchesPlatform(name, suffix string) bool {
102
// Ergo assets look like: ergo-v2.14.0-linux-x86_64.tar.gz
103
return len(name) > 0 &&
104
filepath.Ext(name) == ".gz" &&
105
contains(name, suffix) &&
106
contains(name, ".tar.")
107
}
108
109
func contains(s, sub string) bool {
110
return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub))
111
}
112
113
func containsStr(s, sub string) bool {
114
for i := 0; i <= len(s)-len(sub); i++ {
115
if s[i:i+len(sub)] == sub {
116
return true
117
}
118
}
119
return false
120
}
121
122
// downloadAndExtract downloads a .tar.gz and extracts the "ergo" binary into destDir.
123
func downloadAndExtract(url, destDir string) error {
124
resp, err := http.Get(url) //nolint:gosec // URL from GitHub API
125
if err != nil {
126
return err
127
}
128
defer resp.Body.Close()
129
130
gz, err := gzip.NewReader(resp.Body)
131
if err != nil {
132
return fmt.Errorf("gzip: %w", err)
133
}
134
defer gz.Close()
135
136
tr := tar.NewReader(gz)
137
for {
138
hdr, err := tr.Next()
139
if err == io.EOF {
140
break
141
}
142
if err != nil {
143
return fmt.Errorf("tar: %w", err)
144
}
145
// Extract only the "ergo" binary (may be at root or in a subdirectory).
146
if filepath.Base(hdr.Name) != "ergo" || hdr.Typeflag != tar.TypeReg {
147
continue
148
}
149
150
dest := filepath.Join(destDir, "ergo")
151
f, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
152
if err != nil {
153
return err
154
}
155
if _, err := io.Copy(f, tr); err != nil { //nolint:gosec // size bounded by release binary
156
f.Close()
157
return err
158
}
159
return f.Close()
160
}
161
162
return fmt.Errorf("ergo binary not found in archive")
163
}
164

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button