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