mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-24 18:06:41 +00:00
feat: initial Windows OS support
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
resticui
|
||||
resticui-*
|
||||
dist
|
||||
resticui.exe
|
||||
@@ -12,6 +12,12 @@ The goals of this project are:
|
||||
* Easy to pull back the curtain: all common operations should be possible from the UI, but it should be easy to drop down to the command line and use restic directly if needed.
|
||||
* Lightweight: your backup orchestration should blend into the background. The web UI binary is fully self contained as a single executable and the binary is <20 MB with very light memory overhead at runtime.
|
||||
|
||||
OS Support
|
||||
|
||||
* Linux
|
||||
* MacOS (Darwin)
|
||||
* Windows (note: must be run as administrator on first execution to install the restic binary in Program Files).
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Running
|
||||
|
||||
@@ -5,11 +5,11 @@ go 1.21.3
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.3
|
||||
github.com/gitploy-io/cronexpr v0.2.2
|
||||
github.com/google/renameio v1.0.1
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/natefinch/atomic v1.0.1
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
go.uber.org/zap v1.26.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4
|
||||
@@ -20,6 +20,7 @@ require (
|
||||
require (
|
||||
github.com/daaku/go.zipexe v1.0.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/renameio v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
|
||||
@@ -31,6 +31,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -14,16 +15,20 @@ var (
|
||||
EnvVarBinPath = "RESTICUI_RESTIC_BIN_PATH"
|
||||
)
|
||||
|
||||
// ConfigFilePath
|
||||
// - *nix systems use $XDG_CONFIG_HOME/resticui/config.json
|
||||
// - windows uses %APPDATA%/resticui/config.json
|
||||
func ConfigFilePath() string {
|
||||
if val := os.Getenv(EnvVarConfigPath); val != "" {
|
||||
return val
|
||||
}
|
||||
if val := os.Getenv("XDG_CONFIG_HOME"); val != "" {
|
||||
return path.Join(val, "resticui/config.json")
|
||||
}
|
||||
return path.Join(getHomeDir(), ".config/resticui/config.json")
|
||||
|
||||
return path.Join(getConfigDir(), "resticui/config.json")
|
||||
}
|
||||
|
||||
// DataDir
|
||||
// - *nix systems use $XDG_DATA_HOME/resticui
|
||||
// - windows uses %APPDATA%/resticui/data
|
||||
func DataDir() string {
|
||||
if val := os.Getenv(EnvVarDataDir); val != "" {
|
||||
return val
|
||||
@@ -31,6 +36,10 @@ func DataDir() string {
|
||||
if val := os.Getenv("XDG_DATA_HOME"); val != "" {
|
||||
return path.Join(val, "resticui")
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
return path.Join(getConfigDir(), "resticui/data")
|
||||
}
|
||||
return path.Join(getHomeDir(), ".local/share/resticui")
|
||||
}
|
||||
|
||||
@@ -58,3 +67,11 @@ func getHomeDir() string {
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
||||
func getConfigDir() string {
|
||||
cfgDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("couldn't determine config directory: %v", err))
|
||||
}
|
||||
return cfgDir
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -8,13 +9,13 @@ import (
|
||||
"sync"
|
||||
|
||||
v1 "github.com/garethgeorge/resticui/gen/go/v1"
|
||||
"github.com/google/renameio"
|
||||
"github.com/natefinch/atomic"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
type JsonFileStore struct {
|
||||
Path string
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ ConfigStore = &JsonFileStore{}
|
||||
@@ -32,7 +33,7 @@ func (f *JsonFileStore) Get() (*v1.Config, error) {
|
||||
}
|
||||
|
||||
var config v1.Config
|
||||
|
||||
|
||||
if err = protojson.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
@@ -53,8 +54,8 @@ func (f *JsonFileStore) Update(config *v1.Config) error {
|
||||
}
|
||||
|
||||
data, err := protojson.MarshalOptions{
|
||||
Indent: " ",
|
||||
Multiline: true,
|
||||
Indent: " ",
|
||||
Multiline: true,
|
||||
EmitUnpopulated: true,
|
||||
}.Marshal(config)
|
||||
if err != nil {
|
||||
@@ -66,7 +67,7 @@ func (f *JsonFileStore) Update(config *v1.Config) error {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
err = renameio.WriteFile(f.Path, data, 0644)
|
||||
err = atomic.WriteFile(f.Path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package resticinstaller
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package resticinstaller
|
||||
|
||||
func withFlock(lock string, do func() error) error {
|
||||
// TODO: windows file locking. Not a major issue as locking is only needed for test runs.
|
||||
return do()
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package resticinstaller
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/bzip2"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -28,10 +30,13 @@ var (
|
||||
)
|
||||
|
||||
func resticDownloadURL(version string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/restic_%v_windows_%v.zip", version, version, runtime.GOARCH)
|
||||
}
|
||||
return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/restic_%v_%v_%v.bz2", version, version, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
func downloadFile(url string, path string) error {
|
||||
func downloadFile(url string, downloadPath string) error {
|
||||
// Download ur as a file and save it to path
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
@@ -42,19 +47,38 @@ func downloadFile(url string, path string) error {
|
||||
var body io.Reader = resp.Body
|
||||
if strings.HasSuffix(url, ".bz2") {
|
||||
body = bzip2.NewReader(resp.Body)
|
||||
} else if strings.HasSuffix(url, ".zip") {
|
||||
var fullBody bytes.Buffer
|
||||
_, err := io.Copy(&fullBody, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy response body to buffer: %w", err)
|
||||
}
|
||||
|
||||
archive, err := zip.NewReader(bytes.NewReader(fullBody.Bytes()), int64(fullBody.Len()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open zip archive: %w", err)
|
||||
}
|
||||
|
||||
if len(archive.File) != 1 {
|
||||
return fmt.Errorf("expected zip archive to contain exactly one file, got %v", len(archive.File))
|
||||
}
|
||||
body, err = archive.File[0].Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open zip archive file %v: %w", archive.File[0].Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := os.Create(path)
|
||||
out, err := os.Create(downloadPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create file %v: %w", path, err)
|
||||
return fmt.Errorf("create file %v: %w", downloadPath, err)
|
||||
}
|
||||
defer out.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create file %v: %w", path, err)
|
||||
return fmt.Errorf("create file %v: %w", downloadPath, err)
|
||||
}
|
||||
_, err = io.Copy(out, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy response body to file %v: %w", path, err)
|
||||
return fmt.Errorf("copy response body to file %v: %w", downloadPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -115,6 +139,13 @@ func FindOrInstallResticBinary() (string, error) {
|
||||
|
||||
// Check for restic installation in data directory.
|
||||
resticInstallPath := path.Join(config.DataDir(), fmt.Sprintf("restic-%v", RequiredResticVersion))
|
||||
if runtime.GOOS == "windows" {
|
||||
programFiles := os.Getenv("programfiles(x86)")
|
||||
if programFiles == "" {
|
||||
programFiles = os.Getenv("programfiles")
|
||||
}
|
||||
resticInstallPath = path.Join(programFiles, "restic", fmt.Sprintf("restic-%v.exe", RequiredResticVersion))
|
||||
}
|
||||
if _, err := os.Stat(resticInstallPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("could not stat restic binary at %v: %w", resticBin, err)
|
||||
@@ -125,7 +156,7 @@ func FindOrInstallResticBinary() (string, error) {
|
||||
}
|
||||
didTryInstall = true
|
||||
|
||||
zap.S().Infof("Installing restic %v...", RequiredResticVersion)
|
||||
zap.S().Infof("Installing restic %v to %v...", resticInstallPath, RequiredResticVersion)
|
||||
if err := installResticIfNotExists(resticInstallPath); err != nil {
|
||||
return "", fmt.Errorf("install restic: %w", err)
|
||||
}
|
||||
|
||||
@@ -467,7 +467,12 @@ func WithEnv(env ...string) GenericOption {
|
||||
}
|
||||
}
|
||||
|
||||
var EnvToPropagate = []string{"PATH", "HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME"}
|
||||
var EnvToPropagate = []string{
|
||||
// *nix systems
|
||||
"PATH", "HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME",
|
||||
// windows
|
||||
"APPDATA", "LOCALAPPDATA",
|
||||
}
|
||||
|
||||
func WithPropagatedEnvVars(extras ...string) GenericOption {
|
||||
var extension []string
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "RESTICUI_BUILD_VERSION=$(git describe --tags --abbrev=0) parcel serve src/index.html",
|
||||
"start": "parcel serve src/index.html",
|
||||
"build": "RESTICUI_BUILD_VERSION=$(git describe --tags --abbrev=0) parcel build src/index.html",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user