mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
305 lines
8.1 KiB
Go
305 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
lm "github.com/hrfee/jfa-go/logmessages"
|
|
)
|
|
|
|
const (
|
|
BACKUP_PREFIX = "jfa-go-db"
|
|
BACKUP_PREFIX_OLD = "jfa-go-db-"
|
|
BACKUP_COMMIT_PREFIX = "-c-"
|
|
BACKUP_DATE_PREFIX = "-d-"
|
|
BACKUP_UPLOAD_PREFIX = "upload-"
|
|
BACKUP_DATEFMT = "2006-01-02T15-04-05"
|
|
BACKUP_SUFFIX = ".bak"
|
|
)
|
|
|
|
type Backup struct {
|
|
Date time.Time
|
|
Commit string
|
|
Upload bool
|
|
}
|
|
|
|
func (b Backup) IsZero() bool { return b.Date.IsZero() && b.Commit == "" && b.Upload == false }
|
|
|
|
func (b Backup) Equals(a Backup) bool {
|
|
return a.Date.Equal(b.Date) && a.Commit == b.Commit && a.Upload == b.Upload
|
|
}
|
|
|
|
// Pre 21/03/25 format: "{BACKUP_PREFIX_OLD}{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-2006-01-02T15-04-05.bak"
|
|
// Post 21/03/25 format: "{BACKUP_PREFIX}-c-{commit}-d-{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-c-0b92060-d-2006-01-02T15-04-05.bak"
|
|
|
|
func (b Backup) String() string {
|
|
t := b.Date
|
|
if t.IsZero() {
|
|
t = time.Now()
|
|
}
|
|
out := BACKUP_PREFIX
|
|
if b.Upload {
|
|
out = BACKUP_UPLOAD_PREFIX + out
|
|
}
|
|
if b.Commit != "" {
|
|
out += BACKUP_COMMIT_PREFIX + b.Commit
|
|
}
|
|
out += BACKUP_DATE_PREFIX + t.Local().Format(BACKUP_DATEFMT) + BACKUP_SUFFIX
|
|
return out
|
|
}
|
|
|
|
func (b *Backup) FromString(f string) error {
|
|
of := f
|
|
if strings.HasPrefix(f, BACKUP_UPLOAD_PREFIX) {
|
|
b.Upload = true
|
|
f = f[len(BACKUP_UPLOAD_PREFIX):]
|
|
}
|
|
if !strings.HasPrefix(f, BACKUP_PREFIX) {
|
|
return fmt.Errorf("file doesn't have correct prefix (\"%s\")", BACKUP_PREFIX)
|
|
}
|
|
f = f[len(BACKUP_PREFIX):]
|
|
if !strings.HasSuffix(f, BACKUP_SUFFIX) {
|
|
return fmt.Errorf("file doesn't have correct suffix (\"%s\")", BACKUP_SUFFIX)
|
|
}
|
|
for range 2 {
|
|
if strings.HasPrefix(f, BACKUP_COMMIT_PREFIX) {
|
|
f = f[len(BACKUP_COMMIT_PREFIX):]
|
|
commitEnd := strings.Index(f, BACKUP_DATE_PREFIX)
|
|
if commitEnd == -1 {
|
|
commitEnd = strings.Index(f, BACKUP_SUFFIX)
|
|
}
|
|
if commitEnd == -1 {
|
|
return fmt.Errorf("end of commit (\"%s\" or \"%s\") not found in \"%s\"", BACKUP_DATE_PREFIX, BACKUP_PREFIX, f)
|
|
}
|
|
b.Commit = f[:commitEnd]
|
|
f = f[commitEnd:]
|
|
} else if strings.HasPrefix(f, BACKUP_DATE_PREFIX) {
|
|
f = f[len(BACKUP_DATE_PREFIX):]
|
|
dateEnd := strings.Index(f, BACKUP_COMMIT_PREFIX)
|
|
if dateEnd == -1 {
|
|
dateEnd = strings.Index(f, BACKUP_SUFFIX)
|
|
}
|
|
if dateEnd == -1 {
|
|
return fmt.Errorf("end of date (\"%s\" or \"%s\") not found in \"%s\"", BACKUP_COMMIT_PREFIX, BACKUP_PREFIX, f)
|
|
}
|
|
t, err := time.Parse(BACKUP_DATEFMT, f[:dateEnd])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Date = t
|
|
f = f[dateEnd:]
|
|
}
|
|
}
|
|
if b.Date.IsZero() {
|
|
return b.FromOldString(of)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Backup) FromOldString(f string) error {
|
|
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(f, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX+"-"), BACKUP_SUFFIX))
|
|
if err != nil {
|
|
return fmt.Errorf(lm.FailedParseTime, err)
|
|
}
|
|
b.Date = t
|
|
return nil
|
|
|
|
}
|
|
|
|
type BackupList struct {
|
|
files []os.DirEntry
|
|
info []Backup
|
|
count int
|
|
}
|
|
|
|
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.info[i], bl.info[j] = bl.info[j], bl.info[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.info[i].Date.IsZero() {
|
|
return false
|
|
}
|
|
if bl.info[j].Date.IsZero() {
|
|
return true
|
|
}
|
|
// Sort by oldest first
|
|
return bl.info[j].Date.After(bl.info[i].Date)
|
|
}
|
|
|
|
// Get human-readable file size from f.Size() result.
|
|
// https://programming.guide/go/formatting-byte-size-to-human-readable-format.html
|
|
func fileSize(l int64) string {
|
|
const unit = 1000
|
|
if l < unit {
|
|
return fmt.Sprintf("%dB", l)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := l / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f%c", float64(l)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
func (app *appContext) getBackups() *BackupList {
|
|
path := app.config.Section("backups").Key("path").String()
|
|
err := os.MkdirAll(path, 0755)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedCreateDir, path, err)
|
|
return nil
|
|
}
|
|
items, err := os.ReadDir(path)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedReading, path, err)
|
|
return nil
|
|
}
|
|
backups := &BackupList{}
|
|
backups.files = items
|
|
backups.info = make([]Backup, len(items))
|
|
backups.count = 0
|
|
for i, item := range items {
|
|
// Even though Backup{} can parse and check validity, still check if the file ends in .bak, we don't need to print an error if a file isn't a .bak.
|
|
if item.IsDir() || !(strings.HasSuffix(item.Name(), BACKUP_SUFFIX)) {
|
|
continue
|
|
}
|
|
b := Backup{}
|
|
if err := b.FromString(item.Name()); err != nil {
|
|
app.debug.Printf(lm.FailedParseBackup, item.Name(), err)
|
|
continue
|
|
}
|
|
backups.info[i] = b
|
|
backups.count++
|
|
}
|
|
return backups
|
|
}
|
|
|
|
func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) {
|
|
toKeep := app.config.Section("backups").Key("keep_n_backups").MustInt(20)
|
|
keepPreviousVersions := app.config.Section("backups").Key("keep_previous_version_backup").MustBool(true)
|
|
|
|
b := Backup{Commit: commit}
|
|
fname := b.String()
|
|
path := app.config.Section("backups").Key("path").String()
|
|
backups := app.getBackups()
|
|
if backups == nil {
|
|
return
|
|
}
|
|
toDelete := backups.count + 1 - toKeep
|
|
if toDelete > 0 || keepPreviousVersions {
|
|
sort.Sort(backups)
|
|
}
|
|
backupsByCommit := map[string]int{}
|
|
if keepPreviousVersions {
|
|
// Count backups by commit
|
|
for _, b := range backups.info {
|
|
if b.IsZero() {
|
|
continue
|
|
}
|
|
// If b.Commit is empty, the backup is pre-versions-in-backup-names.
|
|
// Still use the empty string as a key, considering these as a single version.
|
|
count, ok := backupsByCommit[b.Commit]
|
|
if !ok {
|
|
count = 0
|
|
}
|
|
count += 1
|
|
backupsByCommit[b.Commit] = count
|
|
}
|
|
}
|
|
// fmt.Printf("toDelete: %d, backCount: %d, keep: %d, length: %d\n", toDelete, backups.count, toKeep, len(backups.files))
|
|
if toDelete > 0 && toDelete <= backups.count {
|
|
for i := range toDelete {
|
|
backupsRemaining, ok := backupsByCommit[backups.info[i].Commit]
|
|
app.debug.Println("item", backups.files[i], "remaining", backupsRemaining)
|
|
if keepPreviousVersions && ok && backupsRemaining <= 1 {
|
|
continue
|
|
}
|
|
|
|
item := backups.files[i]
|
|
fullpath := filepath.Join(path, item.Name())
|
|
err := os.Remove(fullpath)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedDeleteOldBackup, fullpath, err)
|
|
return
|
|
}
|
|
app.debug.Printf(lm.DeleteOldBackup, fullpath)
|
|
if keepPreviousVersions && ok {
|
|
backupsRemaining -= 1
|
|
backupsByCommit[backups.info[i].Commit] = backupsRemaining
|
|
}
|
|
}
|
|
}
|
|
fullpath := filepath.Join(path, fname)
|
|
f, err := os.Create(fullpath)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedOpen, fullpath, err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
_, err = app.storage.db.Badger().Backup(f, 0)
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedCreateBackup, err)
|
|
return
|
|
}
|
|
|
|
fstat, err := f.Stat()
|
|
if err != nil {
|
|
app.err.Printf(lm.FailedStat, fullpath, err)
|
|
return
|
|
}
|
|
fileDetails.Size = fileSize(fstat.Size())
|
|
fileDetails.Name = fname
|
|
fileDetails.Path = fullpath
|
|
app.debug.Printf(lm.CreateBackup, fileDetails)
|
|
return
|
|
}
|
|
|
|
func (app *appContext) loadPendingBackup() {
|
|
if LOADBAK == "" {
|
|
return
|
|
}
|
|
oldPath := filepath.Join(app.dataPath, "db-"+strconv.FormatInt(time.Now().Unix(), 10)+"-pre-"+filepath.Base(LOADBAK))
|
|
err := os.Rename(app.storage.db_path, oldPath)
|
|
if err != nil {
|
|
app.err.Fatalf(lm.FailedMoveOldDB, oldPath, err)
|
|
}
|
|
app.info.Printf(lm.MoveOldDB, oldPath)
|
|
|
|
if err := app.storage.Connect(app.config); err != nil {
|
|
app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err)
|
|
}
|
|
defer app.storage.Close()
|
|
|
|
f, err := os.Open(LOADBAK)
|
|
if err != nil {
|
|
app.err.Fatalf(lm.FailedOpen, LOADBAK, err)
|
|
}
|
|
err = app.storage.db.Badger().Load(f, 256)
|
|
f.Close()
|
|
if err != nil {
|
|
app.err.Fatalf(lm.FailedRestoreDB, LOADBAK, err)
|
|
}
|
|
app.info.Printf(lm.RestoreDB, LOADBAK)
|
|
LOADBAK = ""
|
|
}
|
|
|
|
func newBackupDaemon(app *appContext) *GenericDaemon {
|
|
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
|
d := NewGenericDaemon(interval, app,
|
|
func(app *appContext) {
|
|
app.makeBackup()
|
|
},
|
|
)
|
|
d.Name("Backup")
|
|
return d
|
|
}
|