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() *systray.MenuItem { mQuit := systray.AddMenuItem("Quit", "Quit Restictray") go func() { <-mQuit.ClickedCh log.Info().Msg("Requesting quit") wrapper.Cleanup() systray.Quit() }() return mQuit } 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, mnu *resticmenu) { 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 } mnu.latestSnapshot.SetTitle("Latest: " + snapshot.Id + " @ " + snapshot.ShortTime()) mnu.nextSnapshot.SetTitle(msg) mnu.backupNowSucceeded() } func isBackupNeeded(cnf *restic.Config) bool { nextTime := wrapper.LastSnapshot().Time.Add(time.Duration(cnf.BackupTimeInHours) * time.Hour) return time.Now().After(nextTime) } type resticmenu struct { latestSnapshot *systray.MenuItem nextSnapshot *systray.MenuItem backupNow *systray.MenuItem browse *systray.MenuItem logs *systray.MenuItem config *systray.MenuItem quit *systray.MenuItem } func (mnu *resticmenu) backupNowFailed(err error) { mnu.backupNow.Enable() mnu.backupNow.SetTitle("‼️ Backup now - latest failed!") } func (mnu *resticmenu) backupNowSucceeded() { mnu.backupNow.Enable() mnu.backupNow.SetTitle("Backup now") } // 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 } mnu := buildMenu() go updateSnapshots(cnf, mnu) backupCheckTime := make(chan bool, 1) hourlyBackupCheckFn := func() { time.Sleep(1 * time.Hour) backupCheckTime <- true } go hourlyBackupCheckFn() for { select { case <-mnu.config.ClickedCh: onOpenConfig() case <-mnu.logs.ClickedCh: onOpenLogs() case <-backupCheckTime: onBackupCheck(mnu, cnf, hourlyBackupCheckFn) case <-mnu.backupNow.ClickedCh: onBackupNow(mnu, cnf) case <-mnu.browse.ClickedCh: onBroseBackups(cnf) } } } func onBroseBackups(cnf *restic.Config) { go mountBackups(cnf) } func onOpenLogs() error { return restic.OpenLogs() } func onOpenConfig() error { return restic.OpenConfigFile() } func onBackupNow(mnu *resticmenu, cnf *restic.Config) { go backupNow(mnu, cnf, func() { updateSnapshots(cnf, mnu) }, mnu.backupNowFailed) } func onBackupCheck(mnu *resticmenu, cnf *restic.Config, hourlyBackupCheckFn func()) { if !mnu.backupNow.Disabled() && isBackupNeeded(cnf) { go backupNow(mnu, cnf, func() { updateSnapshots(cnf, mnu) go hourlyBackupCheckFn() }, func(err error) { mnu.backupNowFailed(err) go hourlyBackupCheckFn() }) } else { log.Info().Msg("Backup not yet needed/in progress/impossible") go hourlyBackupCheckFn() } } func buildMenu() *resticmenu { mnu := &resticmenu{} mnu.latestSnapshot = systray.AddMenuItem("Latest: (Fetching...)", "Latest Restic snapshot") mnu.nextSnapshot = systray.AddMenuItem("Next @ (Unknown)", "Future Restic snapshot") systray.AddSeparator() mnu.backupNow = systray.AddMenuItem("Backup now", "Backup now") mnu.backupNow.Disable() mnu.browse = systray.AddMenuItem("Browse backups in Finder...", "Mount and browse backups") mnu.logs = systray.AddMenuItem("Open logfile...", "Open logging file") mnu.config = systray.AddMenuItem("Open config file...", "Open config file") systray.AddSeparator() mnu.quit = addMenuQuit() return mnu } // 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 *resticmenu, cnf *restic.Config, onSuccessFn func(), onErrFn func(error)) { log.Debug().Msg("Backup triggered") mnu.backupNow.SetTitle("🔄 Backup in progress...") mnu.backupNow.Disable() err := wrapper.Backup(cnf) if err != nil { log.Err(err).Msg("backup error") onErrFn(err) return } onSuccessFn() }