You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
6.0 KiB
234 lines
6.0 KiB
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 handleFatalError(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.
|
|
// This should be resilient as it's frequently called.
|
|
func updateSnapshots(cnf *restic.Config, mnu *resticmenu) {
|
|
err := wrapper.UpdateLatestSnapshots(cnf)
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("update snapshot")
|
|
mnu.latestSnapshot.SetTitle("‼️ " + err.Error())
|
|
return
|
|
}
|
|
|
|
updateLatestSnapshotTitle(mnu)
|
|
updateFutureSnapshotTitle(cnf, mnu)
|
|
mnu.backupNowEnable()
|
|
}
|
|
|
|
func updateLatestSnapshotTitle(mnu *resticmenu) {
|
|
snapshot := wrapper.LastSnapshot()
|
|
mnu.latestSnapshot.SetTitle("Latest: " + snapshot.Id + " @ " + snapshot.ShortTime())
|
|
}
|
|
|
|
func updateFutureSnapshotTitle(cnf *restic.Config, mnu *resticmenu) {
|
|
msg := strconv.Itoa(len(wrapper.LatestSnapshots)) + " snapshots. "
|
|
if isBackupNeeded(cnf) {
|
|
msg += "⚠️ Overdue"
|
|
} else {
|
|
next := wrapper.LastSnapshot().Time.Add(cnf.BackupTimeInDuration())
|
|
msg += "Next @ " + next.Format(restic.ShortTimeFormat)
|
|
}
|
|
|
|
mnu.nextSnapshot.SetTitle(msg)
|
|
}
|
|
|
|
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) backupNowEnable() {
|
|
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 {
|
|
handleFatalError(err)
|
|
return
|
|
}
|
|
|
|
mnu := buildMenu()
|
|
|
|
backupCheckTime := make(chan bool, 1)
|
|
hourlyBackupCheckFn := func() {
|
|
time.Sleep(1 * time.Hour)
|
|
backupCheckTime <- true
|
|
}
|
|
go hourlyBackupCheckFn()
|
|
go func() {
|
|
updateSnapshots(cnf, mnu)
|
|
backupCheckTime <- true // As soon as the snapshots are loaded,trigger a check.
|
|
}()
|
|
|
|
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 folders...", "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 {
|
|
handleFatalError(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()
|
|
}
|