From 30043b2a68e56a5c4992de3c85c0ae3cbaae75db Mon Sep 17 00:00:00 2001 From: Martin Smith Date: Tue, 25 Feb 2025 13:56:34 +0000 Subject: [PATCH] Add code to support downloading and updating restic for Windows. --- .gitignore | 3 ++- internal/bins/bins.go | 28 +++++++++++--------- internal/bins/decompress.go | 17 ++++++++++++ internal/bins/decompress_windows.go | 41 +++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 internal/bins/decompress.go create mode 100644 internal/bins/decompress_windows.go diff --git a/.gitignore b/.gitignore index 8103f40..a277fca 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ test autorestic data -dist \ No newline at end of file +dist +*.exe diff --git a/internal/bins/bins.go b/internal/bins/bins.go index dd9fea2..9f52e3d 100644 --- a/internal/bins/bins.go +++ b/internal/bins/bins.go @@ -1,7 +1,6 @@ package bins import ( - "compress/bzip2" "encoding/json" "errors" "fmt" @@ -18,8 +17,6 @@ import ( "github.com/cupcakearmy/autorestic/internal/flags" ) -const INSTALL_PATH = "/usr/local/bin" - type GithubReleaseAsset struct { Name string `json:"name"` Link string `json:"browser_download_url"` @@ -46,11 +43,11 @@ func dlJSON(url string) (GithubRelease, error) { } func Uninstall(restic bool) error { - if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil { + if err := os.Remove(path.Join(installPath(), exeName("autorestic"))); err != nil { return err } if restic { - if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil { + if err := os.Remove(path.Join(installPath(), exeName("restic"))); err != nil { return err } } @@ -58,7 +55,7 @@ func Uninstall(restic bool) error { } func downloadAndInstallAsset(body GithubRelease, name string) error { - ending := fmt.Sprintf("_%s_%s.bz2", runtime.GOOS, runtime.GOARCH) + ending := fmt.Sprintf("_%s_%s.%s", runtime.GOOS, runtime.GOARCH, formatName) for _, asset := range body.Assets { if strings.HasSuffix(asset.Name, ending) { // Download archive @@ -70,12 +67,15 @@ func downloadAndInstallAsset(body GithubRelease, name string) error { defer resp.Body.Close() // Uncompress - bz := bzip2.NewReader(resp.Body) + bz, err := decompress(resp) + if err != nil { + return err + } // Save to tmp file in the same directory as the install directory - // Linux does not support overwriting the file that is currently being running - // But it can be delete the old one and a new one moved in its place. - tmp, err := os.CreateTemp(INSTALL_PATH, "autorestic-") + // Linux does not support overwriting the file that is currently running + // But it can delete the old one and a new one can be moved in its place. + tmp, err := os.CreateTemp(installPath(), "autorestic-") if err != nil { return err } @@ -86,8 +86,10 @@ func downloadAndInstallAsset(body GithubRelease, name string) error { if _, err := io.Copy(tmp, bz); err != nil { return err } + // On Windows the rename below will fail if the file remains open. + tmp.Close() - to := path.Join(INSTALL_PATH, name) + to := path.Join(installPath(), exeName(name)) defer os.Remove(tmp.Name()) // Cleanup temporary file after thread exits mode := os.FileMode(0755) @@ -109,7 +111,7 @@ func downloadAndInstallAsset(body GithubRelease, name string) error { return err } - colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH) + colors.Success.Printf("Successfully installed '%s' under %s\n", name, installPath()) return nil } } @@ -132,7 +134,7 @@ func InstallRestic() error { func upgradeRestic() error { _, _, err := internal.ExecuteCommand(internal.ExecuteOptions{ - Command: flags.RESTIC_BIN, + Command: exeName(flags.RESTIC_BIN), }, "self-update") return err } diff --git a/internal/bins/decompress.go b/internal/bins/decompress.go new file mode 100644 index 0000000..59fa6d6 --- /dev/null +++ b/internal/bins/decompress.go @@ -0,0 +1,17 @@ +//go:build !windows + +package bins + +const formatName string = "bz2" + +func installPath() string { + return "/usr/local/bin" +} + +func decompress(resp *http.Response) (io.ReadCloser, error) { + return bzip2.NewReader(resp.Body) +} + +func exeName(f string) string { + return f +} diff --git a/internal/bins/decompress_windows.go b/internal/bins/decompress_windows.go new file mode 100644 index 0000000..4d7038a --- /dev/null +++ b/internal/bins/decompress_windows.go @@ -0,0 +1,41 @@ +//go:build windows + +package bins + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "net/http" + "os" +) + +const formatName string = "zip" + +func installPath() string { + return fmt.Sprintf("%s%cSystem32", os.Getenv("SYSTEMROOT"), os.PathSeparator) +} + +func decompress(resp *http.Response) (io.ReadCloser, error) { + // Have to copy the response as we need to get an io.ReaderAt for the zip API. + buff := bytes.NewBuffer([]byte{}) + size, err := io.Copy(buff, resp.Body) + if err != nil { + return nil, err + } + z, err := zip.NewReader(bytes.NewReader(buff.Bytes()), size) + if err != nil { + return nil, err + } + + if len(z.File) != 1 { + return nil, fmt.Errorf("Expecting one file in zip download, got:%d", len(z.File)) + } + + return z.File[0].Open() +} + +func exeName(f string) string { + return fmt.Sprintf("%s/%s.exe", installPath, f) +}