From 8151bcddff7aa837de3c9f19c506e01d1370a138 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Fri, 22 Aug 2025 09:00:05 +0200 Subject: [PATCH] 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. --- internal/db/sqlite/db_open.go | 11 ++++++---- internal/db/sqlite/db_update.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/internal/db/sqlite/db_open.go b/internal/db/sqlite/db_open.go index 2a601a42e..1c9bc53cc 100644 --- a/internal/db/sqlite/db_open.go +++ b/internal/db/sqlite/db_open.go @@ -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 } diff --git a/internal/db/sqlite/db_update.go b/internal/db/sqlite/db_update.go index df4e82892..7f7f29a2e 100644 --- a/internal/db/sqlite/db_update.go +++ b/internal/db/sqlite/db_update.go @@ -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 {