1package main
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "log/slog"
11 "os"
12 "path/filepath"
13 "runtime"
14 "strconv"
15 "strings"
16 "syscall"
17 "time"
18
19 "github.com/mjl-/bstore"
20
21 "github.com/mjl-/mox/dmarcdb"
22 "github.com/mjl-/mox/mox-"
23 "github.com/mjl-/mox/moxvar"
24 "github.com/mjl-/mox/mtastsdb"
25 "github.com/mjl-/mox/queue"
26 "github.com/mjl-/mox/store"
27 "github.com/mjl-/mox/tlsrptdb"
28)
29
30func backupctl(ctx context.Context, ctl *ctl) {
31 /* protocol:
32 > "backup"
33 > destdir
34 > "verbose" or ""
35 < stream
36 < "ok" or error
37 */
38
39 // Convention in this function: variables containing "src" or "dst" are file system
40 // paths that can be passed to os.Open and such. Variables with dirs/paths without
41 // "src" or "dst" are incomplete paths relative to the source or destination data
42 // directories.
43
44 dstDir := ctl.xread()
45 verbose := ctl.xread() == "verbose"
46
47 // Set when an error is encountered. At the end, we warn if set.
48 var incomplete bool
49
50 // We'll be writing output, and logging both to mox and the ctl stream.
51 writer := ctl.writer()
52
53 // Format easily readable output for the user.
54 formatLog := func(prefix, text string, err error, attrs ...slog.Attr) []byte {
55 var b bytes.Buffer
56 fmt.Fprint(&b, prefix)
57 fmt.Fprint(&b, text)
58 if err != nil {
59 fmt.Fprint(&b, ": "+err.Error())
60 }
61 for _, a := range attrs {
62 fmt.Fprintf(&b, "; %s=%v", a.Key, a.Value)
63 }
64 fmt.Fprint(&b, "\n")
65 return b.Bytes()
66 }
67
68 // Log an error to both the mox service as the user running "mox backup".
69 pkglogx := func(prefix, text string, err error, attrs ...slog.Attr) {
70 ctl.log.Errorx(text, err, attrs...)
71
72 _, werr := writer.Write(formatLog(prefix, text, err, attrs...))
73 ctl.xcheck(werr, "write to ctl")
74 }
75
76 // Log an error but don't mark backup as failed.
77 xwarnx := func(text string, err error, attrs ...slog.Attr) {
78 pkglogx("warning: ", text, err, attrs...)
79 }
80
81 // Log an error that causes the backup to be marked as failed. We typically
82 // continue processing though.
83 xerrx := func(text string, err error, attrs ...slog.Attr) {
84 incomplete = true
85 pkglogx("error: ", text, err, attrs...)
86 }
87
88 // If verbose is enabled, log to the cli command. Always log as info level.
89 xvlog := func(text string, attrs ...slog.Attr) {
90 ctl.log.Info(text, attrs...)
91 if verbose {
92 _, werr := writer.Write(formatLog("", text, nil, attrs...))
93 ctl.xcheck(werr, "write to ctl")
94 }
95 }
96
97 dstConfigDir := filepath.Join(dstDir, "config")
98 dstDataDir := filepath.Join(dstDir, "data")
99
100 // Warn if directories already exist, will likely cause failures when trying to
101 // write files that already exist.
102 if _, err := os.Stat(dstConfigDir); err == nil {
103 xwarnx("destination config directory already exists", nil, slog.String("configdir", dstConfigDir))
104 }
105 if _, err := os.Stat(dstDataDir); err == nil {
106 xwarnx("destination data directory already exists", nil, slog.String("datadir", dstDataDir))
107 }
108
109 os.MkdirAll(dstDir, 0770)
110 os.MkdirAll(dstConfigDir, 0770)
111 os.MkdirAll(dstDataDir, 0770)
112
113 // Copy all files in the config dir.
114 srcConfigDir := filepath.Clean(mox.ConfigDirPath("."))
115 err := filepath.WalkDir(srcConfigDir, func(srcPath string, d fs.DirEntry, err error) error {
116 if err != nil {
117 return err
118 }
119
120 if srcConfigDir == srcPath {
121 return nil
122 }
123
124 // Trim directory and separator.
125 relPath := srcPath[len(srcConfigDir)+1:]
126
127 destPath := filepath.Join(dstConfigDir, relPath)
128
129 if d.IsDir() {
130 if info, err := os.Stat(srcPath); err != nil {
131 return fmt.Errorf("stat config dir %s: %v", srcPath, err)
132 } else if err := os.Mkdir(destPath, info.Mode()&0777); err != nil {
133 return fmt.Errorf("mkdir %s: %v", destPath, err)
134 }
135 return nil
136 }
137 if d.Type()&fs.ModeSymlink != 0 {
138 linkDest, err := os.Readlink(srcPath)
139 if err != nil {
140 return fmt.Errorf("reading symlink %s: %v", srcPath, err)
141 }
142 if err := os.Symlink(linkDest, destPath); err != nil {
143 return fmt.Errorf("creating symlink %s: %v", destPath, err)
144 }
145 return nil
146 }
147 if !d.Type().IsRegular() {
148 xwarnx("skipping non-regular/dir/symlink file in config dir", nil, slog.String("path", srcPath))
149 return nil
150 }
151
152 sf, err := os.Open(srcPath)
153 if err != nil {
154 return fmt.Errorf("open config file %s: %v", srcPath, err)
155 }
156 info, err := sf.Stat()
157 if err != nil {
158 return fmt.Errorf("stat config file %s: %v", srcPath, err)
159 }
160 df, err := os.OpenFile(destPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0777&info.Mode())
161 if err != nil {
162 return fmt.Errorf("create destination config file %s: %v", destPath, err)
163 }
164 defer func() {
165 if df != nil {
166 err := df.Close()
167 ctl.log.Check(err, "closing file")
168 }
169 }()
170 defer func() {
171 err := sf.Close()
172 ctl.log.Check(err, "closing file")
173 }()
174 if _, err := io.Copy(df, sf); err != nil {
175 return fmt.Errorf("copying config file %s to %s: %v", srcPath, destPath, err)
176 }
177 if err := df.Close(); err != nil {
178 return fmt.Errorf("closing destination config file %s: %v", srcPath, err)
179 }
180 df = nil
181 return nil
182 })
183 if err != nil {
184 xerrx("storing config directory", err)
185 }
186
187 srcDataDir := filepath.Clean(mox.DataDirPath("."))
188
189 // When creating a file in the destination, we first ensure its directory exists.
190 // We track which directories we created, to prevent needless syscalls.
191 createdDirs := map[string]struct{}{}
192 ensureDestDir := func(dstpath string) {
193 dstdir := filepath.Dir(dstpath)
194 if _, ok := createdDirs[dstdir]; !ok {
195 err := os.MkdirAll(dstdir, 0770)
196 if err != nil {
197 xerrx("creating directory", err)
198 }
199 createdDirs[dstdir] = struct{}{}
200 }
201 }
202
203 // Backup a single file by copying (never hardlinking, the file may change).
204 backupFile := func(path string) {
205 tmFile := time.Now()
206 srcpath := filepath.Join(srcDataDir, path)
207 dstpath := filepath.Join(dstDataDir, path)
208
209 sf, err := os.Open(srcpath)
210 if err != nil {
211 xerrx("open source file (not backed up)", err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
212 return
213 }
214 defer sf.Close()
215
216 ensureDestDir(dstpath)
217 df, err := os.OpenFile(dstpath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
218 if err != nil {
219 xerrx("creating destination file (not backed up)", err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
220 return
221 }
222 defer func() {
223 if df != nil {
224 df.Close()
225 }
226 }()
227 if _, err := io.Copy(df, sf); err != nil {
228 xerrx("copying file (not backed up properly)", err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
229 return
230 }
231 err = df.Close()
232 df = nil
233 if err != nil {
234 xerrx("closing destination file (not backed up properly)", err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
235 return
236 }
237 xvlog("backed up file", slog.String("path", path), slog.Duration("duration", time.Since(tmFile)))
238 }
239
240 // Back up the files in a directory (by copying).
241 backupDir := func(dir string) {
242 tmDir := time.Now()
243 srcdir := filepath.Join(srcDataDir, dir)
244 dstdir := filepath.Join(dstDataDir, dir)
245 err := filepath.WalkDir(srcdir, func(srcpath string, d fs.DirEntry, err error) error {
246 if err != nil {
247 xerrx("walking file (not backed up)", err, slog.String("srcpath", srcpath))
248 return nil
249 }
250 if d.IsDir() {
251 return nil
252 }
253 backupFile(srcpath[len(srcDataDir)+1:])
254 return nil
255 })
256 if err != nil {
257 xerrx("copying directory (not backed up properly)", err,
258 slog.String("srcdir", srcdir),
259 slog.String("dstdir", dstdir),
260 slog.Duration("duration", time.Since(tmDir)))
261 return
262 }
263 xvlog("backed up directory", slog.String("dir", dir), slog.Duration("duration", time.Since(tmDir)))
264 }
265
266 // Backup a database by copying it in a readonly transaction.
267 // Always logs on error, so caller doesn't have to, but also returns the error so
268 // callers can see result.
269 backupDB := func(db *bstore.DB, path string) (rerr error) {
270 defer func() {
271 if rerr != nil {
272 xerrx("backing up database", rerr, slog.String("path", path))
273 }
274 }()
275
276 tmDB := time.Now()
277
278 dstpath := filepath.Join(dstDataDir, path)
279 ensureDestDir(dstpath)
280 df, err := os.OpenFile(dstpath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
281 if err != nil {
282 return fmt.Errorf("creating destination file: %v", err)
283 }
284 defer func() {
285 if df != nil {
286 df.Close()
287 }
288 }()
289 err = db.Read(ctx, func(tx *bstore.Tx) error {
290 // Using regular WriteTo seems fine, and fast. It just copies pages.
291 //
292 // bolt.Compact is slower, it writes all key/value pairs, building up new data
293 // structures. My compacted test database was ~60% of original size. Lz4 on the
294 // uncompacted database got it to 14%. Lz4 on the compacted database got it to 13%.
295 // Backups are likely archived somewhere with compression, so we don't compact.
296 //
297 // Tests with WriteTo and os.O_DIRECT were slower than without O_DIRECT, but
298 // probably because everything fit in the page cache. It may be better to use
299 // O_DIRECT when copying many large or inactive databases.
300 _, err := tx.WriteTo(df)
301 return err
302 })
303 if err != nil {
304 return fmt.Errorf("copying database: %v", err)
305 }
306 err = df.Close()
307 df = nil
308 if err != nil {
309 return fmt.Errorf("closing destination database after copy: %v", err)
310 }
311 xvlog("backed up database file", slog.String("path", path), slog.Duration("duration", time.Since(tmDB)))
312 return nil
313 }
314
315 // Try to create a hardlink. Fall back to copying the file (e.g. when on different file system).
316 warnedHardlink := false // We warn once about failing to hardlink.
317 linkOrCopy := func(srcpath, dstpath string) (bool, error) {
318 ensureDestDir(dstpath)
319
320 if err := os.Link(srcpath, dstpath); err == nil {
321 return true, nil
322 } else if os.IsNotExist(err) {
323 // No point in trying with regular copy, we would warn twice.
324 return false, err
325 } else if !warnedHardlink {
326 var hardlinkHint string
327 if runtime.GOOS == "linux" && errors.Is(err, syscall.EXDEV) {
328 hardlinkHint = " (hint: if running under systemd, ReadWritePaths in mox.service may cause multiple mountpoints; consider merging paths into a single parent directory to prevent cross-device/mountpoint hardlinks)"
329 }
330 xwarnx("creating hardlink to message failed, will be doing regular file copies and not warn again"+hardlinkHint, err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
331 warnedHardlink = true
332 }
333
334 // Fall back to copying.
335 sf, err := os.Open(srcpath)
336 if err != nil {
337 return false, fmt.Errorf("open source path %s: %v", srcpath, err)
338 }
339 defer func() {
340 err := sf.Close()
341 ctl.log.Check(err, "closing copied source file")
342 }()
343
344 df, err := os.OpenFile(dstpath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
345 if err != nil {
346 return false, fmt.Errorf("create destination path %s: %v", dstpath, err)
347 }
348 defer func() {
349 if df != nil {
350 err := df.Close()
351 ctl.log.Check(err, "closing partial destination file")
352 }
353 }()
354 if _, err := io.Copy(df, sf); err != nil {
355 return false, fmt.Errorf("coping: %v", err)
356 }
357 err = df.Close()
358 df = nil
359 if err != nil {
360 return false, fmt.Errorf("closing destination file: %v", err)
361 }
362 return false, nil
363 }
364
365 // Start making the backup.
366 tmStart := time.Now()
367
368 ctl.log.Print("making backup", slog.String("destdir", dstDataDir))
369
370 if err := os.MkdirAll(dstDataDir, 0770); err != nil {
371 xerrx("creating destination data directory", err)
372 }
373
374 if err := os.WriteFile(filepath.Join(dstDataDir, "moxversion"), []byte(moxvar.Version), 0660); err != nil {
375 xerrx("writing moxversion", err)
376 }
377 backupDB(store.AuthDB, "auth.db")
378 backupDB(dmarcdb.ReportsDB, "dmarcrpt.db")
379 backupDB(dmarcdb.EvalDB, "dmarceval.db")
380 backupDB(mtastsdb.DB, "mtasts.db")
381 backupDB(tlsrptdb.ReportDB, "tlsrpt.db")
382 backupDB(tlsrptdb.ResultDB, "tlsrptresult.db")
383 backupFile("receivedid.key")
384
385 // Acme directory is optional.
386 srcAcmeDir := filepath.Join(srcDataDir, "acme")
387 if _, err := os.Stat(srcAcmeDir); err == nil {
388 backupDir("acme")
389 } else if err != nil && !os.IsNotExist(err) {
390 xerrx("copying acme/", err)
391 }
392
393 // Copy the queue database and all message files.
394 backupQueue := func(path string) {
395 tmQueue := time.Now()
396
397 if err := backupDB(queue.DB, path); err != nil {
398 xerrx("queue not backed up", err, slog.String("path", path), slog.Duration("duration", time.Since(tmQueue)))
399 return
400 }
401
402 dstdbpath := filepath.Join(dstDataDir, path)
403 opts := bstore.Options{MustExist: true, RegisterLogger: ctl.log.Logger}
404 db, err := bstore.Open(ctx, dstdbpath, &opts, queue.DBTypes...)
405 if err != nil {
406 xerrx("open copied queue database", err, slog.String("dstpath", dstdbpath), slog.Duration("duration", time.Since(tmQueue)))
407 return
408 }
409
410 defer func() {
411 if db != nil {
412 err := db.Close()
413 ctl.log.Check(err, "closing new queue db")
414 }
415 }()
416
417 // Link/copy known message files. If a message has been removed while we read the
418 // database, our backup is not consistent and the backup will be marked failed.
419 tmMsgs := time.Now()
420 seen := map[string]struct{}{}
421 var nlinked, ncopied int
422 var maxID int64
423 err = bstore.QueryDB[queue.Msg](ctx, db).ForEach(func(m queue.Msg) error {
424 if m.ID > maxID {
425 maxID = m.ID
426 }
427 mp := store.MessagePath(m.ID)
428 seen[mp] = struct{}{}
429 srcpath := filepath.Join(srcDataDir, "queue", mp)
430 dstpath := filepath.Join(dstDataDir, "queue", mp)
431 if linked, err := linkOrCopy(srcpath, dstpath); err != nil {
432 xerrx("linking/copying queue message", err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
433 } else if linked {
434 nlinked++
435 } else {
436 ncopied++
437 }
438 return nil
439 })
440 if err != nil {
441 xerrx("processing queue messages (not backed up properly)", err, slog.Duration("duration", time.Since(tmMsgs)))
442 } else {
443 xvlog("queue message files linked/copied",
444 slog.Int("linked", nlinked),
445 slog.Int("copied", ncopied),
446 slog.Duration("duration", time.Since(tmMsgs)))
447 }
448
449 // Read through all files in queue directory and warn about anything we haven't
450 // handled yet. Message files that are newer than we expect from our consistent
451 // database snapshot are ignored.
452 tmWalk := time.Now()
453 srcqdir := filepath.Join(srcDataDir, "queue")
454 err = filepath.WalkDir(srcqdir, func(srcqpath string, d fs.DirEntry, err error) error {
455 if err != nil {
456 xerrx("walking files in queue", err, slog.String("srcpath", srcqpath))
457 return nil
458 }
459 if d.IsDir() {
460 return nil
461 }
462 p := srcqpath[len(srcqdir)+1:]
463 if _, ok := seen[p]; ok {
464 return nil
465 }
466 if p == "index.db" {
467 return nil
468 }
469 // Skip any messages that were added since we started on our consistent snapshot.
470 // We don't want to cause spurious backup warnings.
471 if id, err := strconv.ParseInt(filepath.Base(p), 10, 64); err == nil && maxID > 0 && id > maxID && p == store.MessagePath(id) {
472 return nil
473 }
474
475 qp := filepath.Join("queue", p)
476 xwarnx("backing up unrecognized file in queue directory", nil, slog.String("path", qp))
477 backupFile(qp)
478 return nil
479 })
480 if err != nil {
481 xerrx("walking queue directory (not backed up properly)", err, slog.String("dir", "queue"), slog.Duration("duration", time.Since(tmWalk)))
482 } else {
483 xvlog("walked queue directory", slog.Duration("duration", time.Since(tmWalk)))
484 }
485
486 xvlog("queue backed finished", slog.Duration("duration", time.Since(tmQueue)))
487 }
488 backupQueue(filepath.FromSlash("queue/index.db"))
489
490 backupAccount := func(acc *store.Account) {
491 defer acc.Close()
492
493 tmAccount := time.Now()
494
495 // Copy database file.
496 dbpath := filepath.Join("accounts", acc.Name, "index.db")
497 err := backupDB(acc.DB, dbpath)
498 if err != nil {
499 xerrx("copying account database", err, slog.String("path", dbpath), slog.Duration("duration", time.Since(tmAccount)))
500 }
501
502 // todo: should document/check not taking a rlock on account.
503
504 // Copy junkfilter files, if configured.
505 if jf, _, err := acc.OpenJunkFilter(ctx, ctl.log); err != nil {
506 if !errors.Is(err, store.ErrNoJunkFilter) {
507 xerrx("opening junk filter for account (not backed up)", err)
508 }
509 } else {
510 db := jf.DB()
511 jfpath := filepath.Join("accounts", acc.Name, "junkfilter.db")
512 backupDB(db, jfpath)
513 bloompath := filepath.Join("accounts", acc.Name, "junkfilter.bloom")
514 backupFile(bloompath)
515 db = nil
516 err := jf.Close()
517 ctl.log.Check(err, "closing junkfilter")
518 }
519
520 dstdbpath := filepath.Join(dstDataDir, dbpath)
521 opts := bstore.Options{MustExist: true, RegisterLogger: ctl.log.Logger}
522 db, err := bstore.Open(ctx, dstdbpath, &opts, store.DBTypes...)
523 if err != nil {
524 xerrx("open copied account database", err, slog.String("dstpath", dstdbpath), slog.Duration("duration", time.Since(tmAccount)))
525 return
526 }
527
528 defer func() {
529 if db != nil {
530 err := db.Close()
531 ctl.log.Check(err, "close account database")
532 }
533 }()
534
535 // Link/copy known message files. If a message has been removed while we read the
536 // database, our backup is not consistent and the backup will be marked failed.
537 tmMsgs := time.Now()
538 seen := map[string]struct{}{}
539 var maxID int64
540 var nlinked, ncopied int
541 err = bstore.QueryDB[store.Message](ctx, db).FilterEqual("Expunged", false).ForEach(func(m store.Message) error {
542 if m.ID > maxID {
543 maxID = m.ID
544 }
545 mp := store.MessagePath(m.ID)
546 seen[mp] = struct{}{}
547 amp := filepath.Join("accounts", acc.Name, "msg", mp)
548 srcpath := filepath.Join(srcDataDir, amp)
549 dstpath := filepath.Join(dstDataDir, amp)
550 if linked, err := linkOrCopy(srcpath, dstpath); err != nil {
551 xerrx("linking/copying account message", err, slog.String("srcpath", srcpath), slog.String("dstpath", dstpath))
552 } else if linked {
553 nlinked++
554 } else {
555 ncopied++
556 }
557 return nil
558 })
559 if err != nil {
560 xerrx("processing account messages (not backed up properly)", err, slog.Duration("duration", time.Since(tmMsgs)))
561 } else {
562 xvlog("account message files linked/copied",
563 slog.Int("linked", nlinked),
564 slog.Int("copied", ncopied),
565 slog.Duration("duration", time.Since(tmMsgs)))
566 }
567
568 // Read through all files in queue directory and warn about anything we haven't
569 // handled yet. Message files that are newer than we expect from our consistent
570 // database snapshot are ignored.
571 tmWalk := time.Now()
572 srcadir := filepath.Join(srcDataDir, "accounts", acc.Name)
573 err = filepath.WalkDir(srcadir, func(srcapath string, d fs.DirEntry, err error) error {
574 if err != nil {
575 xerrx("walking files in account", err, slog.String("srcpath", srcapath))
576 return nil
577 }
578 if d.IsDir() {
579 return nil
580 }
581 p := srcapath[len(srcadir)+1:]
582 l := strings.Split(p, string(filepath.Separator))
583 if l[0] == "msg" {
584 mp := filepath.Join(l[1:]...)
585 if _, ok := seen[mp]; ok {
586 return nil
587 }
588
589 // Skip any messages that were added since we started on our consistent snapshot.
590 // We don't want to cause spurious backup warnings.
591 if id, err := strconv.ParseInt(l[len(l)-1], 10, 64); err == nil && id > maxID && mp == store.MessagePath(id) {
592 return nil
593 }
594 }
595 switch p {
596 case "index.db", "junkfilter.db", "junkfilter.bloom":
597 return nil
598 }
599 ap := filepath.Join("accounts", acc.Name, p)
600 if strings.HasPrefix(p, "msg"+string(filepath.Separator)) {
601 xwarnx("backing up unrecognized file in account message directory (should be moved away)", nil, slog.String("path", ap))
602 } else {
603 xwarnx("backing up unrecognized file in account directory", nil, slog.String("path", ap))
604 }
605 backupFile(ap)
606 return nil
607 })
608 if err != nil {
609 xerrx("walking account directory (not backed up properly)", err, slog.String("srcdir", srcadir), slog.Duration("duration", time.Since(tmWalk)))
610 } else {
611 xvlog("walked account directory", slog.Duration("duration", time.Since(tmWalk)))
612 }
613
614 xvlog("account backup finished", slog.String("dir", filepath.Join("accounts", acc.Name)), slog.Duration("duration", time.Since(tmAccount)))
615 }
616
617 // For each configured account, open it, make a copy of the database and
618 // hardlink/copy the messages. We track the accounts we handled, and skip the
619 // account directories when handling "all other files" below.
620 accounts := map[string]struct{}{}
621 for _, accName := range mox.Conf.Accounts() {
622 acc, err := store.OpenAccount(ctl.log, accName, false)
623 if err != nil {
624 xerrx("opening account for copying (will try to copy as regular files later)", err, slog.String("account", accName))
625 continue
626 }
627 accounts[accName] = struct{}{}
628 backupAccount(acc)
629 }
630
631 // Copy all other files, that aren't part of the known files, databases, queue or accounts.
632 tmWalk := time.Now()
633 err = filepath.WalkDir(srcDataDir, func(srcpath string, d fs.DirEntry, err error) error {
634 if err != nil {
635 xerrx("walking path", err, slog.String("path", srcpath))
636 return nil
637 }
638
639 if srcpath == srcDataDir {
640 return nil
641 }
642 p := srcpath[len(srcDataDir)+1:]
643 if p == "queue" || p == "acme" || p == "tmp" {
644 return fs.SkipDir
645 }
646 l := strings.Split(p, string(filepath.Separator))
647 if len(l) >= 2 && l[0] == "accounts" {
648 name := l[1]
649 if _, ok := accounts[name]; ok {
650 return fs.SkipDir
651 }
652 }
653
654 // Only files are explicitly backed up.
655 if d.IsDir() {
656 return nil
657 }
658
659 switch p {
660 case "auth.db", "dmarcrpt.db", "dmarceval.db", "mtasts.db", "tlsrpt.db", "tlsrptresult.db", "receivedid.key", "ctl":
661 // Already handled.
662 return nil
663 case "lastknownversion": // Optional file, not yet handled.
664 default:
665 xwarnx("backing up unrecognized file", nil, slog.String("path", p))
666 }
667 backupFile(p)
668 return nil
669 })
670 if err != nil {
671 xerrx("walking other files (not backed up properly)", err, slog.Duration("duration", time.Since(tmWalk)))
672 } else {
673 xvlog("walking other files finished", slog.Duration("duration", time.Since(tmWalk)))
674 }
675
676 xvlog("backup finished", slog.Duration("duration", time.Since(tmStart)))
677
678 writer.xclose()
679
680 if incomplete {
681 ctl.xwrite("errors were encountered during backup")
682 } else {
683 ctl.xwriteok()
684 }
685}
686