Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.1
2.1.2
4 changes: 2 additions & 2 deletions update.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ func cleanStaleUpdateBinaries() {
exeDir := filepath.Dir(self)

patterns := []string{
filepath.Join(exeDir, "EggLedger_*_new"),
filepath.Join(exeDir, "EggLedger_*_new.exe"),
filepath.Join(exeDir, "EggLedger*_new"),
filepath.Join(exeDir, "EggLedger*_new.exe"),
}
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
Expand Down
143 changes: 133 additions & 10 deletions updater_bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ package main
// }

import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -57,12 +61,12 @@ func handleDownloadAndInstallUpdate(tag string) error {
return wrap(err)
}

assetName := expectedAssetName()
tempName := strings.TrimSuffix(assetName, ".exe") + "_new"
exeName := filepath.Base(exePath)
tempBinName := strings.TrimSuffix(exeName, ".exe") + "_new"
if runtime.GOOS == "windows" {
tempName += ".exe"
tempBinName += ".exe"
}
tempPath := filepath.Join(exeDir, tempName)
tempPath := filepath.Join(exeDir, tempBinName)

// Clean up any previous failed attempt.
_ = os.Remove(tempPath)
Expand All @@ -71,13 +75,28 @@ func handleDownloadAndInstallUpdate(tag string) error {
go _ui.Eval(fmt.Sprintf(`globalThis.updateDownloadProgress && globalThis.updateDownloadProgress(%d, %d)`, downloaded, total))
}

if err := downloadUpdate(assetURL, tempPath, progressCb); err != nil {
_ = os.Remove(tempPath)
return wrap(err)
}
if runtime.GOOS == "windows" {
// Windows asset is a raw binary; download directly to tempPath.
if err := downloadUpdate(assetURL, tempPath, progressCb); err != nil {
_ = os.Remove(tempPath)
return wrap(err)
}
} else {
// Non-Windows assets are archives; download then extract the binary.
archivePath := filepath.Join(os.TempDir(), expectedAssetName())
_ = os.Remove(archivePath)

if err := downloadUpdate(assetURL, archivePath, progressCb); err != nil {
_ = os.Remove(archivePath)
return wrap(err)
}
if err := extractBinaryFromArchive(archivePath, tempPath); err != nil {
_ = os.Remove(archivePath)
_ = os.Remove(tempPath)
return wrap(err)
}
_ = os.Remove(archivePath)

// Make executable on Unix.
if runtime.GOOS != "windows" {
if err := os.Chmod(tempPath, 0755); err != nil {
_ = os.Remove(tempPath)
return wrap(err)
Expand All @@ -99,3 +118,107 @@ func handleDownloadAndInstallUpdate(tag string) error {
_ui.Close()
return nil
}

// extractBinaryFromArchive dispatches to the correct extractor based on archive extension.
func extractBinaryFromArchive(archivePath, destPath string) error {
wrap := func(err error) error { return errors.Wrap(err, "extractBinaryFromArchive") }
switch {
case strings.HasSuffix(archivePath, ".tar.gz"):
return wrap(extractFromTarGz(archivePath, destPath))
case strings.HasSuffix(archivePath, ".zip"):
return wrap(extractFromZip(archivePath, destPath))
default:
return wrap(errors.Errorf("unsupported archive format: %s", filepath.Base(archivePath)))
}
}

// extractFromTarGz extracts the first regular file from a .tar.gz archive.
func extractFromTarGz(archivePath, destPath string) error {
f, err := os.Open(archivePath)
if err != nil {
return err
}
defer f.Close()

gr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gr.Close()

tr := tar.NewReader(gr)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if hdr.Typeflag != tar.TypeReg || hdr.Size == 0 {
continue
}
out, err := os.Create(destPath)
if err != nil {
return err
}
if _, err := io.Copy(out, tr); err != nil {
out.Close()
return err
}
return out.Close()
}
return errors.New("no regular file found in archive")
}

// extractFromZip extracts the EggLedger binary from a .zip archive.
// Prefers an entry under a MacOS/ path (macOS app bundle layout), then
// falls back to the first extensionless regular file.
func extractFromZip(archivePath, destPath string) error {
zr, err := zip.OpenReader(archivePath)
if err != nil {
return err
}
defer zr.Close()

var target *zip.File
for _, f := range zr.File {
if f.FileInfo().IsDir() {
continue
}
if strings.Contains(f.Name, "/MacOS/") {
target = f
break
}
}
if target == nil {
for _, f := range zr.File {
if f.FileInfo().IsDir() {
continue
}
if filepath.Ext(f.Name) == "" {
target = f
break
}
}
}
if target == nil {
return errors.New("no suitable binary found in zip archive")
}

rc, err := target.Open()
if err != nil {
return err
}
defer rc.Close()

out, err := os.Create(destPath)
if err != nil {
return err
}
if _, err := io.Copy(out, rc); err != nil {
out.Close()
return err
}
return out.Close()
}
19 changes: 13 additions & 6 deletions version.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,21 @@ func getLatestTagIncludingPreReleases() (string, string, error) {
return highestTag, highestBody, nil
}

// expectedAssetName returns the expected binary name for the current platform.
// e.g. "EggLedger_windows_amd64.exe" or "EggLedger_darwin_arm64"
// expectedAssetName returns the release asset filename for the current platform.
func expectedAssetName() string {
name := fmt.Sprintf("EggLedger_%s_%s", runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
name += ".exe"
switch runtime.GOOS {
case "windows":
return "EggLedger.exe"
case "linux":
return "EggLedger-linux.tar.gz"
case "darwin":
if runtime.GOARCH == "arm64" {
return "EggLedger-mac-arm64.zip"
}
return "EggLedger-mac.zip"
default:
return fmt.Sprintf("EggLedger_%s_%s", runtime.GOOS, runtime.GOARCH)
}
return name
}

// getUpdateAssetURL fetches the GitHub releases API for the given tag and returns
Expand Down
39 changes: 32 additions & 7 deletions www/src/components/modals/UpdateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,26 @@
class="text-left flex-1 bg-darkerer overflow-auto gh-markdown-content max-h-60vh p-1rem rounded-md"
v-html="renderedNotes"
></div>
<div class="mt-1rem flex-auto justify-center items-center">
<a v-external-link class="items-center justify-center text-gray-200 hover:text-gray-300" href="https://github.com/DavidArthurCole/EggLedger/releases/latest">
<button class="min-w-30vw btn btn-outline-dark p-0_5rem pr-2rem pl-2rem rounded-md bg-blue-500 border-blue-600 hover:bg-blue-600 hover:border-blue-700">
Download
</button>
</a>
<div class="mt-1rem flex-auto justify-center items-center space-y-2">
<div v-if="updateInProgress" class="text-sm text-blue-300">
Downloading<span v-if="updateProgress.total > 0"> - {{ Math.round(updateProgress.downloaded / updateProgress.total * 100) }}%</span>...
</div>
<p v-if="updateError" class="text-sm text-red-400">{{ updateError }}</p>
<button
v-if="!updateInProgress"
class="min-w-30vw btn btn-outline-dark p-0_5rem pr-2rem pl-2rem rounded-md bg-blue-500 border-blue-600 hover:bg-blue-600 hover:border-blue-700"
@click="startUpdate"
>
Update Now
</button>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { marked } from 'marked'

const props = defineProps<{
Expand All @@ -42,4 +48,23 @@ const props = defineProps<{
defineEmits<{ close: [] }>()

const renderedNotes = computed(() => marked.parse(props.releaseNotes || '') as string)

const updateInProgress = ref(false)
const updateProgress = ref({ downloaded: 0, total: 0 })
const updateError = ref('')

async function startUpdate() {
if (!props.releaseTag) return
updateInProgress.value = true
updateError.value = ''
globalThis.updateDownloadProgress = (downloaded, total) => {
updateProgress.value = { downloaded, total }
}
try {
await globalThis.downloadAndInstallUpdate(props.releaseTag)
} catch (e: unknown) {
updateError.value = e instanceof Error ? e.message : String(e)
updateInProgress.value = false
}
}
</script>
Loading