Skip to content

Commit

Permalink
feat: initial Windows OS support
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Dec 7, 2023
1 parent 063f086 commit f048cbf
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
resticui
resticui-*
dist
resticui.exe
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
25 changes: 21 additions & 4 deletions internal/config/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path"
"runtime"
"strings"
)

Expand All @@ -14,23 +15,31 @@ 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
}
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")
}

Expand Down Expand Up @@ -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
}
13 changes: 7 additions & 6 deletions internal/config/jsonstore.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package config

import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"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{}
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions internal/resticinstaller/flock.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build linux || darwin
// +build linux darwin

package resticinstaller

import (
Expand Down
9 changes: 9 additions & 0 deletions internal/resticinstaller/flockwindows.go
Original file line number Diff line number Diff line change
@@ -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()
}
43 changes: 37 additions & 6 deletions internal/resticinstaller/resticinstaller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package resticinstaller

import (
"archive/zip"
"bytes"
"compress/bzip2"
"errors"
"fmt"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/restic/restic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down

0 comments on commit f048cbf

Please sign in to comment.