浏览代码

lib/db: Keep folder meta data persistently in db (fixes #4400)

This keeps the data we need about sequence numbers and object counts
persistently in the database. The sizeTracker is expanded into a
metadataTracker than handled multiple folders, and the Counts struct is
made protobuf serializable. It gains a Sequence field to assist in
tracking that as well, and a collection of Counts become a CountsSet
(for serialization purposes).

The initial database scan is also a consistency check of the global
entries. This shouldn't strictly be necessary. Nonetheless I added a
created timestamp to the metadata and set a variable to compare against
that. When the time since the metadata creation is old enough, we drop
the metadata and rebuild from scratch like we used to, while also
consistency checking.

A new environment variable STCHECKDBEVERY can override this interval,
and for example be set to zero to force the check immediately.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4547
LGTM: imsodin
Jakob Borg 7 年之前
父节点
当前提交
d1d967f0cf

+ 5 - 0
cmd/syncthing/main.go

@@ -197,6 +197,11 @@ are mostly useful for developers. Use with care.
                    "minio" for the github.com/minio/sha256-simd implementation,
                    "minio" for the github.com/minio/sha256-simd implementation,
                    and blank (the default) for auto detection.
                    and blank (the default) for auto detection.
 
 
+ STDBCHECKEVERY    Set to a time interval to override the default database
+                   check interval of 30 days (720h). The interval understands
+                   "h", "m" and "s" abbreviations for hours minutes and seconds.
+                   Valid values are like "720h", "30s", etc.
+
  GOMAXPROCS        Set the maximum number of CPU cores to use. Defaults to all
  GOMAXPROCS        Set the maximum number of CPU cores to use. Defaults to all
                    available CPU cores.
                    available CPU cores.
 
 

+ 3 - 3
cmd/syncthing/usage_report.go

@@ -51,10 +51,10 @@ func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf,
 	var totBytes, maxBytes int64
 	var totBytes, maxBytes int64
 	for folderID := range cfg.Folders() {
 	for folderID := range cfg.Folders() {
 		global := m.GlobalSize(folderID)
 		global := m.GlobalSize(folderID)
-		totFiles += global.Files
+		totFiles += int(global.Files)
 		totBytes += global.Bytes
 		totBytes += global.Bytes
-		if global.Files > maxFiles {
-			maxFiles = global.Files
+		if int(global.Files) > maxFiles {
+			maxFiles = int(global.Files)
 		}
 		}
 		if global.Bytes > maxBytes {
 		if global.Bytes > maxBytes {
 			maxBytes = global.Bytes
 			maxBytes = global.Bytes

+ 1 - 0
lib/db/leveldb.go

@@ -25,6 +25,7 @@ const (
 	KeyTypeFolderIdx
 	KeyTypeFolderIdx
 	KeyTypeDeviceIdx
 	KeyTypeDeviceIdx
 	KeyTypeIndexID
 	KeyTypeIndexID
+	KeyTypeFolderMeta
 )
 )
 
 
 func (l VersionList) String() string {
 func (l VersionList) String() string {

+ 21 - 12
lib/db/leveldb_dbinstance.go

@@ -93,12 +93,11 @@ func (db *Instance) Location() string {
 	return db.location
 	return db.location
 }
 }
 
 
-func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) {
+func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
 	t := db.newReadWriteTransaction()
 	t := db.newReadWriteTransaction()
 	defer t.close()
 	defer t.close()
 
 
 	var fk []byte
 	var fk []byte
-	isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
 	for _, f := range fs {
 	for _, f := range fs {
 		name := []byte(f.Name)
 		name := []byte(f.Name)
 		fk = db.deviceKeyInto(fk[:cap(fk)], folder, device, name)
 		fk = db.deviceKeyInto(fk[:cap(fk)], folder, device, name)
@@ -116,15 +115,14 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
 			continue
 			continue
 		}
 		}
 
 
-		if isLocalDevice {
-			if err == nil {
-				localSize.removeFile(ef)
-			}
-			localSize.addFile(f)
+		devID := protocol.DeviceIDFromBytes(device)
+		if err == nil {
+			meta.removeFile(devID, ef)
 		}
 		}
+		meta.addFile(devID, f)
 
 
 		t.insertFile(folder, device, f)
 		t.insertFile(folder, device, f)
-		t.updateGlobal(folder, device, f, globalSize)
+		t.updateGlobal(folder, device, f, meta)
 
 
 		// Write out and reuse the batch every few records, to avoid the batch
 		// Write out and reuse the batch every few records, to avoid the batch
 		// growing too large and thus allocating unnecessarily much memory.
 		// growing too large and thus allocating unnecessarily much memory.
@@ -465,7 +463,7 @@ func (db *Instance) dropFolder(folder []byte) {
 	dbi.Release()
 	dbi.Release()
 }
 }
 
 
-func (db *Instance) dropDeviceFolder(device, folder []byte, globalSize *sizeTracker) {
+func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) {
 	t := db.newReadWriteTransaction()
 	t := db.newReadWriteTransaction()
 	defer t.close()
 	defer t.close()
 
 
@@ -475,13 +473,13 @@ func (db *Instance) dropDeviceFolder(device, folder []byte, globalSize *sizeTrac
 	for dbi.Next() {
 	for dbi.Next() {
 		key := dbi.Key()
 		key := dbi.Key()
 		name := db.deviceKeyName(key)
 		name := db.deviceKeyName(key)
-		t.removeFromGlobal(folder, device, name, globalSize)
+		t.removeFromGlobal(folder, device, name, meta)
 		t.Delete(key)
 		t.Delete(key)
 		t.checkFlush()
 		t.checkFlush()
 	}
 	}
 }
 }
 
 
-func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
+func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
 	t := db.newReadWriteTransaction()
 	t := db.newReadWriteTransaction()
 	defer t.close()
 	defer t.close()
 
 
@@ -520,7 +518,7 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
 
 
 			if i == 0 {
 			if i == 0 {
 				if fi, ok := t.getFile(folder, version.Device, name); ok {
 				if fi, ok := t.getFile(folder, version.Device, name); ok {
-					globalSize.addFile(fi)
+					meta.addFile(globalDeviceID, fi)
 				}
 				}
 			}
 			}
 		}
 		}
@@ -760,6 +758,13 @@ func (db *Instance) mtimesKey(folder []byte) []byte {
 	return prefix
 	return prefix
 }
 }
 
 
+func (db *Instance) folderMetaKey(folder []byte) []byte {
+	prefix := make([]byte, 5) // key type + 4 bytes folder idx number
+	prefix[0] = KeyTypeFolderMeta
+	binary.BigEndian.PutUint32(prefix[1:], db.folderIdx.ID(folder))
+	return prefix
+}
+
 // DropDeltaIndexIDs removes all index IDs from the database. This will
 // DropDeltaIndexIDs removes all index IDs from the database. This will
 // cause a full index transmission on the next connection.
 // cause a full index transmission on the next connection.
 func (db *Instance) DropDeltaIndexIDs() {
 func (db *Instance) DropDeltaIndexIDs() {
@@ -770,6 +775,10 @@ func (db *Instance) dropMtimes(folder []byte) {
 	db.dropPrefix(db.mtimesKey(folder))
 	db.dropPrefix(db.mtimesKey(folder))
 }
 }
 
 
+func (db *Instance) dropFolderMeta(folder []byte) {
+	db.dropPrefix(db.folderMetaKey(folder))
+}
+
 func (db *Instance) dropPrefix(prefix []byte) {
 func (db *Instance) dropPrefix(prefix []byte) {
 	t := db.newReadWriteTransaction()
 	t := db.newReadWriteTransaction()
 	defer t.close()
 	defer t.close()

+ 9 - 9
lib/db/leveldb_transactions.go

@@ -85,7 +85,7 @@ func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.Fi
 // updateGlobal adds this device+version to the version list for the given
 // updateGlobal adds this device+version to the version list for the given
 // file. If the device is already present in the list, the version is updated.
 // file. If the device is already present in the list, the version is updated.
 // If the file does not have an entry in the global list, it is created.
 // If the file does not have an entry in the global list, it is created.
-func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.FileInfo, globalSize *sizeTracker) bool {
+func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.FileInfo, meta *metadataTracker) bool {
 	l.Debugf("update global; folder=%q device=%v file=%q version=%v invalid=%v", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version, file.Invalid)
 	l.Debugf("update global; folder=%q device=%v file=%q version=%v invalid=%v", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version, file.Invalid)
 	name := []byte(file.Name)
 	name := []byte(file.Name)
 	gk := t.db.globalKey(folder, name)
 	gk := t.db.globalKey(folder, name)
@@ -106,7 +106,7 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
 
 
 				if i == 0 {
 				if i == 0 {
 					// Keep the current newest file around so we can subtract it from
 					// Keep the current newest file around so we can subtract it from
-					// the globalSize if we replace it.
+					// the metadata if we replace it.
 					oldFile, hasOldFile = t.getFile(folder, fl.Versions[0].Device, name)
 					oldFile, hasOldFile = t.getFile(folder, fl.Versions[0].Device, name)
 				}
 				}
 
 
@@ -169,16 +169,16 @@ insert:
 		// We just inserted a new newest version. Fixup the global size
 		// We just inserted a new newest version. Fixup the global size
 		// calculation.
 		// calculation.
 		if !file.Version.Equal(oldFile.Version) {
 		if !file.Version.Equal(oldFile.Version) {
-			globalSize.addFile(file)
+			meta.addFile(globalDeviceID, file)
 			if hasOldFile {
 			if hasOldFile {
 				// We have the old file that was removed at the head of the list.
 				// We have the old file that was removed at the head of the list.
-				globalSize.removeFile(oldFile)
+				meta.removeFile(globalDeviceID, oldFile)
 			} else if len(fl.Versions) > 1 {
 			} else if len(fl.Versions) > 1 {
 				// The previous newest version is now at index 1, grab it from there.
 				// The previous newest version is now at index 1, grab it from there.
 				if oldFile, ok := t.getFile(folder, fl.Versions[1].Device, name); ok {
 				if oldFile, ok := t.getFile(folder, fl.Versions[1].Device, name); ok {
 					// A failure to get the file here is surprising and our
 					// A failure to get the file here is surprising and our
 					// global size data will be incorrect until a restart...
 					// global size data will be incorrect until a restart...
-					globalSize.removeFile(oldFile)
+					meta.removeFile(globalDeviceID, oldFile)
 				}
 				}
 			}
 			}
 		}
 		}
@@ -193,7 +193,7 @@ insert:
 // removeFromGlobal removes the device from the global version list for the
 // removeFromGlobal removes the device from the global version list for the
 // given file. If the version list is empty after this, the file entry is
 // given file. If the version list is empty after this, the file entry is
 // removed entirely.
 // removed entirely.
-func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, globalSize *sizeTracker) {
+func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, meta *metadataTracker) {
 	l.Debugf("remove from global; folder=%q device=%v file=%q", folder, protocol.DeviceIDFromBytes(device), file)
 	l.Debugf("remove from global; folder=%q device=%v file=%q", folder, protocol.DeviceIDFromBytes(device), file)
 
 
 	gk := t.db.globalKey(folder, file)
 	gk := t.db.globalKey(folder, file)
@@ -214,13 +214,13 @@ func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, glob
 	removed := false
 	removed := false
 	for i := range fl.Versions {
 	for i := range fl.Versions {
 		if bytes.Equal(fl.Versions[i].Device, device) {
 		if bytes.Equal(fl.Versions[i].Device, device) {
-			if i == 0 && globalSize != nil {
+			if i == 0 && meta != nil {
 				f, ok := t.getFile(folder, device, file)
 				f, ok := t.getFile(folder, device, file)
 				if !ok {
 				if !ok {
 					// didn't exist anyway, apparently
 					// didn't exist anyway, apparently
 					continue
 					continue
 				}
 				}
-				globalSize.removeFile(f)
+				meta.removeFile(globalDeviceID, f)
 				removed = true
 				removed = true
 			}
 			}
 			fl.Versions = append(fl.Versions[:i], fl.Versions[i+1:]...)
 			fl.Versions = append(fl.Versions[:i], fl.Versions[i+1:]...)
@@ -238,7 +238,7 @@ func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, glob
 		if f, ok := t.getFile(folder, fl.Versions[0].Device, file); ok {
 		if f, ok := t.getFile(folder, fl.Versions[0].Device, file); ok {
 			// A failure to get the file here is surprising and our
 			// A failure to get the file here is surprising and our
 			// global size data will be incorrect until a restart...
 			// global size data will be incorrect until a restart...
-			globalSize.addFile(f)
+			meta.addFile(globalDeviceID, f)
 		}
 		}
 	}
 	}
 }
 }

+ 220 - 0
lib/db/meta.go

@@ -0,0 +1,220 @@
+// Copyright (C) 2017 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 (
+	"time"
+
+	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/sync"
+)
+
+// like protocol.LocalDeviceID but with 0xf8 in all positions
+var globalDeviceID = protocol.DeviceID{0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8}
+
+type metadataTracker struct {
+	mut     sync.RWMutex
+	counts  CountsSet
+	indexes map[protocol.DeviceID]int // device ID -> index in counts
+}
+
+func newMetadataTracker() *metadataTracker {
+	return &metadataTracker{
+		mut:     sync.NewRWMutex(),
+		indexes: make(map[protocol.DeviceID]int),
+	}
+}
+
+// Unmarshal loads a metadataTracker from the corresponding protobuf
+// representation
+func (m *metadataTracker) Unmarshal(bs []byte) error {
+	if err := m.counts.Unmarshal(bs); err != nil {
+		return err
+	}
+
+	// Initialize the index map
+	for i, c := range m.counts.Counts {
+		m.indexes[protocol.DeviceIDFromBytes(c.DeviceID)] = i
+	}
+	return nil
+}
+
+// Unmarshal returns the protobuf representation of the metadataTracker
+func (m *metadataTracker) Marshal() ([]byte, error) {
+	return m.counts.Marshal()
+}
+
+// toDB saves the marshalled metadataTracker to the given db, under the key
+// corresponding to the given folder
+func (m *metadataTracker) toDB(db *Instance, folder []byte) error {
+	key := db.folderMetaKey(folder)
+	bs, err := m.Marshal()
+	if err != nil {
+		return err
+	}
+	return db.Put(key, bs, nil)
+}
+
+// fromDB initializes the metadataTracker from the marshalled data found in
+// the database under the key corresponding to the given folder
+func (m *metadataTracker) fromDB(db *Instance, folder []byte) error {
+	key := db.folderMetaKey(folder)
+	bs, err := db.Get(key, nil)
+	if err != nil {
+		return err
+	}
+	return m.Unmarshal(bs)
+}
+
+// countsPtr returns a pointer to the corresponding Counts struct, if
+// necessary allocating one in the process
+func (m *metadataTracker) countsPtr(dev protocol.DeviceID) *Counts {
+	// must be called with the mutex held
+
+	idx, ok := m.indexes[dev]
+	if !ok {
+		idx = len(m.counts.Counts)
+		m.counts.Counts = append(m.counts.Counts, Counts{DeviceID: dev[:]})
+		m.indexes[dev] = idx
+	}
+	return &m.counts.Counts[idx]
+}
+
+// addFile adds a file to the counts, adjusting the sequence number as
+// appropriate
+func (m *metadataTracker) addFile(dev protocol.DeviceID, f FileIntf) {
+	if f.IsInvalid() {
+		return
+	}
+
+	m.mut.Lock()
+	cp := m.countsPtr(dev)
+
+	switch {
+	case f.IsDeleted():
+		cp.Deleted++
+	case f.IsDirectory() && !f.IsSymlink():
+		cp.Directories++
+	case f.IsSymlink():
+		cp.Symlinks++
+	default:
+		cp.Files++
+	}
+	cp.Bytes += f.FileSize()
+
+	if seq := f.SequenceNo(); seq > cp.Sequence {
+		cp.Sequence = seq
+	}
+
+	m.mut.Unlock()
+}
+
+// removeFile removes a file from the counts
+func (m *metadataTracker) removeFile(dev protocol.DeviceID, f FileIntf) {
+	if f.IsInvalid() {
+		return
+	}
+
+	m.mut.Lock()
+	cp := m.countsPtr(dev)
+
+	switch {
+	case f.IsDeleted():
+		cp.Deleted--
+	case f.IsDirectory() && !f.IsSymlink():
+		cp.Directories--
+	case f.IsSymlink():
+		cp.Symlinks--
+	default:
+		cp.Files--
+	}
+	cp.Bytes -= f.FileSize()
+
+	if cp.Deleted < 0 || cp.Files < 0 || cp.Directories < 0 || cp.Symlinks < 0 {
+		panic("bug: removed more than added")
+	}
+
+	m.mut.Unlock()
+}
+
+// resetAll resets all metadata for the given device
+func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
+	m.mut.Lock()
+	*m.countsPtr(dev) = Counts{DeviceID: dev[:]}
+	m.mut.Unlock()
+}
+
+// resetCounts resets the file, dir, etc. counters, while retaining the
+// sequence number
+func (m *metadataTracker) resetCounts(dev protocol.DeviceID) {
+	m.mut.Lock()
+
+	c := m.countsPtr(dev)
+	c.Bytes = 0
+	c.Deleted = 0
+	c.Directories = 0
+	c.Files = 0
+	c.Symlinks = 0
+	// c.Sequence deliberately untouched
+
+	m.mut.Unlock()
+}
+
+// Counts returns the counts for the given device ID
+func (m *metadataTracker) Counts(dev protocol.DeviceID) Counts {
+	m.mut.RLock()
+	defer m.mut.RUnlock()
+
+	idx, ok := m.indexes[dev]
+	if !ok {
+		return Counts{}
+	}
+
+	return m.counts.Counts[idx]
+}
+
+// nextSeq allocates a new sequence number for the given device
+func (m *metadataTracker) nextSeq(dev protocol.DeviceID) int64 {
+	m.mut.Lock()
+	defer m.mut.Unlock()
+
+	c := m.countsPtr(dev)
+	c.Sequence++
+	return c.Sequence
+}
+
+// devices returns the list of devices tracked, excluding the local device
+// (which we don't know the ID of)
+func (m *metadataTracker) devices() []protocol.DeviceID {
+	devs := make([]protocol.DeviceID, 0, len(m.counts.Counts))
+
+	m.mut.RLock()
+	for _, dev := range m.counts.Counts {
+		if dev.Sequence > 0 {
+			id := protocol.DeviceIDFromBytes(dev.DeviceID)
+			if id == globalDeviceID || id == protocol.LocalDeviceID {
+				continue
+			}
+			devs = append(devs, id)
+		}
+	}
+	m.mut.RUnlock()
+
+	return devs
+}
+
+func (m *metadataTracker) Created() time.Time {
+	m.mut.RLock()
+	defer m.mut.RUnlock()
+	return time.Unix(0, m.counts.Created)
+}
+
+func (m *metadataTracker) SetCreated() {
+	m.mut.Lock()
+	m.counts.Created = time.Now().UnixNano()
+	m.mut.Unlock()
+}

+ 56 - 141
lib/db/set.go

@@ -13,8 +13,8 @@
 package db
 package db
 
 
 import (
 import (
-	stdsync "sync"
-	"sync/atomic"
+	"os"
+	"time"
 
 
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/osutil"
@@ -23,16 +23,13 @@ import (
 )
 )
 
 
 type FileSet struct {
 type FileSet struct {
-	sequence   int64 // Our local sequence number
-	folder     string
-	fs         fs.Filesystem
-	db         *Instance
-	blockmap   *BlockMap
-	localSize  sizeTracker
-	globalSize sizeTracker
-
-	remoteSequence map[protocol.DeviceID]int64 // Highest seen sequence numbers for other devices
-	updateMutex    sync.Mutex                  // protects remoteSequence and database updates
+	folder   string
+	fs       fs.Filesystem
+	db       *Instance
+	blockmap *BlockMap
+	meta     *metadataTracker
+
+	updateMutex sync.Mutex // protects database updates and the corresponding metadata changes
 }
 }
 
 
 // FileIntf is the set of methods implemented by both protocol.FileInfo and
 // FileIntf is the set of methods implemented by both protocol.FileInfo and
@@ -45,6 +42,7 @@ type FileIntf interface {
 	IsDirectory() bool
 	IsDirectory() bool
 	IsSymlink() bool
 	IsSymlink() bool
 	HasPermissionBits() bool
 	HasPermissionBits() bool
+	SequenceNo() int64
 }
 }
 
 
 // The Iterator is called with either a protocol.FileInfo or a
 // The Iterator is called with either a protocol.FileInfo or a
@@ -52,102 +50,49 @@ type FileIntf interface {
 // continue iteration, false to stop.
 // continue iteration, false to stop.
 type Iterator func(f FileIntf) bool
 type Iterator func(f FileIntf) bool
 
 
-type Counts struct {
-	Files       int
-	Directories int
-	Symlinks    int
-	Deleted     int
-	Bytes       int64
-}
-
-type sizeTracker struct {
-	Counts
-	mut stdsync.Mutex
-}
-
-func (s *sizeTracker) addFile(f FileIntf) {
-	if f.IsInvalid() {
-		return
-	}
+var databaseRecheckInterval = 30 * 24 * time.Hour
 
 
-	s.mut.Lock()
-	switch {
-	case f.IsDeleted():
-		s.Deleted++
-	case f.IsDirectory() && !f.IsSymlink():
-		s.Directories++
-	case f.IsSymlink():
-		s.Symlinks++
-	default:
-		s.Files++
+func init() {
+	if dur, err := time.ParseDuration(os.Getenv("STRECHECKDBEVERY")); err == nil {
+		databaseRecheckInterval = dur
 	}
 	}
-	s.Bytes += f.FileSize()
-	s.mut.Unlock()
 }
 }
 
 
-func (s *sizeTracker) removeFile(f FileIntf) {
-	if f.IsInvalid() {
-		return
+func NewFileSet(folder string, fs fs.Filesystem, db *Instance) *FileSet {
+	var s = FileSet{
+		folder:      folder,
+		fs:          fs,
+		db:          db,
+		blockmap:    NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
+		meta:        newMetadataTracker(),
+		updateMutex: sync.NewMutex(),
 	}
 	}
 
 
-	s.mut.Lock()
-	switch {
-	case f.IsDeleted():
-		s.Deleted--
-	case f.IsDirectory() && !f.IsSymlink():
-		s.Directories--
-	case f.IsSymlink():
-		s.Symlinks--
-	default:
-		s.Files--
+	if err := s.meta.fromDB(db, []byte(folder)); err != nil {
+		l.Infof("No stored folder metadata for %q: recalculating", folder)
+		s.recalcCounts()
+	} else if age := time.Since(s.meta.Created()); age > databaseRecheckInterval {
+		l.Infof("Stored folder metadata for %q is %v old; recalculating", folder, age)
+		s.recalcCounts()
 	}
 	}
-	s.Bytes -= f.FileSize()
-	if s.Deleted < 0 || s.Files < 0 || s.Directories < 0 || s.Symlinks < 0 {
-		panic("bug: removed more than added")
-	}
-	s.mut.Unlock()
-}
 
 
-func (s *sizeTracker) reset() {
-	s.mut.Lock()
-	defer s.mut.Unlock()
-	s.Counts = Counts{}
+	return &s
 }
 }
 
 
-func (s *sizeTracker) Size() Counts {
-	s.mut.Lock()
-	defer s.mut.Unlock()
-	return s.Counts
-}
+func (s *FileSet) recalcCounts() {
+	s.meta = newMetadataTracker()
 
 
-func NewFileSet(folder string, fs fs.Filesystem, db *Instance) *FileSet {
-	var s = FileSet{
-		remoteSequence: make(map[protocol.DeviceID]int64),
-		folder:         folder,
-		fs:             fs,
-		db:             db,
-		blockmap:       NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
-		updateMutex:    sync.NewMutex(),
-	}
-
-	s.db.checkGlobals([]byte(folder), &s.globalSize)
+	s.db.checkGlobals([]byte(s.folder), s.meta)
 
 
 	var deviceID protocol.DeviceID
 	var deviceID protocol.DeviceID
-	s.db.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
+	s.db.withAllFolderTruncated([]byte(s.folder), func(device []byte, f FileInfoTruncated) bool {
 		copy(deviceID[:], device)
 		copy(deviceID[:], device)
-		if deviceID == protocol.LocalDeviceID {
-			if f.Sequence > s.sequence {
-				s.sequence = f.Sequence
-			}
-			s.localSize.addFile(f)
-		} else if f.Sequence > s.remoteSequence[deviceID] {
-			s.remoteSequence[deviceID] = f.Sequence
-		}
+		s.meta.addFile(deviceID, f)
 		return true
 		return true
 	})
 	})
-	l.Debugf("loaded sequence for %q: %#v", folder, s.sequence)
 
 
-	return &s
+	s.meta.SetCreated()
+	s.meta.toDB(s.db, []byte(s.folder))
 }
 }
 
 
 func (s *FileSet) Drop(device protocol.DeviceID) {
 func (s *FileSet) Drop(device protocol.DeviceID) {
@@ -156,22 +101,25 @@ func (s *FileSet) Drop(device protocol.DeviceID) {
 	s.updateMutex.Lock()
 	s.updateMutex.Lock()
 	defer s.updateMutex.Unlock()
 	defer s.updateMutex.Unlock()
 
 
-	s.db.dropDeviceFolder(device[:], []byte(s.folder), &s.globalSize)
+	s.db.dropDeviceFolder(device[:], []byte(s.folder), s.meta)
 
 
 	if device == protocol.LocalDeviceID {
 	if device == protocol.LocalDeviceID {
 		s.blockmap.Drop()
 		s.blockmap.Drop()
-		s.localSize.reset()
-		// We deliberately do not reset s.sequence here. Dropping all files
-		// for the local device ID only happens in testing - which expects
-		// the sequence to be retained, like an old Replace() of all files
-		// would do. However, if we ever did it "in production" we would
-		// anyway want to retain the sequence for delta indexes to be happy.
+		s.meta.resetCounts(device)
+		// We deliberately do not reset the sequence number here. Dropping
+		// all files for the local device ID only happens in testing - which
+		// expects the sequence to be retained, like an old Replace() of all
+		// files would do. However, if we ever did it "in production" we
+		// would anyway want to retain the sequence for delta indexes to be
+		// happy.
 	} else {
 	} else {
 		// Here, on the other hand, we want to make sure that any file
 		// Here, on the other hand, we want to make sure that any file
 		// announced from the remote is newer than our current sequence
 		// announced from the remote is newer than our current sequence
 		// number.
 		// number.
-		s.remoteSequence[device] = 0
+		s.meta.resetAll(device)
 	}
 	}
+
+	s.meta.toDB(s.db, []byte(s.folder))
 }
 }
 
 
 func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
 func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
@@ -181,12 +129,6 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
 	s.updateMutex.Lock()
 	s.updateMutex.Lock()
 	defer s.updateMutex.Unlock()
 	defer s.updateMutex.Unlock()
 
 
-	s.updateLocked(device, fs)
-}
-
-func (s *FileSet) updateLocked(device protocol.DeviceID, fs []protocol.FileInfo) {
-	// names must be normalized and the lock held
-
 	if device == protocol.LocalDeviceID {
 	if device == protocol.LocalDeviceID {
 		discards := make([]protocol.FileInfo, 0, len(fs))
 		discards := make([]protocol.FileInfo, 0, len(fs))
 		updates := make([]protocol.FileInfo, 0, len(fs))
 		updates := make([]protocol.FileInfo, 0, len(fs))
@@ -200,7 +142,7 @@ func (s *FileSet) updateLocked(device protocol.DeviceID, fs []protocol.FileInfo)
 				continue
 				continue
 			}
 			}
 
 
-			nf.Sequence = atomic.AddInt64(&s.sequence, 1)
+			nf.Sequence = s.meta.nextSeq(protocol.LocalDeviceID)
 			fs = append(fs, nf)
 			fs = append(fs, nf)
 
 
 			if ok {
 			if ok {
@@ -210,10 +152,10 @@ func (s *FileSet) updateLocked(device protocol.DeviceID, fs []protocol.FileInfo)
 		}
 		}
 		s.blockmap.Discard(discards)
 		s.blockmap.Discard(discards)
 		s.blockmap.Update(updates)
 		s.blockmap.Update(updates)
-	} else {
-		s.remoteSequence[device] = maxSequence(fs)
 	}
 	}
-	s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
+
+	s.db.updateFiles([]byte(s.folder), device[:], fs, s.meta)
+	s.meta.toDB(s.db, []byte(s.folder))
 }
 }
 
 
 func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
 func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
@@ -298,21 +240,15 @@ func (s *FileSet) Availability(file string) []protocol.DeviceID {
 }
 }
 
 
 func (s *FileSet) Sequence(device protocol.DeviceID) int64 {
 func (s *FileSet) Sequence(device protocol.DeviceID) int64 {
-	if device == protocol.LocalDeviceID {
-		return atomic.LoadInt64(&s.sequence)
-	}
-
-	s.updateMutex.Lock()
-	defer s.updateMutex.Unlock()
-	return s.remoteSequence[device]
+	return s.meta.Counts(device).Sequence
 }
 }
 
 
 func (s *FileSet) LocalSize() Counts {
 func (s *FileSet) LocalSize() Counts {
-	return s.localSize.Size()
+	return s.meta.Counts(protocol.LocalDeviceID)
 }
 }
 
 
 func (s *FileSet) GlobalSize() Counts {
 func (s *FileSet) GlobalSize() Counts {
-	return s.globalSize.Size()
+	return s.meta.Counts(globalDeviceID)
 }
 }
 
 
 func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
 func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
