From 4f03427c91e87157414c18a47588cb6b6ebaf9ec Mon Sep 17 00:00:00 2001 From: Gareth George Date: Mon, 25 Nov 2024 21:44:46 -0800 Subject: [PATCH] chore: fix resticinstaller for windows tests --- Dockerfile.alpine | 2 +- Dockerfile.scratch | 2 +- README.md | 25 +++--- internal/resticinstaller/resticinstaller.go | 98 ++++++++++++--------- 4 files changed, 69 insertions(+), 58 deletions(-) diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 516b7529..307eef68 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -4,7 +4,7 @@ RUN apk --no-cache add tini ca-certificates curl bash rclone openssh tzdata dock RUN mkdir -p /tmp COPY backrest /backrest RUN /backrest --install-deps-only -RUN mkdir -p /bin && mv /root/.local/share/backrest/* /bin +RUN mkdir -p /bin && mv /root/.local/share/backrest/restic /bin/restic ENTRYPOINT ["/sbin/tini", "--"] CMD ["/backrest", "--bind-address", ":9898"] diff --git a/Dockerfile.scratch b/Dockerfile.scratch index 950883ff..cd11f9c1 100644 --- a/Dockerfile.scratch +++ b/Dockerfile.scratch @@ -3,7 +3,7 @@ RUN apk add --no-cache ca-certificates tini-static RUN mkdir /tmp-orig COPY backrest /backrest RUN /backrest --install-deps-only -RUN mkdir -p /bin && mv /root/.local/share/backrest/* /bin +RUN mkdir -p /bin && mv /root/.local/share/backrest/restic /bin/restic FROM scratch LABEL org.opencontainers.image.source="https://github.com/garethgeorge/backrest" diff --git a/README.md b/README.md index d0af8e2e..3299f083 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Download options Backrest is accessible from a web browser. By default it binds to `127.0.0.1:9898` and can be accessed at `http://localhost:9898`. Change the port with the `BACKREST_PORT` environment variable e.g. `BACKREST_PORT=0.0.0.0:9898 backrest` to listen on all network interfaces. On first startup backrest will prompt you to create a default username and password, this can be changed later in the settings page. > [!Note] -> Backrest installs a specific restic version to ensure that it is compatible. If you wish to use a different version of restic OR if you would prefer to install restic manually, use the `BACKREST_RESTIC_COMMAND` environment variable to specify the path of your restic install. +> Backrest will use your system install of restic if it is available and matches Backrest's required version. Otherwise it will download and install a compatible version of restic in its data directory. Backrest will keep restic up to date with the latest version. You force use of a specific restic binary (or non-standard version) by setting the `BACKREST_RESTIC_COMMAND` environment variable to the path of your restic binary. ## Running with Docker Compose @@ -142,7 +142,7 @@ Create a systemd service file at `/etc/systemd/system/backrest.service` with the ```ini [Unit] -Description=ResticWeb +Description=Backrest After=network.target [Service] @@ -189,15 +189,14 @@ Backrest is provided as a [homebrew tap](https://github.com/garethgeorge/homebre brew tap garethgeorge/homebrew-backrest-tap brew install backrest brew services start backrest +# optionally, install restic +brew install restic ``` This tap uses [Brew services](https://github.com/Homebrew/homebrew-services) to launch and manage Backrest's lifecycle. Backrest will launch on startup and run on port ':9898` by default. > [!NOTE] -> You may need to grant Full Disk Access to your restic install. To do this, go to `System Preferences > Security & Privacy > Privacy > Full Disk Access` and add the path to your restic install which is typically ~/.local/share/backrest/restic . - -> [!NOTE] -> You may optionally install `restic` through homebrew as well, but you may need to regrant Full Disk Access to the homebrew managed binary on each update. You should ensure that you update backrest and restic together if using homebrew to manage both dependencies. +> You may need to grant Full Disk Access to backrest. To do this, go to `System Preferences > Security & Privacy > Privacy > Full Disk Access` and add the path to backrest (typically /usr/local/bin/backrest). #### Manually using the install script @@ -220,19 +219,15 @@ The install script will: Read the script before running it to make sure you are comfortable with these operations. -#### Manually - -If setting up Backrest manually, it is recommended to install the binary to `/usr/local/bin` and run it manually. You can also create a launch agent to run it on startup or may run it manually when needed. - ## Running on Windows -Download a Windows release from the [releases page](https://github.com/garethgeorge/backrest/releases) and install it to `C:\Program Files\Backrest\backrest.exe` (create the path if it does not exist). The binary should be run as administrator on first launch, otherwise the restic installation will fail and the process will terminate. +#### Windows Installer -To run the binary on login, create a shortcut to the binary and place it in the `shell:startup` folder. See [this windows support article](https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd) for more details. +Download a the Windows installer for your architecture from the [releases page](https://github.com/garethgeorge/backrest/releases). The installer is named Backrest-setup-[arch].exe. Run the installer and follow the prompts. -> [!WARNING] -> * If you receive filesystem errors, you may need to run Backrest as an administrator for full filesystem access. -> * Backrest is **not** tested on Windows to the same extent as Linux and macOS. Some features may not work as expected. +The installer will place backrest and a GUI tray application to monitor backrest in `%localappdata%\Programs\Backrest\`. The GUI tray application will start on login by default. + +> [!NOTE] You can optionally override the default port of the installation by using PowerShell to run the installer with the `BACKREST_PORT` environment variable set to the desired port. E.g. to run backrest on port 8080, run the following command in PowerShell: `BACKREST_PORT=:8080 .\Backrest-setup-x86_64.exe` # Configuration diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index d7f200ab..054825a6 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -24,8 +24,9 @@ var ( var ( RequiredResticVersion = "0.17.3" - findResticMu sync.Mutex - didTryInstall bool + tryFindRestic sync.Once + findResticErr error + foundResticPath string ) func getResticVersion(binary string) (string, error) { @@ -94,12 +95,6 @@ func verify(sha256 string) error { } func installResticIfNotExists(resticInstallPath string) error { - lock := flock.New(filepath.Join(filepath.Dir(resticInstallPath), "install.lock")) - if err := lock.Lock(); err != nil { - return fmt.Errorf("lock %v: %w", lock.Path(), err) - } - defer lock.Unlock() - if _, err := os.Stat(resticInstallPath); err == nil { // file is now installed, probably by another process. We can return. return nil @@ -148,25 +143,40 @@ func removeOldVersions(installDir string) { } } -// FindOrInstallResticBinary first tries to find the restic binary if provided as an environment variable. Otherwise it downloads restic if not already installed. -func FindOrInstallResticBinary() (string, error) { - findResticMu.Lock() - defer findResticMu.Unlock() +func installResticHelper(resticInstallPath string) { + if _, err := os.Stat(resticInstallPath); err == nil { + zap.S().Infof("replacing restic binary in data dir due to failed check: %w", err) + if err := os.Remove(resticInstallPath); err != nil { + zap.S().Errorf("failed to remove old restic binary %v: %v", resticInstallPath, err) + } + } + zap.S().Infof("downloading restic %v to %v...", RequiredResticVersion, resticInstallPath) + if err := installResticIfNotExists(resticInstallPath); err != nil { + zap.S().Errorf("failed to install restic %v: %v", RequiredResticVersion, err) + return + } + zap.S().Infof("installed restic %v", RequiredResticVersion) + + // TODO: this check is no longer needed, remove it after a few releases. + removeOldVersions(path.Dir(resticInstallPath)) +} + +func tryFindOrInstall() (string, error) { // Check if restic is provided. - resticBin := env.ResticBinPath() - if resticBin != "" { - if err := assertResticVersion(resticBin); err != nil { - zap.S().Warnf("restic binary %q may not be supported by backrest", resticBin, err) + resticBinOverride := env.ResticBinPath() + if resticBinOverride != "" { + if err := assertResticVersion(resticBinOverride); err != nil { + zap.S().Warnf("restic binary %q may not be supported by backrest: %v", resticBinOverride, err) } - if _, err := os.Stat(resticBin); err != nil { + if _, err := os.Stat(resticBinOverride); err != nil { if !errors.Is(err, os.ErrNotExist) { - return "", fmt.Errorf("stat(%v): %w", resticBin, err) + return "", fmt.Errorf("check if restic binary exists at %v: %v", resticBinOverride, err) } - return "", fmt.Errorf("no binary found at path %v: %w", resticBin, ErrResticNotFound) + return "", fmt.Errorf("no restic binary found at %v", resticBinOverride) } - return resticBin, nil + return resticBinOverride, nil } // Search the PATH for the specific restic version. @@ -180,40 +190,46 @@ func FindOrInstallResticBinary() (string, error) { } // Check for restic installation in data directory. - resticInstallPath := path.Join(env.DataDir(), "restic") + var resticInstallPath string if runtime.GOOS == "windows" { // on windows use a path relative to the executable. - resticInstallPath, _ = filepath.Abs(path.Join(path.Dir(os.Args[0]), "restic")) + resticInstallPath, _ = filepath.Abs(path.Join(path.Dir(os.Args[0]), "restic.exe")) + } else { + resticInstallPath = filepath.Join(env.DataDir(), "restic") } - - if err := os.MkdirAll(path.Dir(resticInstallPath), 0700); err != nil { + if err := os.MkdirAll(filepath.Dir(resticInstallPath), 0700); err != nil { return "", fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) } // Install restic if not found OR if the version is not the required version if err := assertResticVersion(resticInstallPath); err != nil { - if _, err := os.Stat(resticInstallPath); err == nil { - zap.S().Infof("reinstalling restic binary in data dir due to failed checks: %w", err) - if err := os.Remove(resticInstallPath); err != nil { - return "", fmt.Errorf("remove old restic binary %v: %w", resticInstallPath, err) - } + lock := flock.New(filepath.Join(filepath.Dir(resticInstallPath), "install.lock")) + if err := lock.Lock(); err != nil { + return "", fmt.Errorf("acquire lock on restic install dir %v: %v", lock.Path(), err) } + defer lock.Unlock() - if didTryInstall { - return "", fmt.Errorf("already tried to install: %w", ErrResticNotFound) + // Check again after acquiring the lock. + if err := assertResticVersion(resticInstallPath); err != nil { + installResticHelper(resticInstallPath) } - didTryInstall = true - - zap.S().Infof("downloading restic %v to %v...", RequiredResticVersion, resticInstallPath) - if err := installResticIfNotExists(resticInstallPath); err != nil { - return "", fmt.Errorf("install restic: %w", err) - } - zap.S().Infof("installed restic %v", RequiredResticVersion) - - // TODO: this check is no longer needed, remove it after a few releases. - removeOldVersions(path.Dir(resticInstallPath)) } zap.S().Infof("restic binary %v in data dir will be used as no system install matching required version %v is found", resticInstallPath, RequiredResticVersion) return resticInstallPath, nil } + +// FindOrInstallResticBinary first tries to find the restic binary if provided as an environment variable. Otherwise it downloads restic if not already installed. +func FindOrInstallResticBinary() (string, error) { + tryFindRestic.Do(func() { + foundResticPath, findResticErr = tryFindOrInstall() + }) + + if findResticErr != nil { + return "", findResticErr + } + if foundResticPath == "" { + return "", ErrResticNotFound + } + return foundResticPath, nil +}