restictray/main.go

218 lines
5.5 KiB
Go

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()
}