fix(db): clean files for dropped folders at startup (#10280)

This adds a cleanup stage to remove database files for folders that no
longer exist on startup. Folder database files were already removed when
dropping a folder, assuming that the folder database had been opened at
that point. This won't be the case though when a folder is removed from
the config when Syncthing isn't running, or when a folder is dropped and
re-migrated in a restarted migration.
This commit is contained in:
Jakob Borg
2025-08-22 09:00:05 +02:00
committed by GitHub
parent d776657b52
commit 8151bcddff
2 changed files with 43 additions and 4 deletions

View File

@@ -82,6 +82,10 @@ func Open(path string, opts ...Option) (*DB, error) {
opt(db)
}
if err := db.cleanDroppedFolders(); err != nil {
slog.Warn("Failed to clean dropped folders", slogutil.Error(err))
}
return db, nil
}
@@ -120,10 +124,9 @@ func OpenForMigration(path string) (*DB, error) {
folderDBOpener: openFolderDBForMigration,
}
// // Touch device IDs that should always exist and have a low index
// // numbers, and will never change
// db.localDeviceIdx, _ = db.deviceIdxLocked(protocol.LocalDeviceID)
// db.tplInput["LocalDeviceIdx"] = db.localDeviceIdx
if err := db.cleanDroppedFolders(); err != nil {
slog.Warn("Failed to clean dropped folders", slogutil.Error(err))
}
return db, nil
}

View File

@@ -8,9 +8,14 @@ package sqlite
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"github.com/syncthing/syncthing/internal/slogutil"
)
func (s *DB) DropFolder(folder string) error {
@@ -41,6 +46,37 @@ func (s *DB) ListFolders() ([]string, error) {
return res, wrap(err)
}
// cleanDroppedFolders removes old database files for folders that no longer
// exist in the main database.
func (s *DB) cleanDroppedFolders() error {
// All expected folder databeses.
var names []string
err := s.stmt(`SELECT database_name FROM folders`).Select(&names)
if err != nil {
return wrap(err)
}
// All folder database files on disk.
files, err := filepath.Glob(filepath.Join(s.pathBase, "folder.*"))
if err != nil {
return wrap(err)
}
// Any files that don't match a name in the database are removed.
for _, file := range files {
base := filepath.Base(file)
inDB := slices.ContainsFunc(names, func(name string) bool { return strings.HasPrefix(base, name) })
if !inDB {
if err := os.Remove(file); err != nil {
slog.Warn("Failed to remove database file for old, dropped folder", slogutil.FilePath(base))
} else {
slog.Info("Cleaned out database file for old, dropped folder", slogutil.FilePath(base))
}
}
}
return nil
}
// wrap returns the error wrapped with the calling function name and
// optional extra context strings as prefix. A nil error wraps to nil.
func wrap(err error, context ...string) error {