Do not prune after a backup that found a volume not mounted (closes #395)
check / check (push) Waiting to run
check / check (push) Waiting to run
snapshot create --prune purged the older snapshots holding a volume's files when the volume, mounted below a configured path, was not mounted: its empty mount point read as deleted files. The scan now records in the local index each directory at or below a configured path that is on a different device from its parent directory. Under --prune, a run that does not find such a directory recorded by an earlier snapshot of that name skips the prune, keeps every older snapshot, names the directory and its snapshot, and exits non-zero, as for an empty configured path. A volume retired on purpose is cleared by removing the older snapshots with snapshot purge. Model: opus-5-5
This commit is contained in:
@@ -73,6 +73,9 @@ type Scanner struct {
|
||||
skipErrors bool
|
||||
// ui is the user-facing output; never nil (defaults to a discarding writer).
|
||||
ui *ui.Writer
|
||||
// deviceNumber returns the number of the device that holds a file;
|
||||
// never nil (defaults to deviceNumberFromStat).
|
||||
deviceNumber func(info os.FileInfo) (uint64, bool)
|
||||
|
||||
// In-memory cache of known chunk hashes for fast existence checks
|
||||
knownChunks map[string]struct{}
|
||||
@@ -136,6 +139,10 @@ type ScannerConfig struct {
|
||||
// PreviousSnapshotID is the most recent completed snapshot of the
|
||||
// same name, "" for none.
|
||||
PreviousSnapshotID string
|
||||
// DeviceNumber returns the number of the device that holds a file,
|
||||
// and false when there is none to read. nil reads it from the file's
|
||||
// stat; tests replace it, since the in-memory test filesystem has none.
|
||||
DeviceNumber func(info os.FileInfo) (uint64, bool)
|
||||
}
|
||||
|
||||
// ScanResult contains the results of a scan operation. Files and bytes
|
||||
@@ -214,6 +221,11 @@ func NewScanner(cfg ScannerConfig) *Scanner {
|
||||
uiw = ui.NewWithColor(io.Discard, false)
|
||||
}
|
||||
|
||||
deviceNumber := cfg.DeviceNumber
|
||||
if deviceNumber == nil {
|
||||
deviceNumber = deviceNumberFromStat
|
||||
}
|
||||
|
||||
return &Scanner{
|
||||
fs: cfg.FS,
|
||||
chunker: chunker.NewChunker(cfg.ChunkSize),
|
||||
@@ -229,6 +241,7 @@ func NewScanner(cfg ScannerConfig) *Scanner {
|
||||
skipErrors: cfg.SkipErrors,
|
||||
previousSnapshotID: cfg.PreviousSnapshotID,
|
||||
ui: uiw,
|
||||
deviceNumber: deviceNumber,
|
||||
pendingChunkHashes: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
@@ -1290,9 +1303,9 @@ func (s *Scanner) buildDirectoryEntry(path string, info os.FileInfo) *database.F
|
||||
}
|
||||
}
|
||||
|
||||
// recordDirectories records every directory the walk found. A directory
|
||||
// has no data to back up, so it is not among the files counted and
|
||||
// backed up.
|
||||
// recordDirectories records every directory the walk found, and its
|
||||
// mount points (see recordMountPoints). A directory has no data to back
|
||||
// up, so it is not among the files counted and backed up.
|
||||
func (s *Scanner) recordDirectories(
|
||||
ctx context.Context, directories []*FileToProcess,
|
||||
) error {
|
||||
@@ -1303,7 +1316,97 @@ func (s *Scanner) recordDirectories(
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return s.recordMountPoints(ctx, directories)
|
||||
}
|
||||
|
||||
// recordMountPoints records with the snapshot each directory the walk
|
||||
// found on a different device from its parent directory, as a mount
|
||||
// point is, the scanned path included.
|
||||
func (s *Scanner) recordMountPoints(
|
||||
ctx context.Context, directories []*FileToProcess,
|
||||
) error {
|
||||
mountPoints, err := s.findMountPoints(directories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repos.WithTx(ctx, func(txCtx context.Context, tx *sql.Tx) error {
|
||||
for _, dir := range mountPoints {
|
||||
log.Info("Found a mount point", "path", dir)
|
||||
|
||||
err := s.repos.Snapshots.AddMountPoint(txCtx, tx, s.snapshotID, dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("recording mount point %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// findMountPoints returns the directories, among those the walk found,
|
||||
// that are on a different device from their parent directory. The
|
||||
// scanned path is compared with the directory that holds it, which the
|
||||
// walk did not reach.
|
||||
func (s *Scanner) findMountPoints(
|
||||
directories []*FileToProcess,
|
||||
) ([]string, error) {
|
||||
walked := make(map[string]os.FileInfo, len(directories))
|
||||
for _, dir := range directories {
|
||||
walked[dir.Path] = dir.FileInfo
|
||||
}
|
||||
|
||||
var mountPoints []string
|
||||
|
||||
for _, dir := range directories {
|
||||
parentPath := filepath.Dir(dir.Path)
|
||||
|
||||
parent, ok := walked[parentPath]
|
||||
if !ok {
|
||||
var err error
|
||||
|
||||
parent, err = s.fs.Stat(parentPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"looking up the parent of %s: %w", dir.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.onOtherDevice(dir.FileInfo, parent) {
|
||||
mountPoints = append(mountPoints, dir.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return mountPoints, nil
|
||||
}
|
||||
|
||||
// onOtherDevice reports whether dir is on a different device from
|
||||
// parent. A file whose device number cannot be read counts as on the
|
||||
// same device as its parent.
|
||||
func (s *Scanner) onOtherDevice(dir, parent os.FileInfo) bool {
|
||||
dirDevice, ok := s.deviceNumber(dir)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
parentDevice, ok := s.deviceNumber(parent)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return dirDevice != parentDevice
|
||||
}
|
||||
|
||||
// deviceNumberFromStat returns the number of the device that holds the
|
||||
// file info describes, and false when info carries no stat, as from the
|
||||
// in-memory test filesystem.
|
||||
func deviceNumberFromStat(info os.FileInfo) (uint64, bool) {
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return uint64(stat.Dev), true //nolint:unconvert // Dev is an int32 on darwin
|
||||
}
|
||||
|
||||
// recordNonRegularFile writes a symlink or directory entry to the database
|
||||
|
||||
Reference in New Issue
Block a user