mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-08 22:31:04 +00:00
Add Local Version field to files, send index in segments.
This commit is contained in:
parent
fccdd85cc1
commit
8b349945de
@ -180,7 +180,7 @@ func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request)
|
|||||||
var repo = qs.Get("repo")
|
var repo = qs.Get("repo")
|
||||||
var res = make(map[string]interface{})
|
var res = make(map[string]interface{})
|
||||||
|
|
||||||
res["version"] = m.Version(repo)
|
res["version"] = m.LocalVersion(repo)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
@ -210,7 +210,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
||||||
|
|
||||||
res["state"] = m.State(repo)
|
res["state"] = m.State(repo)
|
||||||
res["version"] = m.Version(repo)
|
res["version"] = m.LocalVersion(repo)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
|
@ -290,11 +290,6 @@ func main() {
|
|||||||
rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
|
rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
|
||||||
}
|
}
|
||||||
|
|
||||||
havePersistentIndex := false
|
|
||||||
if fi, err := os.Stat(filepath.Join(confDir, "index")); err == nil && fi.IsDir() {
|
|
||||||
havePersistentIndex = true
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), nil)
|
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("leveldb.OpenFile():", err)
|
l.Fatalln("leveldb.OpenFile():", err)
|
||||||
@ -364,11 +359,6 @@ nextRepo:
|
|||||||
// Walk the repository and update the local model before establishing any
|
// Walk the repository and update the local model before establishing any
|
||||||
// connections to other nodes.
|
// connections to other nodes.
|
||||||
|
|
||||||
if !havePersistentIndex {
|
|
||||||
// There's no new style index, load old ones
|
|
||||||
l.Infoln("Loading legacy index files")
|
|
||||||
m.LoadIndexes(confDir)
|
|
||||||
}
|
|
||||||
m.CleanRepos()
|
m.CleanRepos()
|
||||||
l.Infoln("Performing initial repository scan")
|
l.Infoln("Performing initial repository scan")
|
||||||
m.ScanRepos()
|
m.ScanRepos()
|
||||||
@ -645,9 +635,10 @@ next:
|
|||||||
if rateBucket != nil {
|
if rateBucket != nil {
|
||||||
wr = &limitedWriter{conn, rateBucket}
|
wr = &limitedWriter{conn, rateBucket}
|
||||||
}
|
}
|
||||||
protoConn := protocol.NewConnection(remoteID, conn, wr, m)
|
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
||||||
|
protoConn := protocol.NewConnection(remoteID, conn, wr, m, name)
|
||||||
|
|
||||||
l.Infof("Established secure connection to %s at %v", remoteID, conn.RemoteAddr())
|
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||||
if debugNet {
|
if debugNet {
|
||||||
l.Debugf("cipher suite %04X", conn.ConnectionState().CipherSuite)
|
l.Debugf("cipher suite %04X", conn.ConnectionState().CipherSuite)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package files
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/calmh/syncthing/lamport"
|
"github.com/calmh/syncthing/lamport"
|
||||||
"github.com/calmh/syncthing/protocol"
|
"github.com/calmh/syncthing/protocol"
|
||||||
@ -12,6 +13,22 @@ import (
|
|||||||
"github.com/syndtr/goleveldb/leveldb/util"
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
clockTick uint64
|
||||||
|
clockMut sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func clock(v uint64) uint64 {
|
||||||
|
clockMut.Lock()
|
||||||
|
defer clockMut.Unlock()
|
||||||
|
if v > clockTick {
|
||||||
|
clockTick = v + 1
|
||||||
|
} else {
|
||||||
|
clockTick++
|
||||||
|
}
|
||||||
|
return clockTick
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
keyTypeNode = iota
|
keyTypeNode = iota
|
||||||
keyTypeGlobal
|
keyTypeGlobal
|
||||||
@ -91,11 +108,11 @@ func globalKeyName(key []byte) []byte {
|
|||||||
return key[1+64:]
|
return key[1+64:]
|
||||||
}
|
}
|
||||||
|
|
||||||
type deletionHandler func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) bool
|
type deletionHandler func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64
|
||||||
|
|
||||||
type fileIterator func(f protocol.FileInfo) bool
|
type fileIterator func(f protocol.FileInfo) bool
|
||||||
|
|
||||||
func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo, deleteFn deletionHandler) bool {
|
func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo, deleteFn deletionHandler) uint64 {
|
||||||
sort.Sort(fileList(fs)) // sort list on name, same as on disk
|
sort.Sort(fileList(fs)) // sort list on name, same as on disk
|
||||||
|
|
||||||
start := nodeKey(repo, node, nil) // before all repo/node files
|
start := nodeKey(repo, node, nil) // before all repo/node files
|
||||||
@ -112,7 +129,7 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
|||||||
|
|
||||||
moreDb := dbi.Next()
|
moreDb := dbi.Next()
|
||||||
fsi := 0
|
fsi := 0
|
||||||
changed := false
|
var maxLocalVer uint64
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var newName, oldName []byte
|
var newName, oldName []byte
|
||||||
@ -144,9 +161,10 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case moreFs && (!moreDb || cmp == -1):
|
case moreFs && (!moreDb || cmp == -1):
|
||||||
changed = true
|
|
||||||
// Disk is missing this file. Insert it.
|
// Disk is missing this file. Insert it.
|
||||||
ldbInsert(batch, repo, node, newName, fs[fsi])
|
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
|
||||||
|
maxLocalVer = lv
|
||||||
|
}
|
||||||
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
||||||
fsi++
|
fsi++
|
||||||
|
|
||||||
@ -155,9 +173,10 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
|||||||
var ef protocol.FileInfo
|
var ef protocol.FileInfo
|
||||||
ef.UnmarshalXDR(dbi.Value())
|
ef.UnmarshalXDR(dbi.Value())
|
||||||
if fs[fsi].Version > ef.Version {
|
if fs[fsi].Version > ef.Version {
|
||||||
ldbInsert(batch, repo, node, newName, fs[fsi])
|
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
|
||||||
|
maxLocalVer = lv
|
||||||
|
}
|
||||||
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
// Iterate both sides.
|
// Iterate both sides.
|
||||||
fsi++
|
fsi++
|
||||||
@ -165,8 +184,8 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
|||||||
|
|
||||||
case moreDb && (!moreFs || cmp == 1):
|
case moreDb && (!moreFs || cmp == 1):
|
||||||
if deleteFn != nil {
|
if deleteFn != nil {
|
||||||
if deleteFn(snap, batch, repo, node, oldName, dbi) {
|
if lv := deleteFn(snap, batch, repo, node, oldName, dbi); lv > maxLocalVer {
|
||||||
changed = true
|
maxLocalVer = lv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
moreDb = dbi.Next()
|
moreDb = dbi.Next()
|
||||||
@ -178,23 +197,24 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return changed
|
return maxLocalVer
|
||||||
}
|
}
|
||||||
|
|
||||||
func ldbReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) bool {
|
func ldbReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
|
||||||
return ldbGenericReplace(db, repo, node, fs, func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) bool {
|
// TODO: Return the remaining maxLocalVer?
|
||||||
|
return ldbGenericReplace(db, repo, node, fs, func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64 {
|
||||||
// Disk has files that we are missing. Remove it.
|
// Disk has files that we are missing. Remove it.
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("delete; repo=%q node=%x name=%q", repo, node, name)
|
l.Debugf("delete; repo=%q node=%x name=%q", repo, node, name)
|
||||||
}
|
}
|
||||||
batch.Delete(dbi.Key())
|
batch.Delete(dbi.Key())
|
||||||
ldbRemoveFromGlobal(db, batch, repo, node, name)
|
ldbRemoveFromGlobal(db, batch, repo, node, name)
|
||||||
return true
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) bool {
|
func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
|
||||||
return ldbGenericReplace(db, repo, node, fs, func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) bool {
|
return ldbGenericReplace(db, repo, node, fs, func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64 {
|
||||||
var f protocol.FileInfo
|
var f protocol.FileInfo
|
||||||
err := f.UnmarshalXDR(dbi.Value())
|
err := f.UnmarshalXDR(dbi.Value())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -204,18 +224,20 @@ func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileI
|
|||||||
if debug {
|
if debug {
|
||||||
l.Debugf("mark deleted; repo=%q node=%x name=%q", repo, node, name)
|
l.Debugf("mark deleted; repo=%q node=%x name=%q", repo, node, name)
|
||||||
}
|
}
|
||||||
|
ts := clock(f.LocalVersion)
|
||||||
f.Blocks = nil
|
f.Blocks = nil
|
||||||
f.Version = lamport.Default.Tick(f.Version)
|
f.Version = lamport.Default.Tick(f.Version)
|
||||||
f.Flags |= protocol.FlagDeleted
|
f.Flags |= protocol.FlagDeleted
|
||||||
|
f.LocalVersion = ts
|
||||||
batch.Put(dbi.Key(), f.MarshalXDR())
|
batch.Put(dbi.Key(), f.MarshalXDR())
|
||||||
ldbUpdateGlobal(db, batch, repo, node, nodeKeyName(dbi.Key()), f.Version)
|
ldbUpdateGlobal(db, batch, repo, node, nodeKeyName(dbi.Key()), f.Version)
|
||||||
return true
|
return ts
|
||||||
}
|
}
|
||||||
return false
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) bool {
|
func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
|
||||||
batch := new(leveldb.Batch)
|
batch := new(leveldb.Batch)
|
||||||
snap, err := db.GetSnapshot()
|
snap, err := db.GetSnapshot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -223,12 +245,15 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) bool {
|
|||||||
}
|
}
|
||||||
defer snap.Release()
|
defer snap.Release()
|
||||||
|
|
||||||
|
var maxLocalVer uint64
|
||||||
for _, f := range fs {
|
for _, f := range fs {
|
||||||
name := []byte(f.Name)
|
name := []byte(f.Name)
|
||||||
fk := nodeKey(repo, node, name)
|
fk := nodeKey(repo, node, name)
|
||||||
bs, err := snap.Get(fk, nil)
|
bs, err := snap.Get(fk, nil)
|
||||||
if err == leveldb.ErrNotFound {
|
if err == leveldb.ErrNotFound {
|
||||||
ldbInsert(batch, repo, node, name, f)
|
if lv := ldbInsert(batch, repo, node, name, f); lv > maxLocalVer {
|
||||||
|
maxLocalVer = lv
|
||||||
|
}
|
||||||
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -239,7 +264,9 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) bool {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if ef.Version != f.Version {
|
if ef.Version != f.Version {
|
||||||
ldbInsert(batch, repo, node, name, f)
|
if lv := ldbInsert(batch, repo, node, name, f); lv > maxLocalVer {
|
||||||
|
maxLocalVer = lv
|
||||||
|
}
|
||||||
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,16 +276,22 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) bool {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return maxLocalVer
|
||||||
}
|
}
|
||||||
|
|
||||||
func ldbInsert(batch dbWriter, repo, node, name []byte, file protocol.FileInfo) {
|
func ldbInsert(batch dbWriter, repo, node, name []byte, file protocol.FileInfo) uint64 {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("insert; repo=%q node=%x %v", repo, node, file)
|
l.Debugf("insert; repo=%q node=%x %v", repo, node, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if file.LocalVersion == 0 {
|
||||||
|
file.LocalVersion = clock(0)
|
||||||
|
}
|
||||||
|
|
||||||
nk := nodeKey(repo, node, name)
|
nk := nodeKey(repo, node, name)
|
||||||
batch.Put(nk, file.MarshalXDR())
|
batch.Put(nk, file.MarshalXDR())
|
||||||
|
|
||||||
|
return file.LocalVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// ldbUpdateGlobal adds this node+version to the version list for the given
|
// ldbUpdateGlobal adds this node+version to the version list for the given
|
||||||
|
41
files/set.go
41
files/set.go
@ -21,18 +21,29 @@ type fileRecord struct {
|
|||||||
type bitset uint64
|
type bitset uint64
|
||||||
|
|
||||||
type Set struct {
|
type Set struct {
|
||||||
changes map[protocol.NodeID]uint64
|
localVersion map[protocol.NodeID]uint64
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
repo string
|
repo string
|
||||||
db *leveldb.DB
|
db *leveldb.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSet(repo string, db *leveldb.DB) *Set {
|
func NewSet(repo string, db *leveldb.DB) *Set {
|
||||||
var s = Set{
|
var s = Set{
|
||||||
changes: make(map[protocol.NodeID]uint64),
|
localVersion: make(map[protocol.NodeID]uint64),
|
||||||
repo: repo,
|
repo: repo,
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lv uint64
|
||||||
|
ldbWithHave(db, []byte(repo), protocol.LocalNodeID[:], func(f protocol.FileInfo) bool {
|
||||||
|
if f.LocalVersion > lv {
|
||||||
|
lv = f.LocalVersion
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
s.localVersion[protocol.LocalNodeID] = lv
|
||||||
|
clock(lv)
|
||||||
|
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +53,8 @@ func (s *Set) Replace(node protocol.NodeID, fs []protocol.FileInfo) {
|
|||||||
}
|
}
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
if ldbReplace(s.db, []byte(s.repo), node[:], fs) {
|
if lv := ldbReplace(s.db, []byte(s.repo), node[:], fs); lv > s.localVersion[node] {
|
||||||
s.changes[node]++
|
s.localVersion[node] = lv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +64,8 @@ func (s *Set) ReplaceWithDelete(node protocol.NodeID, fs []protocol.FileInfo) {
|
|||||||
}
|
}
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
if ldbReplaceWithDelete(s.db, []byte(s.repo), node[:], fs) {
|
if lv := ldbReplaceWithDelete(s.db, []byte(s.repo), node[:], fs); lv > s.localVersion[node] {
|
||||||
s.changes[node]++
|
s.localVersion[node] = lv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,8 +75,8 @@ func (s *Set) Update(node protocol.NodeID, fs []protocol.FileInfo) {
|
|||||||
}
|
}
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
if ldbUpdate(s.db, []byte(s.repo), node[:], fs) {
|
if lv := ldbUpdate(s.db, []byte(s.repo), node[:], fs); lv > s.localVersion[node] {
|
||||||
s.changes[node]++
|
s.localVersion[node] = lv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,8 +113,8 @@ func (s *Set) Availability(file string) []protocol.NodeID {
|
|||||||
return ldbAvailability(s.db, []byte(s.repo), []byte(file))
|
return ldbAvailability(s.db, []byte(s.repo), []byte(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Set) Changes(node protocol.NodeID) uint64 {
|
func (s *Set) LocalVersion(node protocol.NodeID) uint64 {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
return s.changes[node]
|
return s.localVersion[node]
|
||||||
}
|
}
|
||||||
|
@ -554,7 +554,7 @@ func TestNeed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChanges(t *testing.T) {
|
func TestLocalVersion(t *testing.T) {
|
||||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -578,17 +578,17 @@ func TestChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.ReplaceWithDelete(protocol.LocalNodeID, local1)
|
m.ReplaceWithDelete(protocol.LocalNodeID, local1)
|
||||||
c0 := m.Changes(protocol.LocalNodeID)
|
c0 := m.LocalVersion(protocol.LocalNodeID)
|
||||||
|
|
||||||
m.ReplaceWithDelete(protocol.LocalNodeID, local2)
|
m.ReplaceWithDelete(protocol.LocalNodeID, local2)
|
||||||
c1 := m.Changes(protocol.LocalNodeID)
|
c1 := m.LocalVersion(protocol.LocalNodeID)
|
||||||
if !(c1 > c0) {
|
if !(c1 > c0) {
|
||||||
t.Fatal("Change number should have incremented")
|
t.Fatal("Local version number should have incremented")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.ReplaceWithDelete(protocol.LocalNodeID, local2)
|
m.ReplaceWithDelete(protocol.LocalNodeID, local2)
|
||||||
c2 := m.Changes(protocol.LocalNodeID)
|
c2 := m.LocalVersion(protocol.LocalNodeID)
|
||||||
if c2 != c1 {
|
if c2 != c1 {
|
||||||
t.Fatal("Change number should be unchanged")
|
t.Fatal("Local version number should be unchanged")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
"github.com/calmh/syncthing/protocol"
|
"github.com/calmh/syncthing/protocol"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SortBy func(p protocol.FileInfo) int
|
type SortBy func(p protocol.FileInfo) int
|
||||||
|
@ -112,6 +112,9 @@ alterFiles() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
pkill -CONT syncthing
|
pkill -CONT syncthing
|
||||||
|
|
||||||
|
echo "Restarting instance 2"
|
||||||
|
curl -HX-API-Key:abc123 -X POST "http://localhost:8082/rest/restart"
|
||||||
}
|
}
|
||||||
|
|
||||||
rm -rf h?/*.idx.gz h?/index
|
rm -rf h?/*.idx.gz h?/index
|
||||||
|
@ -17,8 +17,8 @@ var (
|
|||||||
// Generate returns a check digit for the string s, which should be composed
|
// Generate returns a check digit for the string s, which should be composed
|
||||||
// of characters from the Alphabet a.
|
// of characters from the Alphabet a.
|
||||||
func (a Alphabet) Generate(s string) (rune, error) {
|
func (a Alphabet) Generate(s string) (rune, error) {
|
||||||
if err:=a.check();err!=nil{
|
if err := a.check(); err != nil {
|
||||||
return 0,err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
factor := 1
|
factor := 1
|
||||||
|
250
model/model.go
250
model/model.go
@ -5,8 +5,6 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
|
||||||
"crypto/sha1"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -21,7 +19,6 @@ import (
|
|||||||
"github.com/calmh/syncthing/events"
|
"github.com/calmh/syncthing/events"
|
||||||
"github.com/calmh/syncthing/files"
|
"github.com/calmh/syncthing/files"
|
||||||
"github.com/calmh/syncthing/lamport"
|
"github.com/calmh/syncthing/lamport"
|
||||||
"github.com/calmh/syncthing/osutil"
|
|
||||||
"github.com/calmh/syncthing/protocol"
|
"github.com/calmh/syncthing/protocol"
|
||||||
"github.com/calmh/syncthing/scanner"
|
"github.com/calmh/syncthing/scanner"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
@ -42,6 +39,9 @@ const (
|
|||||||
// transfer to bring the systems into synchronization.
|
// transfer to bring the systems into synchronization.
|
||||||
const zeroEntrySize = 128
|
const zeroEntrySize = 128
|
||||||
|
|
||||||
|
// How many files to send in each Index/IndexUpdate message.
|
||||||
|
const indexBatchSize = 1000
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
indexDir string
|
indexDir string
|
||||||
cfg *config.Configuration
|
cfg *config.Configuration
|
||||||
@ -65,6 +65,9 @@ type Model struct {
|
|||||||
nodeVer map[protocol.NodeID]string
|
nodeVer map[protocol.NodeID]string
|
||||||
pmut sync.RWMutex // protects protoConn and rawConn
|
pmut sync.RWMutex // protects protoConn and rawConn
|
||||||
|
|
||||||
|
sentLocalVer map[protocol.NodeID]map[string]uint64
|
||||||
|
slMut sync.Mutex
|
||||||
|
|
||||||
sup suppressor
|
sup suppressor
|
||||||
|
|
||||||
addedRepo bool
|
addedRepo bool
|
||||||
@ -95,6 +98,7 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
|
|||||||
protoConn: make(map[protocol.NodeID]protocol.Connection),
|
protoConn: make(map[protocol.NodeID]protocol.Connection),
|
||||||
rawConn: make(map[protocol.NodeID]io.Closer),
|
rawConn: make(map[protocol.NodeID]io.Closer),
|
||||||
nodeVer: make(map[protocol.NodeID]string),
|
nodeVer: make(map[protocol.NodeID]string),
|
||||||
|
sentLocalVer: make(map[protocol.NodeID]map[string]uint64),
|
||||||
sup: suppressor{threshold: int64(cfg.Options.MaxChangeKbps)},
|
sup: suppressor{threshold: int64(cfg.Options.MaxChangeKbps)},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +112,6 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
|
|||||||
deadlockDetect(&m.rmut, time.Duration(timeout)*time.Second)
|
deadlockDetect(&m.rmut, time.Duration(timeout)*time.Second)
|
||||||
deadlockDetect(&m.smut, time.Duration(timeout)*time.Second)
|
deadlockDetect(&m.smut, time.Duration(timeout)*time.Second)
|
||||||
deadlockDetect(&m.pmut, time.Duration(timeout)*time.Second)
|
deadlockDetect(&m.pmut, time.Duration(timeout)*time.Second)
|
||||||
go m.broadcastIndexLoop()
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,13 +395,13 @@ func (m *Model) Close(node protocol.NodeID, err error) {
|
|||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.pmut.Lock()
|
||||||
m.rmut.RLock()
|
m.rmut.RLock()
|
||||||
for _, repo := range m.nodeRepos[node] {
|
for _, repo := range m.nodeRepos[node] {
|
||||||
m.repoFiles[repo].Replace(node, nil)
|
m.repoFiles[repo].Replace(node, nil)
|
||||||
}
|
}
|
||||||
m.rmut.RUnlock()
|
m.rmut.RUnlock()
|
||||||
|
|
||||||
m.pmut.Lock()
|
|
||||||
conn, ok := m.rawConn[node]
|
conn, ok := m.rawConn[node]
|
||||||
if ok {
|
if ok {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
@ -502,6 +505,7 @@ func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
|
|||||||
// repository changes.
|
// repository changes.
|
||||||
func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection) {
|
func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection) {
|
||||||
nodeID := protoConn.ID()
|
nodeID := protoConn.ID()
|
||||||
|
|
||||||
m.pmut.Lock()
|
m.pmut.Lock()
|
||||||
if _, ok := m.protoConn[nodeID]; ok {
|
if _, ok := m.protoConn[nodeID]; ok {
|
||||||
panic("add existing node")
|
panic("add existing node")
|
||||||
@ -511,48 +515,99 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
|
|||||||
panic("add existing node")
|
panic("add existing node")
|
||||||
}
|
}
|
||||||
m.rawConn[nodeID] = rawConn
|
m.rawConn[nodeID] = rawConn
|
||||||
m.pmut.Unlock()
|
|
||||||
|
|
||||||
cm := m.clusterConfig(nodeID)
|
cm := m.clusterConfig(nodeID)
|
||||||
protoConn.ClusterConfig(cm)
|
protoConn.ClusterConfig(cm)
|
||||||
|
|
||||||
var idxToSend = make(map[string][]protocol.FileInfo)
|
|
||||||
|
|
||||||
m.rmut.RLock()
|
m.rmut.RLock()
|
||||||
for _, repo := range m.nodeRepos[nodeID] {
|
for _, repo := range m.nodeRepos[nodeID] {
|
||||||
idxToSend[repo] = m.protocolIndex(repo)
|
fs := m.repoFiles[repo]
|
||||||
|
go sendIndexes(protoConn, repo, fs)
|
||||||
}
|
}
|
||||||
m.rmut.RUnlock()
|
m.rmut.RUnlock()
|
||||||
|
m.pmut.Unlock()
|
||||||
go func() {
|
|
||||||
for repo, idx := range idxToSend {
|
|
||||||
if debug {
|
|
||||||
l.Debugf("IDX(out/initial): %s: %q: %d files", nodeID, repo, len(idx))
|
|
||||||
}
|
|
||||||
const batchSize = 1000
|
|
||||||
for i := 0; i < len(idx); i += batchSize {
|
|
||||||
if len(idx[i:]) < batchSize {
|
|
||||||
protoConn.Index(repo, idx[i:])
|
|
||||||
} else {
|
|
||||||
protoConn.Index(repo, idx[i:i+batchSize])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// protocolIndex returns the current local index in protocol data types.
|
func sendIndexes(conn protocol.Connection, repo string, fs *files.Set) {
|
||||||
func (m *Model) protocolIndex(repo string) []protocol.FileInfo {
|
nodeID := conn.ID()
|
||||||
var fs []protocol.FileInfo
|
name := conn.Name()
|
||||||
m.repoFiles[repo].WithHave(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
|
||||||
fs = append(fs, f)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return fs
|
if debug {
|
||||||
|
l.Debugf("sendIndexes for %s-%s@/%q starting", nodeID, name, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
initial := true
|
||||||
|
minLocalVer := uint64(0)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if debug {
|
||||||
|
l.Debugf("sendIndexes for %s-%s@/%q exiting: %v", nodeID, name, repo, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for err == nil {
|
||||||
|
if !initial && fs.LocalVersion(protocol.LocalNodeID) <= minLocalVer {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||||
|
maxLocalVer := uint64(0)
|
||||||
|
|
||||||
|
fs.WithHave(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
||||||
|
if f.LocalVersion <= minLocalVer {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.LocalVersion > maxLocalVer {
|
||||||
|
maxLocalVer = f.LocalVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(batch) == indexBatchSize {
|
||||||
|
if initial {
|
||||||
|
if err = conn.Index(repo, batch); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugf("sendIndexes for %s-%s/%q: %d files (initial index)", nodeID, name, repo, len(batch))
|
||||||
|
}
|
||||||
|
initial = false
|
||||||
|
} else {
|
||||||
|
if err = conn.IndexUpdate(repo, batch); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugf("sendIndexes for %s-%s/%q: %d files (batched update)", nodeID, name, repo, len(batch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = make([]protocol.FileInfo, 0, indexBatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = append(batch, f)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if initial {
|
||||||
|
err = conn.Index(repo, batch)
|
||||||
|
if debug && err == nil {
|
||||||
|
l.Debugf("sendIndexes for %s-%s/%q: %d files (small initial index)", nodeID, name, repo, len(batch))
|
||||||
|
}
|
||||||
|
initial = false
|
||||||
|
} else if len(batch) > 0 {
|
||||||
|
err = conn.IndexUpdate(repo, batch)
|
||||||
|
if debug && err == nil {
|
||||||
|
l.Debugf("sendIndexes for %s-%s/%q: %d files (last batch)", nodeID, name, repo, len(batch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minLocalVer = maxLocalVer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) updateLocal(repo string, f protocol.FileInfo) {
|
func (m *Model) updateLocal(repo string, f protocol.FileInfo) {
|
||||||
|
f.LocalVersion = 0
|
||||||
m.rmut.RLock()
|
m.rmut.RLock()
|
||||||
m.repoFiles[repo].Update(protocol.LocalNodeID, []protocol.FileInfo{f})
|
m.repoFiles[repo].Update(protocol.LocalNodeID, []protocol.FileInfo{f})
|
||||||
m.rmut.RUnlock()
|
m.rmut.RUnlock()
|
||||||
@ -575,49 +630,6 @@ func (m *Model) requestGlobal(nodeID protocol.NodeID, repo, name string, offset
|
|||||||
return nc.Request(repo, name, offset, size)
|
return nc.Request(repo, name, offset, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) broadcastIndexLoop() {
|
|
||||||
// TODO: Rewrite to send index in segments
|
|
||||||
var lastChange = map[string]uint64{}
|
|
||||||
for {
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
|
|
||||||
m.pmut.RLock()
|
|
||||||
m.rmut.RLock()
|
|
||||||
|
|
||||||
var indexWg sync.WaitGroup
|
|
||||||
for repo, fs := range m.repoFiles {
|
|
||||||
repo := repo
|
|
||||||
|
|
||||||
c := fs.Changes(protocol.LocalNodeID)
|
|
||||||
if c == lastChange[repo] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lastChange[repo] = c
|
|
||||||
|
|
||||||
idx := m.protocolIndex(repo)
|
|
||||||
|
|
||||||
for _, nodeID := range m.repoNodes[repo] {
|
|
||||||
nodeID := nodeID
|
|
||||||
if conn, ok := m.protoConn[nodeID]; ok {
|
|
||||||
indexWg.Add(1)
|
|
||||||
if debug {
|
|
||||||
l.Debugf("IDX(out/loop): %s: %d files", nodeID, len(idx))
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
conn.Index(repo, idx)
|
|
||||||
indexWg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.rmut.RUnlock()
|
|
||||||
m.pmut.RUnlock()
|
|
||||||
|
|
||||||
indexWg.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
|
func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
|
||||||
if m.started {
|
if m.started {
|
||||||
panic("cannot add repo to started model")
|
panic("cannot add repo to started model")
|
||||||
@ -709,88 +721,6 @@ func (m *Model) ScanRepo(repo string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) LoadIndexes(dir string) {
|
|
||||||
m.rmut.RLock()
|
|
||||||
for repo := range m.repoCfgs {
|
|
||||||
fs := m.loadIndex(repo, dir)
|
|
||||||
|
|
||||||
var sfs = make([]protocol.FileInfo, len(fs))
|
|
||||||
for i := 0; i < len(fs); i++ {
|
|
||||||
lamport.Default.Tick(fs[i].Version)
|
|
||||||
fs[i].Flags &= ^uint32(protocol.FlagInvalid) // we might have saved an index with files that were suppressed; the should not be on startup
|
|
||||||
}
|
|
||||||
|
|
||||||
m.repoFiles[repo].Replace(protocol.LocalNodeID, sfs)
|
|
||||||
}
|
|
||||||
m.rmut.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) error {
|
|
||||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
|
|
||||||
name := id + ".idx.gz"
|
|
||||||
name = filepath.Join(dir, name)
|
|
||||||
tmp := fmt.Sprintf("%s.tmp.%d", name, time.Now().UnixNano())
|
|
||||||
idxf, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer os.Remove(tmp)
|
|
||||||
|
|
||||||
gzw := gzip.NewWriter(idxf)
|
|
||||||
|
|
||||||
n, err := protocol.IndexMessage{
|
|
||||||
Repository: repo,
|
|
||||||
Files: fs,
|
|
||||||
}.EncodeXDR(gzw)
|
|
||||||
if err != nil {
|
|
||||||
gzw.Close()
|
|
||||||
idxf.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = gzw.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = idxf.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugln("wrote index,", n, "bytes uncompressed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return osutil.Rename(tmp, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) loadIndex(repo string, dir string) []protocol.FileInfo {
|
|
||||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
|
|
||||||
name := id + ".idx.gz"
|
|
||||||
name = filepath.Join(dir, name)
|
|
||||||
|
|
||||||
idxf, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer idxf.Close()
|
|
||||||
|
|
||||||
gzr, err := gzip.NewReader(idxf)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer gzr.Close()
|
|
||||||
|
|
||||||
var im protocol.IndexMessage
|
|
||||||
err = im.DecodeXDR(gzr)
|
|
||||||
if err != nil || im.Repository != repo {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return im.Files
|
|
||||||
}
|
|
||||||
|
|
||||||
// clusterConfig returns a ClusterConfigMessage that is correct for the given peer node
|
// clusterConfig returns a ClusterConfigMessage that is correct for the given peer node
|
||||||
func (m *Model) clusterConfig(node protocol.NodeID) protocol.ClusterConfigMessage {
|
func (m *Model) clusterConfig(node protocol.NodeID) protocol.ClusterConfigMessage {
|
||||||
cm := protocol.ClusterConfigMessage{
|
cm := protocol.ClusterConfigMessage{
|
||||||
@ -868,12 +798,12 @@ func (m *Model) Override(repo string) {
|
|||||||
// Version returns the change version for the given repository. This is
|
// Version returns the change version for the given repository. This is
|
||||||
// guaranteed to increment if the contents of the local or global repository
|
// guaranteed to increment if the contents of the local or global repository
|
||||||
// has changed.
|
// has changed.
|
||||||
func (m *Model) Version(repo string) uint64 {
|
func (m *Model) LocalVersion(repo string) uint64 {
|
||||||
var ver uint64
|
var ver uint64
|
||||||
|
|
||||||
m.rmut.Lock()
|
m.rmut.Lock()
|
||||||
for _, n := range m.repoNodes[repo] {
|
for _, n := range m.repoNodes[repo] {
|
||||||
ver += m.repoFiles[repo].Changes(n)
|
ver += m.repoFiles[repo].LocalVersion(n)
|
||||||
}
|
}
|
||||||
m.rmut.Unlock()
|
m.rmut.Unlock()
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ func (p *puller) run() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := p.model.Version(p.repoCfg.ID); v > prevVer {
|
if v := p.model.LocalVersion(p.repoCfg.ID); v > prevVer {
|
||||||
// Queue more blocks to fetch, if any
|
// Queue more blocks to fetch, if any
|
||||||
p.queueNeededBlocks()
|
p.queueNeededBlocks()
|
||||||
prevVer = v
|
prevVer = v
|
||||||
@ -335,15 +335,21 @@ func (p *puller) handleRequestResult(res requestResult) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, of.err = of.file.WriteAt(res.data, res.offset)
|
if res.err != nil {
|
||||||
|
of.err = res.err
|
||||||
|
if debug {
|
||||||
|
l.Debugf("pull: not writing %q / %q offset %d: %v", p.repoCfg.ID, f.Name, res.offset, res.err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, of.err = of.file.WriteAt(res.data, res.offset)
|
||||||
|
if debug {
|
||||||
|
l.Debugf("pull: wrote %q / %q offset %d len %d outstanding %d done %v", p.repoCfg.ID, f.Name, res.offset, len(res.data), of.outstanding, of.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
of.outstanding--
|
of.outstanding--
|
||||||
p.openFiles[f.Name] = of
|
p.openFiles[f.Name] = of
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugf("pull: wrote %q / %q offset %d outstanding %d done %v", p.repoCfg.ID, f.Name, res.offset, of.outstanding, of.done)
|
|
||||||
}
|
|
||||||
|
|
||||||
if of.done && of.outstanding == 0 {
|
if of.done && of.outstanding == 0 {
|
||||||
p.closeFile(f)
|
p.closeFile(f)
|
||||||
}
|
}
|
||||||
@ -526,7 +532,7 @@ func (p *puller) handleRequestBlock(b bqBlock) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node := p.oustandingPerNode.leastBusyNode(of.availability)
|
node := p.oustandingPerNode.leastBusyNode(of.availability)
|
||||||
if len(node) == 0 {
|
if node == (protocol.NodeID{}) {
|
||||||
of.err = errNoNode
|
of.err = errNoNode
|
||||||
if of.file != nil {
|
if of.file != nil {
|
||||||
of.file.Close()
|
of.file.Close()
|
||||||
@ -662,7 +668,7 @@ func (p *puller) closeFile(f protocol.FileInfo) {
|
|||||||
|
|
||||||
for i := range hb {
|
for i := range hb {
|
||||||
if bytes.Compare(hb[i].Hash, f.Blocks[i].Hash) != 0 {
|
if bytes.Compare(hb[i].Hash, f.Blocks[i].Hash) != 0 {
|
||||||
l.Debugf("pull: %q / %q: block %d hash mismatch", p.repoCfg.ID, f.Name, i)
|
l.Debugf("pull: %q / %q: block %d hash mismatch\n\thave: %x\n\twant: %x", p.repoCfg.ID, f.Name, i, hb[i].Hash, f.Blocks[i].Hash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ Cluster Config messages MUST NOT be sent after the initial exchange.
|
|||||||
| Flags |
|
| Flags |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| |
|
| |
|
||||||
+ Max Version (64 bits) +
|
+ Max Local Version (64 bits) +
|
||||||
| |
|
| |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
@ -255,13 +255,13 @@ The Node Flags field contains the following single bit flags:
|
|||||||
|
|
||||||
Exactly one of the T, R or S bits MUST be set.
|
Exactly one of the T, R or S bits MUST be set.
|
||||||
|
|
||||||
The Node Max Version field contains the highest file version number of
|
The Node Max Local Version field contains the highest local file version
|
||||||
the files already known to be in the index sent by this node. If nothing
|
number of the files already known to be in the index sent by this node.
|
||||||
is known about the index of a given node, this field MUST be set to
|
If nothing is known about the index of a given node, this field MUST be
|
||||||
zero. When receiving a Cluster Config message with a non-zero Max
|
set to zero. When receiving a Cluster Config message with a non-zero Max
|
||||||
Version for the local node ID, a node MAY elect to send an Index Update
|
Version for the local node ID, a node MAY elect to send an Index Update
|
||||||
message containing only files with higher version numbers in place of
|
message containing only files with higher local version numbers in place
|
||||||
the initial Index message.
|
of the initial Index message.
|
||||||
|
|
||||||
The Options field contain option values to be used in an implementation
|
The Options field contain option values to be used in an implementation
|
||||||
specific manner. The options list is conceptually a map of Key => Value
|
specific manner. The options list is conceptually a map of Key => Value
|
||||||
@ -292,7 +292,7 @@ peers acting in a specific manner as a result of sent options.
|
|||||||
struct Node {
|
struct Node {
|
||||||
string ID<>;
|
string ID<>;
|
||||||
unsigned int Flags;
|
unsigned int Flags;
|
||||||
unsigned hyper MaxVersion;
|
unsigned hyper MaxLocalVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Option {
|
struct Option {
|
||||||
@ -359,6 +359,10 @@ Index message MUST be sent. There is no response to the Index message.
|
|||||||
+ Version (64 bits) +
|
+ Version (64 bits) +
|
||||||
| |
|
| |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| |
|
||||||
|
+ Local Version (64 bits) +
|
||||||
|
| |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Number of Blocks |
|
| Number of Blocks |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
/ /
|
/ /
|
||||||
@ -400,6 +404,10 @@ detected and received change. The combination of Repository, Name and
|
|||||||
Version uniquely identifies the contents of a file at a given point in
|
Version uniquely identifies the contents of a file at a given point in
|
||||||
time.
|
time.
|
||||||
|
|
||||||
|
The Local Version field is the value of a node local monotonic clock at
|
||||||
|
the time of last local database update to a file. The clock ticks on
|
||||||
|
every local database update.
|
||||||
|
|
||||||
The Flags field is made up of the following single bit flags:
|
The Flags field is made up of the following single bit flags:
|
||||||
|
|
||||||
0 1 2 3
|
0 1 2 3
|
||||||
@ -458,6 +466,7 @@ block which may represent a smaller amount of data.
|
|||||||
unsigned int Flags;
|
unsigned int Flags;
|
||||||
hyper Modified;
|
hyper Modified;
|
||||||
unsigned hyper Version;
|
unsigned hyper Version;
|
||||||
|
unsigned hyper LocalVer;
|
||||||
BlockInfo Blocks<>;
|
BlockInfo Blocks<>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,11 +12,12 @@ type IndexMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string // max:1024
|
Name string // max:1024
|
||||||
Flags uint32
|
Flags uint32
|
||||||
Modified int64
|
Modified int64
|
||||||
Version uint64
|
Version uint64
|
||||||
Blocks []BlockInfo // max:1000000
|
LocalVersion uint64
|
||||||
|
Blocks []BlockInfo // max:1000000
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f FileInfo) String() string {
|
func (f FileInfo) String() string {
|
||||||
@ -69,9 +70,9 @@ type Repository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
ID []byte // max:32
|
ID []byte // max:32
|
||||||
Flags uint32
|
Flags uint32
|
||||||
MaxVersion uint64
|
MaxLocalVersion uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
|
@ -121,6 +121,10 @@ FileInfo Structure:
|
|||||||
+ Version (64 bits) +
|
+ Version (64 bits) +
|
||||||
| |
|
| |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| |
|
||||||
|
+ Local Version (64 bits) +
|
||||||
|
| |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Number of Blocks |
|
| Number of Blocks |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
/ /
|
/ /
|
||||||
@ -134,6 +138,7 @@ struct FileInfo {
|
|||||||
unsigned int Flags;
|
unsigned int Flags;
|
||||||
hyper Modified;
|
hyper Modified;
|
||||||
unsigned hyper Version;
|
unsigned hyper Version;
|
||||||
|
unsigned hyper LocalVersion;
|
||||||
BlockInfo Blocks<1000000>;
|
BlockInfo Blocks<1000000>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +168,7 @@ func (o FileInfo) encodeXDR(xw *xdr.Writer) (int, error) {
|
|||||||
xw.WriteUint32(o.Flags)
|
xw.WriteUint32(o.Flags)
|
||||||
xw.WriteUint64(uint64(o.Modified))
|
xw.WriteUint64(uint64(o.Modified))
|
||||||
xw.WriteUint64(o.Version)
|
xw.WriteUint64(o.Version)
|
||||||
|
xw.WriteUint64(o.LocalVersion)
|
||||||
if len(o.Blocks) > 1000000 {
|
if len(o.Blocks) > 1000000 {
|
||||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||||
}
|
}
|
||||||
@ -189,6 +195,7 @@ func (o *FileInfo) decodeXDR(xr *xdr.Reader) error {
|
|||||||
o.Flags = xr.ReadUint32()
|
o.Flags = xr.ReadUint32()
|
||||||
o.Modified = int64(xr.ReadUint64())
|
o.Modified = int64(xr.ReadUint64())
|
||||||
o.Version = xr.ReadUint64()
|
o.Version = xr.ReadUint64()
|
||||||
|
o.LocalVersion = xr.ReadUint64()
|
||||||
_BlocksSize := int(xr.ReadUint32())
|
_BlocksSize := int(xr.ReadUint32())
|
||||||
if _BlocksSize > 1000000 {
|
if _BlocksSize > 1000000 {
|
||||||
return xdr.ErrElementSizeExceeded
|
return xdr.ErrElementSizeExceeded
|
||||||
@ -567,7 +574,7 @@ Node Structure:
|
|||||||
| Flags |
|
| Flags |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| |
|
| |
|
||||||
+ Max Version (64 bits) +
|
+ Max Local Version (64 bits) +
|
||||||
| |
|
| |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
@ -575,7 +582,7 @@ Node Structure:
|
|||||||
struct Node {
|
struct Node {
|
||||||
opaque ID<32>;
|
opaque ID<32>;
|
||||||
unsigned int Flags;
|
unsigned int Flags;
|
||||||
unsigned hyper MaxVersion;
|
unsigned hyper MaxLocalVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@ -602,7 +609,7 @@ func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
|
|||||||
}
|
}
|
||||||
xw.WriteBytes(o.ID)
|
xw.WriteBytes(o.ID)
|
||||||
xw.WriteUint32(o.Flags)
|
xw.WriteUint32(o.Flags)
|
||||||
xw.WriteUint64(o.MaxVersion)
|
xw.WriteUint64(o.MaxLocalVersion)
|
||||||
return xw.Tot(), xw.Error()
|
return xw.Tot(), xw.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,7 +627,7 @@ func (o *Node) UnmarshalXDR(bs []byte) error {
|
|||||||
func (o *Node) decodeXDR(xr *xdr.Reader) error {
|
func (o *Node) decodeXDR(xr *xdr.Reader) error {
|
||||||
o.ID = xr.ReadBytesMax(32)
|
o.ID = xr.ReadBytesMax(32)
|
||||||
o.Flags = xr.ReadUint32()
|
o.Flags = xr.ReadUint32()
|
||||||
o.MaxVersion = xr.ReadUint64()
|
o.MaxLocalVersion = xr.ReadUint64()
|
||||||
return xr.Error()
|
return xr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,9 @@ type Model interface {
|
|||||||
|
|
||||||
type Connection interface {
|
type Connection interface {
|
||||||
ID() NodeID
|
ID() NodeID
|
||||||
Index(repo string, files []FileInfo)
|
Name() string
|
||||||
|
Index(repo string, files []FileInfo) error
|
||||||
|
IndexUpdate(repo string, files []FileInfo) error
|
||||||
Request(repo string, name string, offset int64, size int) ([]byte, error)
|
Request(repo string, name string, offset int64, size int) ([]byte, error)
|
||||||
ClusterConfig(config ClusterConfigMessage)
|
ClusterConfig(config ClusterConfigMessage)
|
||||||
Statistics() Statistics
|
Statistics() Statistics
|
||||||
@ -74,6 +76,7 @@ type Connection interface {
|
|||||||
|
|
||||||
type rawConnection struct {
|
type rawConnection struct {
|
||||||
id NodeID
|
id NodeID
|
||||||
|
name string
|
||||||
receiver Model
|
receiver Model
|
||||||
state int
|
state int
|
||||||
|
|
||||||
@ -87,8 +90,7 @@ type rawConnection struct {
|
|||||||
awaiting []chan asyncResult
|
awaiting []chan asyncResult
|
||||||
awaitingMut sync.Mutex
|
awaitingMut sync.Mutex
|
||||||
|
|
||||||
idxSent map[string]map[string]uint64
|
idxMut sync.Mutex // ensures serialization of Index calls
|
||||||
idxMut sync.Mutex // ensures serialization of Index calls
|
|
||||||
|
|
||||||
nextID chan int
|
nextID chan int
|
||||||
outbox chan []encodable
|
outbox chan []encodable
|
||||||
@ -106,15 +108,16 @@ const (
|
|||||||
pingIdleTime = 60 * time.Second
|
pingIdleTime = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver Model, name string) Connection {
|
||||||
cr := &countingReader{Reader: reader}
|
cr := &countingReader{Reader: reader}
|
||||||
cw := &countingWriter{Writer: writer}
|
cw := &countingWriter{Writer: writer}
|
||||||
|
|
||||||
rb := bufio.NewReader(cr)
|
rb := bufio.NewReader(cr)
|
||||||
wb := bufio.NewWriter(cw)
|
wb := bufio.NewWriterSize(cw, 65536)
|
||||||
|
|
||||||
c := rawConnection{
|
c := rawConnection{
|
||||||
id: nodeID,
|
id: nodeID,
|
||||||
|
name: name,
|
||||||
receiver: nativeModel{receiver},
|
receiver: nativeModel{receiver},
|
||||||
state: stateInitial,
|
state: stateInitial,
|
||||||
cr: cr,
|
cr: cr,
|
||||||
@ -123,7 +126,6 @@ func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver M
|
|||||||
wb: wb,
|
wb: wb,
|
||||||
xw: xdr.NewWriter(wb),
|
xw: xdr.NewWriter(wb),
|
||||||
awaiting: make([]chan asyncResult, 0x1000),
|
awaiting: make([]chan asyncResult, 0x1000),
|
||||||
idxSent: make(map[string]map[string]uint64),
|
|
||||||
outbox: make(chan []encodable),
|
outbox: make(chan []encodable),
|
||||||
nextID: make(chan int),
|
nextID: make(chan int),
|
||||||
closed: make(chan struct{}),
|
closed: make(chan struct{}),
|
||||||
@ -142,36 +144,34 @@ func (c *rawConnection) ID() NodeID {
|
|||||||
return c.id
|
return c.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *rawConnection) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
// Index writes the list of file information to the connected peer node
|
// Index writes the list of file information to the connected peer node
|
||||||
func (c *rawConnection) Index(repo string, idx []FileInfo) {
|
func (c *rawConnection) Index(repo string, idx []FileInfo) error {
|
||||||
|
select {
|
||||||
|
case <-c.closed:
|
||||||
|
return ErrClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
c.idxMut.Lock()
|
c.idxMut.Lock()
|
||||||
defer c.idxMut.Unlock()
|
c.send(header{0, -1, messageTypeIndex}, IndexMessage{repo, idx})
|
||||||
|
c.idxMut.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var msgType int
|
// IndexUpdate writes the list of file information to the connected peer node as an update
|
||||||
if c.idxSent[repo] == nil {
|
func (c *rawConnection) IndexUpdate(repo string, idx []FileInfo) error {
|
||||||
// This is the first time we send an index.
|
select {
|
||||||
msgType = messageTypeIndex
|
case <-c.closed:
|
||||||
|
return ErrClosed
|
||||||
c.idxSent[repo] = make(map[string]uint64)
|
default:
|
||||||
for _, f := range idx {
|
|
||||||
c.idxSent[repo][f.Name] = f.Version
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We have sent one full index. Only send updates now.
|
|
||||||
msgType = messageTypeIndexUpdate
|
|
||||||
var diff []FileInfo
|
|
||||||
for _, f := range idx {
|
|
||||||
if vs, ok := c.idxSent[repo][f.Name]; !ok || f.Version != vs {
|
|
||||||
diff = append(diff, f)
|
|
||||||
c.idxSent[repo][f.Name] = f.Version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx = diff
|
|
||||||
}
|
|
||||||
|
|
||||||
if msgType == messageTypeIndex || len(idx) > 0 {
|
|
||||||
c.send(header{0, -1, msgType}, IndexMessage{repo, idx})
|
|
||||||
}
|
}
|
||||||
|
c.idxMut.Lock()
|
||||||
|
c.send(header{0, -1, messageTypeIndexUpdate}, IndexMessage{repo, idx})
|
||||||
|
c.idxMut.Unlock()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request returns the bytes for the specified block after fetching them from the connected peer.
|
// Request returns the bytes for the specified block after fetching them from the connected peer.
|
||||||
@ -445,15 +445,13 @@ func (c *rawConnection) send(h header, es ...encodable) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *rawConnection) writerLoop() {
|
func (c *rawConnection) writerLoop() {
|
||||||
var err error
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case es := <-c.outbox:
|
case es := <-c.outbox:
|
||||||
for _, e := range es {
|
for _, e := range es {
|
||||||
e.encodeXDR(c.xw)
|
e.encodeXDR(c.xw)
|
||||||
}
|
}
|
||||||
|
if err := c.flush(); err != nil {
|
||||||
if err = c.flush(); err != nil {
|
|
||||||
c.close(err)
|
c.close(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -471,11 +469,9 @@ func (c *rawConnection) flush() error {
|
|||||||
if err := c.xw.Error(); err != nil {
|
if err := c.xw.Error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.wb.Flush(); err != nil {
|
if err := c.wb.Flush(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,11 @@ func (c wireFormatConnection) ID() NodeID {
|
|||||||
return c.next.ID()
|
return c.next.ID()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c wireFormatConnection) Index(repo string, fs []FileInfo) {
|
func (c wireFormatConnection) Name() string {
|
||||||
|
return c.next.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c wireFormatConnection) Index(repo string, fs []FileInfo) error {
|
||||||
var myFs = make([]FileInfo, len(fs))
|
var myFs = make([]FileInfo, len(fs))
|
||||||
copy(myFs, fs)
|
copy(myFs, fs)
|
||||||
|
|
||||||
@ -26,7 +30,18 @@ func (c wireFormatConnection) Index(repo string, fs []FileInfo) {
|
|||||||
myFs[i].Name = norm.NFC.String(filepath.ToSlash(myFs[i].Name))
|
myFs[i].Name = norm.NFC.String(filepath.ToSlash(myFs[i].Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.next.Index(repo, myFs)
|
return c.next.Index(repo, myFs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c wireFormatConnection) IndexUpdate(repo string, fs []FileInfo) error {
|
||||||
|
var myFs = make([]FileInfo, len(fs))
|
||||||
|
copy(myFs, fs)
|
||||||
|
|
||||||
|
for i := range fs {
|
||||||
|
myFs[i].Name = norm.NFC.String(filepath.ToSlash(myFs[i].Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.next.IndexUpdate(repo, myFs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c wireFormatConnection) Request(repo, name string, offset int64, size int) ([]byte, error) {
|
func (c wireFormatConnection) Request(repo, name string, offset int64, size int) ([]byte, error) {
|
||||||
|
@ -6,6 +6,7 @@ package scanner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"code.google.com/p/go.text/unicode/norm"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -14,7 +15,6 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"code.google.com/p/go.text/unicode/norm"
|
|
||||||
|
|
||||||
"github.com/calmh/syncthing/lamport"
|
"github.com/calmh/syncthing/lamport"
|
||||||
"github.com/calmh/syncthing/protocol"
|
"github.com/calmh/syncthing/protocol"
|
||||||
@ -216,6 +216,7 @@ func (w *Walker) walkAndHashFiles(res *[]protocol.FileInfo, ign map[string][]str
|
|||||||
l.Infof("Changes to %q are being temporarily suppressed because it changes too frequently.", p)
|
l.Infof("Changes to %q are being temporarily suppressed because it changes too frequently.", p)
|
||||||
cf.Flags |= protocol.FlagInvalid
|
cf.Flags |= protocol.FlagInvalid
|
||||||
cf.Version = lamport.Default.Tick(cf.Version)
|
cf.Version = lamport.Default.Tick(cf.Version)
|
||||||
|
cf.LocalVersion = 0
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugln("suppressed:", cf)
|
l.Debugln("suppressed:", cf)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user