From 890c75367a2fbd3d6a639c19ad451792236ea19b Mon Sep 17 00:00:00 2001
From: Mechiel Lukkien <mechiel@ueber.net>
Date: Fri, 24 Jan 2025 12:24:57 +0100
Subject: [PATCH] mox backup: skip message files that were added to queue or
 account message directories while making the backup, instead of storing them
 and warning about them

by storing them, a restore may need the -fix flag to become usable again.
it makes more sense to just skip these files. they are not part of the
consistent snapshot.
---
 backup.go | 39 +++++++++++++++++++++++++++++++--------
 1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/backup.go b/backup.go
index e3787af..f4db8ec 100644
--- a/backup.go
+++ b/backup.go
@@ -11,6 +11,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"strconv"
 	"strings"
 	"syscall"
 	"time"
@@ -413,13 +414,16 @@ func backupctl(ctx context.Context, ctl *ctl) {
 			}
 		}()
 
-		// Link/copy known message files. Warn if files are missing or unexpected
-		// (though a message file could have been removed just now due to delivery, or a
-		// new message may have been queued).
+		// Link/copy known message files. If a message has been removed while we read the
+		// database, our backup is not consistent and the backup will be marked failed.
 		tmMsgs := time.Now()
 		seen := map[string]struct{}{}
 		var nlinked, ncopied int
+		var maxID int64
 		err = bstore.QueryDB[queue.Msg](ctx, db).ForEach(func(m queue.Msg) error {
+			if m.ID > maxID {
+				maxID = m.ID
+			}
 			mp := store.MessagePath(m.ID)
 			seen[mp] = struct{}{}
 			srcpath := filepath.Join(srcDataDir, "queue", mp)
@@ -442,7 +446,9 @@ func backupctl(ctx context.Context, ctl *ctl) {
 				slog.Duration("duration", time.Since(tmMsgs)))
 		}
 
-		// Read through all files in queue directory and warn about anything we haven't handled yet.
+		// Read through all files in queue directory and warn about anything we haven't
+		// handled yet. Message files that are newer than we expect from our consistent
+		// database snapshot are ignored.
 		tmWalk := time.Now()
 		srcqdir := filepath.Join(srcDataDir, "queue")
 		err = filepath.WalkDir(srcqdir, func(srcqpath string, d fs.DirEntry, err error) error {
@@ -460,6 +466,12 @@ func backupctl(ctx context.Context, ctl *ctl) {
 			if p == "index.db" {
 				return nil
 			}
+			// Skip any messages that were added since we started on our consistent snapshot.
+			// We don't want to cause spurious backup warnings.
+			if id, err := strconv.ParseInt(filepath.Base(p), 10, 64); err == nil && maxID > 0 && id > maxID && p == store.MessagePath(id) {
+				return nil
+			}
+
 			qp := filepath.Join("queue", p)
 			xwarnx("backing up unrecognized file in queue directory", nil, slog.String("path", qp))
 			backupFile(qp)
@@ -520,13 +532,16 @@ func backupctl(ctx context.Context, ctl *ctl) {
 			}
 		}()
 
-		// Link/copy known message files. Warn if files are missing or unexpected (though a
-		// message file could have been added just now due to delivery, or a message have
-		// been removed).
+		// Link/copy known message files. If a message has been removed while we read the
+		// database, our backup is not consistent and the backup will be marked failed.
 		tmMsgs := time.Now()
 		seen := map[string]struct{}{}
+		var maxID int64
 		var nlinked, ncopied int
 		err = bstore.QueryDB[store.Message](ctx, db).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
+			if m.ID > maxID {
+				maxID = m.ID
+			}
 			mp := store.MessagePath(m.ID)
 			seen[mp] = struct{}{}
 			amp := filepath.Join("accounts", acc.Name, "msg", mp)
@@ -550,7 +565,9 @@ func backupctl(ctx context.Context, ctl *ctl) {
 				slog.Duration("duration", time.Since(tmMsgs)))
 		}
 
-		// Read through all files in account directory and warn about anything we haven't handled yet.
+		// Read through all files in queue directory and warn about anything we haven't
+		// handled yet. Message files that are newer than we expect from our consistent
+		// database snapshot are ignored.
 		tmWalk := time.Now()
 		srcadir := filepath.Join(srcDataDir, "accounts", acc.Name)
 		err = filepath.WalkDir(srcadir, func(srcapath string, d fs.DirEntry, err error) error {
@@ -568,6 +585,12 @@ func backupctl(ctx context.Context, ctl *ctl) {
 				if _, ok := seen[mp]; ok {
 					return nil
 				}
+
+				// Skip any messages that were added since we started on our consistent snapshot.
+				// We don't want to cause spurious backup warnings.
+				if id, err := strconv.ParseInt(l[len(l)-1], 10, 64); err == nil && id > maxID && mp == store.MessagePath(id) {
+					return nil
+				}
 			}
 			switch p {
 			case "index.db", "junkfilter.db", "junkfilter.bloom":