···11# Restray
2233-A simple Go application that runs in the background/system tray, that can schedule and manage [restic](https://restic.net) backups and profiles easily. It can also use multiple profiles with different schedules and settings at once. It's also pretty configurable, see config below.
33+Restray is a simple, handy GUI/CLI/daemon to manage and schedule restic backups with ease. Supports multiple profiles and quite a bit of configuration, along with other features (see feature list below).
4455Restray is intended for desktops. Runs on MacOS, Windows, and Linux. If you're on GNOME, you will need to install [this extension](https://extensions.gnome.org/extension/615/appindicator-support) to interact with and use the app since GNOME doesn't natively support the system tray.
66···2828 - I'd recommend something like [restic-browser](https://github.com/emuell/restic-browser) to browse on Windows, or just open the Shell and use the CLI
2929- Full Disk Access detection and prompt on macOS
3030- Config file hot-reloading
3131+- Can runs headless as CLI or daemon, not just the tray GUI
31323233## Installation
3334···3839### Building from source
39404041On [Nix(OS)](https://nixos.org)/nix-darwin, run `nix build` (or add this repo to your flake and simply install the package). Otherwise, install the dependencies from `flake.nix`'s devshell and run `just build` or `just package`. Both default to your current platform. You can also pass a target to cross-compile (e.g. `just build linux amd64`, `just package windows`).
4242+4343+## CLI / daemon
4444+4545+Restray also works without the GUI on MacOS/Linux. Run `restray daemon` to run just the built-in scheduler (no tray GUI), or use one-shot commands for other things. Run `restray --help` for the full command list.
4646+4747+Linux users: systemd unit files are included (`packaging/linux/restray.service` for system-wide, `packaging/linux/restray-user.service` for per-user). If you use NixOS, a `services.restray` module is also exported from the flake :)
41484249## Configuration
4350
+90-9
cmd/restray/cli.go
···11package main
2233import (
44+ "errors"
45 "flag"
56 "fmt"
67 "os"
···89 "os/signal"
910 "runtime"
1011 "strings"
1212+ "sync"
1113 "sync/atomic"
1214 "syscall"
1515+1616+ "github.com/robfig/cron/v3"
1317)
14181519func cliUsage() {
1620 fmt.Fprintf(os.Stderr, `Usage: restray [command]
17211822Commands:
1919- (no command) Launch GUI with built-in scheduler
2323+ (no command) Launch restray (GUI + built-in scheduler)
2424+ daemon Launch restray (built-in scheduler only)
2025 schedule [-p name] Run full schedule with hooks
2121- op|operation backup [-p name] Run backup
2222- op prune [-p name] Prune old snapshots
2323- op check [-p name] Verify repository integrity
2424- op unlock [-p name] Remove stale locks
2525- op shell [-p name] Open shell with restic environment
2626- op mount [-p name] Mount repository (Ctrl+C to unmount)
2727- op pre-hook [-p name] Run pre-hook command
2828- op post-hook [-p name] Run post-hook command
2626+ operation backup [-p name] Run backup
2727+ operation prune [-p name] Prune old snapshots
2828+ operation check [-p name] Verify repository integrity
2929+ operation unlock [-p name] Remove stale locks
3030+ operation shell [-p name] Open shell with restic environment
3131+ operation mount [-p name] Mount repository (Ctrl+C to unmount)
3232+ operation pre-hook [-p name] Run pre-hook command
3333+ operation post-hook [-p name] Run post-hook command
2934 configure config Create/open config file in editor
3035 configure env [-p name] Create/open env file in editor
3136···303308 return 0
304309}
305310311311+func buildDaemonSchedule(wg *sync.WaitGroup) (*cron.Cron, error) {
312312+ sched := cron.New()
313313+ for _, prof := range loadConfig().Profiles {
314314+ if prof.Schedule.Cron == "" {
315315+ continue
316316+ }
317317+ name := prof.displayName()
318318+ _, err := sched.AddFunc(prof.Schedule.Cron, func() {
319319+ current := loadConfig()
320320+ _, prof, err := resolveProfile(current, name)
321321+ if err != nil {
322322+ fmt.Fprintf(os.Stderr, "[%s] skipping scheduled run: %v\n", name, err)
323323+ return
324324+ }
325325+ if skipForBattery(prof) {
326326+ fmt.Fprintf(os.Stderr, "[%s] skipping scheduled run: on battery power\n", prof.displayName())
327327+ return
328328+ }
329329+ wg.Add(1)
330330+ defer wg.Done()
331331+ fmt.Fprintf(os.Stderr, "[%s] running scheduled job\n", prof.displayName())
332332+ cliSchedule(prof)
333333+ })
334334+ if err != nil {
335335+ return nil, fmt.Errorf("invalid cron expression %q for profile %q: %w", prof.Schedule.Cron, name, err)
336336+ }
337337+ }
338338+ if len(sched.Entries()) == 0 {
339339+ return nil, errors.New("no profiles have a schedule configured")
340340+ }
341341+ return sched, nil
342342+}
343343+344344+func cliDaemon() int {
345345+ p, _ := findRestic()
346346+ if p == "" {
347347+ fmt.Fprintln(os.Stderr, "error: restic not found")
348348+ return 1
349349+ }
350350+351351+ var wg sync.WaitGroup
352352+ sched, err := buildDaemonSchedule(&wg)
353353+ if err != nil {
354354+ fmt.Fprintln(os.Stderr, "error:", err)
355355+ return 1
356356+ }
357357+358358+ sigCh := make(chan os.Signal, 1)
359359+ signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
360360+ sched.Start()
361361+ fmt.Fprintln(os.Stderr, "Scheduler running, press Ctrl+C to stop")
362362+363363+ for sig := range sigCh {
364364+ if sig != syscall.SIGHUP {
365365+ break
366366+ }
367367+ next, err := buildDaemonSchedule(&wg)
368368+ if err != nil {
369369+ fmt.Fprintln(os.Stderr, "error: reload failed, keeping previous schedule:", err)
370370+ continue
371371+ }
372372+ <-sched.Stop().Done()
373373+ sched = next
374374+ sched.Start()
375375+ fmt.Fprintln(os.Stderr, "Scheduler reloaded")
376376+ }
377377+378378+ <-sched.Stop().Done()
379379+ fmt.Fprintln(os.Stderr, "Waiting for running jobs to finish...")
380380+ wg.Wait()
381381+ return 0
382382+}
383383+306384func cliResolve(args []string) (Profile, error) {
307385 name, _ := cliParseFlags(args)
308386 cfg := loadConfig()
···317395 }
318396319397 switch args[0] {
398398+ case "daemon":
399399+ return cliDaemon()
400400+320401 case "schedule":
321402 prof, err := cliResolve(args[1:])
322403 if err != nil {