Switch the database from LevelDB to SQLite, for greater stability and simpler code. Co-authored-by: Tommy van der Vorst <tommy@pixelspark.nl> Co-authored-by: bt90 <btom1990@googlemail.com>
This commit is contained in:
co-authored by
Tommy van der Vorst
bt90
parent
b1c8f88a44
commit
025905fcdf
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2014 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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type Counts struct {
|
||||
Files int
|
||||
Directories int
|
||||
Symlinks int
|
||||
Deleted int
|
||||
Bytes int64
|
||||
Sequence int64 // zero for the global state
|
||||
DeviceID protocol.DeviceID // device ID for remote devices, or special values for local/global
|
||||
LocalFlags uint32 // the local flag for this count bucket
|
||||
}
|
||||
|
||||
func (c Counts) Add(other Counts) Counts {
|
||||
return Counts{
|
||||
Files: c.Files + other.Files,
|
||||
Directories: c.Directories + other.Directories,
|
||||
Symlinks: c.Symlinks + other.Symlinks,
|
||||
Deleted: c.Deleted + other.Deleted,
|
||||
Bytes: c.Bytes + other.Bytes,
|
||||
Sequence: c.Sequence + other.Sequence,
|
||||
DeviceID: protocol.EmptyDeviceID,
|
||||
LocalFlags: c.LocalFlags | other.LocalFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Counts) TotalItems() int {
|
||||
return c.Files + c.Directories + c.Symlinks + c.Deleted
|
||||
}
|
||||
|
||||
func (c Counts) String() string {
|
||||
var flags strings.Builder
|
||||
if c.LocalFlags&protocol.FlagLocalNeeded != 0 {
|
||||
flags.WriteString("Need")
|
||||
}
|
||||
if c.LocalFlags&protocol.FlagLocalIgnored != 0 {
|
||||
flags.WriteString("Ignored")
|
||||
}
|
||||
if c.LocalFlags&protocol.FlagLocalMustRescan != 0 {
|
||||
flags.WriteString("Rescan")
|
||||
}
|
||||
if c.LocalFlags&protocol.FlagLocalReceiveOnly != 0 {
|
||||
flags.WriteString("Recvonly")
|
||||
}
|
||||
if c.LocalFlags&protocol.FlagLocalUnsupported != 0 {
|
||||
flags.WriteString("Unsupported")
|
||||
}
|
||||
if c.LocalFlags != 0 {
|
||||
flags.WriteString(fmt.Sprintf("(%x)", c.LocalFlags))
|
||||
}
|
||||
if flags.Len() == 0 {
|
||||
flags.WriteString("---")
|
||||
}
|
||||
return fmt.Sprintf("{Device:%v, Files:%d, Dirs:%d, Symlinks:%d, Del:%d, Bytes:%d, Seq:%d, Flags:%s}", c.DeviceID, c.Files, c.Directories, c.Symlinks, c.Deleted, c.Bytes, c.Sequence, flags.String())
|
||||
}
|
||||
|
||||
// Equal compares the numbers only, not sequence/dev/flags.
|
||||
func (c Counts) Equal(o Counts) bool {
|
||||
return c.Files == o.Files && c.Directories == o.Directories && c.Symlinks == o.Symlinks && c.Deleted == o.Deleted && c.Bytes == o.Bytes
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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 db // import "github.com/syncthing/syncthing/internal/db/sqlite"
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
type DB interface {
|
||||
Service(maintenanceInterval time.Duration) suture.Service
|
||||
|
||||
// Basics
|
||||
Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error
|
||||
Close() error
|
||||
|
||||
// Single files
|
||||
GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error)
|
||||
GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error)
|
||||
GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error)
|
||||
|
||||
// File iterators
|
||||
//
|
||||
// n.b. there is a slight inconsistency in the return types where some
|
||||
// return a FileInfo iterator and some a FileMetadata iterator. The
|
||||
// latter is more lightweight, and the discrepancy depends on how the
|
||||
// functions tend to be used. We can introduce more variations as
|
||||
// required.
|
||||
AllGlobalFiles(folder string) (iter.Seq[FileMetadata], func() error)
|
||||
AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[FileMetadata], func() error)
|
||||
AllLocalBlocksWithHash(hash []byte) (iter.Seq[BlockMapEntry], func() error)
|
||||
AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error)
|
||||
AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error)
|
||||
AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error)
|
||||
AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[FileMetadata], func() error)
|
||||
AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, FileMetadata], func() error)
|
||||
AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error)
|
||||
|
||||
// Cleanup
|
||||
DropAllFiles(folder string, device protocol.DeviceID) error
|
||||
DropDevice(device protocol.DeviceID) error
|
||||
DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
|
||||
DropFolder(folder string) error
|
||||
|
||||
// Various metadata
|
||||
GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error)
|
||||
ListFolders() ([]string, error)
|
||||
ListDevicesForFolder(folder string) ([]protocol.DeviceID, error)
|
||||
RemoteSequences(folder string) (map[protocol.DeviceID]int64, error)
|
||||
|
||||
// Counts
|
||||
CountGlobal(folder string) (Counts, error)
|
||||
CountLocal(folder string, device protocol.DeviceID) (Counts, error)
|
||||
CountNeed(folder string, device protocol.DeviceID) (Counts, error)
|
||||
CountReceiveOnlyChanged(folder string) (Counts, error)
|
||||
|
||||
// Index IDs
|
||||
DropAllIndexIDs() error
|
||||
GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error)
|
||||
SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error
|
||||
|
||||
// MtimeFS
|
||||
DeleteMtime(folder, name string) error
|
||||
GetMtime(folder, name string) (ondisk, virtual time.Time)
|
||||
PutMtime(folder, name string, ondisk, virtual time.Time) error
|
||||
|
||||
KV
|
||||
}
|
||||
|
||||
// Generic KV store
|
||||
type KV interface {
|
||||
GetKV(key string) ([]byte, error)
|
||||
PutKV(key string, val []byte) error
|
||||
DeleteKV(key string) error
|
||||
PrefixKV(prefix string) (iter.Seq[KeyValue], func() error)
|
||||
}
|
||||
|
||||
type BlockMapEntry struct {
|
||||
BlocklistHash []byte
|
||||
Offset int64
|
||||
BlockIndex int
|
||||
Size int
|
||||
}
|
||||
|
||||
type KeyValue struct {
|
||||
Key string
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type FileMetadata struct {
|
||||
Name string
|
||||
Sequence int64
|
||||
ModNanos int64
|
||||
Size int64
|
||||
LocalFlags int64
|
||||
Type protocol.FileInfoType
|
||||
Deleted bool
|
||||
Invalid bool
|
||||
}
|
||||
|
||||
func (f *FileMetadata) ModTime() time.Time {
|
||||
return time.Unix(0, f.ModNanos)
|
||||
}
|
||||
|
||||
func (f *FileMetadata) IsReceiveOnlyChanged() bool {
|
||||
return f.LocalFlags&protocol.FlagLocalReceiveOnly != 0
|
||||
}
|
||||
|
||||
func (f *FileMetadata) IsDirectory() bool {
|
||||
return f.Type == protocol.FileInfoTypeDirectory
|
||||
}
|
||||
|
||||
func (f *FileMetadata) ShouldConflict() bool {
|
||||
return f.LocalFlags&protocol.LocalConflictFlags != 0
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// 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 db
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
var (
|
||||
metricCurrentOperations = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "db",
|
||||
Name: "operations_current",
|
||||
}, []string{"folder", "operation"})
|
||||
metricTotalOperationSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "db",
|
||||
Name: "operation_seconds_total",
|
||||
Help: "Total time spent in database operations, per folder and operation",
|
||||
}, []string{"folder", "operation"})
|
||||
metricTotalOperationsCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "db",
|
||||
Name: "operations_total",
|
||||
Help: "Total number of database operations, per folder and operation",
|
||||
}, []string{"folder", "operation"})
|
||||
metricTotalFilesUpdatedCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "db",
|
||||
Name: "files_updated_total",
|
||||
Help: "Total number of files updated",
|
||||
}, []string{"folder"})
|
||||
)
|
||||
|
||||
func MetricsWrap(db DB) DB {
|
||||
return metricsDB{db}
|
||||
}
|
||||
|
||||
type metricsDB struct {
|
||||
DB
|
||||
}
|
||||
|
||||
func (m metricsDB) account(folder, op string) func() {
|
||||
t0 := time.Now()
|
||||
metricCurrentOperations.WithLabelValues(folder, op).Inc()
|
||||
return func() {
|
||||
if dur := time.Since(t0).Seconds(); dur > 0 {
|
||||
metricTotalOperationSeconds.WithLabelValues(folder, op).Add(dur)
|
||||
}
|
||||
metricTotalOperationsCount.WithLabelValues(folder, op).Inc()
|
||||
metricCurrentOperations.WithLabelValues(folder, op).Dec()
|
||||
}
|
||||
}
|
||||
|
||||
func (m metricsDB) AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[FileMetadata], func() error) {
|
||||
defer m.account(folder, "AllLocalFilesWithBlocksHash")()
|
||||
return m.DB.AllLocalFilesWithBlocksHash(folder, h)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, FileMetadata], func() error) {
|
||||
defer m.account("-", "AllLocalFilesWithBlocksHashAnyFolder")()
|
||||
return m.DB.AllLocalFilesWithBlocksHashAnyFolder(h)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllGlobalFiles(folder string) (iter.Seq[FileMetadata], func() error) {
|
||||
defer m.account(folder, "AllGlobalFiles")()
|
||||
return m.DB.AllGlobalFiles(folder)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[FileMetadata], func() error) {
|
||||
defer m.account(folder, "AllGlobalFilesPrefix")()
|
||||
return m.DB.AllGlobalFilesPrefix(folder, prefix)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
defer m.account(folder, "AllLocalFiles")()
|
||||
return m.DB.AllLocalFiles(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
defer m.account(folder, "AllLocalFilesPrefix")()
|
||||
return m.DB.AllLocalFilesWithPrefix(folder, device, prefix)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
defer m.account(folder, "AllLocalFilesBySequence")()
|
||||
return m.DB.AllLocalFilesBySequence(folder, device, startSeq, limit)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
defer m.account(folder, "AllNeededGlobalFiles")()
|
||||
return m.DB.AllNeededGlobalFiles(folder, device, order, limit, offset)
|
||||
}
|
||||
|
||||
func (m metricsDB) GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error) {
|
||||
defer m.account(folder, "GetGlobalAvailability")()
|
||||
return m.DB.GetGlobalAvailability(folder, file)
|
||||
}
|
||||
|
||||
func (m metricsDB) AllLocalBlocksWithHash(hash []byte) (iter.Seq[BlockMapEntry], func() error) {
|
||||
defer m.account("-", "AllLocalBlocksWithHash")()
|
||||
return m.DB.AllLocalBlocksWithHash(hash)
|
||||
}
|
||||
|
||||
func (m metricsDB) Close() error {
|
||||
defer m.account("-", "Close")()
|
||||
return m.DB.Close()
|
||||
}
|
||||
|
||||
func (m metricsDB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
|
||||
defer m.account(folder, "ListDevicesForFolder")()
|
||||
return m.DB.ListDevicesForFolder(folder)
|
||||
}
|
||||
|
||||
func (m metricsDB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
|
||||
defer m.account(folder, "RemoteSequences")()
|
||||
return m.DB.RemoteSequences(folder)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
|
||||
defer m.account(folder, "DropAllFiles")()
|
||||
return m.DB.DropAllFiles(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropDevice(device protocol.DeviceID) error {
|
||||
defer m.account("-", "DropDevice")()
|
||||
return m.DB.DropDevice(device)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
|
||||
defer m.account(folder, "DropFilesNamed")()
|
||||
return m.DB.DropFilesNamed(folder, device, names)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropFolder(folder string) error {
|
||||
defer m.account(folder, "DropFolder")()
|
||||
return m.DB.DropFolder(folder)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropAllIndexIDs() error {
|
||||
defer m.account("-", "IndexIDDropAll")()
|
||||
return m.DB.DropAllIndexIDs()
|
||||
}
|
||||
|
||||
func (m metricsDB) ListFolders() ([]string, error) {
|
||||
defer m.account("-", "ListFolders")()
|
||||
return m.DB.ListFolders()
|
||||
}
|
||||
|
||||
func (m metricsDB) GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
|
||||
defer m.account(folder, "GetGlobalFile")()
|
||||
return m.DB.GetGlobalFile(folder, file)
|
||||
}
|
||||
|
||||
func (m metricsDB) CountGlobal(folder string) (Counts, error) {
|
||||
defer m.account(folder, "CountGlobal")()
|
||||
return m.DB.CountGlobal(folder)
|
||||
}
|
||||
|
||||
func (m metricsDB) GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error) {
|
||||
defer m.account(folder, "IndexIDGet")()
|
||||
return m.DB.GetIndexID(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error) {
|
||||
defer m.account(folder, "GetDeviceFile")()
|
||||
return m.DB.GetDeviceFile(folder, device, file)
|
||||
}
|
||||
|
||||
func (m metricsDB) CountLocal(folder string, device protocol.DeviceID) (Counts, error) {
|
||||
defer m.account(folder, "CountLocal")()
|
||||
return m.DB.CountLocal(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) CountNeed(folder string, device protocol.DeviceID) (Counts, error) {
|
||||
defer m.account(folder, "CountNeed")()
|
||||
return m.DB.CountNeed(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) CountReceiveOnlyChanged(folder string) (Counts, error) {
|
||||
defer m.account(folder, "CountReceiveOnlyChanged")()
|
||||
return m.DB.CountReceiveOnlyChanged(folder)
|
||||
}
|
||||
|
||||
func (m metricsDB) GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error) {
|
||||
defer m.account(folder, "GetDeviceSequence")()
|
||||
return m.DB.GetDeviceSequence(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error {
|
||||
defer m.account(folder, "IndexIDSet")()
|
||||
return m.DB.SetIndexID(folder, device, id)
|
||||
}
|
||||
|
||||
func (m metricsDB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
|
||||
defer m.account(folder, "Update")()
|
||||
defer metricTotalFilesUpdatedCount.WithLabelValues(folder).Add(float64(len(fs)))
|
||||
return m.DB.Update(folder, device, fs)
|
||||
}
|
||||
|
||||
func (m metricsDB) GetKV(key string) ([]byte, error) {
|
||||
defer m.account("-", "GetKV")()
|
||||
return m.DB.GetKV(key)
|
||||
}
|
||||
|
||||
func (m metricsDB) PutKV(key string, val []byte) error {
|
||||
defer m.account("-", "PutKV")()
|
||||
return m.DB.PutKV(key, val)
|
||||
}
|
||||
|
||||
func (m metricsDB) DeleteKV(key string) error {
|
||||
defer m.account("-", "DeleteKV")()
|
||||
return m.DB.DeleteKV(key)
|
||||
}
|
||||
|
||||
func (m metricsDB) PrefixKV(prefix string) (iter.Seq[KeyValue], func() error) {
|
||||
defer m.account("-", "PrefixKV")()
|
||||
return m.DB.PrefixKV(prefix)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright (C) 2020 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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type ObservedDB struct {
|
||||
kv KV
|
||||
}
|
||||
|
||||
func NewObservedDB(kv KV) *ObservedDB {
|
||||
return &ObservedDB{kv: kv}
|
||||
}
|
||||
|
||||
type ObservedFolder struct {
|
||||
Time time.Time `json:"time"`
|
||||
Label string `json:"label"`
|
||||
ReceiveEncrypted bool `json:"receiveEncrypted"`
|
||||
RemoteEncrypted bool `json:"remoteEncrypted"`
|
||||
}
|
||||
|
||||
func (o *ObservedFolder) toWire() *dbproto.ObservedFolder {
|
||||
return &dbproto.ObservedFolder{
|
||||
Time: timestamppb.New(o.Time),
|
||||
Label: o.Label,
|
||||
ReceiveEncrypted: o.ReceiveEncrypted,
|
||||
RemoteEncrypted: o.RemoteEncrypted,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ObservedFolder) fromWire(w *dbproto.ObservedFolder) {
|
||||
o.Time = w.GetTime().AsTime()
|
||||
o.Label = w.GetLabel()
|
||||
o.ReceiveEncrypted = w.GetReceiveEncrypted()
|
||||
o.RemoteEncrypted = w.GetRemoteEncrypted()
|
||||
}
|
||||
|
||||
type ObservedDevice struct {
|
||||
Time time.Time `json:"time"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
func (o *ObservedDevice) fromWire(w *dbproto.ObservedDevice) {
|
||||
o.Time = w.GetTime().AsTime()
|
||||
o.Name = w.GetName()
|
||||
o.Address = w.GetAddress()
|
||||
}
|
||||
|
||||
func (db *ObservedDB) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
|
||||
key := "device/" + device.String()
|
||||
od := &dbproto.ObservedDevice{
|
||||
Time: timestamppb.New(time.Now().Truncate(time.Second)),
|
||||
Name: name,
|
||||
Address: address,
|
||||
}
|
||||
return db.kv.PutKV(key, mustMarshal(od))
|
||||
}
|
||||
|
||||
func (db *ObservedDB) RemovePendingDevice(device protocol.DeviceID) error {
|
||||
key := "device/" + device.String()
|
||||
return db.kv.DeleteKV(key)
|
||||
}
|
||||
|
||||
// PendingDevices enumerates all entries. Invalid ones are dropped from the database
|
||||
// after a warning log message, as a side-effect.
|
||||
func (db *ObservedDB) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
|
||||
res := make(map[protocol.DeviceID]ObservedDevice)
|
||||
it, errFn := db.kv.PrefixKV("device/")
|
||||
for kv := range it {
|
||||
_, keyDev, ok := strings.Cut(kv.Key, "/")
|
||||
if !ok {
|
||||
if err := db.kv.DeleteKV(kv.Key); err != nil {
|
||||
return nil, fmt.Errorf("delete invalid pending device: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
deviceID, err := protocol.DeviceIDFromString(keyDev)
|
||||
var protoD dbproto.ObservedDevice
|
||||
var od ObservedDevice
|
||||
if err != nil {
|
||||
goto deleteKey
|
||||
}
|
||||
if err = proto.Unmarshal(kv.Value, &protoD); err != nil {
|
||||
goto deleteKey
|
||||
}
|
||||
od.fromWire(&protoD)
|
||||
res[deviceID] = od
|
||||
continue
|
||||
deleteKey:
|
||||
// Deleting invalid entries is the only possible "repair" measure and
|
||||
// appropriate for the importance of pending entries. They will come back
|
||||
// soon if still relevant.
|
||||
if err := db.kv.DeleteKV(kv.Key); err != nil {
|
||||
return nil, fmt.Errorf("delete invalid pending device: %w", err)
|
||||
}
|
||||
}
|
||||
return res, errFn()
|
||||
}
|
||||
|
||||
func (db *ObservedDB) AddOrUpdatePendingFolder(id string, of ObservedFolder, device protocol.DeviceID) error {
|
||||
key := "folder/" + device.String() + "/" + id
|
||||
return db.kv.PutKV(key, mustMarshal(of.toWire()))
|
||||
}
|
||||
|
||||
// RemovePendingFolderForDevice removes entries for specific folder / device combinations.
|
||||
func (db *ObservedDB) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
|
||||
key := "folder/" + device.String() + "/" + id
|
||||
return db.kv.DeleteKV(key)
|
||||
}
|
||||
|
||||
// RemovePendingFolder removes all entries matching a specific folder ID.
|
||||
func (db *ObservedDB) RemovePendingFolder(id string) error {
|
||||
it, errFn := db.kv.PrefixKV("folder/")
|
||||
for kv := range it {
|
||||
parts := strings.Split(kv.Key, "/")
|
||||
if len(parts) != 3 || parts[2] != id {
|
||||
continue
|
||||
}
|
||||
if err := db.kv.DeleteKV(kv.Key); err != nil {
|
||||
return fmt.Errorf("delete pending folder: %w", err)
|
||||
}
|
||||
}
|
||||
return errFn()
|
||||
}
|
||||
|
||||
// Consolidated information about a pending folder
|
||||
type PendingFolder struct {
|
||||
OfferedBy map[protocol.DeviceID]ObservedFolder `json:"offeredBy"`
|
||||
}
|
||||
|
||||
func (db *ObservedDB) PendingFolders() (map[string]PendingFolder, error) {
|
||||
return db.PendingFoldersForDevice(protocol.EmptyDeviceID)
|
||||
}
|
||||
|
||||
// PendingFoldersForDevice enumerates only entries matching the given device ID, unless it
|
||||
// is EmptyDeviceID. Invalid ones are dropped from the database after a info log
|
||||
// message, as a side-effect.
|
||||
func (db *ObservedDB) PendingFoldersForDevice(device protocol.DeviceID) (map[string]PendingFolder, error) {
|
||||
prefix := "folder/"
|
||||
if device != protocol.EmptyDeviceID {
|
||||
prefix += device.String() + "/"
|
||||
}
|
||||
res := make(map[string]PendingFolder)
|
||||
it, errFn := db.kv.PrefixKV(prefix)
|
||||
for kv := range it {
|
||||
parts := strings.Split(kv.Key, "/")
|
||||
if len(parts) != 3 {
|
||||
continue
|
||||
}
|
||||
keyDev := parts[1]
|
||||
deviceID, err := protocol.DeviceIDFromString(keyDev)
|
||||
var protoF dbproto.ObservedFolder
|
||||
var of ObservedFolder
|
||||
var folderID string
|
||||
if err != nil {
|
||||
goto deleteKey
|
||||
}
|
||||
if folderID = parts[2]; len(folderID) < 1 {
|
||||
goto deleteKey
|
||||
}
|
||||
if err = proto.Unmarshal(kv.Value, &protoF); err != nil {
|
||||
goto deleteKey
|
||||
}
|
||||
if _, ok := res[folderID]; !ok {
|
||||
res[folderID] = PendingFolder{
|
||||
OfferedBy: map[protocol.DeviceID]ObservedFolder{},
|
||||
}
|
||||
}
|
||||
of.fromWire(&protoF)
|
||||
res[folderID].OfferedBy[deviceID] = of
|
||||
continue
|
||||
deleteKey:
|
||||
// Deleting invalid entries is the only possible "repair" measure and
|
||||
// appropriate for the importance of pending entries. They will come back
|
||||
// soon if still relevant.
|
||||
if err := db.kv.DeleteKV(kv.Key); err != nil {
|
||||
return nil, fmt.Errorf("delete invalid pending folder: %w", err)
|
||||
}
|
||||
}
|
||||
return res, errFn()
|
||||
}
|
||||
|
||||
func mustMarshal(m proto.Message) []byte {
|
||||
bs, err := proto.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (C) 2019 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 backend
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CommitHook is a function that is executed before a WriteTransaction is
|
||||
// committed or before it is flushed to disk, e.g. on calling CheckPoint. The
|
||||
// transaction can be accessed via a closure.
|
||||
type CommitHook func(WriteTransaction) error
|
||||
|
||||
// The Reader interface specifies the read-only operations available on the
|
||||
// main database and on read-only transactions (snapshots). Note that when
|
||||
// called directly on the database handle these operations may take implicit
|
||||
// transactions and performance may suffer.
|
||||
type Reader interface {
|
||||
Get(key []byte) ([]byte, error)
|
||||
NewPrefixIterator(prefix []byte) (Iterator, error)
|
||||
NewRangeIterator(first, last []byte) (Iterator, error)
|
||||
}
|
||||
|
||||
// The Writer interface specifies the mutating operations available on the
|
||||
// main database and on writable transactions. Note that when called
|
||||
// directly on the database handle these operations may take implicit
|
||||
// transactions and performance may suffer.
|
||||
type Writer interface {
|
||||
Put(key, val []byte) error
|
||||
Delete(key []byte) error
|
||||
}
|
||||
|
||||
// The ReadTransaction interface specifies the operations on read-only
|
||||
// transactions. Every ReadTransaction must be released when no longer
|
||||
// required.
|
||||
type ReadTransaction interface {
|
||||
Reader
|
||||
Release()
|
||||
}
|
||||
|
||||
// The WriteTransaction interface specifies the operations on writable
|
||||
// transactions. Every WriteTransaction must be either committed or released
|
||||
// (i.e., discarded) when no longer required. No further operations must be
|
||||
// performed after release or commit (regardless of whether commit succeeded),
|
||||
// with one exception -- it's fine to release an already committed or released
|
||||
// transaction.
|
||||
//
|
||||
// A Checkpoint is a potential partial commit of the transaction so far, for
|
||||
// purposes of saving memory when transactions are in-RAM. Note that
|
||||
// transactions may be checkpointed *anyway* even if this is not called, due to
|
||||
// resource constraints, but this gives you a chance to decide when. If, and
|
||||
// only if, calling Checkpoint will result in a partial commit/flush, the
|
||||
// CommitHooks passed to Backend.NewWriteTransaction are called before
|
||||
// committing. If any of those returns an error, committing is aborted and the
|
||||
// error bubbled.
|
||||
type WriteTransaction interface {
|
||||
ReadTransaction
|
||||
Writer
|
||||
Checkpoint() error
|
||||
Commit() error
|
||||
}
|
||||
|
||||
// The Iterator interface specifies the operations available on iterators
|
||||
// returned by NewPrefixIterator and NewRangeIterator. The iterator pattern
|
||||
// is to loop while Next returns true, then check Error after the loop. Next
|
||||
// will return false when iteration is complete (Error() == nil) or when
|
||||
// there is an error preventing iteration, which is then returned by
|
||||
// Error(). For example:
|
||||
//
|
||||
// it, err := db.NewPrefixIterator(nil)
|
||||
// if err != nil {
|
||||
// // problem preventing iteration
|
||||
// }
|
||||
// defer it.Release()
|
||||
// for it.Next() {
|
||||
// // ...
|
||||
// }
|
||||
// if err := it.Error(); err != nil {
|
||||
// // there was a database problem while iterating
|
||||
// }
|
||||
//
|
||||
// An iterator must be Released when no longer required. The Error method
|
||||
// can be called either before or after Release with the same results. If an
|
||||
// iterator was created in a transaction (whether read-only or write) it
|
||||
// must be released before the transaction is released (or committed).
|
||||
type Iterator interface {
|
||||
Next() bool
|
||||
Key() []byte
|
||||
Value() []byte
|
||||
Error() error
|
||||
Release()
|
||||
}
|
||||
|
||||
// The Backend interface represents the main database handle. It supports
|
||||
// both read/write operations and opening read-only or writable
|
||||
// transactions. Depending on the actual implementation, individual
|
||||
// read/write operations may be implicitly wrapped in transactions, making
|
||||
// them perform quite badly when used repeatedly. For bulk operations,
|
||||
// consider always using a transaction of the appropriate type. The
|
||||
// transaction isolation level is "read committed" - there are no dirty
|
||||
// reads.
|
||||
// Location returns the path to the database, as given to Open. The returned string
|
||||
// is empty for a db in memory.
|
||||
type Backend interface {
|
||||
Reader
|
||||
NewReadTransaction() (ReadTransaction, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
var (
|
||||
errClosed = errors.New("database is closed")
|
||||
errNotFound = errors.New("key not found")
|
||||
)
|
||||
|
||||
func IsClosed(err error) bool { return errors.Is(err, errClosed) }
|
||||
func IsNotFound(err error) bool { return errors.Is(err, errNotFound) }
|
||||
|
||||
// releaser manages counting on top of a waitgroup
|
||||
type releaser struct {
|
||||
wg *closeWaitGroup
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func newReleaser(wg *closeWaitGroup) (*releaser, error) {
|
||||
if err := wg.Add(1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &releaser{wg: wg}, nil
|
||||
}
|
||||
|
||||
func (r *releaser) Release() {
|
||||
// We use the Once because we may get called multiple times from
|
||||
// Commit() and deferred Release().
|
||||
r.once.Do(r.wg.Done)
|
||||
}
|
||||
|
||||
// closeWaitGroup behaves just like a sync.WaitGroup, but does not require
|
||||
// a single routine to do the Add and Wait calls. If Add is called after
|
||||
// CloseWait, it will return an error, and both are safe to be used concurrently.
|
||||
type closeWaitGroup struct {
|
||||
sync.WaitGroup
|
||||
closed bool
|
||||
closeMut sync.RWMutex
|
||||
}
|
||||
|
||||
func (cg *closeWaitGroup) Add(i int) error {
|
||||
cg.closeMut.RLock()
|
||||
defer cg.closeMut.RUnlock()
|
||||
if cg.closed {
|
||||
return errClosed
|
||||
}
|
||||
cg.WaitGroup.Add(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cg *closeWaitGroup) CloseWait() {
|
||||
cg.closeMut.Lock()
|
||||
cg.closed = true
|
||||
cg.closeMut.Unlock()
|
||||
cg.WaitGroup.Wait()
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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 backend
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// leveldbBackend implements Backend on top of a leveldb
|
||||
type leveldbBackend struct {
|
||||
ldb *leveldb.DB
|
||||
closeWG *closeWaitGroup
|
||||
location string
|
||||
}
|
||||
|
||||
func newLeveldbBackend(ldb *leveldb.DB, location string) *leveldbBackend {
|
||||
return &leveldbBackend{
|
||||
ldb: ldb,
|
||||
closeWG: &closeWaitGroup{},
|
||||
location: location,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) NewReadTransaction() (ReadTransaction, error) {
|
||||
return b.newSnapshot()
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) newSnapshot() (leveldbSnapshot, error) {
|
||||
rel, err := newReleaser(b.closeWG)
|
||||
if err != nil {
|
||||
return leveldbSnapshot{}, err
|
||||
}
|
||||
snap, err := b.ldb.GetSnapshot()
|
||||
if err != nil {
|
||||
rel.Release()
|
||||
return leveldbSnapshot{}, wrapLeveldbErr(err)
|
||||
}
|
||||
return leveldbSnapshot{
|
||||
snap: snap,
|
||||
rel: rel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) Close() error {
|
||||
b.closeWG.CloseWait()
|
||||
return wrapLeveldbErr(b.ldb.Close())
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) Get(key []byte) ([]byte, error) {
|
||||
val, err := b.ldb.Get(key, nil)
|
||||
return val, wrapLeveldbErr(err)
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) NewPrefixIterator(prefix []byte) (Iterator, error) {
|
||||
return &leveldbIterator{b.ldb.NewIterator(util.BytesPrefix(prefix), nil)}, nil
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) NewRangeIterator(first, last []byte) (Iterator, error) {
|
||||
return &leveldbIterator{b.ldb.NewIterator(&util.Range{Start: first, Limit: last}, nil)}, nil
|
||||
}
|
||||
|
||||
func (b *leveldbBackend) Location() string {
|
||||
return b.location
|
||||
}
|
||||
|
||||
// leveldbSnapshot implements backend.ReadTransaction
|
||||
type leveldbSnapshot struct {
|
||||
snap *leveldb.Snapshot
|
||||
rel *releaser
|
||||
}
|
||||
|
||||
func (l leveldbSnapshot) Get(key []byte) ([]byte, error) {
|
||||
val, err := l.snap.Get(key, nil)
|
||||
return val, wrapLeveldbErr(err)
|
||||
}
|
||||
|
||||
func (l leveldbSnapshot) NewPrefixIterator(prefix []byte) (Iterator, error) {
|
||||
return l.snap.NewIterator(util.BytesPrefix(prefix), nil), nil
|
||||
}
|
||||
|
||||
func (l leveldbSnapshot) NewRangeIterator(first, last []byte) (Iterator, error) {
|
||||
return l.snap.NewIterator(&util.Range{Start: first, Limit: last}, nil), nil
|
||||
}
|
||||
|
||||
func (l leveldbSnapshot) Release() {
|
||||
l.snap.Release()
|
||||
l.rel.Release()
|
||||
}
|
||||
|
||||
type leveldbIterator struct {
|
||||
iterator.Iterator
|
||||
}
|
||||
|
||||
func (it *leveldbIterator) Error() error {
|
||||
return wrapLeveldbErr(it.Iterator.Error())
|
||||
}
|
||||
|
||||
// wrapLeveldbErr wraps errors so that the backend package can recognize them
|
||||
func wrapLeveldbErr(err error) error {
|
||||
switch err {
|
||||
case leveldb.ErrClosed:
|
||||
return errClosed
|
||||
case leveldb.ErrNotFound:
|
||||
return errNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 backend
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
const dbMaxOpenFiles = 100
|
||||
|
||||
// OpenLevelDBRO attempts to open the database at the given location, read
|
||||
// only.
|
||||
func OpenLevelDBRO(location string) (Backend, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: dbMaxOpenFiles,
|
||||
ReadOnly: true,
|
||||
}
|
||||
ldb, err := open(location, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLeveldbBackend(ldb, location), nil
|
||||
}
|
||||
|
||||
func open(location string, opts *opt.Options) (*leveldb.DB, error) {
|
||||
return leveldb.OpenFile(location, opts)
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
// 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 olddb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
keyPrefixLen = 1
|
||||
keyFolderLen = 4 // indexed
|
||||
keyDeviceLen = 4 // indexed
|
||||
keySequenceLen = 8
|
||||
keyHashLen = 32
|
||||
|
||||
maxInt64 int64 = 1<<63 - 1
|
||||
)
|
||||
|
||||
const (
|
||||
// KeyTypeDevice <int32 folder ID> <int32 device ID> <file name> = FileInfo
|
||||
KeyTypeDevice byte = 0
|
||||
|
||||
// KeyTypeGlobal <int32 folder ID> <file name> = VersionList
|
||||
KeyTypeGlobal byte = 1
|
||||
|
||||
// KeyTypeBlock <int32 folder ID> <32 bytes hash> <§file name> = int32 (block index)
|
||||
KeyTypeBlock byte = 2
|
||||
|
||||
// KeyTypeDeviceStatistic <device ID as string> <some string> = some value
|
||||
KeyTypeDeviceStatistic byte = 3
|
||||
|
||||
// KeyTypeFolderStatistic <folder ID as string> <some string> = some value
|
||||
KeyTypeFolderStatistic byte = 4
|
||||
|
||||
// KeyTypeVirtualMtime <int32 folder ID> <file name> = mtimeMapping
|
||||
KeyTypeVirtualMtime byte = 5
|
||||
|
||||
// KeyTypeFolderIdx <int32 id> = string value
|
||||
KeyTypeFolderIdx byte = 6
|
||||
|
||||
// KeyTypeDeviceIdx <int32 id> = string value
|
||||
KeyTypeDeviceIdx byte = 7
|
||||
|
||||
// KeyTypeIndexID <int32 device ID> <int32 folder ID> = protocol.IndexID
|
||||
KeyTypeIndexID byte = 8
|
||||
|
||||
// KeyTypeFolderMeta <int32 folder ID> = CountsSet
|
||||
KeyTypeFolderMeta byte = 9
|
||||
|
||||
// KeyTypeMiscData <some string> = some value
|
||||
KeyTypeMiscData byte = 10
|
||||
|
||||
// KeyTypeSequence <int32 folder ID> <int64 sequence number> = KeyTypeDevice key
|
||||
KeyTypeSequence byte = 11
|
||||
|
||||
// KeyTypeNeed <int32 folder ID> <file name> = <nothing>
|
||||
KeyTypeNeed byte = 12
|
||||
|
||||
// KeyTypeBlockList <block list hash> = BlockList
|
||||
KeyTypeBlockList byte = 13
|
||||
|
||||
// KeyTypeBlockListMap <int32 folder ID> <block list hash> <file name> = <nothing>
|
||||
KeyTypeBlockListMap byte = 14
|
||||
|
||||
// KeyTypeVersion <version hash> = Vector
|
||||
KeyTypeVersion byte = 15
|
||||
|
||||
// KeyTypePendingFolder <int32 device ID> <folder ID as string> = ObservedFolder
|
||||
KeyTypePendingFolder byte = 16
|
||||
|
||||
// KeyTypePendingDevice <device ID in wire format> = ObservedDevice
|
||||
KeyTypePendingDevice byte = 17
|
||||
)
|
||||
|
||||
type keyer interface {
|
||||
// device file key stuff
|
||||
GenerateDeviceFileKey(key, folder, device, name []byte) (deviceFileKey, error)
|
||||
NameFromDeviceFileKey(key []byte) []byte
|
||||
DeviceFromDeviceFileKey(key []byte) ([]byte, bool)
|
||||
FolderFromDeviceFileKey(key []byte) ([]byte, bool)
|
||||
|
||||
// global version key stuff
|
||||
GenerateGlobalVersionKey(key, folder, name []byte) (globalVersionKey, error)
|
||||
NameFromGlobalVersionKey(key []byte) []byte
|
||||
|
||||
// block map key stuff (former BlockMap)
|
||||
GenerateBlockMapKey(key, folder, hash, name []byte) (blockMapKey, error)
|
||||
NameFromBlockMapKey(key []byte) []byte
|
||||
GenerateBlockListMapKey(key, folder, hash, name []byte) (blockListMapKey, error)
|
||||
NameFromBlockListMapKey(key []byte) []byte
|
||||
|
||||
// file need index
|
||||
GenerateNeedFileKey(key, folder, name []byte) (needFileKey, error)
|
||||
|
||||
// file sequence index
|
||||
GenerateSequenceKey(key, folder []byte, seq int64) (sequenceKey, error)
|
||||
SequenceFromSequenceKey(key []byte) int64
|
||||
|
||||
// index IDs
|
||||
GenerateIndexIDKey(key, device, folder []byte) (indexIDKey, error)
|
||||
FolderFromIndexIDKey(key []byte) ([]byte, bool)
|
||||
DeviceFromIndexIDKey(key []byte) ([]byte, bool)
|
||||
|
||||
// Mtimes
|
||||
GenerateMtimesKey(key, folder []byte) (mtimesKey, error)
|
||||
|
||||
// Folder metadata
|
||||
GenerateFolderMetaKey(key, folder []byte) (folderMetaKey, error)
|
||||
|
||||
// Block lists
|
||||
GenerateBlockListKey(key []byte, hash []byte) blockListKey
|
||||
|
||||
// Version vectors
|
||||
GenerateVersionKey(key []byte, hash []byte) versionKey
|
||||
|
||||
// Pending (unshared) folders and devices
|
||||
GeneratePendingFolderKey(key, device, folder []byte) (pendingFolderKey, error)
|
||||
FolderFromPendingFolderKey(key []byte) []byte
|
||||
DeviceFromPendingFolderKey(key []byte) ([]byte, bool)
|
||||
|
||||
GeneratePendingDeviceKey(key, device []byte) pendingDeviceKey
|
||||
DeviceFromPendingDeviceKey(key []byte) []byte
|
||||
}
|
||||
|
||||
// defaultKeyer implements our key scheme. It needs folder and device
|
||||
// indexes.
|
||||
type defaultKeyer struct {
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
}
|
||||
|
||||
func newDefaultKeyer(folderIdx, deviceIdx *smallIndex) defaultKeyer {
|
||||
return defaultKeyer{
|
||||
folderIdx: folderIdx,
|
||||
deviceIdx: deviceIdx,
|
||||
}
|
||||
}
|
||||
|
||||
type deviceFileKey []byte
|
||||
|
||||
func (k deviceFileKey) WithoutNameAndDevice() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k deviceFileKey) WithoutName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen+keyDeviceLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateDeviceFileKey(key, folder, device, name []byte) (deviceFileKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceID, err := k.deviceIdx.ID(device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+keyDeviceLen+len(name))
|
||||
key[0] = KeyTypeDevice
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen+keyFolderLen:], deviceID)
|
||||
copy(key[keyPrefixLen+keyFolderLen+keyDeviceLen:], name)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (defaultKeyer) NameFromDeviceFileKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen+keyDeviceLen:]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) DeviceFromDeviceFileKey(key []byte) ([]byte, bool) {
|
||||
return k.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen+keyFolderLen:]))
|
||||
}
|
||||
|
||||
func (k defaultKeyer) FolderFromDeviceFileKey(key []byte) ([]byte, bool) {
|
||||
return k.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
}
|
||||
|
||||
type globalVersionKey []byte
|
||||
|
||||
func (k globalVersionKey) WithoutName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateGlobalVersionKey(key, folder, name []byte) (globalVersionKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+len(name))
|
||||
key[0] = KeyTypeGlobal
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
copy(key[keyPrefixLen+keyFolderLen:], name)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (defaultKeyer) NameFromGlobalVersionKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen:]
|
||||
}
|
||||
|
||||
type blockMapKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateBlockMapKey(key, folder, hash, name []byte) (blockMapKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+keyHashLen+len(name))
|
||||
key[0] = KeyTypeBlock
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
copy(key[keyPrefixLen+keyFolderLen:], hash)
|
||||
copy(key[keyPrefixLen+keyFolderLen+keyHashLen:], name)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (defaultKeyer) NameFromBlockMapKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen+keyHashLen:]
|
||||
}
|
||||
|
||||
func (k blockMapKey) WithoutHashAndName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
type blockListMapKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateBlockListMapKey(key, folder, hash, name []byte) (blockListMapKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+keyHashLen+len(name))
|
||||
key[0] = KeyTypeBlockListMap
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
copy(key[keyPrefixLen+keyFolderLen:], hash)
|
||||
copy(key[keyPrefixLen+keyFolderLen+keyHashLen:], name)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (defaultKeyer) NameFromBlockListMapKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen+keyHashLen:]
|
||||
}
|
||||
|
||||
func (k blockListMapKey) WithoutHashAndName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
type needFileKey []byte
|
||||
|
||||
func (k needFileKey) WithoutName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateNeedFileKey(key, folder, name []byte) (needFileKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+len(name))
|
||||
key[0] = KeyTypeNeed
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
copy(key[keyPrefixLen+keyFolderLen:], name)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
type sequenceKey []byte
|
||||
|
||||
func (k sequenceKey) WithoutSequence() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateSequenceKey(key, folder []byte, seq int64) (sequenceKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+keySequenceLen)
|
||||
key[0] = KeyTypeSequence
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
binary.BigEndian.PutUint64(key[keyPrefixLen+keyFolderLen:], uint64(seq))
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (defaultKeyer) SequenceFromSequenceKey(key []byte) int64 {
|
||||
return int64(binary.BigEndian.Uint64(key[keyPrefixLen+keyFolderLen:]))
|
||||
}
|
||||
|
||||
type indexIDKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateIndexIDKey(key, device, folder []byte) (indexIDKey, error) {
|
||||
deviceID, err := k.deviceIdx.ID(device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyDeviceLen+keyFolderLen)
|
||||
key[0] = KeyTypeIndexID
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], deviceID)
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen+keyDeviceLen:], folderID)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (k defaultKeyer) FolderFromIndexIDKey(key []byte) ([]byte, bool) {
|
||||
return k.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen+keyDeviceLen:]))
|
||||
}
|
||||
|
||||
func (k defaultKeyer) DeviceFromIndexIDKey(key []byte) ([]byte, bool) {
|
||||
return k.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen : keyPrefixLen+keyDeviceLen]))
|
||||
}
|
||||
|
||||
type mtimesKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateMtimesKey(key, folder []byte) (mtimesKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen)
|
||||
key[0] = KeyTypeVirtualMtime
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
type folderMetaKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateFolderMetaKey(key, folder []byte) (folderMetaKey, error) {
|
||||
folderID, err := k.folderIdx.ID(folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyFolderLen)
|
||||
key[0] = KeyTypeFolderMeta
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], folderID)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
type blockListKey []byte
|
||||
|
||||
func (defaultKeyer) GenerateBlockListKey(key []byte, hash []byte) blockListKey {
|
||||
key = resize(key, keyPrefixLen+len(hash))
|
||||
key[0] = KeyTypeBlockList
|
||||
copy(key[keyPrefixLen:], hash)
|
||||
return key
|
||||
}
|
||||
|
||||
func (k blockListKey) Hash() []byte {
|
||||
return k[keyPrefixLen:]
|
||||
}
|
||||
|
||||
type versionKey []byte
|
||||
|
||||
func (defaultKeyer) GenerateVersionKey(key []byte, hash []byte) versionKey {
|
||||
key = resize(key, keyPrefixLen+len(hash))
|
||||
key[0] = KeyTypeVersion
|
||||
copy(key[keyPrefixLen:], hash)
|
||||
return key
|
||||
}
|
||||
|
||||
func (k versionKey) Hash() []byte {
|
||||
return k[keyPrefixLen:]
|
||||
}
|
||||
|
||||
type pendingFolderKey []byte
|
||||
|
||||
func (k defaultKeyer) GeneratePendingFolderKey(key, device, folder []byte) (pendingFolderKey, error) {
|
||||
deviceID, err := k.deviceIdx.ID(device)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = resize(key, keyPrefixLen+keyDeviceLen+len(folder))
|
||||
key[0] = KeyTypePendingFolder
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], deviceID)
|
||||
copy(key[keyPrefixLen+keyDeviceLen:], folder)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (defaultKeyer) FolderFromPendingFolderKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyDeviceLen:]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) DeviceFromPendingFolderKey(key []byte) ([]byte, bool) {
|
||||
return k.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
}
|
||||
|
||||
type pendingDeviceKey []byte
|
||||
|
||||
func (defaultKeyer) GeneratePendingDeviceKey(key, device []byte) pendingDeviceKey {
|
||||
key = resize(key, keyPrefixLen+len(device))
|
||||
key[0] = KeyTypePendingDevice
|
||||
copy(key[keyPrefixLen:], device)
|
||||
return key
|
||||
}
|
||||
|
||||
func (defaultKeyer) DeviceFromPendingDeviceKey(key []byte) []byte {
|
||||
return key[keyPrefixLen:]
|
||||
}
|
||||
|
||||
// resize returns a byte slice of the specified size, reusing bs if possible
|
||||
func resize(bs []byte, size int) []byte {
|
||||
if cap(bs) < size {
|
||||
return make([]byte, size)
|
||||
}
|
||||
return bs[:size]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (C) 2014 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 olddb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db/olddb/backend"
|
||||
)
|
||||
|
||||
// deprecatedLowlevel is the lowest level database interface. It has a very simple
|
||||
// purpose: hold the actual backend 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 deprecatedLowlevel for
|
||||
// any given backend.
|
||||
type deprecatedLowlevel struct {
|
||||
backend.Backend
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
keyer keyer
|
||||
}
|
||||
|
||||
func NewLowlevel(backend backend.Backend) (*deprecatedLowlevel, error) {
|
||||
// Only log restarts in debug mode.
|
||||
db := &deprecatedLowlevel{
|
||||
Backend: backend,
|
||||
folderIdx: newSmallIndex(backend, []byte{KeyTypeFolderIdx}),
|
||||
deviceIdx: newSmallIndex(backend, []byte{KeyTypeDeviceIdx}),
|
||||
}
|
||||
db.keyer = newDefaultKeyer(db.folderIdx, db.deviceIdx)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// ListFolders returns the list of folders currently in the database
|
||||
func (db *deprecatedLowlevel) ListFolders() []string {
|
||||
return db.folderIdx.Values()
|
||||
}
|
||||
|
||||
func (db *deprecatedLowlevel) IterateMtimes(fn func(folder, name string, ondisk, virtual time.Time) error) error {
|
||||
it, err := db.NewPrefixIterator([]byte{KeyTypeVirtualMtime})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
key := it.Key()[1:]
|
||||
folderID, ok := db.folderIdx.Val(binary.BigEndian.Uint32(key))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := key[4:]
|
||||
val := it.Value()
|
||||
var ondisk, virtual time.Time
|
||||
if err := ondisk.UnmarshalBinary(val[:len(val)/2]); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := virtual.UnmarshalBinary(val[len(val)/2:]); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := fn(string(folderID), string(name), ondisk, virtual); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return it.Error()
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (C) 2014 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 provides a set type to track local/remote files with newness
|
||||
// checks. We must do a certain amount of normalization in here. We will get
|
||||
// fed paths with either native or wire-format separators and encodings
|
||||
// depending on who calls us. We transform paths to wire-format (NFC and
|
||||
// slashes) on the way to the database, and transform to native format
|
||||
// (varying separator and encoding) on the way back out.
|
||||
package olddb
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type deprecatedFileSet struct {
|
||||
folder string
|
||||
db *deprecatedLowlevel
|
||||
}
|
||||
|
||||
// The Iterator is called with either a protocol.FileInfo or a
|
||||
// FileInfoTruncated (depending on the method) and returns true to
|
||||
// continue iteration, false to stop.
|
||||
type Iterator func(f protocol.FileInfo) bool
|
||||
|
||||
func NewFileSet(folder string, db *deprecatedLowlevel) (*deprecatedFileSet, error) {
|
||||
s := &deprecatedFileSet{
|
||||
folder: folder,
|
||||
db: db,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
folder string
|
||||
t readOnlyTransaction
|
||||
}
|
||||
|
||||
func (s *deprecatedFileSet) Snapshot() (*Snapshot, error) {
|
||||
t, err := s.db.newReadOnlyTransaction()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Snapshot{
|
||||
folder: s.folder,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Snapshot) Release() {
|
||||
s.t.close()
|
||||
}
|
||||
|
||||
func (s *Snapshot) WithHaveSequence(startSeq int64, fn Iterator) error {
|
||||
return s.t.withHaveSequence([]byte(s.folder), startSeq, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func nativeFileIterator(fn Iterator) Iterator {
|
||||
return func(fi protocol.FileInfo) bool {
|
||||
fi.Name = osutil.NativeFilename(fi.Name)
|
||||
return fn(fi)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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 olddb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db/olddb/backend"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
// A smallIndex is an in memory bidirectional []byte to uint32 map. It gives
|
||||
// fast lookups in both directions and persists to the database. Don't use for
|
||||
// storing more items than fit comfortably in RAM.
|
||||
type smallIndex struct {
|
||||
db backend.Backend
|
||||
prefix []byte
|
||||
id2val map[uint32]string
|
||||
val2id map[string]uint32
|
||||
nextID uint32
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newSmallIndex(db backend.Backend, prefix []byte) *smallIndex {
|
||||
idx := &smallIndex{
|
||||
db: db,
|
||||
prefix: prefix,
|
||||
id2val: make(map[uint32]string),
|
||||
val2id: make(map[string]uint32),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
idx.load()
|
||||
return idx
|
||||
}
|
||||
|
||||
// load iterates over the prefix space in the database and populates the in
|
||||
// memory maps.
|
||||
func (i *smallIndex) load() {
|
||||
it, err := i.db.NewPrefixIterator(i.prefix)
|
||||
if err != nil {
|
||||
panic("loading small index: " + err.Error())
|
||||
}
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
val := string(it.Value())
|
||||
id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):])
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the index number for the given byte slice, allocating a new one
|
||||
// and persisting this to the database if necessary.
|
||||
func (i *smallIndex) ID(val []byte) (uint32, error) {
|
||||
i.mut.Lock()
|
||||
// intentionally avoiding defer here as we want this call to be as fast as
|
||||
// possible in the general case (folder ID already exists). The map lookup
|
||||
// with the conversion of []byte to string is compiler optimized to not
|
||||
// copy the []byte, which is why we don't assign it to a temp variable
|
||||
// here.
|
||||
if id, ok := i.val2id[string(val)]; ok {
|
||||
i.mut.Unlock()
|
||||
return id, nil
|
||||
}
|
||||
|
||||
panic("missing ID")
|
||||
}
|
||||
|
||||
// Val returns the value for the given index number, or (nil, false) if there
|
||||
// is no such index number.
|
||||
func (i *smallIndex) Val(id uint32) ([]byte, bool) {
|
||||
i.mut.Lock()
|
||||
val, ok := i.id2val[id]
|
||||
i.mut.Unlock()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return []byte(val), true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (C) 2014 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 olddb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db/olddb/backend"
|
||||
"github.com/syncthing/syncthing/internal/gen/bep"
|
||||
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
// A readOnlyTransaction represents a database snapshot.
|
||||
type readOnlyTransaction struct {
|
||||
backend.ReadTransaction
|
||||
keyer keyer
|
||||
}
|
||||
|
||||
func (db *deprecatedLowlevel) newReadOnlyTransaction() (readOnlyTransaction, error) {
|
||||
tran, err := db.NewReadTransaction()
|
||||
if err != nil {
|
||||
return readOnlyTransaction{}, err
|
||||
}
|
||||
return db.readOnlyTransactionFromBackendTransaction(tran), nil
|
||||
}
|
||||
|
||||
func (db *deprecatedLowlevel) readOnlyTransactionFromBackendTransaction(tran backend.ReadTransaction) readOnlyTransaction {
|
||||
return readOnlyTransaction{
|
||||
ReadTransaction: tran,
|
||||
keyer: db.keyer,
|
||||
}
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) close() {
|
||||
t.Release()
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getFileByKey(key []byte) (protocol.FileInfo, bool, error) {
|
||||
f, ok, err := t.getFileTrunc(key, false)
|
||||
if err != nil || !ok {
|
||||
return protocol.FileInfo{}, false, err
|
||||
}
|
||||
return f, true, nil
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getFileTrunc(key []byte, trunc bool) (protocol.FileInfo, bool, error) {
|
||||
bs, err := t.Get(key)
|
||||
if backend.IsNotFound(err) {
|
||||
return protocol.FileInfo{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, false, err
|
||||
}
|
||||
f, err := t.unmarshalTrunc(bs, trunc)
|
||||
if backend.IsNotFound(err) {
|
||||
return protocol.FileInfo{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, false, err
|
||||
}
|
||||
return f, true, nil
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) unmarshalTrunc(bs []byte, trunc bool) (protocol.FileInfo, error) {
|
||||
if trunc {
|
||||
var bfi dbproto.FileInfoTruncated
|
||||
err := proto.Unmarshal(bs, &bfi)
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, err
|
||||
}
|
||||
if err := t.fillTruncated(&bfi); err != nil {
|
||||
return protocol.FileInfo{}, err
|
||||
}
|
||||
return protocol.FileInfoFromDBTruncated(&bfi), nil
|
||||
}
|
||||
|
||||
var bfi bep.FileInfo
|
||||
err := proto.Unmarshal(bs, &bfi)
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, err
|
||||
}
|
||||
if err := t.fillFileInfo(&bfi); err != nil {
|
||||
return protocol.FileInfo{}, err
|
||||
}
|
||||
return protocol.FileInfoFromDB(&bfi), nil
|
||||
}
|
||||
|
||||
type blocksIndirectionError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *blocksIndirectionError) Error() string {
|
||||
return fmt.Sprintf("filling Blocks: %v", e.err)
|
||||
}
|
||||
|
||||
func (e *blocksIndirectionError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// fillFileInfo follows the (possible) indirection of blocks and version
|
||||
// vector and fills it out.
|
||||
func (t readOnlyTransaction) fillFileInfo(fi *bep.FileInfo) error {
|
||||
var key []byte
|
||||
|
||||
if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
|
||||
// The blocks list is indirected and we need to load it.
|
||||
key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
|
||||
bs, err := t.Get(key)
|
||||
if err != nil {
|
||||
return &blocksIndirectionError{err}
|
||||
}
|
||||
var bl dbproto.BlockList
|
||||
if err := proto.Unmarshal(bs, &bl); err != nil {
|
||||
return err
|
||||
}
|
||||
fi.Blocks = bl.Blocks
|
||||
}
|
||||
|
||||
if len(fi.VersionHash) != 0 {
|
||||
key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
|
||||
bs, err := t.Get(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("filling Version: %w", err)
|
||||
}
|
||||
var v bep.Vector
|
||||
if err := proto.Unmarshal(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
fi.Version = &v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fillTruncated follows the (possible) indirection of version vector and
|
||||
// fills it.
|
||||
func (t readOnlyTransaction) fillTruncated(fi *dbproto.FileInfoTruncated) error {
|
||||
var key []byte
|
||||
|
||||
if len(fi.VersionHash) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
|
||||
bs, err := t.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v bep.Vector
|
||||
if err := proto.Unmarshal(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
fi.Version = &v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *readOnlyTransaction) withHaveSequence(folder []byte, startSeq int64, fn Iterator) error {
|
||||
first, err := t.keyer.GenerateSequenceKey(nil, folder, startSeq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
last, err := t.keyer.GenerateSequenceKey(nil, folder, maxInt64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbi, err := t.NewRangeIterator(first, last)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
f, ok, err := t.getFileByKey(dbi.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fn(f) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return dbi.Error()
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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 (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
sql *sqlx.DB
|
||||
localDeviceIdx int64
|
||||
updateLock sync.Mutex
|
||||
|
||||
statementsMut sync.RWMutex
|
||||
statements map[string]*sqlx.Stmt
|
||||
tplInput map[string]any
|
||||
}
|
||||
|
||||
var _ db.DB = (*DB)(nil)
|
||||
|
||||
func (s *DB) Close() error {
|
||||
s.updateLock.Lock()
|
||||
s.statementsMut.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
defer s.statementsMut.Unlock()
|
||||
for _, stmt := range s.statements {
|
||||
stmt.Close()
|
||||
}
|
||||
return wrap(s.sql.Close())
|
||||
}
|
||||
|
||||
func (s *DB) Service(maintenanceInterval time.Duration) suture.Service {
|
||||
return newService(s, maintenanceInterval)
|
||||
}
|
||||
|
||||
func (s *DB) ListFolders() ([]string, error) {
|
||||
var res []string
|
||||
err := s.stmt(`
|
||||
SELECT folder_id FROM folders
|
||||
ORDER BY folder_id
|
||||
`).Select(&res)
|
||||
return res, wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
|
||||
var res []string
|
||||
err := s.stmt(`
|
||||
SELECT d.device_id FROM counts s
|
||||
INNER JOIN folders o ON o.idx = s.folder_idx
|
||||
INNER JOIN devices d ON d.idx = s.device_idx
|
||||
WHERE o.folder_id = ? AND s.count > 0 AND s.device_idx != {{.LocalDeviceIdx}}
|
||||
GROUP BY d.device_id
|
||||
ORDER BY d.device_id
|
||||
`).Select(&res, folder)
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
|
||||
devs := make([]protocol.DeviceID, len(res))
|
||||
for i, s := range res {
|
||||
devs[i], err = protocol.DeviceIDFromString(s)
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
}
|
||||
return devs, nil
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/timeutil"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
)
|
||||
|
||||
var globalFi protocol.FileInfo
|
||||
|
||||
func BenchmarkUpdate(b *testing.B) {
|
||||
db, err := OpenTemp()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
})
|
||||
svc := db.Service(time.Hour).(*Service)
|
||||
|
||||
fs := make([]protocol.FileInfo, 100)
|
||||
seed := 0
|
||||
|
||||
size := 10000
|
||||
for size < 200_000 {
|
||||
t0 := time.Now()
|
||||
if err := svc.periodic(context.Background()); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Log("garbage collect in", time.Since(t0))
|
||||
|
||||
for {
|
||||
local, err := db.CountLocal(folderID, protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if local.Files >= size {
|
||||
break
|
||||
}
|
||||
fs := make([]protocol.FileInfo, 1000)
|
||||
for i := range fs {
|
||||
fs[i] = genFile(rand.String(24), 64, 0)
|
||||
}
|
||||
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.Run(fmt.Sprintf("Insert100Loc@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
for i := range fs {
|
||||
fs[i] = genFile(rand.String(24), 64, 0)
|
||||
}
|
||||
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("RepBlocks100@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
for i := range fs {
|
||||
fs[i].Blocks = genBlocks(fs[i].Name, seed, 64)
|
||||
fs[i].Version = fs[i].Version.Update(42)
|
||||
}
|
||||
seed++
|
||||
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("RepSame100@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
for i := range fs {
|
||||
fs[i].Version = fs[i].Version.Update(42)
|
||||
}
|
||||
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("Insert100Rem@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
for i := range fs {
|
||||
fs[i].Blocks = genBlocks(fs[i].Name, seed, 64)
|
||||
fs[i].Version = fs[i].Version.Update(42)
|
||||
fs[i].Sequence = timeutil.StrictlyMonotonicNanos()
|
||||
}
|
||||
if err := db.Update(folderID, protocol.DeviceID{42}, fs); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("GetGlobal100@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
for i := range fs {
|
||||
_, ok, err := db.GetGlobalFile(folderID, fs[i].Name)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
b.Fatal("should exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("LocalSequenced@%d", size), func(b *testing.B) {
|
||||
count := 0
|
||||
for range b.N {
|
||||
cur, err := db.GetDeviceSequence(folderID, protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
it, errFn := db.AllLocalFilesBySequence(folderID, protocol.LocalDeviceID, cur-100, 0)
|
||||
for f := range it {
|
||||
count++
|
||||
globalFi = f
|
||||
}
|
||||
if err := errFn(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("GetDeviceSequenceLoc@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
_, err := db.GetDeviceSequence(folderID, protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run(fmt.Sprintf("GetDeviceSequenceRem@%d", size), func(b *testing.B) {
|
||||
for range b.N {
|
||||
_, err := db.GetDeviceSequence(folderID, protocol.DeviceID{42})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("RemoteNeed@%d", size), func(b *testing.B) {
|
||||
count := 0
|
||||
for range b.N {
|
||||
it, errFn := db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)
|
||||
for f := range it {
|
||||
count++
|
||||
globalFi = f
|
||||
}
|
||||
if err := errFn(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("LocalNeed100Largest@%d", size), func(b *testing.B) {
|
||||
count := 0
|
||||
for range b.N {
|
||||
it, errFn := db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderLargestFirst, 100, 0)
|
||||
for f := range it {
|
||||
globalFi = f
|
||||
count++
|
||||
}
|
||||
if err := errFn(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
|
||||
})
|
||||
|
||||
size <<= 1
|
||||
}
|
||||
}
|
||||
|
||||
func TestBenchmarkDropAllRemote(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("slow test")
|
||||
}
|
||||
|
||||
db, err := OpenTemp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
fs := make([]protocol.FileInfo, 1000)
|
||||
seq := 0
|
||||
for {
|
||||
local, err := db.CountLocal(folderID, protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if local.Files >= 15_000 {
|
||||
break
|
||||
}
|
||||
for i := range fs {
|
||||
seq++
|
||||
fs[i] = genFile(rand.String(24), 64, seq)
|
||||
}
|
||||
if err := db.Update(folderID, protocol.DeviceID{42}, fs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := time.Since(t0)
|
||||
t.Log("drop all took", d)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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 (
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type countsRow struct {
|
||||
Type protocol.FileInfoType
|
||||
Count int
|
||||
Size int64
|
||||
Deleted bool
|
||||
LocalFlags int64 `db:"local_flags"`
|
||||
}
|
||||
|
||||
func (s *DB) CountLocal(folder string, device protocol.DeviceID) (db.Counts, error) {
|
||||
var res []countsRow
|
||||
if err := s.stmt(`
|
||||
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
|
||||
INNER JOIN folders o ON o.idx = s.folder_idx
|
||||
INNER JOIN devices d ON d.idx = s.device_idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ? AND s.local_flags & {{.FlagLocalIgnored}} = 0
|
||||
`).Select(&res, folder, device.String()); err != nil {
|
||||
return db.Counts{}, wrap(err)
|
||||
}
|
||||
return summarizeCounts(res), nil
|
||||
}
|
||||
|
||||
func (s *DB) CountNeed(folder string, device protocol.DeviceID) (db.Counts, error) {
|
||||
if device == protocol.LocalDeviceID {
|
||||
return s.needSizeLocal(folder)
|
||||
}
|
||||
return s.needSizeRemote(folder, device)
|
||||
}
|
||||
|
||||
func (s *DB) CountGlobal(folder string) (db.Counts, error) {
|
||||
// Exclude ignored and receive-only changed files from the global count
|
||||
// (legacy expectation? it's a bit weird since those files can in fact
|
||||
// be global and you can get them with GetGlobal etc.)
|
||||
var res []countsRow
|
||||
err := s.stmt(`
|
||||
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
|
||||
INNER JOIN folders o ON o.idx = s.folder_idx
|
||||
WHERE o.folder_id = ? AND s.local_flags & {{.FlagLocalGlobal}} != 0 AND s.local_flags & {{or .FlagLocalReceiveOnly .FlagLocalIgnored}} = 0
|
||||
`).Select(&res, folder)
|
||||
if err != nil {
|
||||
return db.Counts{}, wrap(err)
|
||||
}
|
||||
return summarizeCounts(res), nil
|
||||
}
|
||||
|
||||
func (s *DB) CountReceiveOnlyChanged(folder string) (db.Counts, error) {
|
||||
var res []countsRow
|
||||
err := s.stmt(`
|
||||
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
|
||||
INNER JOIN folders o ON o.idx = s.folder_idx
|
||||
WHERE o.folder_id = ? AND local_flags & {{.FlagLocalReceiveOnly}} != 0
|
||||
`).Select(&res, folder)
|
||||
if err != nil {
|
||||
return db.Counts{}, wrap(err)
|
||||
}
|
||||
return summarizeCounts(res), nil
|
||||
}
|
||||
|
||||
func (s *DB) needSizeLocal(folder string) (db.Counts, error) {
|
||||
// The need size for the local device is the sum of entries with the
|
||||
// need bit set.
|
||||
var res []countsRow
|
||||
err := s.stmt(`
|
||||
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
|
||||
INNER JOIN folders o ON o.idx = s.folder_idx
|
||||
WHERE o.folder_id = ? AND s.local_flags & {{.FlagLocalNeeded}} != 0
|
||||
`).Select(&res, folder)
|
||||
if err != nil {
|
||||
return db.Counts{}, wrap(err)
|
||||
}
|
||||
return summarizeCounts(res), nil
|
||||
}
|
||||
|
||||
func (s *DB) needSizeRemote(folder string, device protocol.DeviceID) (db.Counts, error) {
|
||||
var res []countsRow
|
||||
// See neededGlobalFilesRemote for commentary as that is the same query without summing
|
||||
if err := s.stmt(`
|
||||
SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
|
||||
INNER JOIN folders o ON o.idx = g.folder_idx
|
||||
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
|
||||
SELECT 1 FROM FILES f
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE f.name = g.name AND f.version = g.version AND f.folder_idx = g.folder_idx AND d.device_id = ?
|
||||
)
|
||||
GROUP BY g.type, g.local_flags, g.deleted
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
|
||||
INNER JOIN folders o ON o.idx = g.folder_idx
|
||||
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
|
||||
SELECT 1 FROM FILES f
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE f.name = g.name AND f.folder_idx = g.folder_idx AND d.device_id = ? AND NOT f.deleted
|
||||
)
|
||||
GROUP BY g.type, g.local_flags, g.deleted
|
||||
`).Select(&res, folder, device.String(),
|
||||
folder, device.String()); err != nil {
|
||||
return db.Counts{}, wrap(err)
|
||||
}
|
||||
|
||||
return summarizeCounts(res), nil
|
||||
}
|
||||
|
||||
func summarizeCounts(res []countsRow) db.Counts {
|
||||
c := db.Counts{
|
||||
DeviceID: protocol.LocalDeviceID,
|
||||
}
|
||||
for _, r := range res {
|
||||
switch {
|
||||
case r.Deleted:
|
||||
c.Deleted += r.Count
|
||||
case r.Type == protocol.FileInfoTypeFile:
|
||||
c.Files += r.Count
|
||||
c.Bytes += r.Size
|
||||
case r.Type == protocol.FileInfoTypeDirectory:
|
||||
c.Directories += r.Count
|
||||
c.Bytes += r.Size
|
||||
case r.Type == protocol.FileInfoTypeSymlink:
|
||||
c.Symlinks += r.Count
|
||||
c.Bytes += r.Size
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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 (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/itererr"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (s *DB) GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
|
||||
file = osutil.NormalizedFilename(file)
|
||||
|
||||
var ind indirectFI
|
||||
err := s.stmt(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
|
||||
INNER JOIN files f on fi.sequence = f.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
WHERE o.folder_id = ? AND f.name = ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
|
||||
`).Get(&ind, folder, file)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return protocol.FileInfo{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, false, wrap(err)
|
||||
}
|
||||
fi, err := ind.FileInfo()
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, false, wrap(err)
|
||||
}
|
||||
return fi, true, nil
|
||||
}
|
||||
|
||||
func (s *DB) GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error) {
|
||||
file = osutil.NormalizedFilename(file)
|
||||
|
||||
var devStrs []string
|
||||
err := s.stmt(`
|
||||
SELECT d.device_id FROM files f
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
INNER JOIN files g ON f.folder_idx = g.folder_idx AND g.version = f.version AND g.name = f.name
|
||||
WHERE o.folder_id = ? AND g.name = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND f.device_idx != {{.LocalDeviceIdx}}
|
||||
ORDER BY d.device_id
|
||||
`).Select(&devStrs, folder, file)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
|
||||
devs := make([]protocol.DeviceID, 0, len(devStrs))
|
||||
for _, s := range devStrs {
|
||||
d, err := protocol.DeviceIDFromString(s)
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
devs = append(devs, d)
|
||||
}
|
||||
|
||||
return devs, nil
|
||||
}
|
||||
|
||||
func (s *DB) AllGlobalFiles(folder string) (iter.Seq[db.FileMetadata], func() error) {
|
||||
it, errFn := iterStructs[db.FileMetadata](s.stmt(`
|
||||
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
WHERE o.folder_id = ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
|
||||
ORDER BY f.name
|
||||
`).Queryx(folder))
|
||||
return itererr.Map(it, errFn, func(m db.FileMetadata) (db.FileMetadata, error) {
|
||||
m.Name = osutil.NativeFilename(m.Name)
|
||||
return m, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DB) AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[db.FileMetadata], func() error) {
|
||||
if prefix == "" {
|
||||
return s.AllGlobalFiles(folder)
|
||||
}
|
||||
|
||||
prefix = osutil.NormalizedFilename(prefix)
|
||||
end := prefixEnd(prefix)
|
||||
|
||||
it, errFn := iterStructs[db.FileMetadata](s.stmt(`
|
||||
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
WHERE o.folder_id = ? AND f.name >= ? AND f.name < ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
|
||||
ORDER BY f.name
|
||||
`).Queryx(folder, prefix, end))
|
||||
return itererr.Map(it, errFn, func(m db.FileMetadata) (db.FileMetadata, error) {
|
||||
m.Name = osutil.NativeFilename(m.Name)
|
||||
return m, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DB) AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
var selectOpts string
|
||||
switch order {
|
||||
case config.PullOrderRandom:
|
||||
selectOpts = "ORDER BY RANDOM()"
|
||||
case config.PullOrderAlphabetic:
|
||||
selectOpts = "ORDER BY g.name ASC"
|
||||
case config.PullOrderSmallestFirst:
|
||||
selectOpts = "ORDER BY g.size ASC"
|
||||
case config.PullOrderLargestFirst:
|
||||
selectOpts = "ORDER BY g.size DESC"
|
||||
case config.PullOrderOldestFirst:
|
||||
selectOpts = "ORDER BY g.modified ASC"
|
||||
case config.PullOrderNewestFirst:
|
||||
selectOpts = "ORDER BY g.modified DESC"
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
selectOpts += fmt.Sprintf(" LIMIT %d", limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
selectOpts += fmt.Sprintf(" OFFSET %d", offset)
|
||||
}
|
||||
|
||||
if device == protocol.LocalDeviceID {
|
||||
return s.neededGlobalFilesLocal(folder, selectOpts)
|
||||
}
|
||||
|
||||
return s.neededGlobalFilesRemote(folder, device, selectOpts)
|
||||
}
|
||||
|
||||
func (s *DB) neededGlobalFilesLocal(folder, selectOpts string) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
// Select all the non-ignored files with the need bit set.
|
||||
it, errFn := iterStructs[indirectFI](s.stmt(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
|
||||
INNER JOIN files g on fi.sequence = g.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = g.folder_idx
|
||||
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalIgnored}} = 0 AND g.local_flags & {{.FlagLocalNeeded}} != 0
|
||||
` + selectOpts).Queryx(folder))
|
||||
return itererr.Map(it, errFn, indirectFI.FileInfo)
|
||||
}
|
||||
|
||||
func (s *DB) neededGlobalFilesRemote(folder string, device protocol.DeviceID, selectOpts string) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
// Select:
|
||||
//
|
||||
// - all the valid, non-deleted global files that don't have a corresponding
|
||||
// remote file with the same version.
|
||||
//
|
||||
// - all the valid, deleted global files that have a corresponding non-deleted
|
||||
// remote file (of any version)
|
||||
|
||||
it, errFn := iterStructs[indirectFI](s.stmt(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
|
||||
INNER JOIN files g on fi.sequence = g.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = g.folder_idx
|
||||
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
|
||||
SELECT 1 FROM FILES f
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE f.name = g.name AND f.version = g.version AND f.folder_idx = g.folder_idx AND d.device_id = ?
|
||||
)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
|
||||
INNER JOIN files g on fi.sequence = g.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = g.folder_idx
|
||||
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
|
||||
SELECT 1 FROM FILES f
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE f.name = g.name AND f.folder_idx = g.folder_idx AND d.device_id = ? AND NOT f.deleted
|
||||
)
|
||||
`+selectOpts).Queryx(
|
||||
folder, device.String(),
|
||||
folder, device.String(),
|
||||
))
|
||||
return itererr.Map(it, errFn, indirectFI.FileInfo)
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
// 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 := OpenTemp()
|
||||
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 := OpenTemp()
|
||||
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 := OpenTemp()
|
||||
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 := OpenTemp()
|
||||
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 TestRemoveDontNeedLocalIgnored(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := OpenTemp()
|
||||
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 := OpenTemp()
|
||||
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 := OpenTemp()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedRemoteSymlinkAndDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := OpenTemp()
|
||||
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 := OpenTemp()
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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 (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/itererr"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (s *DB) GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error) {
|
||||
// Try a fast read-only query to begin with. If it does not find the ID
|
||||
// we'll do the full thing under a lock.
|
||||
var indexID string
|
||||
if err := s.stmt(`
|
||||
SELECT i.index_id FROM indexids i
|
||||
INNER JOIN folders o ON o.idx = i.folder_idx
|
||||
INNER JOIN devices d ON d.idx = i.device_idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ?
|
||||
`).Get(&indexID, folder, device.String()); err == nil && indexID != "" {
|
||||
idx, err := indexIDFromHex(indexID)
|
||||
return idx, wrap(err, "select")
|
||||
}
|
||||
if device != protocol.LocalDeviceID {
|
||||
// For non-local devices we do not create the index ID, so return
|
||||
// zero anyway if we don't have one.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
|
||||
// We are now operating only for the local device ID
|
||||
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return 0, wrap(err)
|
||||
}
|
||||
|
||||
if err := s.stmt(`
|
||||
SELECT index_id FROM indexids WHERE folder_idx = ? AND device_idx = {{.LocalDeviceIdx}}
|
||||
`).Get(&indexID, folderIdx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, wrap(err, "select local")
|
||||
}
|
||||
|
||||
if indexID == "" {
|
||||
// Generate a new index ID. Some trickiness in the query as we need
|
||||
// to find the max sequence of local files if there already exist
|
||||
// any.
|
||||
id := protocol.NewIndexID()
|
||||
if _, err := s.stmt(`
|
||||
INSERT INTO indexids (folder_idx, device_idx, index_id, sequence)
|
||||
SELECT ?, {{.LocalDeviceIdx}}, ?, COALESCE(MAX(sequence), 0) FROM files
|
||||
WHERE folder_idx = ? AND device_idx = {{.LocalDeviceIdx}}
|
||||
ON CONFLICT DO UPDATE SET index_id = ?
|
||||
`).Exec(folderIdx, indexIDToHex(id), folderIdx, indexIDToHex(id)); err != nil {
|
||||
return 0, wrap(err, "insert")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
return indexIDFromHex(indexID)
|
||||
}
|
||||
|
||||
func (s *DB) SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return wrap(err, "folder idx")
|
||||
}
|
||||
deviceIdx, err := s.deviceIdxLocked(device)
|
||||
if err != nil {
|
||||
return wrap(err, "device idx")
|
||||
}
|
||||
|
||||
if _, err := s.stmt(`
|
||||
INSERT OR REPLACE INTO indexids (folder_idx, device_idx, index_id, sequence) values (?, ?, ?, 0)
|
||||
`).Exec(folderIdx, deviceIdx, indexIDToHex(id)); err != nil {
|
||||
return wrap(err, "insert")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DB) DropAllIndexIDs() error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
_, err := s.stmt(`DELETE FROM indexids`).Exec()
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error) {
|
||||
var res sql.NullInt64
|
||||
err := s.stmt(`
|
||||
SELECT sequence FROM indexids i
|
||||
INNER JOIN folders o ON o.idx = i.folder_idx
|
||||
INNER JOIN devices d ON d.idx = i.device_idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ?
|
||||
`).Get(&res, folder, device.String())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, wrap(err)
|
||||
}
|
||||
if !res.Valid {
|
||||
return 0, nil
|
||||
}
|
||||
return res.Int64, nil
|
||||
}
|
||||
|
||||
func (s *DB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
|
||||
type row struct {
|
||||
Device string
|
||||
Seq int64
|
||||
}
|
||||
|
||||
it, errFn := iterStructs[row](s.stmt(`
|
||||
SELECT d.device_id AS device, i.sequence AS seq FROM indexids i
|
||||
INNER JOIN folders o ON o.idx = i.folder_idx
|
||||
INNER JOIN devices d ON d.idx = i.device_idx
|
||||
WHERE o.folder_id = ? AND i.device_idx != {{.LocalDeviceIdx}}
|
||||
`).Queryx(folder))
|
||||
|
||||
res := make(map[protocol.DeviceID]int64)
|
||||
for row, err := range itererr.Zip(it, errFn) {
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
dev, err := protocol.DeviceIDFromString(row.Device)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "device ID")
|
||||
}
|
||||
res[dev] = row.Seq
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func indexIDFromHex(s string) (protocol.IndexID, error) {
|
||||
bs, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("indexIDFromHex: %q: %w", s, err)
|
||||
}
|
||||
var id protocol.IndexID
|
||||
if err := id.Unmarshal(bs); err != nil {
|
||||
return 0, fmt.Errorf("indexIDFromHex: %q: %w", s, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func indexIDToHex(i protocol.IndexID) string {
|
||||
bs, _ := i.Marshal()
|
||||
return hex.EncodeToString(bs)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestIndexIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := OpenTemp()
|
||||
if err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LocalDeviceID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
localID, err := db.GetIndexID("foo", protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if localID == 0 {
|
||||
t.Fatal("should have been generated")
|
||||
}
|
||||
|
||||
again, err := db.GetIndexID("foo", protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again != localID {
|
||||
t.Fatal("should get same again")
|
||||
}
|
||||
|
||||
other, err := db.GetIndexID("bar", protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if other == localID {
|
||||
t.Fatal("should not get same for other folder")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OtherDeviceID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
localID, err := db.GetIndexID("foo", protocol.DeviceID{42})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if localID != 0 {
|
||||
t.Fatal("should have been zero")
|
||||
}
|
||||
|
||||
newID := protocol.NewIndexID()
|
||||
if err := db.SetIndexID("foo", protocol.DeviceID{42}, newID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
again, err := db.GetIndexID("foo", protocol.DeviceID{42})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again != newID {
|
||||
t.Log(again, newID)
|
||||
t.Fatal("should get the ID we set")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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 (
|
||||
"iter"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
)
|
||||
|
||||
func (s *DB) GetKV(key string) ([]byte, error) {
|
||||
var val []byte
|
||||
if err := s.stmt(`
|
||||
SELECT value FROM kv
|
||||
WHERE key = ?
|
||||
`).Get(&val, key); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (s *DB) PutKV(key string, val []byte) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
_, err := s.stmt(`
|
||||
INSERT OR REPLACE INTO kv (key, value)
|
||||
VALUES (?, ?)
|
||||
`).Exec(key, val)
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) DeleteKV(key string) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
_, err := s.stmt(`
|
||||
DELETE FROM kv WHERE key = ?
|
||||
`).Exec(key)
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) PrefixKV(prefix string) (iter.Seq[db.KeyValue], func() error) {
|
||||
var rows *sqlx.Rows
|
||||
var err error
|
||||
if prefix == "" {
|
||||
rows, err = s.stmt(`SELECT key, value FROM kv`).Queryx()
|
||||
} else {
|
||||
end := prefixEnd(prefix)
|
||||
rows, err = s.stmt(`
|
||||
SELECT key, value FROM kv
|
||||
WHERE key >= ? AND key < ?
|
||||
`).Queryx(prefix, end)
|
||||
}
|
||||
if err != nil {
|
||||
return func(_ func(db.KeyValue) bool) {}, func() error { return err }
|
||||
}
|
||||
|
||||
return func(yield func(db.KeyValue) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var key string
|
||||
var val []byte
|
||||
if err = rows.Scan(&key, &val); err != nil {
|
||||
return
|
||||
}
|
||||
if !yield(db.KeyValue{Key: key, Value: val}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
}, func() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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 (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/itererr"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (s *DB) GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error) {
|
||||
file = osutil.NormalizedFilename(file)
|
||||
|
||||
var ind indirectFI
|
||||
err := s.stmt(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
|
||||
INNER JOIN files f on fi.sequence = f.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
|
||||
INNER JOIN devices d ON f.device_idx = d.idx
|
||||
INNER JOIN folders o ON f.folder_idx = o.idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ? AND f.name = ?
|
||||
`).Get(&ind, folder, device.String(), file)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return protocol.FileInfo{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, false, wrap(err)
|
||||
}
|
||||
fi, err := ind.FileInfo()
|
||||
if err != nil {
|
||||
return protocol.FileInfo{}, false, wrap(err, "indirect")
|
||||
}
|
||||
return fi, true, nil
|
||||
}
|
||||
|
||||
func (s *DB) AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
it, errFn := iterStructs[indirectFI](s.stmt(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
|
||||
INNER JOIN files f on fi.sequence = f.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ?
|
||||
`).Queryx(folder, device.String()))
|
||||
return itererr.Map(it, errFn, indirectFI.FileInfo)
|
||||
}
|
||||
|
||||
func (s *DB) AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
var limitStr string
|
||||
if limit > 0 {
|
||||
limitStr = fmt.Sprintf(" LIMIT %d", limit)
|
||||
}
|
||||
it, errFn := iterStructs[indirectFI](s.stmt(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
|
||||
INNER JOIN files f on fi.sequence = f.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ? AND f.sequence >= ?
|
||||
ORDER BY f.sequence`+limitStr).Queryx(
|
||||
folder, device.String(), startSeq))
|
||||
return itererr.Map(it, errFn, indirectFI.FileInfo)
|
||||
}
|
||||
|
||||
func (s *DB) AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error) {
|
||||
if prefix == "" {
|
||||
return s.AllLocalFiles(folder, device)
|
||||
}
|
||||
|
||||
prefix = osutil.NormalizedFilename(prefix)
|
||||
end := prefixEnd(prefix)
|
||||
|
||||
it, errFn := iterStructs[indirectFI](s.sql.Queryx(`
|
||||
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
|
||||
INNER JOIN files f on fi.sequence = f.sequence
|
||||
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
INNER JOIN devices d ON d.idx = f.device_idx
|
||||
WHERE o.folder_id = ? AND d.device_id = ? AND f.name >= ? AND f.name < ?
|
||||
`, folder, device.String(), prefix, end))
|
||||
return itererr.Map(it, errFn, indirectFI.FileInfo)
|
||||
}
|
||||
|
||||
func (s *DB) AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[db.FileMetadata], func() error) {
|
||||
return iterStructs[db.FileMetadata](s.stmt(`
|
||||
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
WHERE o.folder_id = ? AND f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
|
||||
`).Queryx(folder, h))
|
||||
}
|
||||
|
||||
func (s *DB) AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, db.FileMetadata], func() error) {
|
||||
type row struct {
|
||||
FolderID string `db:"folder_id"`
|
||||
db.FileMetadata
|
||||
}
|
||||
it, errFn := iterStructs[row](s.stmt(`
|
||||
SELECT o.folder_id, f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
|
||||
INNER JOIN folders o ON o.idx = f.folder_idx
|
||||
WHERE f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
|
||||
`).Queryx(h))
|
||||
return itererr.Map2(it, errFn, func(r row) (string, db.FileMetadata, error) {
|
||||
return r.FolderID, r.FileMetadata, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DB) AllLocalBlocksWithHash(hash []byte) (iter.Seq[db.BlockMapEntry], func() error) {
|
||||
// We involve the files table in this select because deletion of blocks
|
||||
// & blocklists is deferred (garbage collected) while the files list is
|
||||
// not. This filters out blocks that are in fact deleted.
|
||||
return iterStructs[db.BlockMapEntry](s.stmt(`
|
||||
SELECT f.blocklist_hash as blocklisthash, b.idx as blockindex, b.offset, b.size FROM files f
|
||||
LEFT JOIN blocks b ON f.blocklist_hash = b.blocklist_hash
|
||||
WHERE f.device_idx = {{.LocalDeviceIdx}} AND b.hash = ?
|
||||
`).Queryx(hash))
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/itererr"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestBlocks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := OpenTemp()
|
||||
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
|
||||
|
||||
vals, err := itererr.Collect(db.AllLocalBlocksWithHash([]byte{1, 2, 3}))
|
||||
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")
|
||||
}
|
||||
|
||||
// Get FileInfos for those blocks
|
||||
|
||||
found := 0
|
||||
it, errFn := db.AllLocalFilesWithBlocksHashAnyFolder(vals[0].BlocklistHash)
|
||||
for folder, fileInfo := range it {
|
||||
if folder != folderID {
|
||||
t.Fatal("should be same folder")
|
||||
}
|
||||
if fileInfo.Name != "file1" {
|
||||
t.Fatal("should be file1")
|
||||
}
|
||||
found++
|
||||
}
|
||||
if err := errFn(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found != 1 {
|
||||
t.Fatal("should find one file")
|
||||
}
|
||||
|
||||
// Get the other blocks
|
||||
|
||||
vals, err = itererr.Collect(db.AllLocalBlocksWithHash([]byte{3, 4, 5}))
|
||||
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()
|
||||
|
||||
sdb, err := OpenTemp()
|
||||
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
|
||||
es := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(search))
|
||||
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
|
||||
if hits := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(search)); len(hits) != 0 {
|
||||
t.Log(hits)
|
||||
t.Error("expected no hits")
|
||||
}
|
||||
|
||||
// Searching for the new hash should yield one hits
|
||||
if hits := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(file.Blocks[0].Hash)); len(hits) != 1 {
|
||||
t.Log(hits)
|
||||
t.Error("expected one hit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteSequence(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sdb, err := OpenTemp()
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *DB) GetMtime(folder, name string) (ondisk, virtual time.Time) {
|
||||
var res struct {
|
||||
Ondisk int64
|
||||
Virtual int64
|
||||
}
|
||||
if err := s.stmt(`
|
||||
SELECT m.ondisk, m.virtual FROM mtimes m
|
||||
INNER JOIN folders o ON o.idx = m.folder_idx
|
||||
WHERE o.folder_id = ? AND m.name = ?
|
||||
`).Get(&res, folder, name); err != nil {
|
||||
return time.Time{}, time.Time{}
|
||||
}
|
||||
return time.Unix(0, res.Ondisk), time.Unix(0, res.Virtual)
|
||||
}
|
||||
|
||||
func (s *DB) PutMtime(folder, name string, ondisk, virtual time.Time) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
_, err = s.stmt(`
|
||||
INSERT OR REPLACE INTO mtimes (folder_idx, name, ondisk, virtual)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).Exec(folderIdx, name, ondisk.UnixNano(), virtual.UnixNano())
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) DeleteMtime(folder, name string) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
_, err = s.stmt(`
|
||||
DELETE FROM mtimes
|
||||
WHERE folder_idx = ? AND name = ?
|
||||
`).Exec(folderIdx, name)
|
||||
return wrap(err)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMtimePairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := OpenTemp()
|
||||
if err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t0 := time.Now().Truncate(time.Second)
|
||||
t1 := t0.Add(1234567890)
|
||||
|
||||
// Set a pair
|
||||
if err := db.PutMtime("foo", "bar", t0, t1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check it
|
||||
gt0, gt1 := db.GetMtime("foo", "bar")
|
||||
if !gt0.Equal(t0) || !gt1.Equal(t1) {
|
||||
t.Log(t0, gt0)
|
||||
t.Log(t1, gt1)
|
||||
t.Log("bad times")
|
||||
}
|
||||
|
||||
// Delete it
|
||||
if err := db.DeleteMtime("foo", "bar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check it
|
||||
gt0, gt1 = db.GetMtime("foo", "bar")
|
||||
if !gt0.IsZero() || !gt1.IsZero() {
|
||||
t.Log(gt0, gt1)
|
||||
t.Log("bad times")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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 (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
const maxDBConns = 128
|
||||
|
||||
func Open(path string) (*DB, error) {
|
||||
// Open the database with options to enable foreign keys and recursive
|
||||
// triggers (needed for the delete+insert triggers on row replace).
|
||||
sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions)
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(maxDBConns)
|
||||
if _, err := sqlDB.Exec(`PRAGMA journal_mode = WAL`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA journal_mode")
|
||||
}
|
||||
if _, err := sqlDB.Exec(`PRAGMA optimize = 0x10002`); err != nil {
|
||||
// https://www.sqlite.org/pragma.html#pragma_optimize
|
||||
return nil, wrap(err, "PRAGMA optimize")
|
||||
}
|
||||
if _, err := sqlDB.Exec(`PRAGMA journal_size_limit = 6144000`); err != nil {
|
||||
// https://www.powersync.com/blog/sqlite-optimizations-for-ultra-high-performance
|
||||
return nil, wrap(err, "PRAGMA journal_size_limit")
|
||||
}
|
||||
return openCommon(sqlDB)
|
||||
}
|
||||
|
||||
// Open the database with options suitable for the migration inserts. This
|
||||
// is not a safe mode of operation for normal processing, use only for bulk
|
||||
// inserts with a close afterwards.
|
||||
func OpenForMigration(path string) (*DB, error) {
|
||||
sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions)
|
||||
if err != nil {
|
||||
return nil, wrap(err, "open")
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
if _, err := sqlDB.Exec(`PRAGMA foreign_keys = 0`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA foreign_keys")
|
||||
}
|
||||
if _, err := sqlDB.Exec(`PRAGMA journal_mode = OFF`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA journal_mode")
|
||||
}
|
||||
if _, err := sqlDB.Exec(`PRAGMA synchronous = 0`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA synchronous")
|
||||
}
|
||||
return openCommon(sqlDB)
|
||||
}
|
||||
|
||||
func OpenTemp() (*DB, error) {
|
||||
// SQLite has a memory mode, but it works differently with concurrency
|
||||
// compared to what we need with the WAL mode. So, no memory databases
|
||||
// for now.
|
||||
dir, err := os.MkdirTemp("", "syncthing-db")
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
path := filepath.Join(dir, "db")
|
||||
l.Debugln("Test DB in", path)
|
||||
return Open(path)
|
||||
}
|
||||
|
||||
func openCommon(sqlDB *sqlx.DB) (*DB, error) {
|
||||
if _, err := sqlDB.Exec(`PRAGMA auto_vacuum = INCREMENTAL`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA auto_vacuum")
|
||||
}
|
||||
if _, err := sqlDB.Exec(`PRAGMA default_temp_store = MEMORY`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA default_temp_store")
|
||||
}
|
||||
if _, err := sqlDB.Exec(`PRAGMA temp_store = MEMORY`); err != nil {
|
||||
return nil, wrap(err, "PRAGMA temp_store")
|
||||
}
|
||||
|
||||
db := &DB{
|
||||
sql: sqlDB,
|
||||
statements: make(map[string]*sqlx.Stmt),
|
||||
}
|
||||
|
||||
if err := db.runScripts("sql/schema/*"); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
|
||||
ver, _ := db.getAppliedSchemaVersion()
|
||||
if ver.SchemaVersion > 0 {
|
||||
filter := func(scr string) bool {
|
||||
scr = filepath.Base(scr)
|
||||
nstr, _, ok := strings.Cut(scr, "-")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
n, err := strconv.ParseInt(nstr, 10, 32)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return int(n) > ver.SchemaVersion
|
||||
}
|
||||
if err := db.runScripts("sql/migrations/*", filter); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Touch device IDs that should always exist and have a low index
|
||||
// numbers, and will never change
|
||||
db.localDeviceIdx, _ = db.deviceIdxLocked(protocol.LocalDeviceID)
|
||||
|
||||
// Set the current schema version, if not already set
|
||||
if err := db.setAppliedSchemaVersion(currentSchemaVersion); err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
|
||||
db.tplInput = map[string]any{
|
||||
"FlagLocalUnsupported": protocol.FlagLocalUnsupported,
|
||||
"FlagLocalIgnored": protocol.FlagLocalIgnored,
|
||||
"FlagLocalMustRescan": protocol.FlagLocalMustRescan,
|
||||
"FlagLocalReceiveOnly": protocol.FlagLocalReceiveOnly,
|
||||
"FlagLocalGlobal": protocol.FlagLocalGlobal,
|
||||
"FlagLocalNeeded": protocol.FlagLocalNeeded,
|
||||
"LocalDeviceIdx": db.localDeviceIdx,
|
||||
"SyncthingVersion": build.LongVersion,
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
var tplFuncs = template.FuncMap{
|
||||
"or": func(vs ...int) int {
|
||||
v := vs[0]
|
||||
for _, ov := range vs[1:] {
|
||||
v |= ov
|
||||
}
|
||||
return v
|
||||
},
|
||||
}
|
||||
|
||||
// stmt returns a prepared statement for the given SQL string, after
|
||||
// applying local template expansions. The statement is cached.
|
||||
func (s *DB) stmt(tpl string) stmt {
|
||||
tpl = strings.TrimSpace(tpl)
|
||||
|
||||
// Fast concurrent lookup of cached statement
|
||||
s.statementsMut.RLock()
|
||||
stmt, ok := s.statements[tpl]
|
||||
s.statementsMut.RUnlock()
|
||||
if ok {
|
||||
return stmt
|
||||
}
|
||||
|
||||
// On miss, take the full lock, check again
|
||||
s.statementsMut.Lock()
|
||||
defer s.statementsMut.Unlock()
|
||||
stmt, ok = s.statements[tpl]
|
||||
if ok {
|
||||
return stmt
|
||||
}
|
||||
|
||||
// Apply template expansions
|
||||
var sb strings.Builder
|
||||
compTpl := template.Must(template.New("tpl").Funcs(tplFuncs).Parse(tpl))
|
||||
if err := compTpl.Execute(&sb, s.tplInput); err != nil {
|
||||
panic("bug: bad template: " + err.Error())
|
||||
}
|
||||
|
||||
// Prepare and cache
|
||||
stmt, err := s.sql.Preparex(sb.String())
|
||||
if err != nil {
|
||||
return failedStmt{err}
|
||||
}
|
||||
s.statements[tpl] = stmt
|
||||
return stmt
|
||||
}
|
||||
|
||||
type stmt interface {
|
||||
Exec(args ...any) (sql.Result, error)
|
||||
Get(dest any, args ...any) error
|
||||
Queryx(args ...any) (*sqlx.Rows, error)
|
||||
Select(dest any, args ...any) error
|
||||
}
|
||||
|
||||
type failedStmt struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (f failedStmt) Exec(_ ...any) (sql.Result, error) { return nil, f.err }
|
||||
func (f failedStmt) Get(_ any, _ ...any) error { return f.err }
|
||||
func (f failedStmt) Queryx(_ ...any) (*sqlx.Rows, error) { return nil, f.err }
|
||||
func (f failedStmt) Select(_ any, _ ...any) error { return f.err }
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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/.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
_ "github.com/mattn/go-sqlite3" // register sqlite3 database driver
|
||||
)
|
||||
|
||||
const (
|
||||
dbDriver = "sqlite3"
|
||||
commonOptions = "_fk=true&_rt=true&_cache_size=-65536&_sync=1&_txlock=immediate"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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/.
|
||||
|
||||
//go:build !cgo && !wazero
|
||||
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
_ "modernc.org/sqlite" // register sqlite database driver
|
||||
)
|
||||
|
||||
const (
|
||||
dbDriver = "sqlite"
|
||||
commonOptions = "_pragma=foreign_keys(1)&_pragma=recursive_triggers(1)&_pragma=cache_size(-65536)&_pragma=synchronous(1)"
|
||||
)
|
||||
|
||||
func init() {
|
||||
build.AddTag("modernc-sqlite")
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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 "github.com/jmoiron/sqlx"
|
||||
|
||||
type txPreparedStmts struct {
|
||||
*sqlx.Tx
|
||||
stmts map[string]*sqlx.Stmt
|
||||
}
|
||||
|
||||
func (p *txPreparedStmts) Preparex(query string) (*sqlx.Stmt, error) {
|
||||
if p.stmts == nil {
|
||||
p.stmts = make(map[string]*sqlx.Stmt)
|
||||
}
|
||||
stmt, ok := p.stmts[query]
|
||||
if ok {
|
||||
return stmt, nil
|
||||
}
|
||||
stmt, err := p.Tx.Preparex(query)
|
||||
if err != nil {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
p.stmts[query] = stmt
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (p *txPreparedStmts) Commit() error {
|
||||
for _, s := range p.stmts {
|
||||
s.Close()
|
||||
}
|
||||
return p.Tx.Commit()
|
||||
}
|
||||
|
||||
func (p *txPreparedStmts) Rollback() error {
|
||||
for _, s := range p.stmts {
|
||||
s.Close()
|
||||
}
|
||||
return p.Tx.Rollback()
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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 (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
)
|
||||
|
||||
const currentSchemaVersion = 1
|
||||
|
||||
//go:embed sql/**
|
||||
var embedded embed.FS
|
||||
|
||||
func (s *DB) runScripts(glob string, filter ...func(s string) bool) error {
|
||||
scripts, err := fs.Glob(embedded, glob)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
tx, err := s.sql.Begin()
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
|
||||
nextScript:
|
||||
for _, scr := range scripts {
|
||||
for _, fn := range filter {
|
||||
if !fn(scr) {
|
||||
l.Debugln("Skipping script", scr)
|
||||
continue nextScript
|
||||
}
|
||||
}
|
||||
l.Debugln("Executing script", scr)
|
||||
bs, err := fs.ReadFile(embedded, scr)
|
||||
if err != nil {
|
||||
return wrap(err, scr)
|
||||
}
|
||||
// SQLite requires one statement per exec, so we split the init
|
||||
// files on lines containing only a semicolon and execute them
|
||||
// separately. We require it on a separate line because there are
|
||||
// also statement-internal semicolons in the triggers.
|
||||
for _, stmt := range strings.Split(string(bs), "\n;") {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
return wrap(err, stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
|
||||
type schemaVersion struct {
|
||||
SchemaVersion int
|
||||
AppliedAt int64
|
||||
SyncthingVersion string
|
||||
}
|
||||
|
||||
func (s *schemaVersion) AppliedTime() time.Time {
|
||||
return time.Unix(0, s.AppliedAt)
|
||||
}
|
||||
|
||||
func (s *DB) setAppliedSchemaVersion(ver int) error {
|
||||
_, err := s.stmt(`
|
||||
INSERT OR IGNORE INTO schemamigrations (schema_version, applied_at, syncthing_version)
|
||||
VALUES (?, ?, ?)
|
||||
`).Exec(ver, time.Now().UnixNano(), build.LongVersion)
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) getAppliedSchemaVersion() (schemaVersion, error) {
|
||||
var v schemaVersion
|
||||
err := s.stmt(`
|
||||
SELECT schema_version as schemaversion, applied_at as appliedat, syncthing_version as syncthingversion FROM schemamigrations
|
||||
ORDER BY schema_version DESC
|
||||
LIMIT 1
|
||||
`).Get(&v)
|
||||
return v, wrap(err)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
)
|
||||
|
||||
const (
|
||||
internalMetaPrefix = "dbsvc"
|
||||
lastMaintKey = "lastMaint"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
sdb *DB
|
||||
maintenanceInterval time.Duration
|
||||
internalMeta *db.Typed
|
||||
}
|
||||
|
||||
func newService(sdb *DB, maintenanceInterval time.Duration) *Service {
|
||||
return &Service{
|
||||
sdb: sdb,
|
||||
maintenanceInterval: maintenanceInterval,
|
||||
internalMeta: db.NewTyped(sdb, internalMetaPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Serve(ctx context.Context) error {
|
||||
// Run periodic maintenance
|
||||
|
||||
// Figure out when we last ran maintenance and schedule accordingly. If
|
||||
// it was never, do it now.
|
||||
lastMaint, _, _ := s.internalMeta.Time(lastMaintKey)
|
||||
nextMaint := lastMaint.Add(s.maintenanceInterval)
|
||||
wait := time.Until(nextMaint)
|
||||
if wait < 0 {
|
||||
wait = time.Minute
|
||||
}
|
||||
l.Debugln("Next periodic run in", wait)
|
||||
|
||||
timer := time.NewTimer(wait)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
if err := s.periodic(ctx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
timer.Reset(s.maintenanceInterval)
|
||||
l.Debugln("Next periodic run in", s.maintenanceInterval)
|
||||
_ = s.internalMeta.PutTime(lastMaintKey, time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) periodic(ctx context.Context) error {
|
||||
t0 := time.Now()
|
||||
l.Debugln("Periodic start")
|
||||
|
||||
s.sdb.updateLock.Lock()
|
||||
defer s.sdb.updateLock.Unlock()
|
||||
|
||||
t1 := time.Now()
|
||||
defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
|
||||
|
||||
if err := s.garbageCollectBlocklistsAndBlocksLocked(ctx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
_, _ = s.sdb.sql.ExecContext(ctx, `ANALYZE`)
|
||||
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA optimize`)
|
||||
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA incremental_vacuum`)
|
||||
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) garbageCollectBlocklistsAndBlocksLocked(ctx context.Context) error {
|
||||
// Remove all blocklists not referred to by any files and, by extension,
|
||||
// any blocks not referred to by a blocklist. This is an expensive
|
||||
// operation when run normally, especially if there are a lot of blocks
|
||||
// to collect.
|
||||
//
|
||||
// We make this orders of magnitude faster by disabling foreign keys for
|
||||
// the transaction and doing the cleanup manually. This requires using
|
||||
// an explicit connection and disabling foreign keys before starting the
|
||||
// transaction. We make sure to clean up on the way out.
|
||||
|
||||
conn, err := s.sdb.sql.Connx(ctx)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.ExecContext(ctx, `PRAGMA foreign_keys = 0`); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer func() { //nolint:contextcheck
|
||||
_, _ = conn.ExecContext(context.Background(), `PRAGMA foreign_keys = 1`)
|
||||
}()
|
||||
|
||||
tx, err := conn.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
|
||||
if res, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM blocklists
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM files WHERE files.blocklist_hash = blocklists.blocklist_hash
|
||||
)`); err != nil {
|
||||
return wrap(err, "delete blocklists")
|
||||
} else if shouldDebug() {
|
||||
rows, err := res.RowsAffected()
|
||||
l.Debugln("Blocklist GC:", rows, err)
|
||||
}
|
||||
|
||||
if res, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM blocks
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM blocklists WHERE blocklists.blocklist_hash = blocks.blocklist_hash
|
||||
)`); err != nil {
|
||||
return wrap(err, "delete blocks")
|
||||
} else if shouldDebug() {
|
||||
rows, err := res.RowsAffected()
|
||||
l.Debugln("Blocks GC:", rows, err)
|
||||
}
|
||||
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,549 @@
|
||||
// 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 (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||
"github.com/syncthing/syncthing/internal/itererr"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sliceutil"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func (s *DB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
deviceIdx, err := s.deviceIdxLocked(device)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
tx, err := s.sql.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
txp := &txPreparedStmts{Tx: tx}
|
||||
|
||||
//nolint:sqlclosecheck
|
||||
insertFileStmt, err := txp.Preparex(`
|
||||
INSERT OR REPLACE INTO files (folder_idx, device_idx, remote_sequence, name, type, modified, size, version, deleted, invalid, local_flags, blocklist_hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING sequence
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err, "prepare insert file")
|
||||
}
|
||||
|
||||
//nolint:sqlclosecheck
|
||||
insertFileInfoStmt, err := txp.Preparex(`
|
||||
INSERT INTO fileinfos (sequence, fiprotobuf)
|
||||
VALUES (?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err, "prepare insert fileinfo")
|
||||
}
|
||||
|
||||
//nolint:sqlclosecheck
|
||||
insertBlockListStmt, err := txp.Preparex(`
|
||||
INSERT OR IGNORE INTO blocklists (blocklist_hash, blprotobuf)
|
||||
VALUES (?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err, "prepare insert blocklist")
|
||||
}
|
||||
|
||||
var prevRemoteSeq int64
|
||||
for i, f := range fs {
|
||||
f.Name = osutil.NormalizedFilename(f.Name)
|
||||
|
||||
var blockshash *[]byte
|
||||
if len(f.Blocks) > 0 {
|
||||
f.BlocksHash = protocol.BlocksHash(f.Blocks)
|
||||
blockshash = &f.BlocksHash
|
||||
} else {
|
||||
f.BlocksHash = nil
|
||||
}
|
||||
|
||||
if f.Type == protocol.FileInfoTypeDirectory {
|
||||
f.Size = 128 // synthetic directory size
|
||||
}
|
||||
|
||||
// Insert the file.
|
||||
//
|
||||
// If it is a remote file, set remote_sequence otherwise leave it at
|
||||
// null. Returns the new local sequence.
|
||||
var remoteSeq *int64
|
||||
if device != protocol.LocalDeviceID {
|
||||
if i > 0 && f.Sequence == prevRemoteSeq {
|
||||
return fmt.Errorf("duplicate remote sequence number %d", prevRemoteSeq)
|
||||
}
|
||||
prevRemoteSeq = f.Sequence
|
||||
remoteSeq = &f.Sequence
|
||||
}
|
||||
var localSeq int64
|
||||
if err := insertFileStmt.Get(&localSeq, folderIdx, deviceIdx, remoteSeq, f.Name, f.Type, f.ModTime().UnixNano(), f.Size, f.Version.String(), f.IsDeleted(), f.IsInvalid(), f.LocalFlags, blockshash); err != nil {
|
||||
return wrap(err, "insert file")
|
||||
}
|
||||
|
||||
if len(f.Blocks) > 0 {
|
||||
// Indirect the block list
|
||||
blocks := sliceutil.Map(f.Blocks, protocol.BlockInfo.ToWire)
|
||||
bs, err := proto.Marshal(&dbproto.BlockList{Blocks: blocks})
|
||||
if err != nil {
|
||||
return wrap(err, "marshal blocklist")
|
||||
}
|
||||
if _, err := insertBlockListStmt.Exec(f.BlocksHash, bs); err != nil {
|
||||
return wrap(err, "insert blocklist")
|
||||
}
|
||||
|
||||
if device == protocol.LocalDeviceID {
|
||||
// Insert all blocks
|
||||
if err := s.insertBlocksLocked(txp, f.BlocksHash, f.Blocks); err != nil {
|
||||
return wrap(err, "insert blocks")
|
||||
}
|
||||
}
|
||||
|
||||
f.Blocks = nil
|
||||
}
|
||||
|
||||
// Insert the fileinfo
|
||||
if device == protocol.LocalDeviceID {
|
||||
f.Sequence = localSeq
|
||||
}
|
||||
bs, err := proto.Marshal(f.ToWire(true))
|
||||
if err != nil {
|
||||
return wrap(err, "marshal fileinfo")
|
||||
}
|
||||
if _, err := insertFileInfoStmt.Exec(localSeq, bs); err != nil {
|
||||
return wrap(err, "insert fileinfo")
|
||||
}
|
||||
|
||||
// Update global and need
|
||||
if err := s.recalcGlobalForFile(txp, folderIdx, f.Name); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
|
||||
func (s *DB) DropFolder(folder string) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
_, err := s.stmt(`
|
||||
DELETE FROM folders
|
||||
WHERE folder_id = ?
|
||||
`).Exec(folder)
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) DropDevice(device protocol.DeviceID) error {
|
||||
if device == protocol.LocalDeviceID {
|
||||
panic("bug: cannot drop local device")
|
||||
}
|
||||
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
|
||||
deviceIdx, err := s.deviceIdxLocked(device)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
tx, err := s.sql.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
txp := &txPreparedStmts{Tx: tx}
|
||||
|
||||
// Find all folders where the device is involved
|
||||
var folderIdxs []int64
|
||||
if err := tx.Select(&folderIdxs, `
|
||||
SELECT folder_idx
|
||||
FROM counts
|
||||
WHERE device_idx = ? AND count > 0
|
||||
GROUP BY folder_idx
|
||||
`, deviceIdx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
// Drop the device, which cascades to delete all files etc for it
|
||||
if _, err := tx.Exec(`DELETE FROM devices WHERE device_id = ?`, device.String()); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
// Recalc the globals for all affected folders
|
||||
for _, idx := range folderIdxs {
|
||||
if err := s.recalcGlobalForFolder(txp, idx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
|
||||
func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
|
||||
// This is a two part operation, first dropping all the files and then
|
||||
// recalculating the global state for the entire folder.
|
||||
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
deviceIdx, err := s.deviceIdxLocked(device)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
tx, err := s.sql.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
txp := &txPreparedStmts{Tx: tx}
|
||||
|
||||
// Drop all the file entries
|
||||
|
||||
result, err := tx.Exec(`
|
||||
DELETE FROM files
|
||||
WHERE folder_idx = ? AND device_idx = ?
|
||||
`, folderIdx, deviceIdx)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if n, err := result.RowsAffected(); err == nil && n == 0 {
|
||||
// The delete affected no rows, so we don't need to redo the entire
|
||||
// global/need calculation.
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
|
||||
// Recalc global for the entire folder
|
||||
|
||||
if err := s.recalcGlobalForFolder(txp, folderIdx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
|
||||
func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
|
||||
for i := range names {
|
||||
names[i] = osutil.NormalizedFilename(names[i])
|
||||
}
|
||||
|
||||
s.updateLock.Lock()
|
||||
defer s.updateLock.Unlock()
|
||||
|
||||
folderIdx, err := s.folderIdxLocked(folder)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
deviceIdx, err := s.deviceIdxLocked(device)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
tx, err := s.sql.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
txp := &txPreparedStmts{Tx: tx}
|
||||
|
||||
// Drop the named files
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
DELETE FROM files
|
||||
WHERE folder_idx = ? AND device_idx = ? AND name IN (?)
|
||||
`, folderIdx, deviceIdx, names)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
// Recalc globals for the named files
|
||||
|
||||
for _, name := range names {
|
||||
if err := s.recalcGlobalForFile(txp, folderIdx, name); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return wrap(tx.Commit())
|
||||
}
|
||||
|
||||
func (*DB) insertBlocksLocked(tx *txPreparedStmts, blocklistHash []byte, blocks []protocol.BlockInfo) error {
|
||||
if len(blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
bs := make([]map[string]any, len(blocks))
|
||||
for i, b := range blocks {
|
||||
bs[i] = map[string]any{
|
||||
"hash": b.Hash,
|
||||
"blocklist_hash": blocklistHash,
|
||||
"idx": i,
|
||||
"offset": b.Offset,
|
||||
"size": b.Size,
|
||||
}
|
||||
}
|
||||
_, err := tx.NamedExec(`
|
||||
INSERT OR IGNORE INTO blocks (hash, blocklist_hash, idx, offset, size)
|
||||
VALUES (:hash, :blocklist_hash, :idx, :offset, :size)
|
||||
`, bs)
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
func (s *DB) recalcGlobalForFolder(txp *txPreparedStmts, folderIdx int64) error {
|
||||
// Select files where there is no global, those are the ones we need to
|
||||
// recalculate.
|
||||
//nolint:sqlclosecheck
|
||||
namesStmt, err := txp.Preparex(`
|
||||
SELECT f.name FROM files f
|
||||
WHERE f.folder_idx = ? AND NOT EXISTS (
|
||||
SELECT 1 FROM files g
|
||||
WHERE g.folder_idx = ? AND g.name = f.name AND g.local_flags & ? != 0
|
||||
)
|
||||
GROUP BY name
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
rows, err := namesStmt.Queryx(folderIdx, folderIdx, protocol.FlagLocalGlobal)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if err := s.recalcGlobalForFile(txp, folderIdx, name); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
}
|
||||
return wrap(rows.Err())
|
||||
}
|
||||
|
||||
func (s *DB) recalcGlobalForFile(txp *txPreparedStmts, folderIdx int64, file string) error {
|
||||
//nolint:sqlclosecheck
|
||||
selStmt, err := txp.Preparex(`
|
||||
SELECT name, folder_idx, device_idx, sequence, modified, version, deleted, invalid, local_flags FROM files
|
||||
WHERE folder_idx = ? AND name = ?
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
es, err := itererr.Collect(iterStructs[fileRow](selStmt.Queryx(folderIdx, file)))
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if len(es) == 0 {
|
||||
// shouldn't happen
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort the entries; the global entry is at the head of the list
|
||||
slices.SortFunc(es, fileRow.Compare)
|
||||
|
||||
// The global version is the first one in the list that is not invalid,
|
||||
// or just the first one in the list if all are invalid.
|
||||
var global fileRow
|
||||
globIdx := slices.IndexFunc(es, func(e fileRow) bool { return !e.Invalid })
|
||||
if globIdx < 0 {
|
||||
globIdx = 0
|
||||
}
|
||||
global = es[globIdx]
|
||||
|
||||
// We "have" the file if the position in the list of versions is at the
|
||||
// global version or better, or if the version is the same as the global
|
||||
// file (we might be further down the list due to invalid flags), or if
|
||||
// the global is deleted and we don't have it at all...
|
||||
localIdx := slices.IndexFunc(es, func(e fileRow) bool { return e.DeviceIdx == s.localDeviceIdx })
|
||||
hasLocal := localIdx >= 0 && localIdx <= globIdx || // have a better or equal version
|
||||
localIdx >= 0 && es[localIdx].Version.Equal(global.Version.Vector) || // have an equal version but invalid/ignored
|
||||
localIdx < 0 && global.Deleted // missing it, but the global is also deleted
|
||||
|
||||
// Set the global flag on the global entry. Set the need flag if the
|
||||
// local device needs this file, unless it's invalid.
|
||||
global.LocalFlags |= protocol.FlagLocalGlobal
|
||||
if hasLocal || global.Invalid {
|
||||
global.LocalFlags &= ^protocol.FlagLocalNeeded
|
||||
} else {
|
||||
global.LocalFlags |= protocol.FlagLocalNeeded
|
||||
}
|
||||
//nolint:sqlclosecheck
|
||||
upStmt, err := txp.Prepare(`
|
||||
UPDATE files SET local_flags = ?
|
||||
WHERE folder_idx = ? AND device_idx = ? AND sequence = ?
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if _, err := upStmt.Exec(global.LocalFlags, global.FolderIdx, global.DeviceIdx, global.Sequence); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
// Clear the need and global flags on all other entries
|
||||
//nolint:sqlclosecheck
|
||||
upStmt, err = txp.Prepare(`
|
||||
UPDATE files SET local_flags = local_flags & ?
|
||||
WHERE folder_idx = ? AND name = ? AND sequence != ? AND local_flags & ? != 0
|
||||
`)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if _, err := upStmt.Exec(^(protocol.FlagLocalNeeded | protocol.FlagLocalGlobal), folderIdx, global.Name, global.Sequence, protocol.FlagLocalNeeded|protocol.FlagLocalGlobal); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DB) folderIdxLocked(folderID string) (int64, error) {
|
||||
if _, err := s.stmt(`
|
||||
INSERT OR IGNORE INTO folders(folder_id)
|
||||
VALUES (?)
|
||||
`).Exec(folderID); err != nil {
|
||||
return 0, wrap(err)
|
||||
}
|
||||
var idx int64
|
||||
if err := s.stmt(`
|
||||
SELECT idx FROM folders
|
||||
WHERE folder_id = ?
|
||||
`).Get(&idx, folderID); err != nil {
|
||||
return 0, wrap(err)
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (s *DB) deviceIdxLocked(deviceID protocol.DeviceID) (int64, error) {
|
||||
devStr := deviceID.String()
|
||||
if _, err := s.stmt(`
|
||||
INSERT OR IGNORE INTO devices(device_id)
|
||||
VALUES (?)
|
||||
`).Exec(devStr); err != nil {
|
||||
return 0, wrap(err)
|
||||
}
|
||||
var idx int64
|
||||
if err := s.stmt(`
|
||||
SELECT idx FROM devices
|
||||
WHERE device_id = ?
|
||||
`).Get(&idx, devStr); err != nil {
|
||||
return 0, wrap(err)
|
||||
}
|
||||
|
||||
return idx, 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 {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix := "error"
|
||||
pc, _, _, ok := runtime.Caller(1)
|
||||
details := runtime.FuncForPC(pc)
|
||||
if ok && details != nil {
|
||||
prefix = strings.ToLower(details.Name())
|
||||
if dotIdx := strings.LastIndex(prefix, "."); dotIdx > 0 {
|
||||
prefix = prefix[dotIdx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
if len(context) > 0 {
|
||||
for i := range context {
|
||||
context[i] = strings.TrimSpace(context[i])
|
||||
}
|
||||
extra := strings.Join(context, ", ")
|
||||
return fmt.Errorf("%s (%s): %w", prefix, extra, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: %w", prefix, err)
|
||||
}
|
||||
|
||||
type fileRow struct {
|
||||
Name string
|
||||
Version dbVector
|
||||
FolderIdx int64 `db:"folder_idx"`
|
||||
DeviceIdx int64 `db:"device_idx"`
|
||||
Sequence int64
|
||||
Modified int64
|
||||
Size int64
|
||||
LocalFlags int64 `db:"local_flags"`
|
||||
Deleted bool
|
||||
Invalid bool
|
||||
}
|
||||
|
||||
func (e fileRow) Compare(other fileRow) int {
|
||||
// From FileInfo.WinsConflict
|
||||
vc := e.Version.Vector.Compare(other.Version.Vector)
|
||||
switch vc {
|
||||
case protocol.Equal:
|
||||
if e.Invalid != other.Invalid {
|
||||
if e.Invalid {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Compare the device ID index, lower is better. This is only
|
||||
// deterministic to the extent that LocalDeviceID will always be the
|
||||
// lowest one, order between remote devices is random (and
|
||||
// irrelevant).
|
||||
return cmp.Compare(e.DeviceIdx, other.DeviceIdx)
|
||||
case protocol.Greater: // we are newer
|
||||
return -1
|
||||
case protocol.Lesser: // we are older
|
||||
return 1
|
||||
case protocol.ConcurrentGreater, protocol.ConcurrentLesser: // there is a conflict
|
||||
if e.Invalid != other.Invalid {
|
||||
if e.Invalid { // we are invalid, we lose
|
||||
return 1
|
||||
}
|
||||
return -1 // they are invalid, we win
|
||||
}
|
||||
if e.Deleted != other.Deleted {
|
||||
if e.Deleted { // we are deleted, we lose
|
||||
return 1
|
||||
}
|
||||
return -1 // they are deleted, we win
|
||||
}
|
||||
if d := cmp.Compare(e.Modified, other.Modified); d != 0 {
|
||||
return -d // positive d means we were newer, so we win (negative return)
|
||||
}
|
||||
if vc == protocol.ConcurrentGreater {
|
||||
return -1 // we have a better device ID, we win
|
||||
}
|
||||
return 1 // they win
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 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 (
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
|
||||
var l = logger.DefaultLogger.NewFacility("sqlite", "SQLite database")
|
||||
|
||||
func shouldDebug() bool { return l.ShouldDebug("sqlite") }
|
||||
@@ -0,0 +1,8 @@
|
||||
These SQL scripts are embedded in the binary.
|
||||
|
||||
Scripts in `schema/` are run at every startup, in alphanumerical order.
|
||||
|
||||
Scripts in `migrations/` are run when a migration is needed; the must begin
|
||||
with a number that equals the schema version that results from that
|
||||
migration. Migrations are not run on initial database creation, so the
|
||||
scripts in `schema/` should create the latest version.
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 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/.
|
||||
|
||||
-- The next migration should be number two.
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 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/.
|
||||
|
||||
-- folders map folder IDs as used by Syncthing to database folder indexes
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
idx INTEGER NOT NULL PRIMARY KEY,
|
||||
folder_id TEXT NOT NULL UNIQUE COLLATE BINARY
|
||||
) STRICT
|
||||
;
|
||||
|
||||
-- devices map device IDs as used by Syncthing to database device indexes
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
idx INTEGER NOT NULL PRIMARY KEY,
|
||||
device_id TEXT NOT NULL UNIQUE COLLATE BINARY
|
||||
) STRICT
|
||||
;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 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/.
|
||||
|
||||
-- Schema migrations hold the list of historical migrations applied
|
||||
CREATE TABLE IF NOT EXISTS schemamigrations (
|
||||
schema_version INTEGER NOT NULL,
|
||||
applied_at INTEGER NOT NULL, -- unix nanos
|
||||
syncthing_version TEXT NOT NULL COLLATE BINARY,
|
||||
PRIMARY KEY(schema_version)
|
||||
) STRICT
|
||||
;
|
||||
@@ -0,0 +1,62 @@
|
||||
-- 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/.
|
||||
|
||||
-- Files
|
||||
--
|
||||
-- The files table contains all files announced by any device. Files present
|
||||
-- on this device are filed under the LocalDeviceID, not the actual current
|
||||
-- device ID, for simplicity, consistency and portability. One announced
|
||||
-- version of each file is considered the "global" version - the latest one,
|
||||
-- that all other devices strive to replicate. This instance gets the Global
|
||||
-- flag bit set. There may be other identical instances of this file
|
||||
-- announced by other devices, but only one onstance gets the Global flag;
|
||||
-- this simplifies accounting. If the current device has the Global version,
|
||||
-- the LocalDeviceID instance of the file is the one that has the Global
|
||||
-- bit.
|
||||
--
|
||||
-- If the current device does not have that version of the file it gets the
|
||||
-- Need bit set. Only Global files announced by another device can have the
|
||||
-- Need bit. This allows for very efficient lookup of files needing handling
|
||||
-- on this device, which is a common query.
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
folder_idx INTEGER NOT NULL,
|
||||
device_idx INTEGER NOT NULL, -- actual device ID or LocalDeviceID
|
||||
sequence INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- our local database sequence, for each and every entry
|
||||
remote_sequence INTEGER, -- remote device's sequence number, null for local or synthetic entries
|
||||
name TEXT NOT NULL COLLATE BINARY,
|
||||
type INTEGER NOT NULL, -- protocol.FileInfoType
|
||||
modified INTEGER NOT NULL, -- Unix nanos
|
||||
size INTEGER NOT NULL,
|
||||
version TEXT NOT NULL COLLATE BINARY,
|
||||
deleted INTEGER NOT NULL, -- boolean
|
||||
invalid INTEGER NOT NULL, -- boolean
|
||||
local_flags INTEGER NOT NULL,
|
||||
blocklist_hash BLOB, -- null when there are no blocks
|
||||
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE,
|
||||
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
|
||||
) STRICT
|
||||
;
|
||||
-- FileInfos store the actual protobuf object. We do this separately to keep
|
||||
-- the files rows smaller and more efficient.
|
||||
CREATE TABLE IF NOT EXISTS fileinfos (
|
||||
sequence INTEGER NOT NULL PRIMARY KEY, -- our local database sequence from the files table
|
||||
fiprotobuf BLOB NOT NULL,
|
||||
FOREIGN KEY(sequence) REFERENCES files(sequence) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
|
||||
) STRICT
|
||||
;
|
||||
-- There can be only one file per folder, device, and remote sequence number
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS files_remote_sequence ON files (folder_idx, device_idx, remote_sequence)
|
||||
WHERE remote_sequence IS NOT NULL
|
||||
;
|
||||
-- There can be only one file per folder, device, and name
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS files_device_name ON files (folder_idx, device_idx, name)
|
||||
;
|
||||
-- We want to be able to look up & iterate files based on just folder and name
|
||||
CREATE INDEX IF NOT EXISTS files_name_only ON files (folder_idx, name)
|
||||
;
|
||||
-- We want to be able to look up & iterate files based on blocks hash
|
||||
CREATE INDEX IF NOT EXISTS files_blocklist_hash_only ON files (blocklist_hash, device_idx, folder_idx) WHERE blocklist_hash IS NOT NULL
|
||||
;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- 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/.
|
||||
|
||||
-- indexids holds the index ID and maximum sequence for a given device and folder
|
||||
CREATE TABLE IF NOT EXISTS indexids (
|
||||
device_idx INTEGER NOT NULL,
|
||||
folder_idx INTEGER NOT NULL,
|
||||
index_id TEXT NOT NULL COLLATE BINARY,
|
||||
sequence INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(device_idx, folder_idx),
|
||||
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE,
|
||||
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE
|
||||
) STRICT, WITHOUT ROWID
|
||||
;
|
||||
CREATE TRIGGER IF NOT EXISTS indexids_seq AFTER INSERT ON files
|
||||
BEGIN
|
||||
INSERT INTO indexids (folder_idx, device_idx, index_id, sequence)
|
||||
VALUES (NEW.folder_idx, NEW.device_idx, "", COALESCE(NEW.remote_sequence, NEW.sequence))
|
||||
ON CONFLICT DO UPDATE SET sequence = COALESCE(NEW.remote_sequence, NEW.sequence);
|
||||
END
|
||||
;
|
||||
@@ -0,0 +1,53 @@
|
||||
-- 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/.
|
||||
|
||||
-- Counts
|
||||
--
|
||||
-- Counts and sizes are maintained for each device, folder, type, flag bits
|
||||
-- combination.
|
||||
CREATE TABLE IF NOT EXISTS counts (
|
||||
folder_idx INTEGER NOT NULL,
|
||||
device_idx INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
local_flags INTEGER NOT NULL,
|
||||
count INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
deleted INTEGER NOT NULL, -- boolean
|
||||
PRIMARY KEY(folder_idx, device_idx, type, local_flags, deleted),
|
||||
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE,
|
||||
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
|
||||
) STRICT, WITHOUT ROWID
|
||||
;
|
||||
|
||||
--- Maintain counts when files are added and removed using triggers
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS counts_insert AFTER INSERT ON files
|
||||
BEGIN
|
||||
INSERT INTO counts (folder_idx, device_idx, type, local_flags, count, size, deleted)
|
||||
VALUES (NEW.folder_idx, NEW.device_idx, NEW.type, NEW.local_flags, 1, NEW.size, NEW.deleted)
|
||||
ON CONFLICT DO UPDATE SET count = count + 1, size = size + NEW.size;
|
||||
END
|
||||
;
|
||||
CREATE TRIGGER IF NOT EXISTS counts_delete AFTER DELETE ON files
|
||||
BEGIN
|
||||
UPDATE counts SET count = count - 1, size = size - OLD.size
|
||||
WHERE folder_idx = OLD.folder_idx AND device_idx = OLD.device_idx AND type = OLD.type AND local_flags = OLD.local_flags AND deleted = OLD.deleted;
|
||||
END
|
||||
;
|
||||
CREATE TRIGGER IF NOT EXISTS counts_update AFTER UPDATE OF local_flags ON files
|
||||
WHEN NEW.local_flags != OLD.local_flags
|
||||
BEGIN
|
||||
INSERT INTO counts (folder_idx, device_idx, type, local_flags, count, size, deleted)
|
||||
VALUES (NEW.folder_idx, NEW.device_idx, NEW.type, NEW.local_flags, 1, NEW.size, NEW.deleted)
|
||||
ON CONFLICT DO UPDATE SET count = count + 1, size = size + NEW.size;
|
||||
UPDATE counts SET count = count - 1, size = size - OLD.size
|
||||
WHERE folder_idx = OLD.folder_idx AND device_idx = OLD.device_idx AND type = OLD.type AND local_flags = OLD.local_flags AND deleted = OLD.deleted;
|
||||
END
|
||||
;
|
||||
DROP TRIGGER IF EXISTS counts_update_add -- tmp migration
|
||||
;
|
||||
DROP TRIGGER IF EXISTS counts_update_del -- tmp migration
|
||||
;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- 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/.
|
||||
|
||||
-- Block lists
|
||||
--
|
||||
-- The block lists are extracted from FileInfos and stored separately. This
|
||||
-- reduces the database size by reusing the same block list entry for all
|
||||
-- devices announcing the same file. Doing it for all block lists instead of
|
||||
-- using a size cutoff simplifies queries. Block lists are garbage collected
|
||||
-- "manually", not using a trigger as that was too performance impacting.
|
||||
CREATE TABLE IF NOT EXISTS blocklists (
|
||||
blocklist_hash BLOB NOT NULL PRIMARY KEY,
|
||||
blprotobuf BLOB NOT NULL
|
||||
) STRICT
|
||||
;
|
||||
|
||||
-- Blocks
|
||||
--
|
||||
-- For all local files we store the blocks individually for quick lookup. A
|
||||
-- given block can exist in multiple blocklists and at multiple offsets in a
|
||||
-- blocklist.
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
hash BLOB NOT NULL,
|
||||
blocklist_hash BLOB NOT NULL,
|
||||
idx INTEGER NOT NULL,
|
||||
offset INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
PRIMARY KEY (hash, blocklist_hash, idx),
|
||||
FOREIGN KEY(blocklist_hash) REFERENCES blocklists(blocklist_hash) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
|
||||
) STRICT
|
||||
;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 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/.
|
||||
|
||||
--- Backing for the MtimeFS
|
||||
CREATE TABLE IF NOT EXISTS mtimes (
|
||||
folder_idx INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
ondisk INTEGER NOT NULL, -- unix nanos
|
||||
virtual INTEGER NOT NULL, -- unix nanos
|
||||
PRIMARY KEY(folder_idx, name),
|
||||
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
|
||||
) STRICT, WITHOUT ROWID
|
||||
;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 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/.
|
||||
|
||||
--- Simple KV store. This backs the "miscDB" we use for certain minor pieces
|
||||
-- of data.
|
||||
CREATE TABLE IF NOT EXISTS kv (
|
||||
key TEXT NOT NULL PRIMARY KEY COLLATE BINARY,
|
||||
value BLOB NOT NULL
|
||||
) STRICT
|
||||
;
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"iter"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/internal/gen/bep"
|
||||
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// iterStructs returns an iterator over the given struct type by scanning
|
||||
// the SQL rows. `rows` is closed when the iterator exits.
|
||||
func iterStructs[T any](rows *sqlx.Rows, err error) (iter.Seq[T], func() error) {
|
||||
if err != nil {
|
||||
return func(_ func(T) bool) {}, func() error { return err }
|
||||
}
|
||||
|
||||
var retErr error
|
||||
return func(yield func(T) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
v := new(T)
|
||||
if err := rows.StructScan(v); err != nil {
|
||||
retErr = err
|
||||
break
|
||||
}
|
||||
if cleanuper, ok := any(v).(interface{ cleanup() }); ok {
|
||||
cleanuper.cleanup()
|
||||
}
|
||||
if !yield(*v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil && retErr == nil {
|
||||
retErr = err
|
||||
}
|
||||
}, func() error { return retErr }
|
||||
}
|
||||
|
||||
// dbVector is a wrapper that allows protocol.Vector values to be serialized
|
||||
// to and from the database.
|
||||
type dbVector struct { //nolint:recvcheck
|
||||
protocol.Vector
|
||||
}
|
||||
|
||||
func (v dbVector) Value() (driver.Value, error) {
|
||||
return v.String(), nil
|
||||
}
|
||||
|
||||
func (v *dbVector) Scan(value any) error {
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.New("not a string")
|
||||
}
|
||||
if str == "" {
|
||||
v.Vector = protocol.Vector{}
|
||||
return nil
|
||||
}
|
||||
vec, err := protocol.VectorFromString(str)
|
||||
if err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
v.Vector = vec
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// indirectFI constructs a FileInfo from separate marshalled FileInfo and
|
||||
// BlockList bytes.
|
||||
type indirectFI struct {
|
||||
Name string // not used, must be present as dest for Need iterator
|
||||
FiProtobuf []byte
|
||||
BlProtobuf []byte
|
||||
Size int64 // not used
|
||||
Modified int64 // not used
|
||||
}
|
||||
|
||||
func (i indirectFI) FileInfo() (protocol.FileInfo, error) {
|
||||
var fi bep.FileInfo
|
||||
if err := proto.Unmarshal(i.FiProtobuf, &fi); err != nil {
|
||||
return protocol.FileInfo{}, wrap(err, "unmarshal fileinfo")
|
||||
}
|
||||
if len(i.BlProtobuf) > 0 {
|
||||
var bl dbproto.BlockList
|
||||
if err := proto.Unmarshal(i.BlProtobuf, &bl); err != nil {
|
||||
return protocol.FileInfo{}, wrap(err, "unmarshal blocklist")
|
||||
}
|
||||
fi.Blocks = bl.Blocks
|
||||
}
|
||||
fi.Name = osutil.NativeFilename(fi.Name)
|
||||
return protocol.FileInfoFromDB(&fi), nil
|
||||
}
|
||||
|
||||
func prefixEnd(s string) string {
|
||||
if s == "" {
|
||||
panic("bug: cannot represent end prefix for empty string")
|
||||
}
|
||||
bs := []byte(s)
|
||||
for i := len(bs) - 1; i >= 0; i-- {
|
||||
if bs[i] < 0xff {
|
||||
bs[i]++
|
||||
break
|
||||
}
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (C) 2014 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 (
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed is a simple key-value store using a specific namespace within a
|
||||
// lower level KV.
|
||||
type Typed struct {
|
||||
db KV
|
||||
prefix string
|
||||
}
|
||||
|
||||
func NewMiscDB(db KV) *Typed {
|
||||
return NewTyped(db, "misc")
|
||||
}
|
||||
|
||||
// NewTyped returns a new typed key-value store that lives in the namespace
|
||||
// specified by the prefix.
|
||||
func NewTyped(db KV, prefix string) *Typed {
|
||||
return &Typed{
|
||||
db: db,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// PutInt64 stores a new int64. Any existing value (even if of another type)
|
||||
// is overwritten.
|
||||
func (n *Typed) PutInt64(key string, val int64) error {
|
||||
var valBs [8]byte
|
||||
binary.BigEndian.PutUint64(valBs[:], uint64(val))
|
||||
return n.db.PutKV(n.prefixedKey(key), valBs[:])
|
||||
}
|
||||
|
||||
// Int64 returns the stored value interpreted as an int64 and a boolean that
|
||||
// is false if no value was stored at the key.
|
||||
func (n *Typed) Int64(key string) (int64, bool, error) {
|
||||
valBs, err := n.db.GetKV(n.prefixedKey(key))
|
||||
if err != nil {
|
||||
return 0, false, filterNotFound(err)
|
||||
}
|
||||
val := binary.BigEndian.Uint64(valBs)
|
||||
return int64(val), true, nil
|
||||
}
|
||||
|
||||
// PutTime stores a new time.Time. Any existing value (even if of another
|
||||
// type) is overwritten.
|
||||
func (n *Typed) PutTime(key string, val time.Time) error {
|
||||
valBs, _ := val.MarshalBinary() // never returns an error
|
||||
return n.db.PutKV(n.prefixedKey(key), valBs)
|
||||
}
|
||||
|
||||
// Time returns the stored value interpreted as a time.Time and a boolean
|
||||
// that is false if no value was stored at the key.
|
||||
func (n *Typed) Time(key string) (time.Time, bool, error) {
|
||||
var t time.Time
|
||||
valBs, err := n.db.GetKV(n.prefixedKey(key))
|
||||
if err != nil {
|
||||
return t, false, filterNotFound(err)
|
||||
}
|
||||
err = t.UnmarshalBinary(valBs)
|
||||
return t, err == nil, err
|
||||
}
|
||||
|
||||
// PutString stores a new string. Any existing value (even if of another type)
|
||||
// is overwritten.
|
||||
func (n *Typed) PutString(key, val string) error {
|
||||
return n.db.PutKV(n.prefixedKey(key), []byte(val))
|
||||
}
|
||||
|
||||
// String returns the stored value interpreted as a string and a boolean that
|
||||
// is false if no value was stored at the key.
|
||||
func (n *Typed) String(key string) (string, bool, error) {
|
||||
valBs, err := n.db.GetKV(n.prefixedKey(key))
|
||||
if err != nil {
|
||||
return "", false, filterNotFound(err)
|
||||
}
|
||||
return string(valBs), true, nil
|
||||
}
|
||||
|
||||
// PutBytes stores a new byte slice. Any existing value (even if of another type)
|
||||
// is overwritten.
|
||||
func (n *Typed) PutBytes(key string, val []byte) error {
|
||||
return n.db.PutKV(n.prefixedKey(key), val)
|
||||
}
|
||||
|
||||
// Bytes returns the stored value as a raw byte slice and a boolean that
|
||||
// is false if no value was stored at the key.
|
||||
func (n *Typed) Bytes(key string) ([]byte, bool, error) {
|
||||
valBs, err := n.db.GetKV(n.prefixedKey(key))
|
||||
if err != nil {
|
||||
return nil, false, filterNotFound(err)
|
||||
}
|
||||
return valBs, true, nil
|
||||
}
|
||||
|
||||
// PutBool stores a new boolean. Any existing value (even if of another type)
|
||||
// is overwritten.
|
||||
func (n *Typed) PutBool(key string, val bool) error {
|
||||
if val {
|
||||
return n.db.PutKV(n.prefixedKey(key), []byte{0x0})
|
||||
}
|
||||
return n.db.PutKV(n.prefixedKey(key), []byte{0x1})
|
||||
}
|
||||
|
||||
// Bool returns the stored value as a boolean and a boolean that
|
||||
// is false if no value was stored at the key.
|
||||
func (n *Typed) Bool(key string) (bool, bool, error) {
|
||||
valBs, err := n.db.GetKV(n.prefixedKey(key))
|
||||
if err != nil {
|
||||
return false, false, filterNotFound(err)
|
||||
}
|
||||
return valBs[0] == 0x0, true, nil
|
||||
}
|
||||
|
||||
// Delete deletes the specified key. It is allowed to delete a nonexistent
|
||||
// key.
|
||||
func (n *Typed) Delete(key string) error {
|
||||
return n.db.DeleteKV(n.prefixedKey(key))
|
||||
}
|
||||
|
||||
func (n *Typed) prefixedKey(key string) string {
|
||||
return n.prefix + "/" + key
|
||||
}
|
||||
|
||||
func filterNotFound(err error) error {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (C) 2014 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_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/db/sqlite"
|
||||
)
|
||||
|
||||
func TestNamespacedInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ldb, err := sqlite.OpenTemp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
ldb.Close()
|
||||
})
|
||||
|
||||
n1 := db.NewTyped(ldb, "foo")
|
||||
n2 := db.NewTyped(ldb, "bar")
|
||||
|
||||
t.Run("Int", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Key is missing to start with
|
||||
|
||||
if v, ok, err := n1.Int64("testint"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if v != 0 || ok {
|
||||
t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
|
||||
}
|
||||
|
||||
if err := n1.PutInt64("testint", 42); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// It should now exist in n1
|
||||
|
||||
if v, ok, err := n1.Int64("testint"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if v != 42 || !ok {
|
||||
t.Errorf("Incorrect return v %v != 42 || ok %v != true", v, ok)
|
||||
}
|
||||
|
||||
// ... but not in n2, which is in a different namespace
|
||||
|
||||
if v, ok, err := n2.Int64("testint"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if v != 0 || ok {
|
||||
t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
|
||||
}
|
||||
|
||||
if err := n1.Delete("testint"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// It should no longer exist
|
||||
|
||||
if v, ok, err := n1.Int64("testint"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if v != 0 || ok {
|
||||
t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Time", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if v, ok, err := n1.Time("testtime"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if !v.IsZero() || ok {
|
||||
t.Errorf("Incorrect return v %v != %v || ok %v != false", v, time.Time{}, ok)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := n1.PutTime("testtime", now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v, ok, err := n1.Time("testtime"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if !v.Equal(now) || !ok {
|
||||
t.Errorf("Incorrect return v %v != %v || ok %v != true", v, now, ok)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("String", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if v, ok, err := n1.String("teststring"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if v != "" || ok {
|
||||
t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
|
||||
}
|
||||
|
||||
if err := n1.PutString("teststring", "yo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v, ok, err := n1.String("teststring"); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
} else if v != "yo" || !ok {
|
||||
t.Errorf("Incorrect return v %q != \"yo\" || ok %v != true", v, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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 itererr
|
||||
|
||||
import "iter"
|
||||
|
||||
// Collect returns a slice of the items from the iterator, plus the error if
|
||||
// any.
|
||||
func Collect[T any](it iter.Seq[T], errFn func() error) ([]T, error) {
|
||||
var s []T
|
||||
for v := range it {
|
||||
s = append(s, v)
|
||||
}
|
||||
return s, errFn()
|
||||
}
|
||||
|
||||
// Zip interleaves the iterator value with the error. The iteration ends
|
||||
// after a non-nil error.
|
||||
func Zip[T any](it iter.Seq[T], errFn func() error) iter.Seq2[T, error] {
|
||||
return func(yield func(T, error) bool) {
|
||||
for v := range it {
|
||||
if !yield(v, nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := errFn(); err != nil {
|
||||
var zero T
|
||||
yield(zero, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map returns a new iterator by applying the map function, while respecting
|
||||
// the error function. Additionally, the map function can return an error if
|
||||
// its own.
|
||||
func Map[A, B any](i iter.Seq[A], errFn func() error, mapFn func(A) (B, error)) (iter.Seq[B], func() error) {
|
||||
var retErr error
|
||||
return func(yield func(B) bool) {
|
||||
for v := range i {
|
||||
mapped, err := mapFn(v)
|
||||
if err != nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
if !yield(mapped) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}, func() error {
|
||||
if prevErr := errFn(); prevErr != nil {
|
||||
return prevErr
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
}
|
||||
|
||||
// Map returns a new iterator by applying the map function, while respecting
|
||||
// the error function. Additionally, the map function can return an error if
|
||||
// its own.
|
||||
func Map2[A, B, C any](i iter.Seq[A], errFn func() error, mapFn func(A) (B, C, error)) (iter.Seq2[B, C], func() error) {
|
||||
var retErr error
|
||||
return func(yield func(B, C) bool) {
|
||||
for v := range i {
|
||||
ma, mb, err := mapFn(v)
|
||||
if err != nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
if !yield(ma, mb) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}, func() error {
|
||||
if prevErr := errFn(); prevErr != nil {
|
||||
return prevErr
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
// 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 protoutil
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 timeutil
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var prevNanos atomic.Int64
|
||||
|
||||
// StrictlyMonotonicNanos returns the current time in Unix nanoseconds.
|
||||
// Guaranteed to strictly increase for each call, regardless of the
|
||||
// underlying OS timer resolution or clock jumps.
|
||||
func StrictlyMonotonicNanos() int64 {
|
||||
for {
|
||||
old := prevNanos.Load()
|
||||
now := max(time.Now().UnixNano(), old+1)
|
||||
if prevNanos.CompareAndSwap(old, now) {
|
||||
return now
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user