diff --git a/cmd/stindex/dump.go b/cmd/stindex/dump.go index dc0edf53..34edbdee 100644 --- a/cmd/stindex/dump.go +++ b/cmd/stindex/dump.go @@ -16,7 +16,7 @@ import ( "github.com/syncthing/syncthing/lib/protocol" ) -func dump(ldb *db.Instance) { +func dump(ldb *db.Lowlevel) { it := ldb.NewIterator(nil, nil) for it.Next() { key := it.Key() diff --git a/cmd/stindex/dumpsize.go b/cmd/stindex/dumpsize.go index bc26f691..71c35169 100644 --- a/cmd/stindex/dumpsize.go +++ b/cmd/stindex/dumpsize.go @@ -37,7 +37,7 @@ func (h *ElementHeap) Pop() interface{} { return x } -func dumpsize(ldb *db.Instance) { +func dumpsize(ldb *db.Lowlevel) { h := &ElementHeap{} heap.Init(h) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 59d92dff..597cf529 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -712,11 +712,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) { if err != nil { l.Fatalln("Error opening database:", err) } + if err := db.UpdateSchema(ldb); err != nil { + l.Fatalln("Database schema:", err) + } if runtimeOptions.resetDeltaIdxs { l.Infoln("Reinitializing delta index IDs") - ldb.DropLocalDeltaIndexIDs() - ldb.DropRemoteDeltaIndexIDs() + db.DropDeltaIndexIDs(ldb) } protectedFiles := []string{ @@ -737,7 +739,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // Grab the previously running version string from the database. - miscDB := db.NewNamespacedKV(ldb, string(db.KeyTypeMiscData)) + miscDB := db.NewMiscDataNamespace(ldb) prevVersion, _ := miscDB.String("prevVersion") // Strip away prerelease/beta stuff and just compare the release @@ -753,7 +755,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // Drop delta indexes in case we've changed random stuff we // shouldn't have. We will resend our index on next connect. - ldb.DropLocalDeltaIndexIDs() + db.DropDeltaIndexIDs(ldb) // Remember the new version. miscDB.PutString("prevVersion", Version) diff --git a/lib/db/benchmark_test.go b/lib/db/benchmark_test.go index 144b6670..500517ef 100644 --- a/lib/db/benchmark_test.go +++ b/lib/db/benchmark_test.go @@ -45,7 +45,7 @@ func lazyInitBenchFileSet() { replace(benchS, protocol.LocalDeviceID, firstHalf) } -func tempDB() (*db.Instance, string) { +func tempDB() (*db.Lowlevel, string) { dir, err := ioutil.TempDir("", "syncthing") if err != nil { panic(err) diff --git a/lib/db/blockmap.go b/lib/db/blockmap.go index 7e9f5f56..bb427750 100644 --- a/lib/db/blockmap.go +++ b/lib/db/blockmap.go @@ -22,14 +22,14 @@ var blockFinder *BlockFinder const maxBatchSize = 1000 type BlockMap struct { - db *Instance + db *Lowlevel folder uint32 } -func NewBlockMap(db *Instance, folder uint32) *BlockMap { +func NewBlockMap(db *Lowlevel, folder string) *BlockMap { return &BlockMap{ db: db, - folder: folder, + folder: db.folderIdx.ID([]byte(folder)), } } @@ -139,10 +139,10 @@ func (m *BlockMap) blockKeyInto(o, hash []byte, file string) []byte { } type BlockFinder struct { - db *Instance + db *Lowlevel } -func NewBlockFinder(db *Instance) *BlockFinder { +func NewBlockFinder(db *Lowlevel) *BlockFinder { if blockFinder != nil { return blockFinder } diff --git a/lib/db/blockmap_test.go b/lib/db/blockmap_test.go index b7446272..b55b6d84 100644 --- a/lib/db/blockmap_test.go +++ b/lib/db/blockmap_test.go @@ -48,14 +48,14 @@ func init() { } } -func setup() (*Instance, *BlockFinder) { +func setup() (*Lowlevel, *BlockFinder) { // Setup db := OpenMemory() return db, NewBlockFinder(db) } -func dbEmpty(db *Instance) bool { +func dbEmpty(db *Lowlevel) bool { iter := db.NewIterator(util.BytesPrefix([]byte{KeyTypeBlock}), nil) defer iter.Release() return !iter.Next() @@ -68,7 +68,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) { t.Fatal("db not empty") } - m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1"))) + m := NewBlockMap(db, "folder1") f3.Type = protocol.FileInfoTypeDirectory @@ -152,8 +152,8 @@ func TestBlockMapAddUpdateWipe(t *testing.T) { func TestBlockFinderLookup(t *testing.T) { db, f := setup() - m1 := NewBlockMap(db, db.folderIdx.ID([]byte("folder1"))) - m2 := NewBlockMap(db, db.folderIdx.ID([]byte("folder2"))) + m1 := NewBlockMap(db, "folder1") + m2 := NewBlockMap(db, "folder2") err := m1.Add([]protocol.FileInfo{f1}) if err != nil { diff --git a/lib/db/keyer_test.go b/lib/db/keyer_test.go index 291a4dd5..6ce7c983 100644 --- a/lib/db/keyer_test.go +++ b/lib/db/keyer_test.go @@ -16,7 +16,7 @@ func TestDeviceKey(t *testing.T) { dev := []byte("device67890123456789012345678901") name := []byte("name") - db := OpenMemory() + db := newInstance(OpenMemory()) key := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name) @@ -44,7 +44,7 @@ func TestGlobalKey(t *testing.T) { fld := []byte("folder6789012345678901234567890123456789012345678901234567890123") name := []byte("name") - db := OpenMemory() + db := newInstance(OpenMemory()) key := db.keyer.GenerateGlobalVersionKey(nil, fld, name) @@ -69,7 +69,7 @@ func TestGlobalKey(t *testing.T) { func TestSequenceKey(t *testing.T) { fld := []byte("folder6789012345678901234567890123456789012345678901234567890123") - db := OpenMemory() + db := newInstance(OpenMemory()) const seq = 1234567890 key := db.keyer.GenerateSequenceKey(nil, fld, seq) diff --git a/lib/db/leveldb.go b/lib/db/leveldb.go index d2829689..4e244427 100644 --- a/lib/db/leveldb.go +++ b/lib/db/leveldb.go @@ -31,7 +31,7 @@ func (vl VersionList) String() string { // update brings the VersionList up to date with file. It returns the updated // VersionList, a potentially removed old FileVersion and its index, as well as // the index where the new FileVersion was inserted. -func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *Instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) { +func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) { removedAt, insertedAt = -1, -1 for i, v := range vl.Versions { if bytes.Equal(v.Device, device) { diff --git a/lib/db/leveldb_dbinstance.go b/lib/db/leveldb_dbinstance.go index ead076f7..8c1a972d 100644 --- a/lib/db/leveldb_dbinstance.go +++ b/lib/db/leveldb_dbinstance.go @@ -10,87 +10,28 @@ import ( "bytes" "encoding/binary" "fmt" - "os" - "sort" - "strings" - "sync/atomic" "github.com/syncthing/syncthing/lib/protocol" "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/errors" "github.com/syndtr/goleveldb/leveldb/iterator" - "github.com/syndtr/goleveldb/leveldb/opt" - "github.com/syndtr/goleveldb/leveldb/storage" "github.com/syndtr/goleveldb/leveldb/util" ) type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) -type Instance struct { - committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms - *leveldb.DB - location string - folderIdx *smallIndex - deviceIdx *smallIndex - keyer keyer +type instance struct { + *Lowlevel + keyer keyer } -func Open(file string) (*Instance, error) { - opts := &opt.Options{ - OpenFilesCacheCapacity: 100, - WriteBuffer: 4 << 20, +func newInstance(ll *Lowlevel) *instance { + return &instance{ + Lowlevel: ll, + keyer: newDefaultKeyer(ll.folderIdx, ll.deviceIdx), } - - db, err := leveldb.OpenFile(file, opts) - if leveldbIsCorrupted(err) { - db, err = leveldb.RecoverFile(file, opts) - } - if leveldbIsCorrupted(err) { - // The database is corrupted, and we've tried to recover it but it - // didn't work. At this point there isn't much to do beyond dropping - // the database and reindexing... - l.Infoln("Database corruption detected, unable to recover. Reinitializing...") - if err := os.RemoveAll(file); err != nil { - return nil, errorSuggestion{err, "failed to delete corrupted database"} - } - db, err = leveldb.OpenFile(file, opts) - } - if err != nil { - return nil, errorSuggestion{err, "is another instance of Syncthing running?"} - } - - return newDBInstance(db, file) } -func OpenMemory() *Instance { - db, _ := leveldb.Open(storage.NewMemStorage(), nil) - ldb, _ := newDBInstance(db, "") - return ldb -} - -func newDBInstance(db *leveldb.DB, location string) (*Instance, error) { - i := &Instance{ - DB: db, - location: location, - folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}), - deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}), - } - i.keyer = newDefaultKeyer(i.folderIdx, i.deviceIdx) - err := i.updateSchema() - return i, err -} - -// Committed returns the number of items committed to the database since startup -func (db *Instance) Committed() int64 { - return atomic.LoadInt64(&db.committed) -} - -// Location returns the filesystem path where the database is stored -func (db *Instance) Location() string { - return db.location -} - -func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) { +func (db *instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) { t := db.newReadWriteTransaction() defer t.close() @@ -131,7 +72,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, m } } -func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) { +func (db *instance) addSequences(folder []byte, fs []protocol.FileInfo) { t := db.newReadWriteTransaction() defer t.close() @@ -146,7 +87,7 @@ func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) { } } -func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) { +func (db *instance) removeSequences(folder []byte, fs []protocol.FileInfo) { t := db.newReadWriteTransaction() defer t.close() @@ -158,7 +99,7 @@ func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) { } } -func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) { +func (db *instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) { if len(prefix) > 0 { unslashedPrefix := prefix if bytes.HasSuffix(prefix, []byte{'/'}) { @@ -199,7 +140,7 @@ func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn It } } -func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) { +func (db *instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) { t := db.newReadOnlyTransaction() defer t.close() @@ -226,7 +167,7 @@ func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) } } -func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) { +func (db *instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) { t := db.newReadWriteTransaction() defer t.close() @@ -271,14 +212,14 @@ func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, } } -func (db *Instance) getFile(key []byte) (protocol.FileInfo, bool) { +func (db *instance) getFile(key []byte) (protocol.FileInfo, bool) { if f, ok := db.getFileTrunc(key, false); ok { return f.(protocol.FileInfo), true } return protocol.FileInfo{}, false } -func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) { +func (db *instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) { bs, err := db.Get(key, nil) if err == leveldb.ErrNotFound { return nil, false @@ -296,7 +237,7 @@ func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) { return f, true } -func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) { +func (db *instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) { t := db.newReadOnlyTransaction() defer t.close() @@ -304,7 +245,7 @@ func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, boo return f, ok } -func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) { +func (db *instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) { gk = db.keyer.GenerateGlobalVersionKey(gk, folder, file) bs, err := t.Get(gk, nil) @@ -325,7 +266,7 @@ func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file [] return gk, dk, nil, false } -func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) { +func (db *instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) { if len(prefix) > 0 { unslashedPrefix := prefix if bytes.HasSuffix(prefix, []byte{'/'}) { @@ -370,7 +311,7 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator } } -func (db *Instance) availability(folder, file []byte) []protocol.DeviceID { +func (db *instance) availability(folder, file []byte) []protocol.DeviceID { k := db.keyer.GenerateGlobalVersionKey(nil, folder, file) bs, err := db.Get(k, nil) if err == leveldb.ErrNotFound { @@ -401,7 +342,7 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID { return devices } -func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) { +func (db *instance) withNeed(folder, device []byte, truncate bool, fn Iterator) { if bytes.Equal(device, protocol.LocalDeviceID[:]) { db.withNeedLocal(folder, truncate, fn) return @@ -473,7 +414,7 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) } } -func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) { +func (db *instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) { t := db.newReadOnlyTransaction() defer t.close() @@ -495,31 +436,7 @@ func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) { } } -func (db *Instance) ListFolders() []string { - t := db.newReadOnlyTransaction() - defer t.close() - - dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil) - defer dbi.Release() - - folderExists := make(map[string]bool) - for dbi.Next() { - folder, ok := db.keyer.FolderFromGlobalVersionKey(dbi.Key()) - if ok && !folderExists[string(folder)] { - folderExists[string(folder)] = true - } - } - - folders := make([]string, 0, len(folderExists)) - for k := range folderExists { - folders = append(folders, k) - } - - sort.Strings(folders) - return folders -} - -func (db *Instance) dropFolder(folder []byte) { +func (db *instance) dropFolder(folder []byte) { t := db.newReadWriteTransaction() defer t.close() @@ -537,7 +454,7 @@ func (db *Instance) dropFolder(folder []byte) { } } -func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) { +func (db *instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) { t := db.newReadWriteTransaction() defer t.close() @@ -556,7 +473,7 @@ func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracke } } -func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) { +func (db *instance) checkGlobals(folder []byte, meta *metadataTracker) { t := db.newReadWriteTransaction() defer t.close() @@ -604,7 +521,7 @@ func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) { l.Debugf("db check completed for %q", folder) } -func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID { +func (db *instance) getIndexID(device, folder []byte) protocol.IndexID { key := db.keyer.GenerateIndexIDKey(nil, device, folder) cur, err := db.Get(key, nil) if err != nil { @@ -619,7 +536,7 @@ func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID { return id } -func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) { +func (db *instance) setIndexID(device, folder []byte, id protocol.IndexID) { key := db.keyer.GenerateIndexIDKey(nil, device, folder) bs, _ := id.Marshal() // marshalling can't fail if err := db.Put(key, bs, nil); err != nil { @@ -627,44 +544,15 @@ func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) { } } -// DropLocalDeltaIndexIDs removes all index IDs for the local device ID from -// the database. This will cause a full index transmission on the next -// connection. -func (db *Instance) DropLocalDeltaIndexIDs() { - db.dropDeltaIndexIDs(true) -} - -// DropRemoteDeltaIndexIDs removes all index IDs for the other devices than -// the local one from the database. This will cause them to send us a full -// index on the next connection. -func (db *Instance) DropRemoteDeltaIndexIDs() { - db.dropDeltaIndexIDs(false) -} - -func (db *Instance) dropDeltaIndexIDs(local bool) { - t := db.newReadWriteTransaction() - defer t.close() - - dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil) - defer dbi.Release() - - for dbi.Next() { - device, _ := db.keyer.DeviceFromIndexIDKey(dbi.Key()) - if bytes.Equal(device, protocol.LocalDeviceID[:]) == local { - t.Delete(dbi.Key()) - } - } -} - -func (db *Instance) dropMtimes(folder []byte) { +func (db *instance) dropMtimes(folder []byte) { db.dropPrefix(db.keyer.GenerateMtimesKey(nil, folder)) } -func (db *Instance) dropFolderMeta(folder []byte) { +func (db *instance) dropFolderMeta(folder []byte) { db.dropPrefix(db.keyer.GenerateFolderMetaKey(nil, folder)) } -func (db *Instance) dropPrefix(prefix []byte) { +func (db *instance) dropPrefix(prefix []byte) { t := db.newReadWriteTransaction() defer t.close() @@ -701,22 +589,6 @@ func unmarshalVersionList(data []byte) (VersionList, bool) { return vl, true } -// A "better" version of leveldb's errors.IsCorrupted. -func leveldbIsCorrupted(err error) bool { - switch { - case err == nil: - return false - - case errors.IsCorrupted(err): - return true - - case strings.Contains(err.Error(), "corrupted"): - return true - } - - return false -} - type errorSuggestion struct { inner error suggestion string diff --git a/lib/db/leveldb_dbinstance_updateschema.go b/lib/db/leveldb_dbinstance_updateschema.go index 36d174bf..bed8c9fc 100644 --- a/lib/db/leveldb_dbinstance_updateschema.go +++ b/lib/db/leveldb_dbinstance_updateschema.go @@ -38,8 +38,17 @@ func (e databaseDowngradeError) Error() string { return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion) } -func (db *Instance) updateSchema() error { - miscDB := NewNamespacedKV(db, string(KeyTypeMiscData)) +func UpdateSchema(ll *Lowlevel) error { + updater := &schemaUpdater{newInstance(ll)} + return updater.updateSchema() +} + +type schemaUpdater struct { + *instance +} + +func (db *schemaUpdater) updateSchema() error { + miscDB := NewMiscDataNamespace(db.Lowlevel) prevVersion, _ := miscDB.Int64("dbVersion") if prevVersion > dbVersion { @@ -77,7 +86,7 @@ func (db *Instance) updateSchema() error { return nil } -func (db *Instance) updateSchema0to1() { +func (db *schemaUpdater) updateSchema0to1() { t := db.newReadWriteTransaction() defer t.close() @@ -159,7 +168,7 @@ func (db *Instance) updateSchema0to1() { // updateSchema1to2 introduces a sequenceKey->deviceKey bucket for local items // to allow iteration in sequence order (simplifies sending indexes). -func (db *Instance) updateSchema1to2() { +func (db *schemaUpdater) updateSchema1to2() { t := db.newReadWriteTransaction() defer t.close() @@ -178,7 +187,7 @@ func (db *Instance) updateSchema1to2() { } // updateSchema2to3 introduces a needKey->nil bucket for locally needed files. -func (db *Instance) updateSchema2to3() { +func (db *schemaUpdater) updateSchema2to3() { t := db.newReadWriteTransaction() defer t.close() @@ -209,7 +218,7 @@ func (db *Instance) updateSchema2to3() { // release candidates (dbVersion 3 and 4) // https://github.com/syncthing/syncthing/issues/5007 // https://github.com/syncthing/syncthing/issues/5053 -func (db *Instance) updateSchemaTo5() { +func (db *schemaUpdater) updateSchemaTo5() { t := db.newReadWriteTransaction() var nk []byte for _, folderStr := range db.ListFolders() { @@ -221,7 +230,7 @@ func (db *Instance) updateSchemaTo5() { db.updateSchema2to3() } -func (db *Instance) updateSchema5to6() { +func (db *schemaUpdater) updateSchema5to6() { // For every local file with the Invalid bit set, clear the Invalid bit and // set LocalFlags = FlagLocalIgnored. diff --git a/lib/db/leveldb_test.go b/lib/db/leveldb_test.go index 1699eccf..40ec99c0 100644 --- a/lib/db/leveldb_test.go +++ b/lib/db/leveldb_test.go @@ -7,107 +7,20 @@ package db import ( - "os" "testing" "github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/protocol" ) -func TestDropIndexIDs(t *testing.T) { - db := OpenMemory() - - d1 := []byte("device67890123456789012345678901") - d2 := []byte("device12345678901234567890123456") - - // Set some index IDs - - db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1) - db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2) - db.setIndexID(d1, []byte("foo"), 3) - db.setIndexID(d1, []byte("bar"), 4) - db.setIndexID(d2, []byte("foo"), 5) - db.setIndexID(d2, []byte("bar"), 6) - - // Verify them - - if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 { - t.Fatal("fail local 1") - } - if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 { - t.Fatal("fail local 2") - } - if db.getIndexID(d1, []byte("foo")) != 3 { - t.Fatal("fail remote 1") - } - if db.getIndexID(d1, []byte("bar")) != 4 { - t.Fatal("fail remote 2") - } - if db.getIndexID(d2, []byte("foo")) != 5 { - t.Fatal("fail remote 3") - } - if db.getIndexID(d2, []byte("bar")) != 6 { - t.Fatal("fail remote 4") - } - - // Drop the local ones, verify only they got dropped - - db.DropLocalDeltaIndexIDs() - - if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 0 { - t.Fatal("fail local 1") - } - if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 0 { - t.Fatal("fail local 2") - } - if db.getIndexID(d1, []byte("foo")) != 3 { - t.Fatal("fail remote 1") - } - if db.getIndexID(d1, []byte("bar")) != 4 { - t.Fatal("fail remote 2") - } - if db.getIndexID(d2, []byte("foo")) != 5 { - t.Fatal("fail remote 3") - } - if db.getIndexID(d2, []byte("bar")) != 6 { - t.Fatal("fail remote 4") - } - - // Set local ones again - - db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1) - db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2) - - // Drop the remote ones, verify only they got dropped - - db.DropRemoteDeltaIndexIDs() - - if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 { - t.Fatal("fail local 1") - } - if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 { - t.Fatal("fail local 2") - } - if db.getIndexID(d1, []byte("foo")) != 0 { - t.Fatal("fail remote 1") - } - if db.getIndexID(d1, []byte("bar")) != 0 { - t.Fatal("fail remote 2") - } - if db.getIndexID(d2, []byte("foo")) != 0 { - t.Fatal("fail remote 3") - } - if db.getIndexID(d2, []byte("bar")) != 0 { - t.Fatal("fail remote 4") - } -} - func TestIgnoredFiles(t *testing.T) { ldb, err := openJSONS("testdata/v0.14.48-ignoredfiles.db.jsons") if err != nil { t.Fatal(err) } - db, _ := newDBInstance(ldb, "") + db := NewLowlevel(ldb, "") + UpdateSchema(db) + fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db) // The contents of the database are like this: @@ -228,11 +141,13 @@ func TestUpdate0to3(t *testing.T) { if err != nil { t.Fatal(err) } - db, _ := newDBInstance(ldb, "") + + db := newInstance(NewLowlevel(ldb, "")) + updater := schemaUpdater{db} folder := []byte(update0to3Folder) - db.updateSchema0to1() + updater.updateSchema0to1() if _, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], []byte(slashPrefixed))); ok { t.Error("File prefixed by '/' was not removed during transition to schema 1") @@ -242,7 +157,7 @@ func TestUpdate0to3(t *testing.T) { t.Error("Invalid file wasn't added to global list") } - db.updateSchema1to2() + updater.updateSchema1to2() found := false db.withHaveSequence(folder, 0, func(fi FileIntf) bool { @@ -263,7 +178,7 @@ func TestUpdate0to3(t *testing.T) { t.Error("Local file wasn't added to sequence bucket", err) } - db.updateSchema2to3() + updater.updateSchema2to3() need := map[string]protocol.FileInfo{ haveUpdate0to3[remoteDevice0][0].Name: haveUpdate0to3[remoteDevice0][0], @@ -288,22 +203,17 @@ func TestUpdate0to3(t *testing.T) { } 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) - }() + db := OpenMemory() + UpdateSchema(db) // sets the min version etc - miscDB := NewNamespacedKV(db, string(KeyTypeMiscData)) + // Bump the database version to something newer than we actually support + miscDB := NewMiscDataNamespace(db) miscDB.PutInt64("dbVersion", dbVersion+1) l.Infoln(dbVersion) - db.Close() - db, err = Open(loc) + // Pretend we just opened the DB and attempt to update it again + err := UpdateSchema(db) + if err, ok := err.(databaseDowngradeError); !ok { t.Fatal("Expected error due to database downgrade, got", err) } else if err.minSyncthingVersion != dbMinSyncthingVersion { diff --git a/lib/db/leveldb_transactions.go b/lib/db/leveldb_transactions.go index 7b04192b..e8d4098e 100644 --- a/lib/db/leveldb_transactions.go +++ b/lib/db/leveldb_transactions.go @@ -8,7 +8,6 @@ package db import ( "bytes" - "sync/atomic" "github.com/syncthing/syncthing/lib/protocol" "github.com/syndtr/goleveldb/leveldb" @@ -18,10 +17,10 @@ import ( // A readOnlyTransaction represents a database snapshot. type readOnlyTransaction struct { *leveldb.Snapshot - db *Instance + db *instance } -func (db *Instance) newReadOnlyTransaction() readOnlyTransaction { +func (db *instance) newReadOnlyTransaction() readOnlyTransaction { snap, err := db.GetSnapshot() if err != nil { panic(err) @@ -48,7 +47,7 @@ type readWriteTransaction struct { *leveldb.Batch } -func (db *Instance) newReadWriteTransaction() readWriteTransaction { +func (db *instance) newReadWriteTransaction() readWriteTransaction { t := db.newReadOnlyTransaction() return readWriteTransaction{ readOnlyTransaction: t, @@ -72,7 +71,6 @@ func (t readWriteTransaction) flush() { if err := t.db.Write(t.Batch, nil); err != nil { panic(err) } - atomic.AddInt64(&t.db.committed, int64(t.Batch.Len())) } func (t readWriteTransaction) insertFile(fk, folder, device []byte, file protocol.FileInfo) { diff --git a/lib/db/lowlevel.go b/lib/db/lowlevel.go new file mode 100644 index 00000000..ad3451d7 --- /dev/null +++ b/lib/db/lowlevel.go @@ -0,0 +1,122 @@ +// Copyright (C) 2018 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package db + +import ( + "os" + "strings" + "sync/atomic" + + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/errors" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/storage" +) + +const ( + dbMaxOpenFiles = 100 + dbWriteBuffer = 4 << 20 +) + +// Lowlevel is the lowest level database interface. It has a very simple +// purpose: hold the actual *leveldb.DB database, and the in-memory state +// that belong to that database. In the same way that a single on disk +// database can only be opened once, there should be only one Lowlevel for +// any given *leveldb.DB. +type Lowlevel struct { + committed int64 // atomic, must come first + *leveldb.DB + location string + folderIdx *smallIndex + deviceIdx *smallIndex +} + +// Open attempts to open the database at the given location, and runs +// recovery on it if opening fails. Worst case, if recovery is not possible, +// the database is erased and created from scratch. +func Open(location string) (*Lowlevel, error) { + opts := &opt.Options{ + OpenFilesCacheCapacity: dbMaxOpenFiles, + WriteBuffer: dbWriteBuffer, + } + + db, err := leveldb.OpenFile(location, opts) + if leveldbIsCorrupted(err) { + db, err = leveldb.RecoverFile(location, opts) + } + if leveldbIsCorrupted(err) { + // The database is corrupted, and we've tried to recover it but it + // didn't work. At this point there isn't much to do beyond dropping + // the database and reindexing... + l.Infoln("Database corruption detected, unable to recover. Reinitializing...") + if err := os.RemoveAll(location); err != nil { + return nil, errorSuggestion{err, "failed to delete corrupted database"} + } + db, err = leveldb.OpenFile(location, opts) + } + if err != nil { + return nil, errorSuggestion{err, "is another instance of Syncthing running?"} + } + return NewLowlevel(db, location), nil +} + +// OpenMemory returns a new Lowlevel referencing an in-memory database. +func OpenMemory() *Lowlevel { + db, _ := leveldb.Open(storage.NewMemStorage(), nil) + return NewLowlevel(db, "") +} + +// Location returns the filesystem path where the database is stored +func (db *Lowlevel) Location() string { + return db.location +} + +// ListFolders returns the list of folders currently in the database +func (db *Lowlevel) ListFolders() []string { + return db.folderIdx.Values() +} + +// Committed returns the number of items committed to the database since startup +func (db *Lowlevel) Committed() int64 { + return atomic.LoadInt64(&db.committed) +} + +func (db *Lowlevel) Put(key, val []byte, wo *opt.WriteOptions) error { + atomic.AddInt64(&db.committed, 1) + return db.DB.Put(key, val, wo) +} + +func (db *Lowlevel) Delete(key []byte, wo *opt.WriteOptions) error { + atomic.AddInt64(&db.committed, 1) + return db.DB.Delete(key, wo) +} + +// NewLowlevel wraps the given *leveldb.DB into a *lowlevel +func NewLowlevel(db *leveldb.DB, location string) *Lowlevel { + return &Lowlevel{ + DB: db, + location: location, + folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}), + deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}), + } +} + +// A "better" version of leveldb's errors.IsCorrupted. +func leveldbIsCorrupted(err error) bool { + switch { + case err == nil: + return false + + case errors.IsCorrupted(err): + return true + + case strings.Contains(err.Error(), "corrupted"): + return true + } + + return false +} diff --git a/lib/db/meta.go b/lib/db/meta.go index 097df0a8..dffc3f3e 100644 --- a/lib/db/meta.go +++ b/lib/db/meta.go @@ -20,6 +20,7 @@ type metadataTracker struct { mut sync.RWMutex counts CountsSet indexes map[metaKey]int // device ID + local flags -> index in counts + dirty bool } type metaKey struct { @@ -55,18 +56,31 @@ func (m *metadataTracker) Marshal() ([]byte, error) { // toDB saves the marshalled metadataTracker to the given db, under the key // corresponding to the given folder -func (m *metadataTracker) toDB(db *Instance, folder []byte) error { +func (m *metadataTracker) toDB(db *instance, folder []byte) error { key := db.keyer.GenerateFolderMetaKey(nil, folder) + + m.mut.RLock() + defer m.mut.RUnlock() + + if !m.dirty { + return nil + } + bs, err := m.Marshal() if err != nil { return err } - return db.Put(key, bs, nil) + err = db.Put(key, bs, nil) + if err == nil { + m.dirty = false + } + + return err } // fromDB initializes the metadataTracker from the marshalled data found in // the database under the key corresponding to the given folder -func (m *metadataTracker) fromDB(db *Instance, folder []byte) error { +func (m *metadataTracker) fromDB(db *instance, folder []byte) error { key := db.keyer.GenerateFolderMetaKey(nil, folder) bs, err := db.Get(key, nil) if err != nil { @@ -99,6 +113,7 @@ func (m *metadataTracker) addFile(dev protocol.DeviceID, f FileIntf) { } m.mut.Lock() + m.dirty = true if flags := f.FileLocalFlags(); flags == 0 { // Account regular files in the zero-flags bucket. @@ -141,6 +156,7 @@ func (m *metadataTracker) removeFile(dev protocol.DeviceID, f FileIntf) { } m.mut.Lock() + m.dirty = true if flags := f.FileLocalFlags(); flags == 0 { // Remove regular files from the zero-flags bucket @@ -194,6 +210,7 @@ func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flags uint32, // resetAll resets all metadata for the given device func (m *metadataTracker) resetAll(dev protocol.DeviceID) { m.mut.Lock() + m.dirty = true for i, c := range m.counts.Counts { if bytes.Equal(c.DeviceID, dev[:]) { m.counts.Counts[i] = Counts{ @@ -209,6 +226,7 @@ func (m *metadataTracker) resetAll(dev protocol.DeviceID) { // sequence number func (m *metadataTracker) resetCounts(dev protocol.DeviceID) { m.mut.Lock() + m.dirty = true for i, c := range m.counts.Counts { if bytes.Equal(c.DeviceID, dev[:]) { @@ -285,6 +303,7 @@ func (m *metadataTracker) Created() time.Time { func (m *metadataTracker) SetCreated() { m.mut.Lock() m.counts.Created = time.Now().UnixNano() + m.dirty = true m.mut.Unlock() } diff --git a/lib/db/namespaced.go b/lib/db/namespaced.go index edbdd127..7a26bdff 100644 --- a/lib/db/namespaced.go +++ b/lib/db/namespaced.go @@ -17,13 +17,13 @@ import ( // NamespacedKV is a simple key-value store using a specific namespace within // a leveldb. type NamespacedKV struct { - db *Instance + db *Lowlevel prefix []byte } // NewNamespacedKV returns a new NamespacedKV that lives in the namespace // specified by the prefix. -func NewNamespacedKV(db *Instance, prefix string) *NamespacedKV { +func NewNamespacedKV(db *Lowlevel, prefix string) *NamespacedKV { return &NamespacedKV{ db: db, prefix: []byte(prefix), @@ -157,3 +157,23 @@ func (n NamespacedKV) Delete(key string) { keyBs := append(n.prefix, []byte(key)...) n.db.Delete(keyBs, nil) } + +// Well known namespaces that can be instantiated without knowing the key +// details. + +// NewDeviceStatisticsNamespace creates a KV namespace for device statistics +// for the given device. +func NewDeviceStatisticsNamespace(db *Lowlevel, device string) *NamespacedKV { + return NewNamespacedKV(db, string(KeyTypeDeviceStatistic)+device) +} + +// NewFolderStatisticsNamespace creates a KV namespace for folder statistics +// for the given folder. +func NewFolderStatisticsNamespace(db *Lowlevel, folder string) *NamespacedKV { + return NewNamespacedKV(db, string(KeyTypeFolderStatistic)+folder) +} + +// NewMiscDateNamespace creates a KV namespace for miscellaneous metadata. +func NewMiscDataNamespace(db *Lowlevel) *NamespacedKV { + return NewNamespacedKV(db, string(KeyTypeMiscData)) +} diff --git a/lib/db/set.go b/lib/db/set.go index e979ae0f..a7c8824c 100644 --- a/lib/db/set.go +++ b/lib/db/set.go @@ -21,12 +21,13 @@ import ( "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/sync" + "github.com/syndtr/goleveldb/leveldb/util" ) type FileSet struct { folder string fs fs.Filesystem - db *Instance + db *instance blockmap *BlockMap meta *metadataTracker @@ -66,12 +67,14 @@ func init() { } } -func NewFileSet(folder string, fs fs.Filesystem, db *Instance) *FileSet { +func NewFileSet(folder string, fs fs.Filesystem, ll *Lowlevel) *FileSet { + db := newInstance(ll) + var s = FileSet{ folder: folder, fs: fs, db: db, - blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))), + blockmap: NewBlockMap(ll, folder), meta: newMetadataTracker(), updateMutex: sync.NewMutex(), } @@ -310,7 +313,7 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) { func (s *FileSet) MtimeFS() *fs.MtimeFS { prefix := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder)) - kv := NewNamespacedKV(s.db, string(prefix)) + kv := NewNamespacedKV(s.db.Lowlevel, string(prefix)) return fs.NewMtimeFS(s.fs, kv) } @@ -320,15 +323,26 @@ func (s *FileSet) ListDevices() []protocol.DeviceID { // DropFolder clears out all information related to the given folder from the // database. -func DropFolder(db *Instance, folder string) { +func DropFolder(ll *Lowlevel, folder string) { + db := newInstance(ll) db.dropFolder([]byte(folder)) db.dropMtimes([]byte(folder)) db.dropFolderMeta([]byte(folder)) - bm := &BlockMap{ - db: db, - folder: db.folderIdx.ID([]byte(folder)), - } + bm := NewBlockMap(ll, folder) bm.Drop() + + // Also clean out the folder ID mapping. + db.folderIdx.Delete([]byte(folder)) +} + +// DropDeltaIndexIDs removes all delta index IDs from the database. +// This will cause a full index transmission on the next connection. +func DropDeltaIndexIDs(db *Lowlevel) { + dbi := db.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil) + defer dbi.Release() + for dbi.Next() { + db.Delete(dbi.Key(), nil) + } } func normalizeFilenames(fs []protocol.FileInfo) { diff --git a/lib/db/smallindex.go b/lib/db/smallindex.go index d171e081..9ef00f5f 100644 --- a/lib/db/smallindex.go +++ b/lib/db/smallindex.go @@ -8,6 +8,7 @@ package db import ( "encoding/binary" + "sort" "github.com/syncthing/syncthing/lib/sync" "github.com/syndtr/goleveldb/leveldb" @@ -46,8 +47,11 @@ func (i *smallIndex) load() { for it.Next() { val := string(it.Value()) id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):]) - i.id2val[id] = val - i.val2id[val] = id + if val != "" { + // Empty value means the entry has been deleted. + i.id2val[id] = val + i.val2id[val] = id + } if id >= i.nextID { i.nextID = id + 1 } @@ -96,3 +100,45 @@ func (i *smallIndex) Val(id uint32) ([]byte, bool) { return []byte(val), true } + +func (i *smallIndex) Delete(val []byte) { + i.mut.Lock() + defer i.mut.Unlock() + + // Check the reverse mapping to get the ID for the value. + if id, ok := i.val2id[string(val)]; ok { + // Generate the corresponding database key. + key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id + copy(key, i.prefix) + binary.BigEndian.PutUint32(key[len(i.prefix):], id) + + // Put an empty value into the database. This indicates that the + // entry does not exist any more and prevents the ID from being + // reused in the future. + i.db.Put(key, []byte{}, nil) + + // Delete reverse mapping. + delete(i.id2val, id) + } + + // Delete forward mapping. + delete(i.val2id, string(val)) +} + +// Values returns the set of values in the index +func (i *smallIndex) Values() []string { + // In principle this method should return [][]byte because all the other + // methods deal in []byte keys. However, in practice, where it's used + // wants a []string and it's easier to just create that here rather than + // having to convert both here and there... + + i.mut.Lock() + vals := make([]string, 0, len(i.val2id)) + for val := range i.val2id { + vals = append(vals, val) + } + i.mut.Unlock() + + sort.Strings(vals) + return vals +} diff --git a/lib/db/smallindex_test.go b/lib/db/smallindex_test.go new file mode 100644 index 00000000..60602cd3 --- /dev/null +++ b/lib/db/smallindex_test.go @@ -0,0 +1,52 @@ +// Copyright (C) 2018 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package db + +import "testing" + +func TestSmallIndex(t *testing.T) { + db := OpenMemory() + idx := newSmallIndex(db.DB, []byte{12, 34}) + + // ID zero should be unallocated + if val, ok := idx.Val(0); ok || val != nil { + t.Fatal("Unexpected return for nonexistent ID 0") + } + + // A new key should get ID zero + if id := idx.ID([]byte("hello")); id != 0 { + t.Fatal("Expected 0, not", id) + } + // Looking up ID zero should work + if val, ok := idx.Val(0); !ok || string(val) != "hello" { + t.Fatalf(`Expected true, "hello", not %v, %q`, ok, val) + } + + // Delete the key + idx.Delete([]byte("hello")) + + // Next ID should be one + if id := idx.ID([]byte("key2")); id != 1 { + t.Fatal("Expected 1, not", id) + } + + // Now lets create a new index instance based on what's actually serialized to the database. + idx = newSmallIndex(db.DB, []byte{12, 34}) + + // Status should be about the same as before. + if val, ok := idx.Val(0); ok || val != nil { + t.Fatal("Unexpected return for deleted ID 0") + } + if id := idx.ID([]byte("key2")); id != 1 { + t.Fatal("Expected 1, not", id) + } + + // Setting "hello" again should get us ID 2, not 0 as it was originally. + if id := idx.ID([]byte("hello")); id != 2 { + t.Fatal("Expected 2, not", id) + } +} diff --git a/lib/model/model.go b/lib/model/model.go index 8eb49c8b..21b12add 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -84,7 +84,7 @@ type Model struct { *suture.Supervisor cfg *config.Wrapper - db *db.Instance + db *db.Lowlevel finder *db.BlockFinder progressEmitter *ProgressEmitter id protocol.DeviceID @@ -134,7 +134,7 @@ var ( // NewModel creates and starts a new model. The model starts in read-only mode, // where it sends index information to connected peers and responds to requests // for file data without altering the local folder in any way. -func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Instance, protectedFiles []string) *Model { +func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *Model { m := &Model{ Supervisor: suture.New("model", suture.Spec{ Log: func(line string) { diff --git a/lib/model/model_test.go b/lib/model/model_test.go index c102af0f..f79b2e2e 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -2618,60 +2618,6 @@ func TestIssue4357(t *testing.T) { } } -func TestScanNoDatabaseWrite(t *testing.T) { - // When scanning, nothing should be committed to database unless - // something actually changed. - - db := db.OpenMemory() - m := NewModel(defaultCfgWrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil) - m.AddFolder(defaultFolderConfig) - m.StartFolder("default") - m.ServeBackground() - - // Reach in and update the ignore matcher to one that always does - // reloads when asked to, instead of checking file mtimes. This is - // because we will be changing the files on disk often enough that the - // mtimes will be unreliable to determine change status. - m.fmut.Lock() - m.folderIgnores["default"] = ignore.New(defaultFs, ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged())) - m.fmut.Unlock() - - m.SetIgnores("default", nil) - defer os.Remove("testdata/.stignore") - - // Scan the folder twice. The second scan should be a no-op database wise - - m.ScanFolder("default") - c0 := db.Committed() - - m.ScanFolder("default") - c1 := db.Committed() - - if c1 != c0 { - t.Errorf("scan should not commit data when nothing changed but %d != %d", c1, c0) - } - - // Ignore a file we know exists. It'll be updated in the database. - - m.SetIgnores("default", []string{"foo"}) - - m.ScanFolder("default") - c2 := db.Committed() - - if c2 <= c1 { - t.Errorf("scan should commit data when something got ignored but %d <= %d", c2, c1) - } - - // Scan again. Nothing should happen. - - m.ScanFolder("default") - c3 := db.Committed() - - if c3 != c2 { - t.Errorf("scan should not commit data when nothing changed (with ignores) but %d != %d", c3, c2) - } -} - func TestIssue2782(t *testing.T) { // CheckHealth should accept a symlinked folder, when using tilde-expanded path. diff --git a/lib/stats/device.go b/lib/stats/device.go index 6ba8d8b1..4817882c 100644 --- a/lib/stats/device.go +++ b/lib/stats/device.go @@ -21,10 +21,9 @@ type DeviceStatisticsReference struct { device string } -func NewDeviceStatisticsReference(ldb *db.Instance, device string) *DeviceStatisticsReference { - prefix := string(db.KeyTypeDeviceStatistic) + device +func NewDeviceStatisticsReference(ldb *db.Lowlevel, device string) *DeviceStatisticsReference { return &DeviceStatisticsReference{ - ns: db.NewNamespacedKV(ldb, prefix), + ns: db.NewDeviceStatisticsNamespace(ldb, device), device: device, } } diff --git a/lib/stats/folder.go b/lib/stats/folder.go index 5515a02c..39aae681 100644 --- a/lib/stats/folder.go +++ b/lib/stats/folder.go @@ -28,10 +28,9 @@ type LastFile struct { Deleted bool `json:"deleted"` } -func NewFolderStatisticsReference(ldb *db.Instance, folder string) *FolderStatisticsReference { - prefix := string(db.KeyTypeFolderStatistic) + folder +func NewFolderStatisticsReference(ldb *db.Lowlevel, folder string) *FolderStatisticsReference { return &FolderStatisticsReference{ - ns: db.NewNamespacedKV(ldb, prefix), + ns: db.NewFolderStatisticsNamespace(ldb, folder), folder: folder, } }