Do not prune after a backup that found a configured path empty (closes #388)
check / check (push) Canceled after 0s

snapshot create --prune of a snapshot whose path was an empty
directory, as the mount point of a volume that is not mounted is,
recorded an empty snapshot and then purged the older snapshots that
held the data, with their blobs.

The scan now counts the entries it finds in each configured path.
Under --prune, a run in which a path of a snapshot it made held
nothing skips the prune, keeps every older snapshot, warns naming the
path and snapshot, and exits non-zero, as
#382 does for unreadable
files. Only the current run is looked at. Without --prune nothing
changes. The README's --prune text and the flag's help say so.

Model: opus-5-5
This commit is contained in:
2026-10-14 21:39:25 +00:00
parent 6a57750a7b
commit 24152ce6d6
6 changed files with 260 additions and 36 deletions
+37 -12
View File
@@ -168,6 +168,12 @@ type ScanResult struct {
// skipped because it could not be read or listed; the rest are files
// removed after the walk listed them.
FilesUnreadable int
// EntriesFound counts the entries the walk found that no exclude
// pattern matches, leaving out the scanned path itself when it is a
// directory. It is zero for an empty directory, such as the mount
// point of a volume that is not mounted.
EntriesFound int
}
// NewScanner creates a new scanner instance
@@ -928,6 +934,11 @@ func (s *Scanner) scanPhase(
default:
}
// The scanned directory itself is not an entry found in it.
if filePath != path || !info.IsDir() {
result.EntriesFound++
}
// Handle symlinks and directories
handled, err := s.recordSpecialEntry(
filePath, info, existingFiles, knownFiles, collector, result)
@@ -940,18 +951,8 @@ func (s *Scanner) scanPhase(
return nil
}
// Track this file as existing (for deletion detection)
existingFiles[filePath] = struct{}{}
// Check file against in-memory map (no DB query!)
file := s.buildRegularFileEntry(filePath, info)
needsProcessing := s.checkFileInMemory(file, knownFiles)
filesScanned, changedCount := collector.recordFile(
filePath, info, file, needsProcessing)
// Update result stats
s.updateScanEntryStats(result, needsProcessing, info)
filesScanned, changedCount := s.recordRegularFile(
filePath, info, existingFiles, knownFiles, collector, result)
// Output periodic status
if time.Since(lastStatusTime) >= scanStatusInterval {
@@ -1027,6 +1028,30 @@ func (s *Scanner) recordSpecialEntry(
return false, nil
}
// recordRegularFile records a regular file as recordSpecialEntry does a
// symlink and returns the running scanned and changed counts.
func (s *Scanner) recordRegularFile(
filePath string, info os.FileInfo,
existingFiles map[string]struct{},
knownFiles map[string]*database.File,
collector *scanCollector, result *ScanResult,
) (int64, int) {
// Track this file as existing (for deletion detection)
existingFiles[filePath] = struct{}{}
// Check file against in-memory map (no DB query!)
file := s.buildRegularFileEntry(filePath, info)
needsProcessing := s.checkFileInMemory(file, knownFiles)
filesScanned, changedCount := collector.recordFile(
filePath, info, file, needsProcessing)
// Update result stats
s.updateScanEntryStats(result, needsProcessing, info)
return filesScanned, changedCount
}
// countRemovedDuringWalk counts an entry that was in its directory's
// listing but was gone when the walk looked it up, like a file removed
// before phase 2 reads it: as examined and failed, and not as an error,