diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 9f4eed6e..711b1b23 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -702,7 +702,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { dbFile := locations[locDatabase] ldb, err := db.Open(dbFile) if err != nil { - l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?") + l.Fatalln("Error opening database:", err) } if runtimeOptions.resetDeltaIdxs { diff --git a/lib/db/leveldb_dbinstance.go b/lib/db/leveldb_dbinstance.go index 18ef4117..87b13afa 100644 --- a/lib/db/leveldb_dbinstance.go +++ b/lib/db/leveldb_dbinstance.go @@ -9,6 +9,7 @@ package db import ( "bytes" "encoding/binary" + "fmt" "os" "sort" "strings" @@ -60,31 +61,32 @@ func Open(file string) (*Instance, error) { // the database and reindexing... l.Infoln("Database corruption detected, unable to recover. Reinitializing...") if err := os.RemoveAll(file); err != nil { - return nil, err + return nil, errorSuggestion{err, "failed to delete corrupted database"} } db, err = leveldb.OpenFile(file, opts) } if err != nil { - return nil, err + return nil, errorSuggestion{err, "is another instance of Syncthing running?"} } - return newDBInstance(db, file), nil + return newDBInstance(db, file) } func OpenMemory() *Instance { db, _ := leveldb.Open(storage.NewMemStorage(), nil) - return newDBInstance(db, "") + ldb, _ := newDBInstance(db, "") + return ldb } -func newDBInstance(db *leveldb.DB, location string) *Instance { +func newDBInstance(db *leveldb.DB, location string) (*Instance, error) { i := &Instance{ DB: db, location: location, } i.folderIdx = newSmallIndex(i, []byte{KeyTypeFolderIdx}) i.deviceIdx = newSmallIndex(i, []byte{KeyTypeDeviceIdx}) - i.updateSchema() - return i + err := i.updateSchema() + return i, err } // Committed returns the number of items committed to the database since startup @@ -935,3 +937,12 @@ func resize(k []byte, reqLen int) []byte { } return k[:reqLen] } + +type errorSuggestion struct { + inner error + suggestion string +} + +func (e errorSuggestion) Error() string { + return fmt.Sprintf("%s (%s)", e.inner.Error(), e.suggestion) +} diff --git a/lib/db/leveldb_dbinstance_updateschema.go b/lib/db/leveldb_dbinstance_updateschema.go index 609331df..aeed321b 100644 --- a/lib/db/leveldb_dbinstance_updateschema.go +++ b/lib/db/leveldb_dbinstance_updateschema.go @@ -7,20 +7,50 @@ package db import ( + "fmt" "strings" "github.com/syncthing/syncthing/lib/protocol" "github.com/syndtr/goleveldb/leveldb/util" ) -const dbVersion = 5 +// List of all dbVersion to dbMinSyncthingVersion pairs for convenience +// 0: v0.14.0 +// 1: v0.14.46 +// 2: v0.14.48 +// 3: v0.14.49 +// 4: v0.14.49 +// 5: v0.14.50 +const ( + dbVersion = 5 + dbMinSyncthingVersion = "v0.14.49" +) -func (db *Instance) updateSchema() { +type databaseDowngradeError struct { + minSyncthingVersion string +} + +func (e databaseDowngradeError) Error() string { + if e.minSyncthingVersion == "" { + return "newer Syncthing required" + } + return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion) +} + +func (db *Instance) updateSchema() error { miscDB := NewNamespacedKV(db, string(KeyTypeMiscData)) prevVersion, _ := miscDB.Int64("dbVersion") - if prevVersion >= dbVersion { - return + if prevVersion > dbVersion { + err := databaseDowngradeError{} + if minSyncthingVersion, ok := miscDB.String("dbMinSyncthingVersion"); ok { + err.minSyncthingVersion = minSyncthingVersion + } + return err + } + + if prevVersion == dbVersion { + return nil } if prevVersion < 1 { @@ -41,6 +71,9 @@ func (db *Instance) updateSchema() { } miscDB.PutInt64("dbVersion", dbVersion) + miscDB.PutString("dbMinSyncthingVersion", dbMinSyncthingVersion) + + return nil } func (db *Instance) updateSchema0to1() { diff --git a/lib/db/leveldb_test.go b/lib/db/leveldb_test.go index 1e28cde8..85196259 100644 --- a/lib/db/leveldb_test.go +++ b/lib/db/leveldb_test.go @@ -8,6 +8,7 @@ package db import ( "bytes" + "os" "testing" "github.com/syncthing/syncthing/lib/fs" @@ -159,7 +160,7 @@ func TestIgnoredFiles(t *testing.T) { if err != nil { t.Fatal(err) } - db := newDBInstance(ldb, "") + db, _ := newDBInstance(ldb, "") fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db) // The contents of the database are like this: @@ -280,7 +281,7 @@ func TestUpdate0to3(t *testing.T) { if err != nil { t.Fatal(err) } - db := newDBInstance(ldb, "") + db, _ := newDBInstance(ldb, "") folder := []byte(update0to3Folder) @@ -338,3 +339,27 @@ func TestUpdate0to3(t *testing.T) { t.Errorf(`Missing needed file "%v"`, n) } } + +func TestDowngrade(t *testing.T) { + loc := "testdata/downgrade.db" + db, err := Open(loc) + if err != nil { + t.Fatal(err) + } + defer func() { + db.Close() + os.RemoveAll(loc) + }() + + miscDB := NewNamespacedKV(db, string(KeyTypeMiscData)) + miscDB.PutInt64("dbVersion", dbVersion+1) + l.Infoln(dbVersion) + + db.Close() + db, err = Open(loc) + if err, ok := err.(databaseDowngradeError); !ok { + t.Fatal("Expected error due to database downgrade, got", err) + } else if err.minSyncthingVersion != dbMinSyncthingVersion { + t.Fatalf("Error has %v as min Syncthing version, expected %v", err.minSyncthingVersion, dbMinSyncthingVersion) + } +}