From 136231ad94f6021b90a799db97ec34db37959e23 Mon Sep 17 00:00:00 2001 From: intergrav Date: Thu, 2 Jul 2026 07:42:02 -0400 Subject: [PATCH] feat: support self-manage on linux now self-managed restic is supported on windows and linux as a fallback if not found in PATH. will remain unsupported on darwin due to macos security killing the restic process randomly --- README.md | 6 +- cmd/restray/manage_other.go | 3 +- ...age_windows.go => manage_windows_linux.go} | 74 +++++++++++-------- nix/shared.nix | 2 +- 4 files changed, 50 insertions(+), 35 deletions(-) rename cmd/restray/{manage_windows.go => manage_windows_linux.go} (86%) diff --git a/README.md b/README.md index c909554..d48bee9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This is new and somewhat work-in-progress/beta software. You might possibly enco - Optional automatic pruning and checks after backups - Operations menu for running individual operations (backup, prune, check, unlock, mount, shell, hooks) - Optional automatic retry and removal of stale locks -- On Windows it can also download and update it's own self-managed restic if not found in PATH +- On Windows/Linux it can also download and update it's own self-managed restic if not found in PATH - Open a shell with your profile's environment and restic binary loaded, for restoring or other CLI operations without friction - Mount repository snapshots on macOS/Linux - Requires [FUSE-T](https://www.fuse-t.org) or [macFUSE](https://macfuse.github.io) on macOS. Requires [FUSE](https://github.com/libfuse/libfuse) on Linux @@ -34,7 +34,7 @@ This is new and somewhat work-in-progress/beta software. You might possibly enco To install, grab a build artifact for your OS and architecture from the [repository's tags](https://tangled.org/devins.page/restray/tags). On MacOS you'll need to [remove quarantine](https://disable-gatekeeper.github.io#disabling-gatekeeper-for-one-application-only) from the `.app`, since I can't pay Apple $100/year to sign it. Sorry. -On MacOS, a restic binary is downloaded and bundled to the `.app` at build time, though I'd recommend installing restic to your PATH so you have the latest version. On Windows, Restray can download and update it's own restic binary itself if it isn't already in PATH. On Linux, you should install restic with your package manager. +On MacOS, a restic binary is downloaded and bundled to the `.app` at build time, though I'd recommend installing restic to your PATH so you have the latest version. On Windows and Linux, Restray can download and update it's own restic binary itself if it isn't already in PATH. _If you're a Nix/NixOS/Nix-darwin user, I also provide package/module in the project's [flake](https://tangled.org/devins.page/restray) for Linux and MacOS._ @@ -59,7 +59,7 @@ GUI-only settings, ignored by `restray daemon`. | Key | Type | Default | Description | | ------------------ | ------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `check_updates` | bool | `true` | **MacOS/Windows only.** Check for new Restray versions on startup and periodically. When an update is found, the version item in the menu changes to an update button | -| `manage_restic` | bool | `false` | **Windows only.** If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub. | +| `manage_restic` | bool | `false` | **Windows/Linux only.** If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub. | | `notifications` | string | `"none"` | `"all"` (success + errors), `"errors"` (errors only), or `"none"` (silent) | | `icon` | string | platform | `"color"`, `"mono"`, `"white"`, or `"black"`. Default is `"mono"` on MacOS/Windows and `"color"` elsewhere. `"mono"` follows OS light/dark mode. | | `schedule_display` | string | `"description"` | `"description"` (human-readable, e.g. "every 6 hours"), `"cron"` (raw expression, e.g. "0 \* \* \* \*"), `"last"` (time since last backup, e.g. "2 hours ago"), or `"none"` | diff --git a/cmd/restray/manage_other.go b/cmd/restray/manage_other.go index b3f0f42..3804c51 100644 --- a/cmd/restray/manage_other.go +++ b/cmd/restray/manage_other.go @@ -1,10 +1,9 @@ -//go:build !windows +//go:build !windows && !linux package main import "fyne.io/systray" -// macOS/Linux require system restic, the app never downloads or manages one const selfManagesRestic = false func downloadRestic(prefixedMenuItem) error { return nil } diff --git a/cmd/restray/manage_windows.go b/cmd/restray/manage_windows_linux.go similarity index 86% rename from cmd/restray/manage_windows.go rename to cmd/restray/manage_windows_linux.go index 7c6fa64..1bd3235 100644 --- a/cmd/restray/manage_windows.go +++ b/cmd/restray/manage_windows_linux.go @@ -1,9 +1,10 @@ -//go:build windows +//go:build windows || linux package main import ( "archive/zip" + "compress/bzip2" "fmt" "io" "log" @@ -23,8 +24,15 @@ const selfManagesRestic = true var downloading sync.Mutex +func resticBinaryName() string { + if runtime.GOOS == "windows" { + return "restic.exe" + } + return "restic" +} + func managedRestic() string { - p := filepath.Join(dataDir(), "restic.exe") + p := filepath.Join(dataDir(), resticBinaryName()) if _, err := os.Stat(p); err == nil { return p } @@ -92,8 +100,12 @@ func downloadRestic(mStatus prefixedMenuItem) error { return fmt.Errorf("failed to determine latest version") } - url := fmt.Sprintf("https://github.com/restic/restic/releases/download/v%s/restic_%s_%s_%s.zip", - version, version, runtime.GOOS, runtime.GOARCH) + ext := "bz2" + if runtime.GOOS == "windows" { + ext = "zip" + } + url := fmt.Sprintf("https://github.com/restic/restic/releases/download/v%s/restic_%s_%s_%s.%s", + version, version, runtime.GOOS, runtime.GOARCH, ext) log.Printf("downloading restic %s", version) mStatus.SetTitle("Downloading restic " + version + "...") @@ -108,39 +120,18 @@ func downloadRestic(mStatus prefixedMenuItem) error { return fmt.Errorf("download failed: %s", resp.Status) } - if err = extractResticZip(resp.Body); err == nil { + if err = extractRestic(resp.Body); err == nil { log.Printf("installed restic %s", version) resetResticPath() } return err } -func installBinary(r io.Reader) error { - p := filepath.Join(dataDir(), "restic.exe") - tmp, err := os.CreateTemp(filepath.Dir(p), "restic-download-*") - if err != nil { - return err +func extractRestic(r io.Reader) error { + if runtime.GOOS != "windows" { + return installBinary(bzip2.NewReader(r)) } - tmpName := tmp.Name() - if _, err := io.Copy(tmp, r); err != nil { - tmp.Close() - os.Remove(tmpName) - return err - } - tmp.Close() - if err := os.Chmod(tmpName, 0755); err != nil { - os.Remove(tmpName) - return err - } - os.Remove(p) - if err := os.Rename(tmpName, p); err != nil { - os.Remove(tmpName) - return err - } - return nil -} -func extractResticZip(r io.Reader) error { tmp, err := os.CreateTemp("", "restray-download-*.zip") if err != nil { return err @@ -171,6 +162,31 @@ func extractResticZip(r io.Reader) error { return fmt.Errorf("restic binary not found in zip") } +func installBinary(r io.Reader) error { + p := filepath.Join(dataDir(), resticBinaryName()) + tmp, err := os.CreateTemp(filepath.Dir(p), "restic-download-*") + if err != nil { + return err + } + tmpName := tmp.Name() + if _, err := io.Copy(tmp, r); err != nil { + tmp.Close() + os.Remove(tmpName) + return err + } + tmp.Close() + if err := os.Chmod(tmpName, 0755); err != nil { + os.Remove(tmpName) + return err + } + os.Remove(p) + if err := os.Rename(tmpName, p); err != nil { + os.Remove(tmpName) + return err + } + return nil +} + func checkResticUpdate(resticPath string, autoUpdate bool, mUpdate *systray.MenuItem, mStatus prefixedMenuItem, onUpdated func()) { current := resticVersion(resticPath) latest, err := latestResticVersion() diff --git a/nix/shared.nix b/nix/shared.nix index cef77fe..47518fd 100644 --- a/nix/shared.nix +++ b/nix/shared.nix @@ -12,7 +12,7 @@ manageRestic = lib.mkOption { type = lib.types.bool; default = false; - description = "Windows only. If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub."; + description = "Windows/Linux only. If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub."; }; notifications = lib.mkOption { type = lib.types.str; -- 2.51.2