Harden local runtime safety and error handling
This commit is contained in:
@@ -10,6 +10,10 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type backupPeriod struct {
|
||||
@@ -250,6 +254,12 @@ func pruneOldBackups(periodDir string, keep int) error {
|
||||
}
|
||||
|
||||
func createBackupArchive(destPath, dbPath, configPath string) error {
|
||||
snapshotPath, cleanup, err := createSQLiteSnapshot(dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
file, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -257,12 +267,10 @@ func createBackupArchive(destPath, dbPath, configPath string) error {
|
||||
defer file.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(file)
|
||||
if err := addZipFile(zipWriter, dbPath); err != nil {
|
||||
if err := addZipFileAs(zipWriter, snapshotPath, filepath.Base(dbPath)); err != nil {
|
||||
_ = zipWriter.Close()
|
||||
return err
|
||||
}
|
||||
_ = addZipOptionalFile(zipWriter, dbPath+"-wal")
|
||||
_ = addZipOptionalFile(zipWriter, dbPath+"-shm")
|
||||
|
||||
if strings.TrimSpace(configPath) != "" {
|
||||
_ = addZipOptionalFile(zipWriter, configPath)
|
||||
@@ -274,6 +282,77 @@ func createBackupArchive(destPath, dbPath, configPath string) error {
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
func createSQLiteSnapshot(dbPath string) (string, func(), error) {
|
||||
tempFile, err := os.CreateTemp("", "qfs-backup-*.db")
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
tempPath := tempFile.Name()
|
||||
if err := tempFile.Close(); err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
return "", func() {}, err
|
||||
}
|
||||
if err := os.Remove(tempPath); err != nil && !os.IsNotExist(err) {
|
||||
return "", func() {}, err
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
_ = os.Remove(tempPath)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return "", func() {}, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return "", func() {}, err
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
if err := db.Exec("PRAGMA busy_timeout = 5000").Error; err != nil {
|
||||
cleanup()
|
||||
return "", func() {}, fmt.Errorf("configure sqlite busy_timeout: %w", err)
|
||||
}
|
||||
|
||||
literalPath := strings.ReplaceAll(tempPath, "'", "''")
|
||||
if err := vacuumIntoWithRetry(db, literalPath); err != nil {
|
||||
cleanup()
|
||||
return "", func() {}, err
|
||||
}
|
||||
|
||||
return tempPath, cleanup, nil
|
||||
}
|
||||
|
||||
func vacuumIntoWithRetry(db *gorm.DB, literalPath string) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if err := db.Exec("VACUUM INTO '" + literalPath + "'").Error; err != nil {
|
||||
lastErr = err
|
||||
if !isSQLiteBusyError(err) {
|
||||
return fmt.Errorf("create sqlite snapshot: %w", err)
|
||||
}
|
||||
time.Sleep(time.Duration(attempt+1) * 250 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("create sqlite snapshot after retries: %w", lastErr)
|
||||
}
|
||||
|
||||
func isSQLiteBusyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(err.Error())
|
||||
return strings.Contains(lower, "database is locked") || strings.Contains(lower, "database is busy")
|
||||
}
|
||||
|
||||
func addZipOptionalFile(writer *zip.Writer, path string) error {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil
|
||||
@@ -282,6 +361,10 @@ func addZipOptionalFile(writer *zip.Writer, path string) error {
|
||||
}
|
||||
|
||||
func addZipFile(writer *zip.Writer, path string) error {
|
||||
return addZipFileAs(writer, path, filepath.Base(path))
|
||||
}
|
||||
|
||||
func addZipFileAs(writer *zip.Writer, path string, archiveName string) error {
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -297,7 +380,7 @@ func addZipFile(writer *zip.Writer, path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = filepath.Base(path)
|
||||
header.Name = archiveName
|
||||
header.Method = zip.Deflate
|
||||
|
||||
out, err := writer.CreateHeader(header)
|
||||
|
||||
Reference in New Issue
Block a user