package main import ( "brainbaking.com/restictray/restic" "fyne.io/systray" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "gopkg.in/natefinch/lumberjack.v2" "os" "strconv" "time" ) // I'm ignoring go threading issues here; assume no clicks happen at the same time. var wrapper *restic.Wrapper func addMenuQuit() { mQuit := systray.AddMenuItem("Quit", "Quit Restictray") go func() { <-mQuit.ClickedCh log.Info().Msg("Requesting quit") wrapper.Cleanup() systray.Quit() }() } func addMenuError(err error) { mnuError := systray.AddMenuItem("❗ "+err.Error(), "Restictray Error") go func() { <-mnuError.ClickedCh restic.OpenLogs() }() } func handleError(err error) { log.Err(err).Msg("") systray.ResetMenu() addMenuError(err) systray.AddSeparator() addMenuQuit() } func main() { // init and setup logging rollingLog := &lumberjack.Logger{ Filename: restic.LogFile(), MaxSize: 500, // megabytes MaxBackups: 3, } if restic.IsDev() { zerolog.SetGlobalLevel(zerolog.DebugLevel) log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } else { zerolog.SetGlobalLevel(zerolog.InfoLevel) log.Logger = zerolog.New(rollingLog).With().Timestamp().Logger() } wrapper = &restic.Wrapper{} // bootstrap systray (this is a blocking call, second func is onExit) systray.Run(onSystrayReady, func() {}) } // Updates snapshot information in restic wrapper and menu labels. This is a blocking call. func updateSnapshots(cnf *restic.Config, mnuLatest *systray.MenuItem, mnuNext *systray.MenuItem, mnuBackupNow *systray.MenuItem) { err := wrapper.UpdateLatestSnapshots(cnf) if err != nil { handleError(err) return } snapshot := wrapper.LastSnapshot() msg := strconv.Itoa(len(wrapper.LatestSnapshots)) + " snapshots. Next in " + strconv.Itoa(cnf.BackupTimeInHours) + " hour(s)" if isBackupNeeded(cnf) { msg = "⚠️ Overdue - " + msg } mnuLatest.SetTitle("Latest: " + snapshot.Id + " @ " + snapshot.ShortTime()) mnuNext.SetTitle(msg) mnuBackupNow.Enable() mnuBackupNow.SetTitle("Backup now") } func isBackupNeeded(cnf *restic.Config) bool { nextTime := wrapper.LastSnapshot().Time.Add(time.Duration(cnf.BackupTimeInHours) * time.Hour) return time.Now().After(nextTime) } // See https://github.com/fyne-io/systray/tree/master/example for more examples func onSystrayReady() { systray.SetTemplateIcon(restic.TimeMachineIcon, restic.TimeMachineIcon) systray.SetTooltip("Restictray") cnf, err := restic.ReadConfig() if err != nil { handleError(err) return } mnuLatestSnapshot := systray.AddMenuItem("Latest: (Fetching...)", "Latest Restic snapshot") mnuNextSnapshot := systray.AddMenuItem("Next @ (Unknown)", "Future Restic snapshot") systray.AddSeparator() mnuBackupNow := systray.AddMenuItem("Backup now", "Backup now") mnuBackupNow.Disable() mnuBrowse := systray.AddMenuItem("Browse backups in Finder...", "Mount and browse backups") mnuLogs := systray.AddMenuItem("Open logfile...", "Open logging file") mnuConfig := systray.AddMenuItem("Open config file...", "Open config file") systray.AddSeparator() addMenuQuit() go updateSnapshots(cnf, mnuLatestSnapshot, mnuNextSnapshot, mnuBackupNow) backupCheckTime := make(chan bool, 1) backupCheckFn := func() { time.Sleep(1 * time.Hour) backupCheckTime <- true } go backupCheckFn() for { select { case <-mnuConfig.ClickedCh: restic.OpenConfigFile() case <-mnuLogs.ClickedCh: restic.OpenLogs() case <-backupCheckTime: if !mnuBackupNow.Disabled() && isBackupNeeded(cnf) { go backupNow(mnuBackupNow, cnf, func() { updateSnapshots(cnf, mnuLatestSnapshot, mnuNextSnapshot, mnuBackupNow) go backupCheckFn() }) } else { log.Info().Msg("Backup not yet needed/in progress/impossible") go backupCheckFn() } case <-mnuBackupNow.ClickedCh: go backupNow(mnuBackupNow, cnf, func() { updateSnapshots(cnf, mnuLatestSnapshot, mnuNextSnapshot, mnuBackupNow) }) case <-mnuBrowse.ClickedCh: go mountBackups(cnf) } } } // Allows for browsing inside Restic backups by the "mount" command, using MacFuse internally. This is a NON-blocking call. // Restic allows backing up and consulting snapshots while mounted func mountBackups(cnf *restic.Config) { err := wrapper.MountBackups(cnf) if err != nil { handleError(err) } } // Disables the menu and triggers a restic backup. This is a blocking call. func backupNow(mnu *systray.MenuItem, cnf *restic.Config, onDoneFn func()) { log.Debug().Msg("Backup triggered") mnu.SetTitle("🔄 Backup in progress...") mnu.Disable() err := wrapper.Backup(cnf) if err != nil { handleError(err) } onDoneFn() }