up
This commit is contained in:
+82
-10
@@ -99,6 +99,54 @@ func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation in
|
||||
return dstPath, nil
|
||||
}
|
||||
|
||||
// RunADIF writes a rotating ADIF export of the QSO logbook. It's used when the
|
||||
// log lives on a shared MySQL server, where VACUUM INTO can't snapshot it — an
|
||||
// ADIF export is a portable, re-importable backup of the actual contacts.
|
||||
// writeADIF must write the full ADIF to the path it's handed.
|
||||
func RunADIF(folder string, rotation int, doZip bool, writeADIF func(path string) error) (string, error) {
|
||||
if rotation <= 0 {
|
||||
rotation = 5
|
||||
}
|
||||
if folder == "" {
|
||||
return "", fmt.Errorf("backup folder not set")
|
||||
}
|
||||
if err := os.MkdirAll(folder, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create backup folder: %w", err)
|
||||
}
|
||||
base := "opslog-log-" + time.Now().Format("2006-01-02")
|
||||
tmp := filepath.Join(folder, base+".adi.tmp")
|
||||
_ = os.Remove(tmp)
|
||||
if err := writeADIF(tmp); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", fmt.Errorf("export adif: %w", err)
|
||||
}
|
||||
|
||||
var dstPath string
|
||||
if doZip {
|
||||
dstPath = filepath.Join(folder, base+".adi.zip")
|
||||
if err := copyZipped(tmp, dstPath, base+".adi"); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
_ = os.Remove(tmp)
|
||||
} else {
|
||||
dstPath = filepath.Join(folder, base+".adi")
|
||||
_ = os.Remove(dstPath)
|
||||
if err := os.Rename(tmp, dstPath); err != nil {
|
||||
if cerr := copyFile(tmp, dstPath); cerr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", cerr
|
||||
}
|
||||
_ = os.Remove(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
if err := rotateMatch(folder, rotation, "opslog-log-", ".adi", ".adi.zip"); err != nil {
|
||||
return dstPath, fmt.Errorf("rotate: %w (backup OK at %s)", err, dstPath)
|
||||
}
|
||||
return dstPath, nil
|
||||
}
|
||||
|
||||
// copyFile performs a plain file copy. We don't use os.Rename because
|
||||
// the source is the live database; we want a fresh standalone file.
|
||||
func copyFile(src, dst string) error {
|
||||
@@ -155,10 +203,18 @@ func copyZipped(src, dst, innerName string) error {
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
// rotate keeps the most recent `keep` backups in folder and deletes the
|
||||
// rest. Only files matching the opslog-*.db / opslog-*.db.zip pattern
|
||||
// are touched — never user files that happen to live in the same folder.
|
||||
// rotate keeps the most recent `keep` SQLite backups (opslog-*.db /
|
||||
// opslog-*.db.zip) and deletes the rest.
|
||||
func rotate(folder string, keep int) error {
|
||||
return rotateMatch(folder, keep, "opslog-", ".db", ".db.zip")
|
||||
}
|
||||
|
||||
// rotateMatch keeps the most recent `keep` files in folder whose name has the
|
||||
// given prefix and one of the given suffixes, deleting older ones. Only matching
|
||||
// files are touched — never unrelated user files in the same folder. The suffix
|
||||
// filter keeps the .db family and the .adi family from rotating each other even
|
||||
// though both share the "opslog-" prefix.
|
||||
func rotateMatch(folder string, keep int, prefix string, suffixes ...string) error {
|
||||
entries, err := os.ReadDir(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -173,10 +229,17 @@ func rotate(folder string, keep int) error {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if !strings.HasPrefix(name, "opslog-") {
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
if !(strings.HasSuffix(name, ".db") || strings.HasSuffix(name, ".db.zip")) {
|
||||
ok := false
|
||||
for _, sfx := range suffixes {
|
||||
if strings.HasSuffix(name, sfx) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
@@ -198,16 +261,25 @@ func rotate(folder string, keep int) error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// HasBackupToday returns true if a backup for today's date already exists
|
||||
// in folder. Used by the startup auto-backup to skip when the user has
|
||||
// already restarted the app once today.
|
||||
// HasBackupToday returns true if a SQLite backup for today's date already
|
||||
// exists in folder. Used by the auto-backup to skip when the user has already
|
||||
// restarted the app once today.
|
||||
func HasBackupToday(folder string) bool {
|
||||
return hasBackupToday(folder, "opslog-", ".db", ".db.zip")
|
||||
}
|
||||
|
||||
// HasADIFBackupToday is HasBackupToday for the ADIF log backup (MySQL mode).
|
||||
func HasADIFBackupToday(folder string) bool {
|
||||
return hasBackupToday(folder, "opslog-log-", ".adi", ".adi.zip")
|
||||
}
|
||||
|
||||
func hasBackupToday(folder, prefix string, exts ...string) bool {
|
||||
if folder == "" {
|
||||
return false
|
||||
}
|
||||
stamp := time.Now().Format("2006-01-02")
|
||||
for _, ext := range []string{".db", ".db.zip"} {
|
||||
if _, err := os.Stat(filepath.Join(folder, "opslog-"+stamp+ext)); err == nil {
|
||||
for _, ext := range exts {
|
||||
if _, err := os.Stat(filepath.Join(folder, prefix+stamp+ext)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user