feat(upgrade): cli $0 update work well!
This commit is contained in:
parent
ffd430285b
commit
0b4c26c1db
1 changed files with 469 additions and 329 deletions
|
|
@ -27,51 +27,70 @@ import (
|
||||||
// release of the current project is used. platform/arch can be used to
|
// release of the current project is used. platform/arch can be used to
|
||||||
// select the correct asset (e.g. "linux", "amd64").
|
// select the correct asset (e.g. "linux", "amd64").
|
||||||
func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error) {
|
func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error) {
|
||||||
assetURL, err := findAssetURL(releaseURL, platform, arch)
|
assetURL, err := findAssetURL(releaseURL, platform, arch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download asset to temp file
|
// Download asset to temp file. Use the asset URL extension so
|
||||||
tmpFile, err := os.CreateTemp("", "picoclaw-release-*.archive")
|
// extractArchive can detect the archive format (zip/tar.gz/tar).
|
||||||
if err != nil {
|
tmpPattern := "picoclaw-release-*"
|
||||||
return "", err
|
if u, perr := url.Parse(assetURL); perr == nil {
|
||||||
}
|
base := filepath.Base(u.Path)
|
||||||
tmpPath := tmpFile.Name()
|
lbase := strings.ToLower(base)
|
||||||
defer func() { _ = tmpFile.Close() }()
|
switch {
|
||||||
|
case strings.HasSuffix(lbase, ".zip"):
|
||||||
|
tmpPattern += ".zip"
|
||||||
|
case strings.HasSuffix(lbase, ".tar.gz") || strings.HasSuffix(lbase, ".tgz"):
|
||||||
|
tmpPattern += ".tar.gz"
|
||||||
|
case strings.HasSuffix(lbase, ".tar"):
|
||||||
|
tmpPattern += ".tar"
|
||||||
|
default:
|
||||||
|
tmpPattern += ".archive"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tmpPattern += ".archive"
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.Get(assetURL)
|
tmpFile, err := os.CreateTemp("", tmpPattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(tmpPath)
|
return "", err
|
||||||
return "", err
|
}
|
||||||
}
|
tmpPath := tmpFile.Name()
|
||||||
defer resp.Body.Close()
|
defer func() { _ = tmpFile.Close() }()
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
return "", fmt.Errorf("failed to download asset: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
resp, err := http.Get(assetURL)
|
||||||
os.Remove(tmpPath)
|
if err != nil {
|
||||||
return "", err
|
os.Remove(tmpPath)
|
||||||
}
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
os.Remove(tmpPath)
|
||||||
|
return "", fmt.Errorf("failed to download asset: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
// Extract
|
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
||||||
destDir, err := os.MkdirTemp("", "picoclaw-extract-*")
|
os.Remove(tmpPath)
|
||||||
if err != nil {
|
return "", err
|
||||||
os.Remove(tmpPath)
|
}
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := extractArchive(tmpPath, destDir); err != nil {
|
// Extract
|
||||||
os.Remove(tmpPath)
|
destDir, err := os.MkdirTemp("", "picoclaw-extract-*")
|
||||||
os.RemoveAll(destDir)
|
if err != nil {
|
||||||
return "", err
|
os.Remove(tmpPath)
|
||||||
}
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// cleanup archive file; keep extracted contents
|
if err := extractArchive(tmpPath, destDir); err != nil {
|
||||||
_ = os.Remove(tmpPath)
|
os.Remove(tmpPath)
|
||||||
return destDir, nil
|
os.RemoveAll(destDir)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup archive file; keep extracted contents
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
return destDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSelfFromRelease downloads the release matching the given parameters,
|
// UpdateSelfFromRelease downloads the release matching the given parameters,
|
||||||
|
|
@ -80,352 +99,473 @@ func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error
|
||||||
// If releaseURL is empty, the latest release is used. If platform or arch
|
// If releaseURL is empty, the latest release is used. If platform or arch
|
||||||
// is empty, runtime values are used.
|
// is empty, runtime values are used.
|
||||||
func UpdateSelfFromRelease(releaseURL, platform, arch, programName string) error {
|
func UpdateSelfFromRelease(releaseURL, platform, arch, programName string) error {
|
||||||
if platform == "" {
|
if platform == "" {
|
||||||
platform = runtime.GOOS
|
platform = runtime.GOOS
|
||||||
}
|
}
|
||||||
if arch == "" {
|
if arch == "" {
|
||||||
arch = runtime.GOARCH
|
arch = runtime.GOARCH
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, err := DownloadAndExtractRelease(releaseURL, platform, arch)
|
dir, err := DownloadAndExtractRelease(releaseURL, platform, arch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
binPath, err := findBinaryInDir(dir, programName)
|
binPath, err := findBinaryInDir(dir, programName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure executable bit on non-windows
|
// ensure executable bit on non-windows
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
_ = os.Chmod(binPath, 0755)
|
_ = os.Chmod(binPath, 0755)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(binPath)
|
f, err := os.Open(binPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
if err := selfupdate.Apply(f, selfupdate.Options{}); err != nil {
|
if err := selfupdate.Apply(f, selfupdate.Options{}); err != nil {
|
||||||
return fmt.Errorf("apply update: %w", err)
|
return fmt.Errorf("apply update: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSelf updates the running executable by fetching the latest release
|
// UpdateSelf updates the running executable by fetching the latest release
|
||||||
// and applying the binary matching programName.
|
// and applying the binary matching programName.
|
||||||
func UpdateSelf(programName string) error {
|
func UpdateSelf(programName string) error {
|
||||||
// Use production repo by default.
|
// By default, let findAssetURL select the nightly build when no explicit
|
||||||
return UpdateSelfFromRelease(GetProdReleaseAPIURL(), runtime.GOOS, runtime.GOARCH, programName)
|
// release URL is provided. Passing an empty releaseURL triggers that path.
|
||||||
|
return UpdateSelfFromRelease("", runtime.GOOS, runtime.GOARCH, programName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReleaseAPIURL returns the GitHub Releases API URL for the given repo owner.
|
// GetReleaseAPIURL returns the GitHub Releases API URL for the given repo owner.
|
||||||
// Example: owner="sky5454" -> https://api.github.com/repos/sky5454/picoclaw/releases/latest
|
// Example: owner="sky5454" -> https://api.github.com/repos/sky5454/picoclaw/releases/latest
|
||||||
func GetReleaseAPIURL(owner string) string {
|
func GetReleaseAPIURL(owner string) string {
|
||||||
return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/latest", owner)
|
return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/latest", owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProdReleaseAPIURL returns the production release API URL (upstream).
|
// GetProdReleaseAPIURL returns the production release API URL (upstream).
|
||||||
func GetProdReleaseAPIURL() string {
|
func GetProdReleaseAPIURL() string {
|
||||||
return GetReleaseAPIURL("sipeed")
|
return GetReleaseAPIURL("sipeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReleaseTagAPIURL returns the GitHub Releases API URL for a specific tag.
|
||||||
|
// Example: owner="sipeed", tag="nightly" -> https://api.github.com/repos/sipeed/picoclaw/releases/tags/nightly
|
||||||
|
func GetReleaseTagAPIURL(owner, tag string) string {
|
||||||
|
return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/tags/%s", owner, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNightlyReleaseAPIURL returns the nightly release API URL for the production repo.
|
||||||
|
func GetNightlyReleaseAPIURL() string {
|
||||||
|
return GetReleaseTagAPIURL("sipeed", "nightly")
|
||||||
}
|
}
|
||||||
|
|
||||||
// findAssetURL resolves the appropriate asset URL for the given release
|
// findAssetURL resolves the appropriate asset URL for the given release
|
||||||
// selector. It accepts direct archive URLs as well as GitHub release URLs
|
// selector. It accepts direct archive URLs as well as GitHub release URLs
|
||||||
// or empty (latest release for the project).
|
// or empty (latest release for the project).
|
||||||
func findAssetURL(releaseURL, platform, arch string) (string, error) {
|
func findAssetURL(releaseURL, platform, arch string) (string, error) {
|
||||||
if looksLikeDirectAssetURL(releaseURL) {
|
if looksLikeDirectAssetURL(releaseURL) {
|
||||||
return releaseURL, nil
|
return releaseURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := buildReleaseAPIURL(releaseURL)
|
apiURL := buildReleaseAPIURL(releaseURL)
|
||||||
if apiURL == "" {
|
if apiURL == "" {
|
||||||
// Default to production repo API URL when no explicit release URL is provided.
|
// If caller provided an empty releaseURL, default to the nightly tag
|
||||||
apiURL = GetProdReleaseAPIURL()
|
// from the production repo. Otherwise fall back to the production
|
||||||
}
|
// latest release API URL.
|
||||||
|
if strings.TrimSpace(releaseURL) == "" {
|
||||||
|
apiURL = GetNightlyReleaseAPIURL()
|
||||||
|
} else {
|
||||||
|
apiURL = GetProdReleaseAPIURL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.Get(apiURL)
|
resp, err := http.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return "", fmt.Errorf("failed to query releases: status %d", resp.StatusCode)
|
return "", fmt.Errorf("failed to query releases: status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"`
|
||||||
Assets []struct {
|
Assets []struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
BrowserDownloadURL string `json:"browser_download_url"`
|
BrowserDownloadURL string `json:"browser_download_url"`
|
||||||
} `json:"assets"`
|
} `json:"assets"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefer exact platform+arch match
|
// Selection order: platform -> arch -> extension.
|
||||||
var fallback string
|
platformLower := strings.ToLower(platform)
|
||||||
for _, a := range data.Assets {
|
archLower := strings.ToLower(arch)
|
||||||
n := strings.ToLower(a.Name)
|
|
||||||
if platform != "" && arch != "" {
|
|
||||||
if strings.Contains(n, strings.ToLower(platform)) && strings.Contains(n, strings.ToLower(arch)) {
|
|
||||||
return a.BrowserDownloadURL, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if platform != "" && strings.Contains(n, strings.ToLower(platform)) {
|
|
||||||
if fallback == "" {
|
|
||||||
fallback = a.BrowserDownloadURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fallback == "" {
|
|
||||||
fallback = a.BrowserDownloadURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallback != "" {
|
isZip := func(name string) bool {
|
||||||
return fallback, nil
|
return strings.HasSuffix(name, ".zip")
|
||||||
}
|
}
|
||||||
return "", errors.New("no suitable release asset found")
|
isTarGz := func(name string) bool {
|
||||||
|
return strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".tgz")
|
||||||
|
}
|
||||||
|
isTar := func(name string) bool { return strings.HasSuffix(name, ".tar") }
|
||||||
|
|
||||||
|
// collect indices of assets that contain platform (if provided)
|
||||||
|
var platformIdx []int
|
||||||
|
for i, a := range data.Assets {
|
||||||
|
n := strings.ToLower(a.Name)
|
||||||
|
if platform == "" || strings.Contains(n, platformLower) {
|
||||||
|
platformIdx = append(platformIdx, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pickBest := func(idxs []int) (string, bool) {
|
||||||
|
if len(idxs) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
// prefer arch matches within idxs
|
||||||
|
var archIdx []int
|
||||||
|
if arch != "" {
|
||||||
|
aliases := archAliases(archLower)
|
||||||
|
for _, i := range idxs {
|
||||||
|
n := strings.ToLower(data.Assets[i].Name)
|
||||||
|
for _, ali := range aliases {
|
||||||
|
if strings.Contains(n, ali) {
|
||||||
|
archIdx = append(archIdx, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates := archIdx
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
candidates = idxs
|
||||||
|
}
|
||||||
|
|
||||||
|
// extension preference
|
||||||
|
if platformLower == "windows" {
|
||||||
|
// prefer .zip only
|
||||||
|
for _, i := range candidates {
|
||||||
|
if isZip(strings.ToLower(data.Assets[i].Name)) {
|
||||||
|
return data.Assets[i].BrowserDownloadURL, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if no zip found, fallthrough to first candidate
|
||||||
|
return data.Assets[candidates[0]].BrowserDownloadURL, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-windows: prefer tar.gz/tgz, then tar, then zip
|
||||||
|
for _, i := range candidates {
|
||||||
|
if isTarGz(strings.ToLower(data.Assets[i].Name)) {
|
||||||
|
return data.Assets[i].BrowserDownloadURL, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, i := range candidates {
|
||||||
|
if isTar(strings.ToLower(data.Assets[i].Name)) {
|
||||||
|
return data.Assets[i].BrowserDownloadURL, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, i := range candidates {
|
||||||
|
if isZip(strings.ToLower(data.Assets[i].Name)) {
|
||||||
|
return data.Assets[i].BrowserDownloadURL, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback to first candidate
|
||||||
|
return data.Assets[candidates[0]].BrowserDownloadURL, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try platform matches first
|
||||||
|
if url, ok := pickBest(platformIdx); ok {
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no platform matches, try arch-only matches
|
||||||
|
if arch != "" {
|
||||||
|
var archOnlyIdx []int
|
||||||
|
for i, a := range data.Assets {
|
||||||
|
n := strings.ToLower(a.Name)
|
||||||
|
if strings.Contains(n, archLower) {
|
||||||
|
archOnlyIdx = append(archOnlyIdx, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url, ok := pickBest(archOnlyIdx); ok {
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to first asset
|
||||||
|
if len(data.Assets) > 0 {
|
||||||
|
return data.Assets[0].BrowserDownloadURL, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("no suitable release asset found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func looksLikeDirectAssetURL(u string) bool {
|
func looksLikeDirectAssetURL(u string) bool {
|
||||||
if u == "" {
|
if u == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
lower := strings.ToLower(u)
|
lower := strings.ToLower(u)
|
||||||
if strings.HasSuffix(lower, ".zip") || strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") || strings.HasSuffix(lower, ".tar") {
|
if strings.HasSuffix(lower, ".zip") || strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") || strings.HasSuffix(lower, ".tar") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if strings.Contains(lower, "/releases/download/") {
|
if strings.Contains(lower, "/releases/download/") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildReleaseAPIURL(releaseURL string) string {
|
func buildReleaseAPIURL(releaseURL string) string {
|
||||||
if releaseURL == "" {
|
if releaseURL == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if strings.Contains(releaseURL, "api.github.com") {
|
if strings.Contains(releaseURL, "api.github.com") {
|
||||||
return releaseURL
|
return releaseURL
|
||||||
}
|
}
|
||||||
u, err := url.Parse(releaseURL)
|
u, err := url.Parse(releaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if u.Host != "github.com" {
|
if u.Host != "github.com" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
owner := parts[0]
|
owner := parts[0]
|
||||||
repo := parts[1]
|
repo := parts[1]
|
||||||
// if tag specified
|
// if tag specified
|
||||||
if len(parts) >= 5 && parts[2] == "releases" && parts[3] == "tag" {
|
if len(parts) >= 5 && parts[2] == "releases" && parts[3] == "tag" {
|
||||||
tag := parts[4]
|
tag := parts[4]
|
||||||
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag)
|
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag)
|
||||||
}
|
}
|
||||||
// default to latest
|
// default to latest
|
||||||
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archAliases returns common name variants for an architecture string
|
||||||
|
// so we can match release asset names like "x86_64" vs Go's "amd64".
|
||||||
|
// archAliases returns name variants for an architecture string.
|
||||||
|
// If `arch` is empty or matches the local runtime.GOARCH, prefer the
|
||||||
|
// compile-time architecture aliases provided by archAliasesForLocal
|
||||||
|
// (implemented per-architecture via build tags). For other `arch`
|
||||||
|
// values we use a small synonyms map.
|
||||||
|
func archAliases(arch string) []string {
|
||||||
|
a := strings.ToLower(arch)
|
||||||
|
if syns, ok := archSynonyms[a]; ok {
|
||||||
|
return syns
|
||||||
|
}
|
||||||
|
return []string{a}
|
||||||
|
}
|
||||||
|
|
||||||
|
var archSynonyms = map[string][]string{
|
||||||
|
"amd64": {"amd64", "x86_64", "x64"},
|
||||||
|
"x86_64": {"amd64", "x86_64", "x64"},
|
||||||
|
"x64": {"amd64", "x86_64", "x64"},
|
||||||
|
"386": {"386", "x86"},
|
||||||
|
"x86": {"386", "x86"},
|
||||||
|
"arm64": {"arm64", "aarch64"},
|
||||||
|
"aarch64": {"arm64", "aarch64"},
|
||||||
|
"arm": {"arm"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractArchive(archivePath, destDir string) error {
|
func extractArchive(archivePath, destDir string) error {
|
||||||
lower := strings.ToLower(archivePath)
|
lower := strings.ToLower(archivePath)
|
||||||
if strings.HasSuffix(lower, ".zip") {
|
if strings.HasSuffix(lower, ".zip") {
|
||||||
return extractZip(archivePath, destDir)
|
return extractZip(archivePath, destDir)
|
||||||
}
|
}
|
||||||
// treat .tar.gz and .tgz as gzip+tar
|
// treat .tar.gz and .tgz as gzip+tar
|
||||||
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
|
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
|
||||||
return extractTarGz(archivePath, destDir)
|
return extractTarGz(archivePath, destDir)
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(lower, ".tar") {
|
if strings.HasSuffix(lower, ".tar") {
|
||||||
return extractTar(archivePath, destDir)
|
return extractTar(archivePath, destDir)
|
||||||
}
|
}
|
||||||
// fallback: try tar.gz
|
// fallback: try tar.gz
|
||||||
return extractTarGz(archivePath, destDir)
|
return extractTarGz(archivePath, destDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractZip(archivePath, destDir string) error {
|
func extractZip(archivePath, destDir string) error {
|
||||||
r, err := zip.OpenReader(archivePath)
|
r, err := zip.OpenReader(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
for _, f := range r.File {
|
for _, f := range r.File {
|
||||||
fp := filepath.Join(destDir, f.Name)
|
fp := filepath.Join(destDir, f.Name)
|
||||||
if f.FileInfo().IsDir() {
|
if f.FileInfo().IsDir() {
|
||||||
_ = os.MkdirAll(fp, f.Mode())
|
_ = os.MkdirAll(fp, f.Mode())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(fp), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(fp), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc, err := f.Open()
|
rc, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc.Close()
|
rc.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(out, rc); err != nil {
|
if _, err := io.Copy(out, rc); err != nil {
|
||||||
rc.Close()
|
rc.Close()
|
||||||
out.Close()
|
out.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc.Close()
|
rc.Close()
|
||||||
out.Close()
|
out.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractTarGz(archivePath, destDir string) error {
|
func extractTarGz(archivePath, destDir string) error {
|
||||||
f, err := os.Open(archivePath)
|
f, err := os.Open(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
gzr, err := gzip.NewReader(f)
|
gzr, err := gzip.NewReader(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer gzr.Close()
|
defer gzr.Close()
|
||||||
tr := tar.NewReader(gzr)
|
tr := tar.NewReader(gzr)
|
||||||
for {
|
for {
|
||||||
hdr, err := tr.Next()
|
hdr, err := tr.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
target := filepath.Join(destDir, hdr.Name)
|
target := filepath.Join(destDir, hdr.Name)
|
||||||
switch hdr.Typeflag {
|
switch hdr.Typeflag {
|
||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode))
|
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(out, tr); err != nil {
|
if _, err := io.Copy(out, tr); err != nil {
|
||||||
out.Close()
|
out.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out.Close()
|
out.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractTar(archivePath, destDir string) error {
|
func extractTar(archivePath, destDir string) error {
|
||||||
f, err := os.Open(archivePath)
|
f, err := os.Open(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
tr := tar.NewReader(f)
|
tr := tar.NewReader(f)
|
||||||
for {
|
for {
|
||||||
hdr, err := tr.Next()
|
hdr, err := tr.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
target := filepath.Join(destDir, hdr.Name)
|
target := filepath.Join(destDir, hdr.Name)
|
||||||
switch hdr.Typeflag {
|
switch hdr.Typeflag {
|
||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode))
|
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(out, tr); err != nil {
|
if _, err := io.Copy(out, tr); err != nil {
|
||||||
out.Close()
|
out.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out.Close()
|
out.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findBinaryInDir(dir, programName string) (string, error) {
|
func findBinaryInDir(dir, programName string) (string, error) {
|
||||||
wanted := []string{programName}
|
wanted := []string{programName}
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
wanted = append([]string{programName + ".exe"}, wanted...)
|
wanted = append([]string{programName + ".exe"}, wanted...)
|
||||||
} else {
|
} else {
|
||||||
// also accept programs with .exe in archives targeting windows
|
// also accept programs with .exe in archives targeting windows
|
||||||
wanted = append(wanted, programName+".exe")
|
wanted = append(wanted, programName+".exe")
|
||||||
}
|
}
|
||||||
|
|
||||||
var found string
|
var found string
|
||||||
filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error {
|
filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error {
|
||||||
if err != nil || found != "" {
|
if err != nil || found != "" {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
base := filepath.Base(p)
|
base := filepath.Base(p)
|
||||||
for _, w := range wanted {
|
for _, w := range wanted {
|
||||||
if base == w {
|
if base == w {
|
||||||
found = p
|
found = p
|
||||||
return io.EOF // use EOF to stop walking early
|
return io.EOF // use EOF to stop walking early
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if found == "" {
|
if found == "" {
|
||||||
return "", fmt.Errorf("binary %q not found in archive", programName)
|
return "", fmt.Errorf("binary %q not found in archive", programName)
|
||||||
}
|
}
|
||||||
return found, nil
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUpdateCommand returns a cobra command that triggers UpdateSelfFromRelease.
|
// NewUpdateCommand returns a cobra command that triggers UpdateSelfFromRelease.
|
||||||
func NewUpdateCommand(binaryName string) *cobra.Command {
|
func NewUpdateCommand(binaryName string) *cobra.Command {
|
||||||
var urlStr, platform, arch string
|
var urlStr, platform, arch string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Check and apply updates from GitHub releases",
|
Short: "Check and apply updates from GitHub releases",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if platform == "" {
|
if platform == "" {
|
||||||
platform = runtime.GOOS
|
platform = runtime.GOOS
|
||||||
}
|
}
|
||||||
if arch == "" {
|
if arch == "" {
|
||||||
arch = runtime.GOARCH
|
arch = runtime.GOARCH
|
||||||
}
|
}
|
||||||
fmt.Printf("Current version: %s\n", config.FormatVersion())
|
fmt.Printf("Current version: %s\n", config.FormatVersion())
|
||||||
if err := UpdateSelfFromRelease(urlStr, platform, arch, binaryName); err != nil {
|
if err := UpdateSelfFromRelease(urlStr, platform, arch, binaryName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("Update applied; restart to use the new version.")
|
fmt.Println("Update applied; restart to use the new version.")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVarP(&urlStr, "url", "u", "", "Direct URL to download release asset or release page")
|
cmd.Flags().StringVarP(&urlStr, "url", "u", "", "Direct URL to download release asset or release page")
|
||||||
cmd.Flags().StringVar(&platform, "platform", "", "Target platform (default: runtime.GOOS)")
|
cmd.Flags().StringVar(&platform, "platform", "", "Target platform (default: runtime.GOOS)")
|
||||||
cmd.Flags().StringVar(&arch, "arch", "", "Target arch (default: runtime.GOARCH)")
|
cmd.Flags().StringVar(&arch, "arch", "", "Target arch (default: runtime.GOARCH)")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue