123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626 |
- // 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 (
- "slices"
- "testing"
- "github.com/syncthing/syncthing/lib/config"
- "github.com/syncthing/syncthing/lib/protocol"
- )
- func TestNeed(t *testing.T) {
- t.Helper()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // Some local files
- var v protocol.Vector
- baseV := v.Update(1)
- newerV := baseV.Update(42)
- files := []protocol.FileInfo{
- genFile("test1", 1, 0), // remote need
- genFile("test2", 2, 0), // local need
- genFile("test3", 3, 0), // global
- }
- files[0].Version = baseV
- files[1].Version = baseV
- files[2].Version = newerV
- err = db.Update(folderID, protocol.LocalDeviceID, files)
- if err != nil {
- t.Fatal(err)
- }
- // Some remote files
- remote := []protocol.FileInfo{
- genFile("test2", 2, 100), // global
- genFile("test3", 3, 101), // remote need
- genFile("test4", 4, 102), // local need
- }
- remote[0].Version = newerV
- remote[1].Version = baseV
- remote[2].Version = newerV
- err = db.Update(folderID, protocol.DeviceID{42}, remote)
- if err != nil {
- t.Fatal(err)
- }
- // A couple are needed locally
- localNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)))
- if !slices.Equal(localNeed, []string{"test2", "test4"}) {
- t.Log(localNeed)
- t.Fatal("bad local need")
- }
- // Another couple are needed remotely
- remoteNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)))
- if !slices.Equal(remoteNeed, []string{"test1", "test3"}) {
- t.Log(remoteNeed)
- t.Fatal("bad remote need")
- }
- }
- func TestDropRecalcsGlobal(t *testing.T) {
- // When we drop a device we may get a new global
- t.Parallel()
- t.Run("DropAllFiles", func(t *testing.T) {
- t.Parallel()
- testDropWithDropper(t, func(t *testing.T, db *DB) {
- t.Helper()
- if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
- t.Fatal(err)
- }
- })
- })
- t.Run("DropDevice", func(t *testing.T) {
- t.Parallel()
- testDropWithDropper(t, func(t *testing.T, db *DB) {
- t.Helper()
- if err := db.DropDevice(protocol.DeviceID{42}); err != nil {
- t.Fatal(err)
- }
- })
- })
- t.Run("DropFilesNamed", func(t *testing.T) {
- t.Parallel()
- testDropWithDropper(t, func(t *testing.T, db *DB) {
- t.Helper()
- if err := db.DropFilesNamed(folderID, protocol.DeviceID{42}, []string{"test1", "test42"}); err != nil {
- t.Fatal(err)
- }
- })
- })
- }
- func testDropWithDropper(t *testing.T, dropper func(t *testing.T, db *DB)) {
- t.Helper()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // Some local files
- err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
- genFile("test1", 1, 0),
- genFile("test2", 2, 0),
- })
- if err != nil {
- t.Fatal(err)
- }
- // Some remote files
- remote := []protocol.FileInfo{
- genFile("test1", 3, 0),
- }
- remote[0].Version = remote[0].Version.Update(42)
- err = db.Update(folderID, protocol.DeviceID{42}, remote)
- if err != nil {
- t.Fatal(err)
- }
- // Remote test1 wins as the global, verify.
- count, err := db.CountGlobal(folderID)
- if err != nil {
- t.Fatal(err)
- }
- if count.Bytes != (2+3)*128<<10 {
- t.Log(count)
- t.Fatal("bad global size to begin with")
- }
- if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
- t.Fatal("missing global to begin with")
- } else if g.Size != 3*128<<10 {
- t.Fatal("remote test1 should be the global")
- }
- // Now remove that remote device
- dropper(t, db)
- // Our test1 should now be the global
- count, err = db.CountGlobal(folderID)
- if err != nil {
- t.Fatal(err)
- }
- if count.Bytes != (1+2)*128<<10 {
- t.Log(count)
- t.Fatal("bad global size after drop")
- }
- if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
- t.Fatal("missing global after drop")
- } else if g.Size != 1*128<<10 {
- t.Fatal("local test1 should be the global")
- }
- }
- func TestNeedDeleted(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // Some local files
- err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
- genFile("test1", 1, 0),
- genFile("test2", 2, 0),
- })
- if err != nil {
- t.Fatal(err)
- }
- // A remote deleted file
- remote := []protocol.FileInfo{
- genFile("test1", 1, 101),
- }
- remote[0].SetDeleted(42)
- err = db.Update(folderID, protocol.DeviceID{42}, remote)
- if err != nil {
- t.Fatal(err)
- }
- // We need the one deleted file
- s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Deleted != 1 {
- t.Log(s)
- t.Error("bad need")
- }
- }
- func TestDontNeedIgnored(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // A remote file
- files := []protocol.FileInfo{
- genFile("test1", 1, 103),
- }
- err = db.Update(folderID, protocol.DeviceID{42}, files)
- if err != nil {
- t.Fatal(err)
- }
- // Which we've ignored locally
- files[0].SetIgnored()
- err = db.Update(folderID, protocol.LocalDeviceID, files)
- if err != nil {
- t.Fatal(err)
- }
- // We don't need it
- s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 {
- t.Log(s)
- t.Error("bad need")
- }
- // It shouldn't show up in the need list
- names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 0 {
- t.Log(names)
- t.Error("need no files")
- }
- }
- func TestDontNeedRemoteInvalid(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // A remote file with the invalid bit set
- files := []protocol.FileInfo{
- genFile("test1", 1, 103),
- }
- files[0].LocalFlags = protocol.FlagLocalRemoteInvalid
- err = db.Update(folderID, protocol.DeviceID{42}, files)
- if err != nil {
- t.Fatal(err)
- }
- // It's not part of the global size
- s, err := db.CountGlobal(folderID)
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 {
- t.Log(s)
- t.Error("bad global")
- }
- // We don't need it
- s, err = db.CountNeed(folderID, protocol.LocalDeviceID)
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 {
- t.Log(s)
- t.Error("bad need")
- }
- // It shouldn't show up in the need list
- names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 0 {
- t.Log(names)
- t.Error("need no files")
- }
- }
- func TestRemoteDontNeedLocalIgnored(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // A local ignored file
- file := genFile("test1", 1, 103)
- file.SetIgnored()
- files := []protocol.FileInfo{file}
- err = db.Update(folderID, protocol.LocalDeviceID, files)
- if err != nil {
- t.Fatal(err)
- }
- // Which the remote doesn't have (no update)
- // They don't need it
- s, err := db.CountNeed(folderID, protocol.DeviceID{42})
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 {
- t.Log(s)
- t.Error("bad need")
- }
- // It shouldn't show up in their need list
- names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 0 {
- t.Log(names)
- t.Error("need no files")
- }
- }
- func TestLocalDontNeedDeletedMissing(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // A remote deleted file
- file := genFile("test1", 1, 103)
- file.SetDeleted(42)
- files := []protocol.FileInfo{file}
- err = db.Update(folderID, protocol.DeviceID{42}, files)
- if err != nil {
- t.Fatal(err)
- }
- // Which we don't have (no local update)
- // We don't need it
- s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
- t.Log(s)
- t.Error("bad need")
- }
- // It shouldn't show up in the need list
- names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 0 {
- t.Log(names)
- t.Error("need no files")
- }
- }
- func TestRemoteDontNeedDeletedMissing(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // A local deleted file
- file := genFile("test1", 1, 103)
- file.SetDeleted(42)
- files := []protocol.FileInfo{file}
- err = db.Update(folderID, protocol.LocalDeviceID, files)
- if err != nil {
- t.Fatal(err)
- }
- // Which the remote doesn't have (no local update)
- // They don't need it
- s, err := db.CountNeed(folderID, protocol.DeviceID{42})
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
- t.Log(s)
- t.Error("bad need")
- }
- // It shouldn't show up in their need list
- names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 0 {
- t.Log(names)
- t.Error("need no files")
- }
- // Another remote has announced it, but has set the invalid bit,
- // presumably it's being ignored.
- file = genFile("test1", 1, 103)
- file.SetIgnored()
- err = db.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file})
- if err != nil {
- t.Fatal(err)
- }
- // They don't need it, either
- s, err = db.CountNeed(folderID, protocol.DeviceID{43})
- if err != nil {
- t.Fatal(err)
- }
- if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
- t.Log(s)
- t.Error("bad need")
- }
- // It shouldn't show up in their need list
- names = mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 0 {
- t.Log(names)
- t.Error("need no files")
- }
- }
- func TestNeedRemoteSymlinkAndDir(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // Two remote "specials", a symlink and a directory
- var v protocol.Vector
- v.Update(1)
- files := []protocol.FileInfo{
- {Name: "sym", Type: protocol.FileInfoTypeSymlink, Sequence: 100, Version: v, Blocks: genBlocks("symlink", 0, 1)},
- {Name: "dir", Type: protocol.FileInfoTypeDirectory, Sequence: 101, Version: v},
- }
- err = db.Update(folderID, protocol.DeviceID{42}, files)
- if err != nil {
- t.Fatal(err)
- }
- // We need them
- s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
- if err != nil {
- t.Fatal(err)
- }
- if s.Directories != 1 || s.Symlinks != 1 {
- t.Log(s)
- t.Error("bad need")
- }
- // They should be in the need list
- names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
- if len(names) != 2 {
- t.Log(names)
- t.Error("bad need")
- }
- }
- func TestNeedPagination(t *testing.T) {
- t.Parallel()
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // Several remote files
- var v protocol.Vector
- v.Update(1)
- files := []protocol.FileInfo{
- genFile("test0", 1, 100),
- genFile("test1", 1, 101),
- genFile("test2", 1, 102),
- genFile("test3", 1, 103),
- genFile("test4", 1, 104),
- genFile("test5", 1, 105),
- genFile("test6", 1, 106),
- genFile("test7", 1, 107),
- genFile("test8", 1, 108),
- genFile("test9", 1, 109),
- }
- err = db.Update(folderID, protocol.DeviceID{42}, files)
- if err != nil {
- t.Fatal(err)
- }
- // We should get the first two
- names := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 2, 0)))
- if !slices.Equal(names, []string{"test0", "test1"}) {
- t.Log(names)
- t.Error("bad need")
- }
- // We should get the next three
- names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 3, 2)))
- if !slices.Equal(names, []string{"test2", "test3", "test4"}) {
- t.Log(names)
- t.Error("bad need")
- }
- // We should get the last five
- names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 5, 5)))
- if !slices.Equal(names, []string{"test5", "test6", "test7", "test8", "test9"}) {
- t.Log(names)
- t.Error("bad need")
- }
- }
- func TestDeletedAfterConflict(t *testing.T) {
- t.Parallel()
- // A delete that comes after a conflict should be applied, not lose the
- // conflict and suddenly cause an old conflict version to become
- // promoted.
- // D:\syncthing-windows-amd64-v2.0.0-rc.22.dev.11.gff88430e>syncthing --home=c:\PortableApp\SyncTrayzorPortable-x64\data\syncthing debug database-file tnhbr-gxtuf TreeSizeFreeSetup.exe
- // DEVICE TYPE NAME SEQUENCE DELETED MODIFIED SIZE FLAGS VERSION BLOCKLIST
- // -local- FILE TreeSizeFreeSetup.exe 499 del 2025-07-04T11:52:36.2804841Z 0 ------- HZJYWFM:1751507473,OMKHRPB:1751629956 -nil-
- // J5WNYJ6 FILE TreeSizeFreeSetup.exe 500 del 2025-07-04T11:52:36.2804841Z 0 ------- HZJYWFM:1751507473,OMKHRPB:1751629956 -nil-
- // 23NHXGS FILE TreeSizeFreeSetup.exe 445 --- 2025-06-23T03:16:10.2804841Z 13832808 -nG---- HZJYWFM:1751507473 7B4kLitF
- // JKX6ZDN FILE TreeSizeFreeSetup.exe 320 --- 2025-06-23T03:16:10.2804841Z 13832808 ------- JKX6ZDN:1750992570 7B4kLitF
- db, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- if err := db.Close(); err != nil {
- t.Fatal(err)
- }
- })
- // A file, updated by some remote device. This file is an old, conflicted copy.
- file := genFile("test1", 1, 101)
- file.ModifiedS = 1750992570
- file.Version = protocol.Vector{Counters: []protocol.Counter{{ID: 5 << 60, Value: 1750992570}}}
- if err := db.Update(folderID, protocol.DeviceID{5}, []protocol.FileInfo{file}); err != nil {
- t.Fatal(err)
- }
- // The file, updated by a newer remote device. This file is the newer, conflict-winning copy.
- file.ModifiedS = 1751507473
- file.Version = protocol.Vector{Counters: []protocol.Counter{{ID: 2 << 60, Value: 1751507473}}}
- if err := db.Update(folderID, protocol.DeviceID{2}, []protocol.FileInfo{file}); err != nil {
- t.Fatal(err)
- }
- // The file, deleted locally after syncing the file from the remote above..
- file.SetDeleted(4)
- if err := db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
- t.Fatal(err)
- }
- // The delete should be the global version
- f, _, err := db.GetGlobalFile(folderID, "test1")
- if err != nil {
- t.Fatal(err)
- }
- if !f.IsDeleted() {
- t.Log(f)
- t.Error("should be deleted")
- }
- }
|