fyne-io systray attempt; crude first restic wrapper
This commit is contained in:
parent
0a4c497fda
commit
3a8e84414e
|
@ -1,2 +1,3 @@
|
||||||
|
.idea/
|
||||||
on_exit_*.txt
|
on_exit_*.txt
|
||||||
restictray
|
restictray
|
18
go.mod
18
go.mod
|
@ -3,18 +3,14 @@ module brainbaking.com/restictray
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/getlantern/systray v1.2.1
|
fyne.io/systray v1.10.0
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/rs/zerolog v1.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
github.com/godbus/dbus/v5 v5.0.4 // indirect
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
github.com/tevino/abool v1.2.0 // indirect
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
|
||||||
github.com/go-stack/stack v1.8.0 // indirect
|
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
46
go.sum
46
go.sum
|
@ -1,29 +1,19 @@
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
fyne.io/systray v1.10.0 h1:Yr1D9Lxeiw3+vSuZWPlaHC8BMjIHZXJKkek706AfYQk=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
fyne.io/systray v1.10.0/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||||
github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
|
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||||
github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
|
|
||||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
205
main.go
205
main.go
|
@ -1,107 +1,130 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"brainbaking.com/restictray/restic"
|
||||||
"io/ioutil"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getlantern/systray"
|
"fyne.io/systray"
|
||||||
"github.com/getlantern/systray/example/icon"
|
"fyne.io/systray/example/icon"
|
||||||
"github.com/skratchdot/open-golang/open"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// I'm ignoring go threading issues here; assume no clicks happen at the same time.
|
||||||
|
var wrapper *restic.Wrapper
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
onExit := func() {
|
// init and setup logging
|
||||||
now := time.Now()
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
ioutil.WriteFile(fmt.Sprintf(`on_exit_%d.txt`, now.UnixNano()), []byte(now.String()), 0644)
|
wrapper = &restic.Wrapper{}
|
||||||
|
|
||||||
|
// bootstrap systray (this is a blocking call, second func is onExit)
|
||||||
|
systray.Run(onSystrayReady, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMenuLatestSnapshot() {
|
||||||
|
snapshot := wrapper.LastSnapshot()
|
||||||
|
systray.AddMenuItem("Latest: "+snapshot.Id+" @ "+snapshot.ShortTime(), "Latest Restic snapshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMenuNextTime(cnf *restic.Config) {
|
||||||
|
nextTime := wrapper.LastSnapshot().Time.Add(time.Duration(cnf.BackupTimeInHours) * time.Hour)
|
||||||
|
msg := "Next @ " + nextTime.Format(restic.ShortTimeFormat)
|
||||||
|
if time.Now().After(nextTime) {
|
||||||
|
msg = "⚠️ Overdue - " + msg
|
||||||
}
|
}
|
||||||
|
|
||||||
systray.Run(onReady, onExit)
|
systray.AddMenuItem(msg, "Future Restic snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func onReady() {
|
func addMenuQuit() {
|
||||||
systray.SetTemplateIcon(icon.Data, icon.Data)
|
systray.AddSeparator()
|
||||||
systray.SetTitle("Awesome App")
|
addMenuWithQuitAction("Quit", "Quit Restictray")
|
||||||
systray.SetTooltip("Lantern")
|
}
|
||||||
mQuitOrig := systray.AddMenuItem("Quit", "Quit the whole app")
|
|
||||||
|
func addMenuError(err error) {
|
||||||
|
addMenuWithQuitAction("❗ "+err.Error(), "Restictray Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
log.Err(err).Msg("")
|
||||||
|
systray.ResetMenu()
|
||||||
|
addMenuError(err)
|
||||||
|
addMenuQuit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMenuWithQuitAction(title string, tooltip string) {
|
||||||
|
mQuit := systray.AddMenuItem(title, tooltip)
|
||||||
go func() {
|
go func() {
|
||||||
<-mQuitOrig.ClickedCh
|
<-mQuit.ClickedCh
|
||||||
fmt.Println("Requesting quit")
|
log.Info().Msg("Requesting quit")
|
||||||
systray.Quit()
|
systray.Quit()
|
||||||
fmt.Println("Finished quitting")
|
|
||||||
}()
|
|
||||||
|
|
||||||
// We can manipulate the systray in other goroutines
|
|
||||||
go func() {
|
|
||||||
systray.SetTemplateIcon(icon.Data, icon.Data)
|
|
||||||
systray.SetTitle("Awesome App")
|
|
||||||
systray.SetTooltip("Pretty awesome棒棒嗒")
|
|
||||||
mChange := systray.AddMenuItem("Change Me", "Change Me")
|
|
||||||
mChecked := systray.AddMenuItemCheckbox("Unchecked", "Check Me", true)
|
|
||||||
mEnabled := systray.AddMenuItem("Enabled", "Enabled")
|
|
||||||
// Sets the icon of a menu item. Only available on Mac.
|
|
||||||
mEnabled.SetTemplateIcon(icon.Data, icon.Data)
|
|
||||||
|
|
||||||
systray.AddMenuItem("Ignored", "Ignored")
|
|
||||||
|
|
||||||
subMenuTop := systray.AddMenuItem("SubMenuTop", "SubMenu Test (top)")
|
|
||||||
subMenuMiddle := subMenuTop.AddSubMenuItem("SubMenuMiddle", "SubMenu Test (middle)")
|
|
||||||
subMenuBottom := subMenuMiddle.AddSubMenuItemCheckbox("SubMenuBottom - Toggle Panic!", "SubMenu Test (bottom) - Hide/Show Panic!", false)
|
|
||||||
subMenuBottom2 := subMenuMiddle.AddSubMenuItem("SubMenuBottom - Panic!", "SubMenu Test (bottom)")
|
|
||||||
|
|
||||||
mUrl := systray.AddMenuItem("Open UI", "my home")
|
|
||||||
mQuit := systray.AddMenuItem("退出", "Quit the whole app")
|
|
||||||
|
|
||||||
// Sets the icon of a menu item. Only available on Mac.
|
|
||||||
mQuit.SetIcon(icon.Data)
|
|
||||||
|
|
||||||
systray.AddSeparator()
|
|
||||||
mToggle := systray.AddMenuItem("Toggle", "Toggle the Quit button")
|
|
||||||
shown := true
|
|
||||||
toggle := func() {
|
|
||||||
if shown {
|
|
||||||
subMenuBottom.Check()
|
|
||||||
subMenuBottom2.Hide()
|
|
||||||
mQuitOrig.Hide()
|
|
||||||
mEnabled.Hide()
|
|
||||||
shown = false
|
|
||||||
} else {
|
|
||||||
subMenuBottom.Uncheck()
|
|
||||||
subMenuBottom2.Show()
|
|
||||||
mQuitOrig.Show()
|
|
||||||
mEnabled.Show()
|
|
||||||
shown = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-mChange.ClickedCh:
|
|
||||||
mChange.SetTitle("I've Changed")
|
|
||||||
case <-mChecked.ClickedCh:
|
|
||||||
if mChecked.Checked() {
|
|
||||||
mChecked.Uncheck()
|
|
||||||
mChecked.SetTitle("Unchecked")
|
|
||||||
} else {
|
|
||||||
mChecked.Check()
|
|
||||||
mChecked.SetTitle("Checked")
|
|
||||||
}
|
|
||||||
case <-mEnabled.ClickedCh:
|
|
||||||
mEnabled.SetTitle("Disabled")
|
|
||||||
mEnabled.Disable()
|
|
||||||
case <-mUrl.ClickedCh:
|
|
||||||
open.Run("https://www.getlantern.org")
|
|
||||||
case <-subMenuBottom2.ClickedCh:
|
|
||||||
panic("panic button pressed")
|
|
||||||
case <-subMenuBottom.ClickedCh:
|
|
||||||
toggle()
|
|
||||||
case <-mToggle.ClickedCh:
|
|
||||||
toggle()
|
|
||||||
case <-mQuit.ClickedCh:
|
|
||||||
systray.Quit()
|
|
||||||
fmt.Println("Quit2 now...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func onSystrayReady() {
|
||||||
|
systray.SetTemplateIcon(icon.Data, icon.Data)
|
||||||
|
systray.SetTooltip("Restictray")
|
||||||
|
systray.AddMenuItem("... Initializing", "Initializing, please wait.")
|
||||||
|
addMenuQuit()
|
||||||
|
|
||||||
|
cnf, err := restic.ReadConfig()
|
||||||
|
if err != nil {
|
||||||
|
handleError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := wrapper.UpdateLatestSnapshots(cnf)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAndBuildMainMenu(cnf)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/fyne-io/systray/tree/master/example for more examples
|
||||||
|
func resetAndBuildMainMenu(cnf *restic.Config) {
|
||||||
|
systray.ResetMenu()
|
||||||
|
addMenuLatestSnapshot()
|
||||||
|
addMenuNextTime(cnf)
|
||||||
|
systray.AddSeparator()
|
||||||
|
mnuBackupNow := systray.AddMenuItem("Backup now", "Backup now")
|
||||||
|
mnuBrowse := systray.AddMenuItem("Browse backups in Finder...", "Mount and browse backups")
|
||||||
|
addMenuQuit()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-mnuBackupNow.ClickedCh:
|
||||||
|
onClickedMenuBackupNow(mnuBackupNow, cnf)
|
||||||
|
case <-mnuBrowse.ClickedCh:
|
||||||
|
onClickedMenuBrowse(mnuBrowse, cnf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onClickedMenuBrowse(browse *systray.MenuItem, cnf *restic.Config) {
|
||||||
|
err := wrapper.MountBackups(cnf)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onClickedMenuBackupNow(mnu *systray.MenuItem, cnf *restic.Config) {
|
||||||
|
mnu.SetTitle("🔄 Backup in progress...")
|
||||||
|
mnu.Disable()
|
||||||
|
// TODO after a backup, reinitialize latest snapshot + latest/next menus
|
||||||
|
// TODO not by calling resetAndBuild again: this is from the for{}?
|
||||||
|
// TODO how does this interop with a future goroutine that auto-backups?
|
||||||
|
// TODO need for separate "backupInProgress" bool?
|
||||||
|
err := wrapper.Backup(cnf)
|
||||||
|
mnu.SetTitle("Backup now")
|
||||||
|
mnu.Enable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Backup string `json:"backup"`
|
||||||
|
BackupTimeInHours int `json:"backupTimeInHours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultBackupTimeInHours int = 24
|
||||||
|
ShortTimeFormat string = "2006-01-02T15:04:05"
|
||||||
|
)
|
||||||
|
|
||||||
|
var home, _ = os.UserHomeDir()
|
||||||
|
|
||||||
|
func (cnf *Config) PasswordFile() string {
|
||||||
|
return home + "/.restic/password.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cnf *Config) ExcludeFile() string {
|
||||||
|
return home + "/.restic/excludes.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cnf *Config) CreateMountDirIfDoesntExist() error {
|
||||||
|
if _, err := os.Stat(cnf.MountDir()); os.IsNotExist(err) {
|
||||||
|
return os.Mkdir(cnf.MountDir(), os.ModePerm)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cnf *Config) MountDir() string {
|
||||||
|
return home + "/.restic/mnt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig() (*Config, error) {
|
||||||
|
confData, err := os.ReadFile(home + "/.restic/config.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("No config.json found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &Config{}
|
||||||
|
err = json.Unmarshal(confData, conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("config.json malformed JSON: %w", err)
|
||||||
|
}
|
||||||
|
if conf.Repository == "" || conf.Backup == "" {
|
||||||
|
err := errors.New("config.json is missing required keys 'Backup' or 'Repository'")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if conf.BackupTimeInHours == 0 {
|
||||||
|
log.Warn().Msg("backupTimeInHours missing in config, reverting to 24hrs")
|
||||||
|
conf.BackupTimeInHours = DefaultBackupTimeInHours
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResticSnapshot struct {
|
||||||
|
Time time.Time `json:"time"` // format 2023-03-01T16:15:34.111513+01:00
|
||||||
|
Tree string `json:"tree"`
|
||||||
|
Paths []string `json:"paths"`
|
||||||
|
Id string `json:"short_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs ResticSnapshot) ShortTime() string {
|
||||||
|
return rs.Time.Format(ShortTimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Wrapper struct {
|
||||||
|
LatestSnapshots []ResticSnapshot
|
||||||
|
mountCommand *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) HasSnapshots() bool {
|
||||||
|
return len(w.LatestSnapshots) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) LastSnapshot() ResticSnapshot {
|
||||||
|
if !w.HasSnapshots() {
|
||||||
|
return ResticSnapshot{
|
||||||
|
Id: "(no snapshots yet)",
|
||||||
|
Time: time.Time{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.LatestSnapshots[len(w.LatestSnapshots)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func resticCmd(args ...string) *exec.Cmd {
|
||||||
|
cmd := exec.Command("restic", args...)
|
||||||
|
log.Debug().Msg(cmd.String())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Expected JSON format:
|
||||||
|
[
|
||||||
|
|
||||||
|
{
|
||||||
|
"time": "2023-03-01T16:15:34.111513+01:00",
|
||||||
|
"tree": "d603aa4c6ce2bdef784dbdcd36461970d7f0cc8083d31f46d23cdb9bef172f0a",
|
||||||
|
"paths": [
|
||||||
|
"/Users/wgroeneveld"
|
||||||
|
],
|
||||||
|
"hostname": "Wouters-M1-Air.local",
|
||||||
|
"username": "wgroeneveld",
|
||||||
|
"uid": 501,
|
||||||
|
"gid": 20,
|
||||||
|
"id": "31ae2a213c5750c4f86ebe8a8e989a5d4de2963c911e7513f47ca227723a0d95",
|
||||||
|
"short_id": "31ae2a21"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
func (w *Wrapper) UpdateLatestSnapshots(c *Config) error {
|
||||||
|
cmd := resticCmd("--json", "--password-file", c.PasswordFile(), "-r", c.Repository, "snapshots")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restic snapshots cmd: %s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(out, &w.LatestSnapshots)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Any("snapshots", w.LatestSnapshots).Msg("update")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) MountBackups(c *Config) error {
|
||||||
|
c.CreateMountDirIfDoesntExist()
|
||||||
|
if w.mountCommand != nil && w.mountCommand.Process != nil {
|
||||||
|
// could be killed or terminted due to manual unmount, ignore errors and retry anyway
|
||||||
|
w.mountCommand.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the folder first: MacFuse could take a second; results in occasional weird errors otherwise.
|
||||||
|
openFolder(c.MountDir())
|
||||||
|
|
||||||
|
w.mountCommand = resticCmd("--password-file", c.PasswordFile(), "-r", c.Repository, "mount", c.MountDir())
|
||||||
|
err := w.mountCommand.Start() // restic's mount is a blocking call
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restic mount cmd: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFolder(folder string) error {
|
||||||
|
cmd := exec.Command("open", folder)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open mount dir: %s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) Backup(c *Config) error {
|
||||||
|
cmd := resticCmd("--json", "--password-file", c.PasswordFile(), "-r", c.Repository, "--exclude-file", c.ExcludeFile(), "backup", c.Backup)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restic backup cmd: %s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
log.Debug().Str("out", string(out)).Msg("backup")
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue