backups: add backup daemon to run every n minutes, keep x most recent backups

backups
Harvey Tindall 11 months ago
parent c0c91b4aad
commit 733ab37539
No known key found for this signature in database
GPG Key ID: BBC65952848FB1A2

@ -112,6 +112,9 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("telegram", "show_on_reg", "true")
app.MustSetValue("backups", "every_n_minutes", "1440")
app.MustSetValue("backups", "path", filepath.Join(app.dataPath, "backups"))
app.config.Section("jellyfin").Key("version").SetValue(version)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))

@ -1557,6 +1557,49 @@
}
}
},
"backups": {
"order": [],
"meta": {
"name": "Backups",
"description": "Settings for database backups."
},
"settings": {
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable to generate database backups on a schedule."
},
"path": {
"name": "Backup Path",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"description": "Path to directory to store backups in. defaults to <data_directory>/backups."
},
"every_n_minutes": {
"name": "Backup frequency (Minutes)",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "number",
"value": 1440,
"description": "Backup after this many minutes has passed since the last. Resets every restart."
},
"keep_n_backups": {
"name": "Number of backups to keep",
"required": false,
"requires_restart": true,
"depends_true": "enabled",
"type": "number",
"value": 20,
"description": "Number of most recent backups to keep. Once this is hit, the oldest backup will be deleted before doing a new one."
}
}
},
"welcome_email": {
"order": [],
"meta": {

@ -1,6 +1,10 @@
package main
import (
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/dgraph-io/badger/v3"
@ -8,6 +12,12 @@ import (
"github.com/timshannon/badgerhold/v4"
)
const (
BACKUP_PREFIX = "jfa-go-db-"
BACKUP_DATEFMT = "2006-01-02T15-04-05"
BACKUP_SUFFIX = ".bak"
)
// clearEmails removes stored emails for users which no longer exist.
// meant to be called with other such housekeeping functions, so assumes
// the user cache is fresh.
@ -74,6 +84,87 @@ func (app *appContext) clearTelegram() {
}
}
type BackupList struct {
files []os.DirEntry
dates []time.Time
}
func (bl BackupList) Len() int { return len(bl.files) }
func (bl BackupList) Swap(i, j int) {
bl.files[i], bl.files[j] = bl.files[j], bl.files[i]
bl.dates[i], bl.dates[j] = bl.dates[j], bl.dates[i]
}
func (bl BackupList) Less(i, j int) bool {
// Push non-backup files to the end of the array,
// Since they didn't have a date parsed.
if bl.dates[i].IsZero() {
return false
}
if bl.dates[j].IsZero() {
return true
}
// Sort by oldest first
return bl.dates[j].After(bl.dates[i])
}
func (app *appContext) makeBackup() {
toKeep := app.config.Section("backups").Key("keep_n_backups").MustInt(20)
fname := BACKUP_PREFIX + time.Now().Local().Format(BACKUP_DATEFMT) + BACKUP_SUFFIX
path := app.config.Section("backups").Key("path").String()
err := os.MkdirAll(path, 0755)
if err != nil {
app.err.Printf("Failed to create backup directory \"%s\": %v\n", path, err)
return
}
items, err := os.ReadDir(path)
if err != nil {
app.err.Printf("Failed to read backup directory \"%s\": %v\n", path, err)
return
}
backups := BackupList{}
backups.files = items
backups.dates = make([]time.Time, len(items))
backupCount := 0
for i, item := range items {
if item.IsDir() || !(strings.HasSuffix(item.Name(), BACKUP_SUFFIX)) {
continue
}
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(item.Name(), BACKUP_PREFIX), BACKUP_SUFFIX))
if err != nil {
app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err)
continue
}
backups.dates[i] = t
backupCount++
}
toDelete := backupCount + 1 - toKeep
if toDelete > 0 {
sort.Sort(backups)
for _, item := range backups.files[:toDelete] {
fullpath := filepath.Join(path, item.Name())
app.debug.Printf("Deleting old backup \"%s\"\n", item.Name())
err := os.Remove(fullpath)
if err != nil {
app.err.Printf("Failed to delete old backup \"%s\": %v\n", fullpath, err)
return
}
}
}
fullpath := filepath.Join(path, fname)
f, err := os.Create(fullpath)
if err != nil {
app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err)
return
}
defer f.Close()
_, err = app.storage.db.Badger().Backup(f, 0)
if err != nil {
app.err.Printf("Failed to create backup: %v\n", err)
return
}
}
func (app *appContext) clearActivities() {
app.debug.Println("Husekeeping: Cleaning up Activity log...")
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
@ -116,6 +207,24 @@ type housekeepingDaemon struct {
app *appContext
}
func newBackupDaemon(app *appContext) *housekeepingDaemon {
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
daemon := housekeepingDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
daemon.jobs = []func(app *appContext){
func(app *appContext) {
app.debug.Println("Backups: Creating backup")
app.makeBackup()
},
}
return &daemon
}
func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon {
daemon := housekeepingDaemon{
Stopped: false,

@ -475,6 +475,13 @@ func start(asDaemon, firstCall bool) {
go app.checkForUpdates()
}
var backupDaemon *housekeepingDaemon
if app.config.Section("backups").Key("enabled").MustBool(false) {
backupDaemon = newBackupDaemon(app)
go backupDaemon.run()
defer backupDaemon.Shutdown()
}
if telegramEnabled {
app.telegram, err = newTelegramDaemon(app)
if err != nil {

@ -161,7 +161,6 @@ func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string
if runtime.GOOS == "windows" {
binary += ".exe"
}
fmt.Println("monitoring", tag)
return &Updater{
httpClient: &http.Client{Timeout: 10 * time.Second},
timeoutHandler: common.NewTimeoutHandler("updater", buildroneURL, true),

Loading…
Cancel
Save