2025-03-29 12:50:08 +00:00
|
|
|
// Copyright (C) 2025 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 sqlite
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-04-26 11:58:09 +02:00
|
|
|
"github.com/syncthing/syncthing/internal/db"
|
2025-05-01 20:44:25 +02:00
|
|
|
"github.com/syncthing/syncthing/internal/itererr"
|
2025-03-29 12:50:08 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/protocol"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestBlocks(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2025-08-24 11:58:56 +02:00
|
|
|
db, err := Open(t.TempDir())
|
2025-03-29 12:50:08 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
if err := db.Close(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
files := []protocol.FileInfo{
|
|
|
|
|
{
|
|
|
|
|
Name: "file1",
|
|
|
|
|
Blocks: []protocol.BlockInfo{
|
|
|
|
|
{Hash: []byte{1, 2, 3}, Offset: 0, Size: 42},
|
|
|
|
|
{Hash: []byte{2, 3, 4}, Offset: 42, Size: 42},
|
|
|
|
|
{Hash: []byte{3, 4, 5}, Offset: 84, Size: 42},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "file2",
|
|
|
|
|
Blocks: []protocol.BlockInfo{
|
|
|
|
|
{Hash: []byte{2, 3, 4}, Offset: 0, Size: 42},
|
|
|
|
|
{Hash: []byte{3, 4, 5}, Offset: 42, Size: 42},
|
|
|
|
|
{Hash: []byte{4, 5, 6}, Offset: 84, Size: 42},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.Update("test", protocol.LocalDeviceID, files); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search for blocks
|
|
|
|
|
|
2025-05-01 20:44:25 +02:00
|
|
|
vals, err := itererr.Collect(db.AllLocalBlocksWithHash(folderID, []byte{1, 2, 3}))
|
2025-03-29 12:50:08 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(vals) != 1 {
|
|
|
|
|
t.Log(vals)
|
|
|
|
|
t.Fatal("expected one hit")
|
|
|
|
|
} else if vals[0].BlockIndex != 0 || vals[0].Offset != 0 || vals[0].Size != 42 {
|
|
|
|
|
t.Log(vals[0])
|
|
|
|
|
t.Fatal("bad entry")
|
|
|
|
|
}
|
2025-05-01 20:44:25 +02:00
|
|
|
if vals[0].FileName != "file1" {
|
2025-04-06 05:30:43 -07:00
|
|
|
t.Fatal("should be file1")
|
|
|
|
|
}
|
2025-03-29 12:50:08 +00:00
|
|
|
|
|
|
|
|
// Get the other blocks
|
|
|
|
|
|
2025-05-01 20:44:25 +02:00
|
|
|
vals, err = itererr.Collect(db.AllLocalBlocksWithHash(folderID, []byte{3, 4, 5}))
|
2025-03-29 12:50:08 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(vals) != 2 {
|
|
|
|
|
t.Log(vals)
|
|
|
|
|
t.Fatal("expected two hits")
|
|
|
|
|
}
|
|
|
|
|
// if vals[0].Index != 2 || vals[0].Offset != 84 || vals[0].Size != 42 {
|
|
|
|
|
// t.Log(vals[0])
|
|
|
|
|
// t.Fatal("bad entry 1")
|
|
|
|
|
// }
|
|
|
|
|
// if vals[1].Index != 1 || vals[1].Offset != 42 || vals[1].Size != 42 {
|
|
|
|
|
// t.Log(vals[1])
|
|
|
|
|
// t.Fatal("bad entry 2")
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBlocksDeleted(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2025-08-24 11:58:56 +02:00
|
|
|
sdb, err := Open(t.TempDir())
|
2025-03-29 12:50:08 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
if err := sdb.Close(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Insert a file
|
|
|
|
|
file := genFile("foo", 1, 0)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We should find one entry for the block hash
|
|
|
|
|
search := file.Blocks[0].Hash
|
2025-05-01 20:44:25 +02:00
|
|
|
es, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, search))
|
2025-04-06 05:30:43 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2025-03-29 12:50:08 +00:00
|
|
|
if len(es) != 1 {
|
|
|
|
|
t.Fatal("expected one hit")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the file with a new block hash
|
|
|
|
|
file.Blocks = genBlocks("foo", 42, 1)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Searching for the old hash should yield no hits
|
2025-05-01 20:44:25 +02:00
|
|
|
if hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, search)); err != nil {
|
2025-04-06 05:30:43 -07:00
|
|
|
t.Fatal(err)
|
|
|
|
|
} else if len(hits) != 0 {
|
2025-03-29 12:50:08 +00:00
|
|
|
t.Log(hits)
|
|
|
|
|
t.Error("expected no hits")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Searching for the new hash should yield one hits
|
2025-05-01 20:44:25 +02:00
|
|
|
if hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, file.Blocks[0].Hash)); err != nil {
|
2025-04-06 05:30:43 -07:00
|
|
|
t.Fatal(err)
|
|
|
|
|
} else if len(hits) != 1 {
|
2025-03-29 12:50:08 +00:00
|
|
|
t.Log(hits)
|
|
|
|
|
t.Error("expected one hit")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 11:58:09 +02:00
|
|
|
func TestDropBlockIndex(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
sdb, err := Open(t.TempDir())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { sdb.Close() })
|
|
|
|
|
|
|
|
|
|
// Insert files with blocks
|
|
|
|
|
files := []protocol.FileInfo{
|
|
|
|
|
genFile("a", 3, 0),
|
|
|
|
|
genFile("b", 2, 0),
|
|
|
|
|
}
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify blocks exist
|
|
|
|
|
hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, files[0].Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) == 0 {
|
|
|
|
|
t.Fatal("expected block hits before drop")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop the block index
|
|
|
|
|
if err := sdb.DropBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify blocks are gone
|
|
|
|
|
hits, err = itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, files[0].Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) != 0 {
|
|
|
|
|
t.Fatal("expected no block hits after drop")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dropping again should be a no-op (already empty)
|
|
|
|
|
if err := sdb.DropBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dropping a nonexistent folder should be fine
|
|
|
|
|
if err := sdb.DropBlockIndex("nonexistent"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPopulateBlockIndex(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
sdb, err := Open(t.TempDir())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { sdb.Close() })
|
|
|
|
|
|
|
|
|
|
// Insert files with blocks
|
|
|
|
|
files := []protocol.FileInfo{
|
|
|
|
|
genFile("a", 3, 0),
|
|
|
|
|
genFile("b", 2, 0),
|
|
|
|
|
}
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect the original block entries for comparison
|
|
|
|
|
origHitsA, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, files[0].Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(origHitsA) != 1 {
|
|
|
|
|
t.Fatal("expected one hit for block a[0]")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop the block index
|
|
|
|
|
if err := sdb.DropBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate it back from existing blocklists
|
|
|
|
|
if err := sdb.PopulateBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify all blocks are back
|
|
|
|
|
for i, f := range files {
|
|
|
|
|
for j, b := range f.Blocks {
|
|
|
|
|
hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, b.Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) == 0 {
|
|
|
|
|
t.Errorf("file %d block %d: expected hits after populate", i, j)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populating again should be a no-op (not empty)
|
|
|
|
|
if err := sdb.PopulateBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPopulateBlockIndexSkipsRemoteFiles(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
sdb, err := Open(t.TempDir())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { sdb.Close() })
|
|
|
|
|
|
|
|
|
|
// Insert a local file (blocks indexed) and a remote file (blocks not indexed)
|
|
|
|
|
localFile := genFile("local", 2, 0)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{localFile}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
remoteFile := genFile("remote", 2, 1)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{remoteFile}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop and repopulate
|
|
|
|
|
if err := sdb.DropBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := sdb.PopulateBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Local file blocks should be present
|
|
|
|
|
hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, localFile.Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) == 0 {
|
|
|
|
|
t.Error("expected hits for local file blocks")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remote file blocks should not be present (blocks are only
|
|
|
|
|
// indexed for local files)
|
|
|
|
|
hits, err = itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, remoteFile.Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) != 0 {
|
|
|
|
|
t.Error("expected no hits for remote file blocks")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSkipBlockIndexOnUpdate(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
sdb, err := Open(t.TempDir())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { sdb.Close() })
|
|
|
|
|
|
|
|
|
|
// Insert a file with SkipBlockIndex
|
|
|
|
|
file := genFile("a", 3, 0)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}, db.WithSkipBlockIndex()); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Blocks should not be indexed
|
|
|
|
|
hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, file.Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) != 0 {
|
|
|
|
|
t.Fatal("expected no block hits with SkipBlockIndex")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The blocklist should still be stored (file info is retrievable with blocks)
|
|
|
|
|
fi, ok, err := sdb.GetDeviceFile(folderID, protocol.LocalDeviceID, "a")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("file not found")
|
|
|
|
|
}
|
|
|
|
|
if len(fi.Blocks) != 3 {
|
|
|
|
|
t.Fatalf("expected 3 blocks in file info, got %d", len(fi.Blocks))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate should fill in the blocks
|
|
|
|
|
if err := sdb.PopulateBlockIndex(folderID); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hits, err = itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, file.Blocks[0].Hash))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(hits) != 1 {
|
|
|
|
|
t.Fatal("expected one hit after populate")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 12:50:08 +00:00
|
|
|
func TestRemoteSequence(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2025-08-24 11:58:56 +02:00
|
|
|
sdb, err := Open(t.TempDir())
|
2025-03-29 12:50:08 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
if err := sdb.Close(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Insert a local file
|
|
|
|
|
file := genFile("foo", 1, 0)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert several remote files
|
|
|
|
|
file = genFile("foo1", 1, 42)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
if err := sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
file = genFile("foo2", 1, 43)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
if err := sdb.Update(folderID, protocol.DeviceID{44}, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
file = genFile("foo3", 1, 44)
|
|
|
|
|
if err := sdb.Update(folderID, protocol.DeviceID{44}, []protocol.FileInfo{file}); err != nil {
|
|
|
|
|
t.Fatal()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify remote sequences
|
|
|
|
|
seqs, err := sdb.RemoteSequences(folderID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(seqs) != 3 || seqs[protocol.DeviceID{42}] != 42 ||
|
|
|
|
|
seqs[protocol.DeviceID{43}] != 43 ||
|
|
|
|
|
seqs[protocol.DeviceID{44}] != 44 {
|
|
|
|
|
t.Log(seqs)
|
|
|
|
|
t.Error("bad seqs")
|
|
|
|
|
}
|
|
|
|
|
}
|