@@ -339,29 +275,7 @@ func (s *FileSet) MtimeFS() *fs.MtimeFS {
 }
 }
 
 
 func (s *FileSet) ListDevices() []protocol.DeviceID {
 func (s *FileSet) ListDevices() []protocol.DeviceID {
-	s.updateMutex.Lock()
-	devices := make([]protocol.DeviceID, 0, len(s.remoteSequence))
-	for id, seq := range s.remoteSequence {
-		if seq > 0 {
-			devices = append(devices, id)
-		}
-	}
-	s.updateMutex.Unlock()
-	return devices
-}
-
-// maxSequence returns the highest of the Sequence numbers found in
-// the given slice of FileInfos. This should really be the Sequence of
-// the last item, but Syncthing v0.14.0 and other implementations may not
-// implement update sorting....
-func maxSequence(fs []protocol.FileInfo) int64 {
-	var max int64
-	for _, f := range fs {
-		if f.Sequence > max {
-			max = f.Sequence
-		}
-	}
-	return max
+	return s.meta.devices()
 }
 }
 
 
 // DropFolder clears out all information related to the given folder from the
 // DropFolder clears out all information related to the given folder from the
@@ -369,6 +283,7 @@ func maxSequence(fs []protocol.FileInfo) int64 {
 func DropFolder(db *Instance, folder string) {
 func DropFolder(db *Instance, folder string) {
 	db.dropFolder([]byte(folder))
 	db.dropFolder([]byte(folder))
 	db.dropMtimes([]byte(folder))
 	db.dropMtimes([]byte(folder))
+	db.dropFolderMeta([]byte(folder))
 	bm := &BlockMap{
 	bm := &BlockMap{
 		db:     db,
 		db:     db,
 		folder: db.folderIdx.ID([]byte(folder)),
 		folder: db.folderIdx.ID([]byte(folder)),

+ 2 - 2
lib/db/set_test.go

@@ -169,7 +169,7 @@ func TestGlobalSet(t *testing.T) {
 		t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
 		t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
 	}
 	}
 
 
-	globalFiles, globalDirectories, globalDeleted, globalBytes := 0, 0, 0, int64(0)
+	globalFiles, globalDirectories, globalDeleted, globalBytes := int32(0), int32(0), int32(0), int64(0)
 	for _, f := range g {
 	for _, f := range g {
 		if f.IsInvalid() {
 		if f.IsInvalid() {
 			continue
 			continue
@@ -205,7 +205,7 @@ func TestGlobalSet(t *testing.T) {
 		t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, localTot)
 		t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, localTot)
 	}
 	}
 
 
-	haveFiles, haveDirectories, haveDeleted, haveBytes := 0, 0, 0, int64(0)
+	haveFiles, haveDirectories, haveDeleted, haveBytes := int32(0), int32(0), int32(0), int64(0)
 	for _, f := range h {
 	for _, f := range h {
 		if f.IsInvalid() {
 		if f.IsInvalid() {
 			continue
 			continue

+ 4 - 0
lib/db/structs.go

@@ -64,6 +64,10 @@ func (f FileInfoTruncated) ModTime() time.Time {
 	return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
 	return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
 }
 }
 
 
+func (f FileInfoTruncated) SequenceNo() int64 {
+	return f.Sequence
+}
+
 func (f FileInfoTruncated) ConvertToInvalidFileInfo(invalidatedBy protocol.ShortID) protocol.FileInfo {
 func (f FileInfoTruncated) ConvertToInvalidFileInfo(invalidatedBy protocol.ShortID) protocol.FileInfo {
 	return protocol.FileInfo{
 	return protocol.FileInfo{
 		Name:       f.Name,
 		Name:       f.Name,

+ 499 - 32
lib/db/structs.pb.go

@@ -11,6 +11,8 @@
 		FileVersion
 		FileVersion
 		VersionList
 		VersionList
 		FileInfoTruncated
 		FileInfoTruncated
+		Counts
+		CountsSet
 */
 */
 package db
 package db
 
 
@@ -75,10 +77,39 @@ func (m *FileInfoTruncated) Reset()                    { *m = FileInfoTruncated{
 func (*FileInfoTruncated) ProtoMessage()               {}
 func (*FileInfoTruncated) ProtoMessage()               {}
 func (*FileInfoTruncated) Descriptor() ([]byte, []int) { return fileDescriptorStructs, []int{2} }
 func (*FileInfoTruncated) Descriptor() ([]byte, []int) { return fileDescriptorStructs, []int{2} }
 
 
+// For each folder and device we keep one of these to track the current
+// counts and sequence. We also keep one for the global state of the folder.
+type Counts struct {
+	Files       int32  `protobuf:"varint,1,opt,name=files,proto3" json:"files,omitempty"`
+	Directories int32  `protobuf:"varint,2,opt,name=directories,proto3" json:"directories,omitempty"`
+	Symlinks    int32  `protobuf:"varint,3,opt,name=symlinks,proto3" json:"symlinks,omitempty"`
+	Deleted     int32  `protobuf:"varint,4,opt,name=deleted,proto3" json:"deleted,omitempty"`
+	Bytes       int64  `protobuf:"varint,5,opt,name=bytes,proto3" json:"bytes,omitempty"`
+	Sequence    int64  `protobuf:"varint,6,opt,name=sequence,proto3" json:"sequence,omitempty"`
+	DeviceID    []byte `protobuf:"bytes,17,opt,name=deviceID,proto3" json:"deviceID,omitempty"`
+}
+
+func (m *Counts) Reset()                    { *m = Counts{} }
+func (m *Counts) String() string            { return proto.CompactTextString(m) }
+func (*Counts) ProtoMessage()               {}
+func (*Counts) Descriptor() ([]byte, []int) { return fileDescriptorStructs, []int{3} }
+
+type CountsSet struct {
+	Counts  []Counts `protobuf:"bytes,1,rep,name=counts" json:"counts"`
+	Created int64    `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"`
+}
+
+func (m *CountsSet) Reset()                    { *m = CountsSet{} }
+func (m *CountsSet) String() string            { return proto.CompactTextString(m) }
+func (*CountsSet) ProtoMessage()               {}
+func (*CountsSet) Descriptor() ([]byte, []int) { return fileDescriptorStructs, []int{4} }
+
 func init() {
 func init() {
 	proto.RegisterType((*FileVersion)(nil), "db.FileVersion")
 	proto.RegisterType((*FileVersion)(nil), "db.FileVersion")
 	proto.RegisterType((*VersionList)(nil), "db.VersionList")
 	proto.RegisterType((*VersionList)(nil), "db.VersionList")
 	proto.RegisterType((*FileInfoTruncated)(nil), "db.FileInfoTruncated")
 	proto.RegisterType((*FileInfoTruncated)(nil), "db.FileInfoTruncated")
+	proto.RegisterType((*Counts)(nil), "db.Counts")
+	proto.RegisterType((*CountsSet)(nil), "db.CountsSet")
 }
 }
 func (m *FileVersion) Marshal() (dAtA []byte, err error) {
 func (m *FileVersion) Marshal() (dAtA []byte, err error) {
 	size := m.ProtoSize()
 	size := m.ProtoSize()
@@ -257,6 +288,97 @@ func (m *FileInfoTruncated) MarshalTo(dAtA []byte) (int, error) {
 	return i, nil
 	return i, nil
 }
 }
 
 
+func (m *Counts) Marshal() (dAtA []byte, err error) {
+	size := m.ProtoSize()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalTo(dAtA)
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *Counts) MarshalTo(dAtA []byte) (int, error) {
+	var i int
+	_ = i
+	var l int
+	_ = l
+	if m.Files != 0 {
+		dAtA[i] = 0x8
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Files))
+	}
+	if m.Directories != 0 {
+		dAtA[i] = 0x10
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Directories))
+	}
+	if m.Symlinks != 0 {
+		dAtA[i] = 0x18
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Symlinks))
+	}
+	if m.Deleted != 0 {
+		dAtA[i] = 0x20
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Deleted))
+	}
+	if m.Bytes != 0 {
+		dAtA[i] = 0x28
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Bytes))
+	}
+	if m.Sequence != 0 {
+		dAtA[i] = 0x30
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Sequence))
+	}
+	if len(m.DeviceID) > 0 {
+		dAtA[i] = 0x8a
+		i++
+		dAtA[i] = 0x1
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(len(m.DeviceID)))
+		i += copy(dAtA[i:], m.DeviceID)
+	}
+	return i, nil
+}
+
+func (m *CountsSet) Marshal() (dAtA []byte, err error) {
+	size := m.ProtoSize()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalTo(dAtA)
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *CountsSet) MarshalTo(dAtA []byte) (int, error) {
+	var i int
+	_ = i
+	var l int
+	_ = l
+	if len(m.Counts) > 0 {
+		for _, msg := range m.Counts {
+			dAtA[i] = 0xa
+			i++
+			i = encodeVarintStructs(dAtA, i, uint64(msg.ProtoSize()))
+			n, err := msg.MarshalTo(dAtA[i:])
+			if err != nil {
+				return 0, err
+			}
+			i += n
+		}
+	}
+	if m.Created != 0 {
+		dAtA[i] = 0x10
+		i++
+		i = encodeVarintStructs(dAtA, i, uint64(m.Created))
+	}
+	return i, nil
+}
+
 func encodeFixed64Structs(dAtA []byte, offset int, v uint64) int {
 func encodeFixed64Structs(dAtA []byte, offset int, v uint64) int {
 	dAtA[offset] = uint8(v)
 	dAtA[offset] = uint8(v)
 	dAtA[offset+1] = uint8(v >> 8)
 	dAtA[offset+1] = uint8(v >> 8)
@@ -357,6 +479,49 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
 	return n
 	return n
 }
 }
 
 
+func (m *Counts) ProtoSize() (n int) {
+	var l int
+	_ = l
+	if m.Files != 0 {
+		n += 1 + sovStructs(uint64(m.Files))
+	}
+	if m.Directories != 0 {
+		n += 1 + sovStructs(uint64(m.Directories))
+	}
+	if m.Symlinks != 0 {
+		n += 1 + sovStructs(uint64(m.Symlinks))
+	}
+	if m.Deleted != 0 {
+		n += 1 + sovStructs(uint64(m.Deleted))
+	}
+	if m.Bytes != 0 {
+		n += 1 + sovStructs(uint64(m.Bytes))
+	}
+	if m.Sequence != 0 {
+		n += 1 + sovStructs(uint64(m.Sequence))
+	}
+	l = len(m.DeviceID)
+	if l > 0 {
+		n += 2 + l + sovStructs(uint64(l))
+	}
+	return n
+}
+
+func (m *CountsSet) ProtoSize() (n int) {
+	var l int
+	_ = l
+	if len(m.Counts) > 0 {
+		for _, e := range m.Counts {
+			l = e.ProtoSize()
+			n += 1 + l + sovStructs(uint64(l))
+		}
+	}
+	if m.Created != 0 {
+		n += 1 + sovStructs(uint64(m.Created))
+	}
+	return n
+}
+
 func sovStructs(x uint64) (n int) {
 func sovStructs(x uint64) (n int) {
 	for {
 	for {
 		n++
 		n++
@@ -913,6 +1078,301 @@ func (m *FileInfoTruncated) Unmarshal(dAtA []byte) error {
 	}
 	}
 	return nil
 	return nil
 }
 }
+func (m *Counts) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowStructs
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: Counts: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: Counts: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Files", wireType)
+			}
+			m.Files = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Files |= (int32(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Directories", wireType)
+			}
+			m.Directories = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Directories |= (int32(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 3:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Symlinks", wireType)
+			}
+			m.Symlinks = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Symlinks |= (int32(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 4:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Deleted", wireType)
+			}
+			m.Deleted = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Deleted |= (int32(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 5:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Bytes", wireType)
+			}
+			m.Bytes = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Bytes |= (int64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 6:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Sequence", wireType)
+			}
+			m.Sequence = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Sequence |= (int64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 17:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field DeviceID", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.DeviceID = append(m.DeviceID[:0], dAtA[iNdEx:postIndex]...)
+			if m.DeviceID == nil {
+				m.DeviceID = []byte{}
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipStructs(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *CountsSet) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowStructs
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: CountsSet: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: CountsSet: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Counts", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + msglen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Counts = append(m.Counts, Counts{})
+			if err := m.Counts[len(m.Counts)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Created", wireType)
+			}
+			m.Created = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Created |= (int64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		default:
+			iNdEx = preIndex
+			skippy, err := skipStructs(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func skipStructs(dAtA []byte) (n int, err error) {
 func skipStructs(dAtA []byte) (n int, err error) {
 	l := len(dAtA)
 	l := len(dAtA)
 	iNdEx := 0
 	iNdEx := 0
@@ -1021,36 +1481,43 @@ var (
 func init() { proto.RegisterFile("structs.proto", fileDescriptorStructs) }
 func init() { proto.RegisterFile("structs.proto", fileDescriptorStructs) }
 
 
 var fileDescriptorStructs = []byte{
 var fileDescriptorStructs = []byte{
-	// 487 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0xc1, 0x6a, 0xdb, 0x40,
-	0x10, 0xf5, 0xc6, 0x4a, 0x6c, 0xaf, 0xe2, 0xb4, 0x59, 0x4a, 0x58, 0x0c, 0x95, 0x85, 0xa1, 0x20,
-	0x0a, 0x95, 0x5b, 0x87, 0x5e, 0xda, 0x9b, 0x29, 0x81, 0x40, 0x29, 0x45, 0x09, 0x39, 0x15, 0x8c,
-	0x25, 0x8d, 0xe5, 0xa5, 0xd2, 0xae, 0xa2, 0x5d, 0x19, 0xd4, 0x2f, 0xe9, 0x31, 0x9f, 0xe3, 0x63,
-	0xcf, 0x3d, 0x84, 0xd6, 0xfd, 0x8e, 0x42, 0xd1, 0x4a, 0x56, 0xd4, 0x5b, 0x7b, 0x9b, 0x37, 0x7a,
-	0x6f, 0xdf, 0x9b, 0x19, 0xe1, 0xa1, 0x54, 0x59, 0x1e, 0x28, 0xe9, 0xa6, 0x99, 0x50, 0x82, 0x1c,
-	0x84, 0xfe, 0xe8, 0x45, 0xc4, 0xd4, 0x3a, 0xf7, 0xdd, 0x40, 0x24, 0xd3, 0x48, 0x44, 0x62, 0xaa,
-	0x3f, 0xf9, 0xf9, 0x4a, 0x23, 0x0d, 0x74, 0x55, 0x49, 0x46, 0xaf, 0x5b, 0x74, 0x59, 0xf0, 0x40,
-	0xad, 0x19, 0x8f, 0x5a, 0x55, 0xcc, 0xfc, 0xea, 0x85, 0x40, 0xc4, 0x53, 0x1f, 0xd2, 0x4a, 0x36,
-	0xb9, 0xc5, 0xe6, 0x05, 0x8b, 0xe1, 0x06, 0x32, 0xc9, 0x04, 0x27, 0x2f, 0x71, 0x6f, 0x53, 0x95,
-	0x14, 0xd9, 0xc8, 0x31, 0x67, 0x8f, 0xdd, 0xbd, 0xc8, 0xbd, 0x81, 0x40, 0x89, 0x6c, 0x6e, 0x6c,
-	0xef, 0xc7, 0x1d, 0x6f, 0x4f, 0x23, 0x67, 0xf8, 0x28, 0x84, 0x0d, 0x0b, 0x80, 0x1e, 0xd8, 0xc8,
-	0x39, 0xf6, 0x6a, 0x44, 0x28, 0xee, 0x31, 0xbe, 0x59, 0xc6, 0x2c, 0xa4, 0x5d, 0x1b, 0x39, 0x7d,
-	0x6f, 0x0f, 0x27, 0x17, 0xd8, 0xac, 0xed, 0xde, 0x33, 0xa9, 0xc8, 0x2b, 0xdc, 0xaf, 0xdf, 0x92,
-	0x14, 0xd9, 0x5d, 0xc7, 0x9c, 0x3d, 0x72, 0x43, 0xdf, 0x6d, 0xa5, 0xaa, 0x2d, 0x1b, 0xda, 0x1b,
-	0xe3, 0xeb, 0xdd, 0xb8, 0x33, 0xf9, 0xdd, 0xc5, 0xa7, 0x25, 0xeb, 0x92, 0xaf, 0xc4, 0x75, 0x96,
-	0xf3, 0x60, 0xa9, 0x20, 0x24, 0x04, 0x1b, 0x7c, 0x99, 0x80, 0x8e, 0x3f, 0xf0, 0x74, 0x4d, 0x9e,
-	0x63, 0x43, 0x15, 0x69, 0x95, 0xf0, 0x64, 0x76, 0xf6, 0x30, 0x52, 0x23, 0x2f, 0x52, 0xf0, 0x34,
-	0xa7, 0xd4, 0x4b, 0xf6, 0x05, 0x74, 0xe8, 0xae, 0xa7, 0x6b, 0x62, 0x63, 0x33, 0x85, 0x2c, 0x61,
-	0xb2, 0x4a, 0x69, 0xd8, 0xc8, 0x19, 0x7a, 0xed, 0x16, 0x79, 0x8a, 0x71, 0x22, 0x42, 0xb6, 0x62,
-	0x10, 0x2e, 0x24, 0x3d, 0xd4, 0xda, 0xc1, 0xbe, 0x73, 0x55, 0x2e, 0x23, 0x84, 0x18, 0x14, 0x84,
-	0xf4, 0xa8, 0x5a, 0x46, 0x0d, 0xdb, 0x6b, 0xea, 0xfd, 0xb5, 0x26, 0xf2, 0x0c, 0x9f, 0x70, 0xb1,
-	0x68, 0xfb, 0xf6, 0x35, 0x61, 0xc8, 0xc5, 0xc7, 0x96, 0x73, 0xeb, 0x62, 0x83, 0x7f, 0xbb, 0xd8,
-	0x08, 0xf7, 0x25, 0xdc, 0xe6, 0xc0, 0x03, 0xa0, 0x58, 0x27, 0x6d, 0x30, 0x19, 0x63, 0xb3, 0x99,
-	0x83, 0x4b, 0x6a, 0xda, 0xc8, 0x39, 0xf4, 0x9a, 0xd1, 0x3e, 0x48, 0xf2, 0xa9, 0x45, 0xf0, 0x0b,
-	0x7a, 0x6c, 0x23, 0xc7, 0x98, 0xbf, 0x2d, 0x0d, 0xbe, 0xdf, 0x8f, 0xcf, 0xff, 0xe3, 0x1f, 0x74,
-	0xaf, 0xd6, 0x22, 0x53, 0x97, 0xef, 0x1e, 0x5e, 0x9f, 0x17, 0xe5, 0xcc, 0xb2, 0x48, 0x62, 0xc6,
-	0x3f, 0x2f, 0xd4, 0x32, 0x8b, 0x40, 0xd1, 0x53, 0x7d, 0xc6, 0x61, 0xdd, 0xbd, 0xd6, 0xcd, 0xea,
-	0xfe, 0xf3, 0x27, 0xdb, 0x9f, 0x56, 0x67, 0xbb, 0xb3, 0xd0, 0xb7, 0x9d, 0x85, 0x7e, 0xec, 0xac,
-	0xce, 0xdd, 0x2f, 0x0b, 0xf9, 0x47, 0xda, 0xe0, 0xfc, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfe,
-	0xcd, 0x11, 0xef, 0x52, 0x03, 0x00, 0x00,
+	// 598 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xcd, 0x6a, 0xdb, 0x4c,
+	0x14, 0xb5, 0x62, 0xc9, 0xb1, 0x47, 0x71, 0xbe, 0x2f, 0x43, 0x08, 0xc2, 0x50, 0x5b, 0x18, 0x0a,
+	0xa2, 0x50, 0xa5, 0x4d, 0xe8, 0xa6, 0xdd, 0xb9, 0x21, 0x10, 0x28, 0x6d, 0x99, 0x84, 0xac, 0x0a,
+	0xc1, 0x92, 0xae, 0x9d, 0xa1, 0xf2, 0x8c, 0xa3, 0x19, 0x07, 0xd4, 0x27, 0xe9, 0x32, 0x0f, 0xd3,
+	0x45, 0x96, 0x5d, 0x77, 0x11, 0x5a, 0xf7, 0x39, 0x0a, 0x45, 0x77, 0x24, 0x45, 0xe9, 0xaa, 0xdd,
+	0xdd, 0x73, 0xff, 0xef, 0x39, 0x33, 0xa4, 0xaf, 0x74, 0xb6, 0x8a, 0xb5, 0x0a, 0x97, 0x99, 0xd4,
+	0x92, 0x6e, 0x24, 0xd1, 0xe0, 0xe9, 0x9c, 0xeb, 0xcb, 0x55, 0x14, 0xc6, 0x72, 0xb1, 0x3f, 0x97,
+	0x73, 0xb9, 0x8f, 0xa1, 0x68, 0x35, 0x43, 0x84, 0x00, 0x2d, 0x53, 0x32, 0x78, 0xd1, 0x48, 0x57,
+	0xb9, 0x88, 0xf5, 0x25, 0x17, 0xf3, 0x86, 0x95, 0xf2, 0xc8, 0x74, 0x88, 0x65, 0xba, 0x1f, 0xc1,
+	0xd2, 0x94, 0x8d, 0xaf, 0x88, 0x7b, 0xcc, 0x53, 0x38, 0x87, 0x4c, 0x71, 0x29, 0xe8, 0x33, 0xb2,
+	0x79, 0x6d, 0x4c, 0xcf, 0xf2, 0xad, 0xc0, 0x3d, 0xf8, 0x3f, 0xac, 0x8a, 0xc2, 0x73, 0x88, 0xb5,
+	0xcc, 0x26, 0xf6, 0xed, 0xdd, 0xa8, 0xc5, 0xaa, 0x34, 0xba, 0x47, 0x3a, 0x09, 0x5c, 0xf3, 0x18,
+	0xbc, 0x0d, 0xdf, 0x0a, 0xb6, 0x58, 0x89, 0xa8, 0x47, 0x36, 0xb9, 0xb8, 0x9e, 0xa6, 0x3c, 0xf1,
+	0xda, 0xbe, 0x15, 0x74, 0x59, 0x05, 0xc7, 0xc7, 0xc4, 0x2d, 0xc7, 0xbd, 0xe1, 0x4a, 0xd3, 0xe7,
+	0xa4, 0x5b, 0xf6, 0x52, 0x9e, 0xe5, 0xb7, 0x03, 0xf7, 0xe0, 0xbf, 0x30, 0x89, 0xc2, 0xc6, 0x56,
+	0xe5, 0xc8, 0x3a, 0xed, 0xa5, 0xfd, 0xf9, 0x66, 0xd4, 0x1a, 0xff, 0x6a, 0x93, 0x9d, 0x22, 0xeb,
+	0x44, 0xcc, 0xe4, 0x59, 0xb6, 0x12, 0xf1, 0x54, 0x43, 0x42, 0x29, 0xb1, 0xc5, 0x74, 0x01, 0xb8,
+	0x7e, 0x8f, 0xa1, 0x4d, 0x9f, 0x10, 0x5b, 0xe7, 0x4b, 0xb3, 0xe1, 0xf6, 0xc1, 0xde, 0xfd, 0x49,
+	0x75, 0x79, 0xbe, 0x04, 0x86, 0x39, 0x45, 0xbd, 0xe2, 0x9f, 0x00, 0x97, 0x6e, 0x33, 0xb4, 0xa9,
+	0x4f, 0xdc, 0x25, 0x64, 0x0b, 0xae, 0xcc, 0x96, 0xb6, 0x6f, 0x05, 0x7d, 0xd6, 0x74, 0xd1, 0x47,
+	0x84, 0x2c, 0x64, 0xc2, 0x67, 0x1c, 0x92, 0x0b, 0xe5, 0x39, 0x58, 0xdb, 0xab, 0x3c, 0xa7, 0x05,
+	0x19, 0x09, 0xa4, 0xa0, 0x21, 0xf1, 0x3a, 0x86, 0x8c, 0x12, 0x36, 0x69, 0xda, 0x7c, 0x40, 0x13,
+	0x7d, 0x4c, 0xb6, 0x85, 0xbc, 0x68, 0xce, 0xed, 0x62, 0x42, 0x5f, 0xc8, 0xf7, 0x8d, 0xc9, 0x0d,
+	0xc5, 0x7a, 0x7f, 0xa7, 0xd8, 0x80, 0x74, 0x15, 0x5c, 0xad, 0x40, 0xc4, 0xe0, 0x11, 0xdc, 0xb4,
+	0xc6, 0x74, 0x44, 0xdc, 0xfa, 0x0e, 0xa1, 0x3c, 0xd7, 0xb7, 0x02, 0x87, 0xd5, 0xa7, 0xbd, 0x55,
+	0xf4, 0x43, 0x23, 0x21, 0xca, 0xbd, 0x2d, 0xdf, 0x0a, 0xec, 0xc9, 0xab, 0x62, 0xc0, 0xb7, 0xbb,
+	0xd1, 0xe1, 0x3f, 0xbc, 0xc1, 0xf0, 0xf4, 0x52, 0x66, 0xfa, 0xe4, 0xe8, 0xbe, 0xfb, 0x24, 0x2f,
+	0x6e, 0x56, 0xf9, 0x22, 0xe5, 0xe2, 0xe3, 0x85, 0x9e, 0x66, 0x73, 0xd0, 0xde, 0x0e, 0xca, 0xd8,
+	0x2f, 0xbd, 0x67, 0xe8, 0x2c, 0xf5, 0xff, 0x62, 0x91, 0xce, 0x6b, 0xb9, 0x12, 0x5a, 0xd1, 0x5d,
+	0xe2, 0xcc, 0x78, 0x0a, 0x0a, 0x55, 0x77, 0x98, 0x01, 0x85, 0x6c, 0x09, 0xcf, 0x90, 0x03, 0x0e,
+	0x0a, 0xd5, 0x77, 0x58, 0xd3, 0x85, 0x54, 0x98, 0xce, 0x0a, 0x05, 0x77, 0x58, 0x8d, 0x9b, 0x9a,
+	0xd9, 0x18, 0xaa, 0x35, 0xdb, 0x25, 0x4e, 0x94, 0x6b, 0xa8, 0x74, 0x36, 0xe0, 0x01, 0xad, 0x9d,
+	0x3f, 0x68, 0x1d, 0x90, 0xae, 0xf9, 0x16, 0x27, 0x47, 0x78, 0xd1, 0x16, 0xab, 0xf1, 0xf8, 0x1d,
+	0xe9, 0x99, 0x2b, 0x4e, 0x41, 0xd3, 0x80, 0x74, 0x62, 0x04, 0xe5, 0x57, 0x20, 0xc5, 0x57, 0x30,
+	0xe1, 0x52, 0xc6, 0x32, 0x5e, 0xac, 0x17, 0x67, 0x50, 0x3c, 0x79, 0x3c, 0xac, 0xcd, 0x2a, 0x38,
+	0xd9, 0xbd, 0xfd, 0x31, 0x6c, 0xdd, 0xae, 0x87, 0xd6, 0xd7, 0xf5, 0xd0, 0xfa, 0xbe, 0x1e, 0xb6,
+	0x6e, 0x7e, 0x0e, 0xad, 0xa8, 0x83, 0xc4, 0x1f, 0xfe, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x6e, 0x86,
+	0x97, 0x21, 0x6a, 0x04, 0x00, 0x00,
 }
 }

+ 17 - 0
lib/db/structs.proto

@@ -37,3 +37,20 @@ message FileInfoTruncated {
     int64                 sequence       = 10;
     int64                 sequence       = 10;
     string                symlink_target = 17;
     string                symlink_target = 17;
 }
 }
+
+// For each folder and device we keep one of these to track the current
+// counts and sequence. We also keep one for the global state of the folder.
+message Counts {
+    int32 files       = 1;
+    int32 directories = 2;
+    int32 symlinks    = 3;
+    int32 deleted     = 4;
+    int64 bytes       = 5;
+    int64 sequence    = 6; // zero for the global state
+    bytes deviceID    = 17; // device ID for remote devices, or special values for local/global
+}
+
+message CountsSet {
+    repeated Counts counts  = 1  [(gogoproto.nullable) = false];
+    int64           created = 2; // unix nanos
+}

+ 4 - 3
lib/model/model_test.go

@@ -179,6 +179,7 @@ func genFiles(n int) []protocol.FileInfo {
 			ModifiedS: t,
 			ModifiedS: t,
 			Sequence:  int64(i + 1),
 			Sequence:  int64(i + 1),
 			Blocks:    []protocol.BlockInfo{{Offset: 0, Size: 100, Hash: []byte("some hash bytes")}},
 			Blocks:    []protocol.BlockInfo{{Offset: 0, Size: 100, Hash: []byte("some hash bytes")}},
+			Version:   protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}},
 		}
 		}
 	}
 	}
 
 
@@ -1345,7 +1346,7 @@ func TestROScanRecovery(t *testing.T) {
 	ldb := db.OpenMemory()
 	ldb := db.OpenMemory()
 	set := db.NewFileSet("default", defaultFs, ldb)
 	set := db.NewFileSet("default", defaultFs, ldb)
 	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
 	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-		{Name: "dummyfile"},
+		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
 	})
 	})
 
 
 	fcfg := config.FolderConfiguration{
 	fcfg := config.FolderConfiguration{
@@ -1433,7 +1434,7 @@ func TestRWScanRecovery(t *testing.T) {
 	ldb := db.OpenMemory()
 	ldb := db.OpenMemory()
 	set := db.NewFileSet("default", defaultFs, ldb)
 	set := db.NewFileSet("default", defaultFs, ldb)
 	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
 	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-		{Name: "dummyfile"},
+		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
 	})
 	})
 
 
 	fcfg := config.FolderConfiguration{
 	fcfg := config.FolderConfiguration{
@@ -2530,7 +2531,7 @@ func TestIssue3496(t *testing.T) {
 		// The one we added synthetically above
 		// The one we added synthetically above
 		t.Errorf("Incorrect need size; %d, %d != 1, 1234", need.Files, need.Bytes)
 		t.Errorf("Incorrect need size; %d, %d != 1, 1234", need.Files, need.Bytes)
 	}
 	}
-	if need.Deleted != len(localFiles)-1 {
+	if int(need.Deleted) != len(localFiles)-1 {
 		// The rest
 		// The rest
 		t.Errorf("Incorrect need deletes; %d != %d", need.Deleted, len(localFiles)-1)
 		t.Errorf("Incorrect need deletes; %d != %d", need.Deleted, len(localFiles)-1)
 	}
 	}

+ 57 - 57
lib/protocol/bep.pb.go

@@ -306,7 +306,7 @@ type FileInfo struct {
 	NoPermissions bool         `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
 	NoPermissions bool         `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
 	Version       Vector       `protobuf:"bytes,9,opt,name=version" json:"version"`
 	Version       Vector       `protobuf:"bytes,9,opt,name=version" json:"version"`
 	Sequence      int64        `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
 	Sequence      int64        `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
-	Blocks        []BlockInfo  `protobuf:"bytes,16,rep,name=Blocks,json=blocks" json:"Blocks"`
+	Blocks        []BlockInfo  `protobuf:"bytes,16,rep,name=Blocks" json:"Blocks"`
 	SymlinkTarget string       `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
 	SymlinkTarget string       `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
 }
 }
 
 
@@ -4217,61 +4217,61 @@ var fileDescriptorBep = []byte{
 	0xf3, 0xb0, 0x17, 0x27, 0x71, 0xb8, 0xbe, 0x5c, 0x51, 0x85, 0xa3, 0x3c, 0x4e, 0x42, 0xd3, 0xc3,
 	0xf3, 0xb0, 0x17, 0x27, 0x71, 0xb8, 0xbe, 0x5c, 0x51, 0x85, 0xa3, 0x3c, 0x4e, 0x42, 0xd3, 0xc3,
 	0xe3, 0x36, 0x80, 0xd1, 0x52, 0xae, 0xb2, 0x5a, 0x7c, 0x2f, 0xae, 0xc5, 0xc1, 0x05, 0xf5, 0x82,
 	0xe3, 0x36, 0x80, 0xd1, 0x52, 0xae, 0xb2, 0x5a, 0x7c, 0x2f, 0xae, 0xc5, 0xc1, 0x05, 0xf5, 0x82,
 	0x4e, 0x7b, 0xb3, 0xa2, 0xb9, 0x44, 0x3f, 0x82, 0x42, 0xd3, 0xa1, 0xe3, 0x17, 0xb1, 0xd2, 0xef,
 	0x4e, 0x7b, 0xb3, 0xa2, 0xb9, 0x44, 0x3f, 0x82, 0x42, 0xd3, 0xa1, 0xe3, 0x17, 0xb1, 0xd2, 0xef,
-	0x6c, 0xee, 0xc7, 0xec, 0xa9, 0x7c, 0x16, 0x46, 0x0c, 0x18, 0x86, 0xee, 0x2f, 0x67, 0x8e, 0xed,
-	0xbe, 0x30, 0x02, 0xd3, 0x9b, 0x92, 0x40, 0xde, 0xe3, 0x0d, 0x3f, 0xb2, 0x0e, 0x99, 0xf1, 0xe7,
-	0xe2, 0x1f, 0xbf, 0x39, 0xcc, 0xd4, 0x5d, 0x28, 0x27, 0xfb, 0x84, 0x25, 0x45, 0x27, 0x13, 0x9f,
-	0x04, 0x2c, 0xff, 0x39, 0x1c, 0xcd, 0x92, 0xac, 0x66, 0x59, 0x40, 0x3c, 0xab, 0x08, 0xc4, 0x0b,
-	0xd3, 0xbf, 0x60, 0x99, 0xae, 0x62, 0x36, 0x0e, 0x75, 0xfc, 0x92, 0x98, 0x2f, 0x0c, 0xe6, 0xe0,
-	0x79, 0x2e, 0x85, 0x86, 0x27, 0xa6, 0x7f, 0x11, 0x9d, 0xf7, 0x4b, 0x28, 0x70, 0x5e, 0xd1, 0x17,
-	0x50, 0x1a, 0xd3, 0x85, 0x1b, 0x6c, 0x7a, 0xfd, 0x5e, 0xba, 0x55, 0x30, 0x4f, 0x14, 0x59, 0x02,
-	0xac, 0x9f, 0x42, 0x31, 0x72, 0xa1, 0x87, 0x49, 0x1f, 0x13, 0x9b, 0xf7, 0xae, 0x51, 0xb8, 0xdd,
-	0xfc, 0x2f, 0x4d, 0x67, 0xc1, 0x2f, 0x2f, 0x62, 0x3e, 0xa9, 0xff, 0x45, 0x80, 0x22, 0x0e, 0xd3,
-	0xe6, 0x07, 0xa9, 0x67, 0x23, 0xbf, 0xf5, 0x6c, 0x6c, 0x04, 0x96, 0xdd, 0x12, 0x58, 0xac, 0x91,
-	0x5c, 0x4a, 0x23, 0x1b, 0xe6, 0xc4, 0xb7, 0x32, 0x97, 0x7f, 0x0b, 0x73, 0x85, 0x14, 0x73, 0x0f,
-	0x61, 0x77, 0xe2, 0xd1, 0x19, 0x7b, 0x18, 0xa8, 0x67, 0x7a, 0xcb, 0xa8, 0x9e, 0x77, 0x42, 0xeb,
-	0x30, 0x36, 0xd6, 0x0d, 0x28, 0x61, 0xe2, 0xcf, 0xa9, 0xeb, 0x93, 0x5b, 0xaf, 0x8d, 0x40, 0xb4,
-	0xcc, 0xc0, 0x64, 0x97, 0xae, 0x62, 0x36, 0x46, 0x8f, 0x40, 0x1c, 0x53, 0x8b, 0x5f, 0x79, 0x37,
-	0x5d, 0x43, 0x9a, 0xe7, 0x51, 0xaf, 0x45, 0x2d, 0x82, 0x19, 0xa0, 0x3e, 0x07, 0xa9, 0x4d, 0x5f,
-	0xba, 0x0e, 0x35, 0xad, 0xbe, 0x47, 0xa7, 0x61, 0x83, 0xbe, 0xb5, 0xd1, 0xb4, 0xa1, 0xb8, 0x60,
-	0xad, 0x28, 0x6e, 0x35, 0x0f, 0xb6, 0x5b, 0xc3, 0xf5, 0x8d, 0x78, 0xdf, 0x8a, 0xf5, 0x14, 0x2d,
-	0xad, 0xff, 0x4d, 0x00, 0xe5, 0x76, 0x34, 0xea, 0x40, 0x85, 0x23, 0x8d, 0xd4, 0x3f, 0xc9, 0xd1,
-	0xbb, 0x1c, 0xc4, 0xba, 0x12, 0x2c, 0x92, 0xf1, 0x5b, 0x1f, 0xb4, 0x94, 0xfe, 0x73, 0xef, 0xa6,
-	0xff, 0x47, 0xb0, 0xc3, 0x74, 0x96, 0x3c, 0xdf, 0xa2, 0x9a, 0x3b, 0xca, 0x37, 0xb3, 0x52, 0x06,
-	0x57, 0x47, 0x5c, 0x49, 0xcc, 0x5e, 0x2f, 0x80, 0xd8, 0xb7, 0xdd, 0x69, 0xfd, 0x10, 0xf2, 0x2d,
-	0x87, 0xb2, 0x84, 0x15, 0x3c, 0x62, 0xfa, 0xd4, 0x8d, 0x79, 0xe4, 0xb3, 0xe3, 0xbf, 0x66, 0xa1,
-	0x92, 0xfa, 0xb5, 0x42, 0x8f, 0x61, 0xb7, 0xd5, 0x3d, 0x1f, 0x0c, 0x35, 0x6c, 0xb4, 0x7a, 0xfa,
-	0x69, 0xe7, 0x4c, 0xca, 0x28, 0x07, 0xab, 0xb5, 0x2a, 0xcf, 0x36, 0xa0, 0xed, 0xbf, 0xa6, 0x43,
-	0xc8, 0x77, 0xf4, 0xb6, 0xf6, 0x5b, 0x49, 0x50, 0xee, 0xae, 0xd6, 0xaa, 0x94, 0x02, 0xf2, 0x27,
-	0xe8, 0x13, 0xa8, 0x32, 0x80, 0x71, 0xde, 0x6f, 0x37, 0x86, 0x9a, 0x94, 0x55, 0x94, 0xd5, 0x5a,
-	0xdd, 0xbf, 0x8e, 0x8b, 0x38, 0xff, 0x10, 0x8a, 0x58, 0xfb, 0xcd, 0xb9, 0x36, 0x18, 0x4a, 0x39,
-	0x65, 0x7f, 0xb5, 0x56, 0x51, 0x0a, 0x18, 0xab, 0xe6, 0x21, 0x94, 0xb0, 0x36, 0xe8, 0xf7, 0xf4,
-	0x81, 0x26, 0x89, 0xca, 0x0f, 0x56, 0x6b, 0xf5, 0xce, 0x16, 0x2a, 0xaa, 0xd2, 0x9f, 0xc0, 0x5e,
-	0xbb, 0xf7, 0x95, 0xde, 0xed, 0x35, 0xda, 0x46, 0x1f, 0xf7, 0xce, 0xb0, 0x36, 0x18, 0x48, 0x79,
-	0xe5, 0x70, 0xb5, 0x56, 0xdf, 0x4f, 0xe1, 0x6f, 0x14, 0xdd, 0x07, 0x20, 0xf6, 0x3b, 0xfa, 0x99,
-	0x54, 0x50, 0xee, 0xac, 0xd6, 0xea, 0x7b, 0x29, 0x68, 0x48, 0x6a, 0x18, 0x71, 0xab, 0xdb, 0x1b,
-	0x68, 0x52, 0xf1, 0x46, 0xc4, 0x8c, 0xec, 0xe3, 0xdf, 0x01, 0xba, 0xf9, 0xf3, 0x89, 0x1e, 0x80,
-	0xa8, 0xf7, 0x74, 0x4d, 0xca, 0xf0, 0xf8, 0x6f, 0x22, 0x74, 0xea, 0x12, 0x54, 0x87, 0x5c, 0xf7,
-	0xeb, 0x2f, 0x25, 0x41, 0xf9, 0xe1, 0x6a, 0xad, 0xde, 0xbb, 0x09, 0xea, 0x7e, 0xfd, 0xe5, 0x31,
-	0x85, 0x4a, 0x7a, 0xe3, 0x3a, 0x94, 0x9e, 0x6a, 0xc3, 0x46, 0xbb, 0x31, 0x6c, 0x48, 0x19, 0x7e,
-	0xa5, 0xd8, 0xfd, 0x94, 0x04, 0x26, 0x13, 0xe1, 0x01, 0xe4, 0x75, 0xed, 0x99, 0x86, 0x25, 0x41,
-	0xd9, 0x5b, 0xad, 0xd5, 0x9d, 0x18, 0xa0, 0x93, 0x4b, 0xe2, 0xa1, 0x1a, 0x14, 0x1a, 0xdd, 0xaf,
-	0x1a, 0xcf, 0x07, 0x52, 0x56, 0x41, 0xab, 0xb5, 0xba, 0x1b, 0xbb, 0x1b, 0xce, 0x4b, 0x73, 0xe9,
-	0x1f, 0xff, 0x57, 0x80, 0x6a, 0xfa, 0xc1, 0x45, 0x35, 0x10, 0x4f, 0x3b, 0x5d, 0x2d, 0x3e, 0x2e,
-	0xed, 0x0b, 0xc7, 0xe8, 0x08, 0xca, 0xed, 0x0e, 0xd6, 0x5a, 0xc3, 0x1e, 0x7e, 0x1e, 0xc7, 0x92,
-	0x06, 0xb5, 0x6d, 0x8f, 0x15, 0xf8, 0x12, 0xfd, 0x0c, 0xaa, 0x83, 0xe7, 0x4f, 0xbb, 0x1d, 0xfd,
-	0xd7, 0x06, 0xdb, 0x31, 0xab, 0x3c, 0x5a, 0xad, 0xd5, 0xfb, 0x5b, 0x60, 0x32, 0xf7, 0xc8, 0xd8,
-	0x0c, 0x88, 0x35, 0xe0, 0x8f, 0x48, 0xe8, 0x2c, 0x09, 0xa8, 0x05, 0x7b, 0xf1, 0xd2, 0xcd, 0x61,
-	0x39, 0xe5, 0x93, 0xd5, 0x5a, 0xfd, 0xe8, 0x7b, 0xd7, 0x27, 0xa7, 0x97, 0x04, 0xf4, 0x00, 0x8a,
-	0xd1, 0x26, 0x71, 0x25, 0xa5, 0x97, 0x46, 0x0b, 0x8e, 0xff, 0x2c, 0x40, 0x39, 0x69, 0x57, 0x21,
-	0xe1, 0x7a, 0xcf, 0xd0, 0x30, 0xee, 0xe1, 0x98, 0x81, 0xc4, 0xa9, 0x53, 0x36, 0x44, 0xf7, 0xa1,
-	0x78, 0xa6, 0xe9, 0x1a, 0xee, 0xb4, 0x62, 0x61, 0x24, 0x90, 0x33, 0xe2, 0x12, 0xcf, 0x1e, 0xa3,
-	0x8f, 0xa1, 0xaa, 0xf7, 0x8c, 0xc1, 0x79, 0xeb, 0x49, 0x1c, 0x3a, 0x3b, 0x3f, 0xb5, 0xd5, 0x60,
-	0x31, 0xbe, 0x60, 0x7c, 0x1e, 0x87, 0x1a, 0x7a, 0xd6, 0xe8, 0x76, 0xda, 0x1c, 0x9a, 0x53, 0xe4,
-	0xd5, 0x5a, 0xbd, 0x9b, 0x40, 0x3b, 0xfc, 0xcf, 0x23, 0xc4, 0x1e, 0x5b, 0x50, 0xfb, 0xfe, 0xc6,
-	0x84, 0x54, 0x28, 0x34, 0xfa, 0x7d, 0x4d, 0x6f, 0xc7, 0xb7, 0xdf, 0xf8, 0x1a, 0xf3, 0x39, 0x71,
-	0xad, 0x10, 0x71, 0xda, 0xc3, 0x67, 0xda, 0x30, 0xbe, 0xfc, 0x06, 0x71, 0x4a, 0xc3, 0x17, 0xbc,
-	0x79, 0xf0, 0xfa, 0xbb, 0x5a, 0xe6, 0xdb, 0xef, 0x6a, 0x99, 0xd7, 0x57, 0x35, 0xe1, 0xdb, 0xab,
-	0x9a, 0xf0, 0x8f, 0xab, 0x5a, 0xe6, 0x5f, 0x57, 0x35, 0xe1, 0x9b, 0x7f, 0xd6, 0x84, 0x51, 0x81,
-	0x35, 0xb2, 0x2f, 0xfe, 0x17, 0x00, 0x00, 0xff, 0xff, 0xe3, 0x49, 0x45, 0xc4, 0x8f, 0x0e, 0x00,
+	0x6c, 0xee, 0xc7, 0xec, 0xa9, 0x7c, 0x46, 0xc0, 0x30, 0x74, 0x7f, 0x39, 0x73, 0x6c, 0xf7, 0x85,
+	0x11, 0x98, 0xde, 0x94, 0x04, 0xf2, 0x1e, 0x6f, 0xf8, 0x91, 0x75, 0xc8, 0x8c, 0x3f, 0x17, 0xff,
+	0xf8, 0xcd, 0x61, 0xa6, 0xee, 0x42, 0x39, 0xd9, 0x27, 0x2c, 0x29, 0x3a, 0x99, 0xf8, 0x24, 0x60,
+	0xf9, 0xcf, 0xe1, 0x68, 0x96, 0x64, 0x35, 0xcb, 0x02, 0xe2, 0x59, 0x45, 0x20, 0x5e, 0x98, 0xfe,
+	0x05, 0xcb, 0x74, 0x15, 0xb3, 0x71, 0xa8, 0xe3, 0x97, 0xc4, 0x7c, 0x61, 0x30, 0x07, 0xcf, 0x73,
+	0x29, 0x34, 0x3c, 0x31, 0xfd, 0x8b, 0xe8, 0xbc, 0x5f, 0x42, 0x81, 0xf3, 0x8a, 0xbe, 0x80, 0xd2,
+	0x98, 0x2e, 0xdc, 0x60, 0xd3, 0xeb, 0xf7, 0xd2, 0xad, 0x82, 0x79, 0xa2, 0xc8, 0x12, 0x60, 0xfd,
+	0x14, 0x8a, 0x91, 0x0b, 0x3d, 0x4c, 0xfa, 0x98, 0xd8, 0xbc, 0x77, 0x8d, 0xc2, 0xed, 0xe6, 0x7f,
+	0x69, 0x3a, 0x0b, 0x7e, 0x79, 0x11, 0xf3, 0x49, 0xfd, 0x2f, 0x02, 0x14, 0x71, 0x98, 0x36, 0x3f,
+	0x48, 0x3d, 0x1b, 0xf9, 0xad, 0x67, 0x63, 0x23, 0xb0, 0xec, 0x96, 0xc0, 0x62, 0x8d, 0xe4, 0x52,
+	0x1a, 0xd9, 0x30, 0x27, 0xbe, 0x95, 0xb9, 0xfc, 0x5b, 0x98, 0x2b, 0xa4, 0x98, 0x7b, 0x08, 0xbb,
+	0x13, 0x8f, 0xce, 0xd8, 0xc3, 0x40, 0x3d, 0xd3, 0x5b, 0x46, 0xf5, 0xbc, 0x13, 0x5a, 0x87, 0xb1,
+	0xb1, 0x6e, 0x40, 0x09, 0x13, 0x7f, 0x4e, 0x5d, 0x9f, 0xdc, 0x7a, 0x6d, 0x04, 0xa2, 0x65, 0x06,
+	0x26, 0xbb, 0x74, 0x15, 0xb3, 0x31, 0x7a, 0x04, 0xe2, 0x98, 0x5a, 0xfc, 0xca, 0xbb, 0xe9, 0x1a,
+	0xd2, 0x3c, 0x8f, 0x7a, 0x2d, 0x6a, 0x11, 0xcc, 0x00, 0xf5, 0x39, 0x48, 0x6d, 0xfa, 0xd2, 0x75,
+	0xa8, 0x69, 0xf5, 0x3d, 0x3a, 0x0d, 0x1b, 0xf4, 0xad, 0x8d, 0xa6, 0x0d, 0xc5, 0x05, 0x6b, 0x45,
+	0x71, 0xab, 0x79, 0xb0, 0xdd, 0x1a, 0xae, 0x6f, 0xc4, 0xfb, 0x56, 0xac, 0xa7, 0x68, 0x69, 0xfd,
+	0x6f, 0x02, 0x28, 0xb7, 0xa3, 0x51, 0x07, 0x2a, 0x1c, 0x69, 0xa4, 0xfe, 0x49, 0x8e, 0xde, 0xe5,
+	0x20, 0xd6, 0x95, 0x60, 0x91, 0x8c, 0xdf, 0xfa, 0xa0, 0xa5, 0xf4, 0x9f, 0x7b, 0x37, 0xfd, 0x3f,
+	0x82, 0x9d, 0x51, 0x28, 0x98, 0xe4, 0xf9, 0x16, 0xd5, 0xdc, 0x51, 0xbe, 0x99, 0x95, 0x32, 0xb8,
+	0x3a, 0xe2, 0x4a, 0x62, 0xf6, 0x7a, 0x01, 0xc4, 0xbe, 0xed, 0x4e, 0xeb, 0x87, 0x90, 0x6f, 0x39,
+	0x94, 0x25, 0xac, 0xe0, 0x11, 0xd3, 0xa7, 0x6e, 0xcc, 0x23, 0x9f, 0x1d, 0xff, 0x35, 0x0b, 0x95,
+	0xd4, 0xaf, 0x15, 0x7a, 0x0c, 0xbb, 0xad, 0xee, 0xf9, 0x60, 0xa8, 0x61, 0xa3, 0xd5, 0xd3, 0x4f,
+	0x3b, 0x67, 0x52, 0x46, 0x39, 0x58, 0xad, 0x55, 0x79, 0xb6, 0x01, 0x6d, 0xff, 0x35, 0x1d, 0x42,
+	0xbe, 0xa3, 0xb7, 0xb5, 0xdf, 0x4a, 0x82, 0x72, 0x77, 0xb5, 0x56, 0xa5, 0x14, 0x90, 0x3f, 0x41,
+	0x9f, 0x40, 0x95, 0x01, 0x8c, 0xf3, 0x7e, 0xbb, 0x31, 0xd4, 0xa4, 0xac, 0xa2, 0xac, 0xd6, 0xea,
+	0xfe, 0x75, 0x5c, 0xc4, 0xf9, 0x87, 0x50, 0xc4, 0xda, 0x6f, 0xce, 0xb5, 0xc1, 0x50, 0xca, 0x29,
+	0xfb, 0xab, 0xb5, 0x8a, 0x52, 0xc0, 0x58, 0x35, 0x0f, 0xa1, 0x84, 0xb5, 0x41, 0xbf, 0xa7, 0x0f,
+	0x34, 0x49, 0x54, 0x7e, 0xb0, 0x5a, 0xab, 0x77, 0xb6, 0x50, 0x51, 0x95, 0xfe, 0x04, 0xf6, 0xda,
+	0xbd, 0xaf, 0xf4, 0x6e, 0xaf, 0xd1, 0x36, 0xfa, 0xb8, 0x77, 0x86, 0xb5, 0xc1, 0x40, 0xca, 0x2b,
+	0x87, 0xab, 0xb5, 0xfa, 0x7e, 0x0a, 0x7f, 0xa3, 0xe8, 0x3e, 0x00, 0xb1, 0xdf, 0xd1, 0xcf, 0xa4,
+	0x82, 0x72, 0x67, 0xb5, 0x56, 0xdf, 0x4b, 0x41, 0x43, 0x52, 0xc3, 0x88, 0x5b, 0xdd, 0xde, 0x40,
+	0x93, 0x8a, 0x37, 0x22, 0x66, 0x64, 0x1f, 0xff, 0x0e, 0xd0, 0xcd, 0x9f, 0x4f, 0xf4, 0x00, 0x44,
+	0xbd, 0xa7, 0x6b, 0x52, 0x86, 0xc7, 0x7f, 0x13, 0xa1, 0x53, 0x97, 0xa0, 0x3a, 0xe4, 0xba, 0x5f,
+	0x7f, 0x29, 0x09, 0xca, 0x0f, 0x57, 0x6b, 0xf5, 0xde, 0x4d, 0x50, 0xf7, 0xeb, 0x2f, 0x8f, 0x29,
+	0x54, 0xd2, 0x1b, 0xd7, 0xa1, 0xf4, 0x54, 0x1b, 0x36, 0xda, 0x8d, 0x61, 0x43, 0xca, 0xf0, 0x2b,
+	0xc5, 0xee, 0xa7, 0x24, 0x30, 0x99, 0x08, 0x0f, 0x20, 0xaf, 0x6b, 0xcf, 0x34, 0x2c, 0x09, 0xca,
+	0xde, 0x6a, 0xad, 0xee, 0xc4, 0x00, 0x9d, 0x5c, 0x12, 0x0f, 0xd5, 0xa0, 0xd0, 0xe8, 0x7e, 0xd5,
+	0x78, 0x3e, 0x90, 0xb2, 0x0a, 0x5a, 0xad, 0xd5, 0xdd, 0xd8, 0xdd, 0x70, 0x5e, 0x9a, 0x4b, 0xff,
+	0xf8, 0xbf, 0x02, 0x54, 0xd3, 0x0f, 0x2e, 0xaa, 0x81, 0x78, 0xda, 0xe9, 0x6a, 0xf1, 0x71, 0x69,
+	0x5f, 0x38, 0x46, 0x47, 0x50, 0x6e, 0x77, 0xb0, 0xd6, 0x1a, 0xf6, 0xf0, 0xf3, 0x38, 0x96, 0x34,
+	0xa8, 0x6d, 0x7b, 0xac, 0xc0, 0x97, 0xe8, 0x67, 0x50, 0x1d, 0x3c, 0x7f, 0xda, 0xed, 0xe8, 0xbf,
+	0x36, 0xd8, 0x8e, 0x59, 0xe5, 0xd1, 0x6a, 0xad, 0xde, 0xdf, 0x02, 0x93, 0xb9, 0x47, 0xc6, 0x66,
+	0x40, 0xac, 0x01, 0x7f, 0x44, 0x42, 0x67, 0x49, 0x40, 0x2d, 0xd8, 0x8b, 0x97, 0x6e, 0x0e, 0xcb,
+	0x29, 0x9f, 0xac, 0xd6, 0xea, 0x47, 0xdf, 0xbb, 0x3e, 0x39, 0xbd, 0x24, 0xa0, 0x07, 0x50, 0x8c,
+	0x36, 0x89, 0x2b, 0x29, 0xbd, 0x34, 0x5a, 0x70, 0xfc, 0x67, 0x01, 0xca, 0x49, 0xbb, 0x0a, 0x09,
+	0xd7, 0x7b, 0x86, 0x86, 0x71, 0x0f, 0xc7, 0x0c, 0x24, 0x4e, 0x9d, 0xb2, 0x21, 0xba, 0x0f, 0xc5,
+	0x33, 0x4d, 0xd7, 0x70, 0xa7, 0x15, 0x0b, 0x23, 0x81, 0x9c, 0x11, 0x97, 0x78, 0xf6, 0x18, 0x7d,
+	0x0c, 0x55, 0xbd, 0x67, 0x0c, 0xce, 0x5b, 0x4f, 0xe2, 0xd0, 0xd9, 0xf9, 0xa9, 0xad, 0x06, 0x8b,
+	0xf1, 0x05, 0xe3, 0xf3, 0x38, 0xd4, 0xd0, 0xb3, 0x46, 0xb7, 0xd3, 0xe6, 0xd0, 0x9c, 0x22, 0xaf,
+	0xd6, 0xea, 0xdd, 0x04, 0xda, 0xe1, 0x7f, 0x1e, 0x21, 0xf6, 0xd8, 0x82, 0xda, 0xf7, 0x37, 0x26,
+	0xa4, 0x42, 0xa1, 0xd1, 0xef, 0x6b, 0x7a, 0x3b, 0xbe, 0xfd, 0xc6, 0xd7, 0x98, 0xcf, 0x89, 0x6b,
+	0x85, 0x88, 0xd3, 0x1e, 0x3e, 0xd3, 0x86, 0xf1, 0xe5, 0x37, 0x88, 0x53, 0x1a, 0xbe, 0xe0, 0xcd,
+	0x83, 0xd7, 0xdf, 0xd5, 0x32, 0xdf, 0x7e, 0x57, 0xcb, 0xbc, 0xbe, 0xaa, 0x09, 0xdf, 0x5e, 0xd5,
+	0x84, 0x7f, 0x5c, 0xd5, 0x32, 0xff, 0xba, 0xaa, 0x09, 0xdf, 0xfc, 0xb3, 0x26, 0x8c, 0x0a, 0xac,
+	0x91, 0x7d, 0xf1, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xad, 0x8a, 0xef, 0x7f, 0x8f, 0x0e, 0x00,
 	0x00,
 	0x00,
 }
 }

+ 4 - 0
lib/protocol/bep_extensions.go

@@ -88,6 +88,10 @@ func (f FileInfo) ModTime() time.Time {
 	return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
 	return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
 }
 }
 
 
+func (f FileInfo) SequenceNo() int64 {
+	return f.Sequence
+}
+
 // WinsConflict returns true if "f" is the one to choose when it is in
 // WinsConflict returns true if "f" is the one to choose when it is in
 // conflict with "other".
 // conflict with "other".
 func (f FileInfo) WinsConflict(other FileInfo) bool {
 func (f FileInfo) WinsConflict(other FileInfo) bool {