added config for scheduled and manual backups

This commit is contained in:
BroodjeAap 2023-01-23 20:46:25 +00:00
parent 06f922bc5d
commit 1890ea7276
7 changed files with 303 additions and 10 deletions

1
.gitignore vendored
View file

@ -24,3 +24,4 @@ go.work
tmp/build-errors.log
tmp/main
watch.db
backups

View file

@ -38,6 +38,9 @@ notifiers:
database:
dsn: "/config/watch.db" # for docker usage
prune: "@every 1h"
backup:
schedule: "@every 1d"
path: "/backup/{{.Year}}_{{.Month}}_{{.Day}}T{{.Hour}}-{{.Minute}}-{{.Second}}.gzip" # https://pkg.go.dev/time available
proxy:
proxy_url: http://proxy.com:1234
browserless:

204
main.go
View file

@ -1,6 +1,8 @@
package main
import (
"bytes"
"compress/gzip"
"embed"
"encoding/json"
"errors"
@ -13,6 +15,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -191,6 +194,10 @@ func (web *Web) initRouter() {
web.router.GET("/notifiers/view", web.notifiersView)
web.router.POST("/notifiers/test", web.notifiersTest)
web.router.GET("/backup/view", web.backupView)
web.router.GET("/backup/create", web.backupCreate)
web.router.GET("/backup/test/:id", web.backupTest)
web.router.SetTrustedProxies(nil)
}
@ -213,6 +220,9 @@ func (web *Web) initTemplates() {
web.templates.Add("notifiersView", template.Must(template.ParseFS(templatesFS, "base.html", "notifiers.html")))
web.templates.Add("backupView", template.Must(template.ParseFS(templatesFS, "base.html", "backup/view.html")))
web.templates.Add("backupTest", template.Must(template.ParseFS(templatesFS, "base.html", "backup/test.html")))
web.templates.Add("500", template.Must(template.ParseFS(templatesFS, "base.html", "500.html")))
}
@ -228,6 +238,26 @@ func (web *Web) initCronJobs() {
web.cron = cron.New()
web.cron.Start()
// db prune job is started if there is a database.prune set
if viper.IsSet("database.prune") {
pruneSchedule := viper.GetString("database.prune")
_, err := web.cron.AddFunc(pruneSchedule, web.pruneDB)
if err != nil {
web.startupWarning("Could not parse database.prune:", err)
}
log.Println("Started DB prune cronjob:", pruneSchedule)
}
// backup job is started if there is a schedule and path
if viper.IsSet("database.backup.schedule") && viper.IsSet("database.backup.path") {
backupSchedule := viper.GetString("database.backup.schedule")
_, err := web.cron.AddFunc(backupSchedule, web.scheduledBackup)
if err != nil {
web.startupWarning("Could not parse database.backup.schedule:", err)
}
log.Println("Backup schedule set:", backupSchedule)
}
// add some delay to cron jobs, so watches with the same schedule don't
// 'burst' at the same time after restarting GoWatch
cronDelayStr := viper.GetString("schedule.delay")
@ -246,16 +276,6 @@ func (web *Web) initCronJobs() {
time.Sleep(cronDelay)
}
}
// db prune job is started if there is a database.prune set
if viper.IsSet("database.prune") {
pruneSchedule := viper.GetString("database.prune")
_, err := web.cron.AddFunc(pruneSchedule, web.pruneDB)
if err != nil {
web.startupWarning("Could not parse database.prune:", err)
}
log.Println("Started DB prune cronjob:", pruneSchedule)
}
}
// initNotifiers initializes the notifiers configured in the config
@ -791,6 +811,170 @@ func (web *Web) cacheClear(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// backupView (/backup/view) lists the stored backups
func (web *Web) backupView(c *gin.Context) {
if !viper.IsSet("database.backup") {
c.HTML(http.StatusOK, "backupView", gin.H{"Error": "database.backup not set"})
return
}
if !viper.IsSet("database.backup.schedule") {
c.HTML(http.StatusOK, "backupView", gin.H{"Error": "database.backup.schedule not set"})
return
}
if !viper.IsSet("database.backup.path") {
c.HTML(http.StatusOK, "backupView", gin.H{"Error": "database.backup.path not set"})
return
}
backupPath := viper.GetString("database.backup.path")
backupDir, err := filepath.Abs(filepath.Dir(backupPath))
if err != nil {
c.HTML(http.StatusOK, "backupView", gin.H{"Error": err})
return
}
filesInBackupDir, err := ioutil.ReadDir(backupDir)
if err != nil {
c.HTML(http.StatusOK, "backupView", gin.H{"Error": err})
return
}
filePaths := make([]string, 0, len(filesInBackupDir))
for _, fileInBackupDir := range filesInBackupDir {
fullPath := filepath.Join(backupDir, fileInBackupDir.Name())
filePaths = append(filePaths, fullPath)
}
c.HTML(http.StatusOK, "backupView", gin.H{"Backups": filePaths})
}
// backupCreate (/backup/create) creates a new backup
func (web *Web) backupCreate(c *gin.Context) {
if !viper.IsSet("database.backup") {
c.HTML(http.StatusBadRequest, "backupView", gin.H{"Error": "database.backup not set"})
return
}
if !viper.IsSet("database.backup.path") {
c.HTML(http.StatusBadRequest, "backupView", gin.H{"Error": "database.backup.path not set"})
return
}
backupDir := filepath.Dir(viper.GetString("database.backup.path"))
backupName := fmt.Sprintf("gowatch_%s.gzip", time.Now().Format(time.RFC3339))
backupName = strings.Replace(backupName, ":", "-", -1)
backupPath := filepath.Join(backupDir, backupName)
err := web.createBackup(backupPath)
if err != nil {
c.HTML(http.StatusBadRequest, "backupView", gin.H{"Error": err})
return
}
c.Redirect(http.StatusSeeOther, "/backup/view")
}
func (web *Web) scheduledBackup() {
log.Println("Starting scheduled backup")
backupPath := viper.GetString("database.backup.path")
// compare abs backup path to abs dir path, if they are the same, it's a dir
// avoids an Open(backupPath).Stat, which will fail if it's a file that doesn't exist
absBackupPath, err := filepath.Abs(backupPath)
if err != nil {
log.Println("Could not get abs path of database.backup.path")
return
}
backupDir, err := filepath.Abs(filepath.Dir(backupPath))
if err != nil {
log.Println("Could not get abs path of dir(database.backup.path)")
return
}
if absBackupPath == backupDir {
backupName := fmt.Sprintf("gowatch_%s.gzip", time.Now().Format(time.RFC3339))
backupPath = filepath.Join(backupPath, backupName)
log.Println(backupPath)
} else {
backupTemplate, err := template.New("backup").Parse(backupPath)
if err != nil {
log.Println("Could not parse backup path as template:", err)
return
}
var backupNameBytes bytes.Buffer
err = backupTemplate.Execute(&backupNameBytes, time.Now())
if err != nil {
log.Println("Could not execute backup template:", err)
return
}
backupPath = backupNameBytes.String()
}
err = web.createBackup(backupPath)
if err != nil {
log.Println("Could not create scheduled backup:", err)
return
}
log.Println("Backup succesful:", backupPath)
}
// createBackup is the function that actually creates the backup
func (web *Web) createBackup(backupPath string) error {
backupFile, err := os.OpenFile(backupPath, os.O_CREATE|os.O_WRONLY, 0660)
if err != nil {
return err
}
defer backupFile.Close()
backupWriter := gzip.NewWriter(backupFile)
defer backupWriter.Close()
var watches []Watch
tx := web.db.Find(&watches)
if tx.Error != nil {
return tx.Error
}
var filters []Filter
tx = web.db.Find(&filters)
if tx.Error != nil {
return tx.Error
}
var connections []FilterConnection
tx = web.db.Find(&connections)
if tx.Error != nil {
return tx.Error
}
var values []FilterOutput
tx = web.db.Find(&values)
if tx.Error != nil {
return tx.Error
}
backup := Backup{
Watches: watches,
Filters: filters,
Connections: connections,
Values: values,
}
jsn, err := json.Marshal(backup)
if err != nil {
return err
}
_, err = backupWriter.Write(jsn)
if err != nil {
return err
}
return nil
}
// backupTest (/backup/test) tests the selected backup file
func (web *Web) backupTest(c *gin.Context) {
}
// exportWatch (/watch/export/:id) creates a json export of the current watch
func (web *Web) exportWatch(c *gin.Context) {
watchID := c.Param("id")

View file

@ -54,3 +54,10 @@ type WatchExport struct {
Filters []Filter `json:"filters"`
Connections []FilterConnection `json:"connections"`
}
type Backup struct {
Watches []Watch `json:"watches"`
Filters []Filter `json:"filters"`
Connections []FilterConnection `json:"connections"`
Values []FilterOutput `json:"values"`
}

View file

@ -0,0 +1,43 @@
{{define "title"}}
GoWatch Backups
{{end}}
{{define "content"}}
<div class="container row">
<div class="row h3 justify-content-center">
Backups
</div>
{{ if .Error }}
<div class="row h3 justify-content-center text-danger">
{{ .Error }}
</div>
{{ end }}
<table class="table table-striped table-hover">
<thead>
<tr class="table-dark">
<th>File</th>
<th>Test</th>
<th>Restore</th>
</tr>
</thead>
<tbody>
{{ range $i, $backup := .Backups }}
<tr>
<td class="h5">{{ $backup }}</td>
<td>
<a class="btn btn-success">
Test
</a>
</td>
<td>
<a class="btn btn-danger">
Restore
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{end}}

View file

@ -0,0 +1,52 @@
{{define "title"}}
GoWatch Backups
{{end}}
{{define "content"}}
<div class="container row">
<div class="row h3 justify-content-center">
Backups
</div>
{{ if .Error }}
<div class="row h3 justify-content-center text-danger">
{{ .Error }}
</div>
{{ end }}
<table class="table table-striped table-hover">
<thead>
<tr class="table-dark">
<th>File</th>
<th>Test</th>
<th>Restore</th>
<th>Download</th>
</tr>
</thead>
<tbody>
{{ range $i, $backup := .Backups }}
<tr>
<td class="h5">{{ $backup }}</td>
<td>
<a class="btn btn-success">
Test
</a>
</td>
<td>
<a class="btn btn-danger">
Restore
</a>
</td>
<td>
<a class="btn btn-secondary">
Download
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
<a href="/backup/create" class="btn btn-warning btn-lg">
Backup Now
</a>
</div>
{{end}}

View file

@ -29,6 +29,9 @@
<li class="nav-item">
<a class="nav-link" aria-current="page" id="newWatchLink" href="/watch/create">New</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/backup/view">Backups</a>
</li>
{{ template "navbar" .}}
</ul>
<div class="d-flex">