Ver Fonte

all: Add invalid/ignored files to global list, announce to peers (fixes #623)

This lets us determine accurate completion status for remote peers when they
have ignored files.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4460
Simon Frei há 8 anos atrás
pai
commit
c080f677cb

+ 11 - 0
cmd/syncthing/main.go

@@ -761,6 +761,17 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		// Converts old symlink types to new in the entire database.
 		ldb.ConvertSymlinkTypes()
 	}
+	if cfg.RawCopy().OriginalVersion < 26 {
+		// Adds invalid (ignored) files to global list of files
+		changed := 0
+		for folderID, folderCfg := range folders {
+			changed += ldb.AddInvalidToGlobal([]byte(folderID), protocol.LocalDeviceID[:])
+			for _, deviceCfg := range folderCfg.Devices {
+				changed += ldb.AddInvalidToGlobal([]byte(folderID), deviceCfg.DeviceID[:])
+			}
+		}
+		l.Infof("Database update: Added %d ignored files to the global list", changed)
+	}
 
 	m := model.NewModel(cfg, myID, "syncthing", Version, ldb, protectedFiles)
 

+ 9 - 1
lib/config/config.go

@@ -32,7 +32,7 @@ import (
 
 const (
 	OldestHandledVersion = 10
-	CurrentVersion       = 25
+	CurrentVersion       = 26
 	MaxRescanIntervalS   = 365 * 24 * 60 * 60
 )
 
@@ -329,6 +329,9 @@ func (cfg *Configuration) clean() error {
 	if cfg.Version == 24 {
 		convertV24V25(cfg)
 	}
+	if cfg.Version == 25 {
+		convertV25V26(cfg)
+	}
 
 	// Build a list of available devices
 	existingDevices := make(map[protocol.DeviceID]bool)
@@ -378,6 +381,11 @@ func (cfg *Configuration) clean() error {
 	return nil
 }
 
+func convertV25V26(cfg *Configuration) {
+	// triggers database update
+	cfg.Version = 26
+}
+
 func convertV24V25(cfg *Configuration) {
 	for i := range cfg.Folders {
 		cfg.Folders[i].FSWatcherDelayS = 10

+ 16 - 0
lib/config/testdata/v26.xml

@@ -0,0 +1,16 @@
+<configuration version="26">
+    <folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" fsNotifications="false" notifyDelayS="10" autoNormalize="true">
+        <filesystemType>basic</filesystemType>
+        <device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
+        <device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
+        <minDiskFree unit="%">1</minDiskFree>
+        <maxConflicts>-1</maxConflicts>
+        <fsync>true</fsync>
+    </folder>
+    <device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
+        <address>tcp://a</address>
+    </device>
+    <device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
+        <address>tcp://b</address>
+    </device>
+</configuration>

+ 111 - 24
lib/db/leveldb_dbinstance.go

@@ -134,11 +134,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
 			if isLocalDevice {
 				localSize.addFile(fs[fsi])
 			}
-			if fs[fsi].IsInvalid() {
-				t.removeFromGlobal(folder, device, newName, globalSize)
-			} else {
-				t.updateGlobal(folder, device, fs[fsi], globalSize)
-			}
+			t.updateGlobal(folder, device, fs[fsi], globalSize)
 			fsi++
 
 		case moreFs && moreDb && cmp == 0:
@@ -155,11 +151,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
 					localSize.removeFile(ef)
 					localSize.addFile(fs[fsi])
 				}
-				if fs[fsi].IsInvalid() {
-					t.removeFromGlobal(folder, device, newName, globalSize)
-				} else {
-					t.updateGlobal(folder, device, fs[fsi], globalSize)
-				}
+				t.updateGlobal(folder, device, fs[fsi], globalSize)
 			} else {
 				l.Debugln("generic replace; equal - ignore")
 			}
@@ -219,11 +211,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
 		}
 
 		t.insertFile(folder, device, f)
-		if f.IsInvalid() {
-			t.removeFromGlobal(folder, device, name, globalSize)
-		} else {
-			t.updateGlobal(folder, device, f, globalSize)
-		}
+		t.updateGlobal(folder, device, f, globalSize)
 
 		// Write out and reuse the batch every few records, to avoid the batch
 		// growing too large and thus allocating unnecessarily much memory.
@@ -415,6 +403,9 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
 		if !v.Version.Equal(vl.Versions[0].Version) {
 			break
 		}
+		if v.Invalid {
+			continue
+		}
 		n := protocol.DeviceIDFromBytes(v.Device)
 		devices = append(devices, n)
 	}
@@ -422,7 +413,7 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
 	return devices
 }
 
-func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
+func (db *Instance) withNeed(folder, device []byte, truncate bool, needAllInvalid bool, fn Iterator) {
 	t := db.newReadOnlyTransaction()
 	defer t.close()
 
@@ -444,11 +435,17 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
 
 		have := false // If we have the file, any version
 		need := false // If we have a lower version of the file
-		var haveVersion protocol.Vector
+		var haveFileVersion FileVersion
 		for _, v := range vl.Versions {
 			if bytes.Equal(v.Device, device) {
 				have = true
-				haveVersion = v.Version
+				haveFileVersion = v
+				// We need invalid files regardless of version when
+				// ignore patterns changed
+				if v.Invalid && needAllInvalid {
+					need = true
+					break
+				}
 				// XXX: This marks Concurrent (i.e. conflicting) changes as
 				// needs. Maybe we should do that, but it needs special
 				// handling in the puller.
@@ -463,12 +460,19 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
 
 		name := db.globalKeyName(dbi.Key())
 		needVersion := vl.Versions[0].Version
+		needDevice := protocol.DeviceIDFromBytes(vl.Versions[0].Device)
 
 		for i := range vl.Versions {
 			if !vl.Versions[i].Version.Equal(needVersion) {
 				// We haven't found a valid copy of the file with the needed version.
 				break
 			}
+
+			if vl.Versions[i].Invalid {
+				// The file is marked invalid, don't use it.
+				continue
+			}
+
 			fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.Versions[i].Device, name)
 			bs, err := t.Get(fk, nil)
 			if err != nil {
@@ -482,17 +486,12 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
 				continue
 			}
 
-			if gf.IsInvalid() {
-				// The file is marked invalid for whatever reason, don't use it.
-				continue
-			}
-
 			if gf.IsDeleted() && !have {
 				// We don't need deleted files that we don't have
 				break
 			}
 
-			l.Debugf("need folder=%q device=%v name=%q need=%v have=%v haveV=%d globalV=%d", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveVersion, vl.Versions[0].Version)
+			l.Debugf("need folder=%q device=%v name=%q need=%v have=%v invalid=%v haveV=%d globalV=%d globalDev=%v", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveFileVersion.Invalid, haveFileVersion.Version, needVersion, needDevice)
 
 			if cont := fn(gf); !cont {
 				return
@@ -640,6 +639,94 @@ func (db *Instance) ConvertSymlinkTypes() {
 	l.Infof("Updated symlink type for %d index entries", conv)
 }
 
+// AddInvalidToGlobal searches for invalid files and adds them to the global list.
+// Invalid files exist in the db if they once were not ignored and subsequently
+// ignored. In the new system this is still valid, but invalid files must also be
+// in the global list such that they cannot be mistaken for missing files.
+func (db *Instance) AddInvalidToGlobal(folder, device []byte) int {
+	t := db.newReadWriteTransaction()
+	defer t.close()
+
+	dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, nil)[:keyPrefixLen+keyFolderLen+keyDeviceLen]), nil)
+	defer dbi.Release()
+
+	changed := 0
+	for dbi.Next() {
+		var file protocol.FileInfo
+		if err := file.Unmarshal(dbi.Value()); err != nil {
+			// probably can't happen
+			continue
+		}
+		if file.Invalid {
+			changed++
+
+			l.Debugf("add invalid to global; folder=%q device=%v file=%q version=%d", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version)
+
+			// this is an adapted version of readWriteTransaction.updateGlobal
+			name := []byte(file.Name)
+			gk := t.db.globalKey(folder, name)
+
+			var fl VersionList
+			if svl, err := t.Get(gk, nil); err == nil {
+				fl.Unmarshal(svl) // skip error, range handles success case
+			}
+
+			nv := FileVersion{
+				Device:  device,
+				Version: file.Version,
+				Invalid: file.Invalid,
+			}
+
+			inserted := false
+			// Find a position in the list to insert this file. The file at the front
+			// of the list is the newer, the "global".
+		insert:
+			for i := range fl.Versions {
+				switch fl.Versions[i].Version.Compare(file.Version) {
+				case protocol.Equal:
+					// Invalid files should go after a valid file of equal version
+					if nv.Invalid {
+						continue insert
+					}
+					fallthrough
+
+				case protocol.Lesser:
+					// The version at this point in the list is equal to or lesser
+					// ("older") than us. We insert ourselves in front of it.
+					fl.Versions = insertVersion(fl.Versions, i, nv)
+					inserted = true
+					break insert
+
+				case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
+					// The version at this point is in conflict with us. We must pull
+					// the actual file metadata to determine who wins. If we win, we
+					// insert ourselves in front of the loser here. (The "Lesser" and
+					// "Greater" in the condition above is just based on the device
+					// IDs in the version vector, which is not the only thing we use
+					// to determine the winner.)
+					//
+					// A surprise missing file entry here is counted as a win for us.
+					of, ok := t.getFile(folder, fl.Versions[i].Device, name)
+					if !ok || file.WinsConflict(of) {
+						fl.Versions = insertVersion(fl.Versions, i, nv)
+						inserted = true
+						break insert
+					}
+				}
+			}
+
+			if !inserted {
+				// We didn't find a position for an insert above, so append to the end.
+				fl.Versions = append(fl.Versions, nv)
+			}
+
+			t.Put(gk, mustMarshal(&fl))
+		}
+	}
+
+	return changed
+}
+
 // deviceKey returns a byte slice encoding the following information:
 //	   keyTypeDevice (1 byte)
 //	   folder (4 bytes)

+ 27 - 18
lib/db/leveldb_transactions.go

@@ -99,7 +99,7 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
 		fl.Unmarshal(svl) // skip error, range handles success case
 		for i := range fl.Versions {
 			if bytes.Equal(fl.Versions[i].Device, device) {
-				if fl.Versions[i].Version.Equal(file.Version) {
+				if fl.Versions[i].Version.Equal(file.Version) && fl.Versions[i].Invalid == file.Invalid {
 					// No need to do anything
 					return false
 				}
@@ -119,19 +119,27 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
 	nv := FileVersion{
 		Device:  device,
 		Version: file.Version,
+		Invalid: file.Invalid,
 	}
 
-	var insertedAt int
+	insertedAt := -1
 	// Find a position in the list to insert this file. The file at the front
 	// of the list is the newer, the "global".
+insert:
 	for i := range fl.Versions {
 		switch fl.Versions[i].Version.Compare(file.Version) {
-		case protocol.Equal, protocol.Lesser:
+		case protocol.Equal:
+			if nv.Invalid {
+				continue insert
+			}
+			fallthrough
+
+		case protocol.Lesser:
 			// The version at this point in the list is equal to or lesser
 			// ("older") than us. We insert ourselves in front of it.
 			fl.Versions = insertVersion(fl.Versions, i, nv)
 			insertedAt = i
-			goto done
+			break insert
 
 		case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
 			// The version at this point is in conflict with us. We must pull
@@ -146,16 +154,17 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
 			if !ok || file.WinsConflict(of) {
 				fl.Versions = insertVersion(fl.Versions, i, nv)
 				insertedAt = i
-				goto done
+				break insert
 			}
 		}
 	}
 
-	// We didn't find a position for an insert above, so append to the end.
-	fl.Versions = append(fl.Versions, nv)
-	insertedAt = len(fl.Versions) - 1
+	if insertedAt == -1 {
+		// We didn't find a position for an insert above, so append to the end.
+		fl.Versions = append(fl.Versions, nv)
+		insertedAt = len(fl.Versions) - 1
+	}
 
-done:
 	if insertedAt == 0 {
 		// We just inserted a new newest version. Fixup the global size
 		// calculation.
@@ -221,15 +230,15 @@ func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, glob
 
 	if len(fl.Versions) == 0 {
 		t.Delete(gk)
-	} else {
-		l.Debugf("new global after remove: %v", fl)
-		t.Put(gk, mustMarshal(&fl))
-		if removed {
-			if f, ok := t.getFile(folder, fl.Versions[0].Device, file); ok {
-				// A failure to get the file here is surprising and our
-				// global size data will be incorrect until a restart...
-				globalSize.addFile(f)
-			}
+		return
+	}
+	l.Debugf("new global after remove: %v", fl)
+	t.Put(gk, mustMarshal(&fl))
+	if removed {
+		if f, ok := t.getFile(folder, fl.Versions[0].Device, file); ok {
+			// A failure to get the file here is surprising and our
+			// global size data will be incorrect until a restart...
+			globalSize.addFile(f)
 		}
 	}
 }

+ 14 - 2
lib/db/set.go

@@ -206,12 +206,24 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
 
 func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
 	l.Debugf("%s WithNeed(%v)", s.folder, device)
-	s.db.withNeed([]byte(s.folder), device[:], false, nativeFileIterator(fn))
+	s.db.withNeed([]byte(s.folder), device[:], false, false, nativeFileIterator(fn))
 }
 
 func (s *FileSet) WithNeedTruncated(device protocol.DeviceID, fn Iterator) {
 	l.Debugf("%s WithNeedTruncated(%v)", s.folder, device)
-	s.db.withNeed([]byte(s.folder), device[:], true, nativeFileIterator(fn))
+	s.db.withNeed([]byte(s.folder), device[:], true, false, nativeFileIterator(fn))
+}
+
+// WithNeedOrInvalid considers all invalid files as needed, regardless of their version
+// (e.g. for pulling when ignore patterns changed)
+func (s *FileSet) WithNeedOrInvalid(device protocol.DeviceID, fn Iterator) {
+	l.Debugf("%s WithNeedExcludingInvalid(%v)", s.folder, device)
+	s.db.withNeed([]byte(s.folder), device[:], false, true, nativeFileIterator(fn))
+}
+
+func (s *FileSet) WithNeedOrInvalidTruncated(device protocol.DeviceID, fn Iterator) {
+	l.Debugf("%s WithNeedExcludingInvalidTruncated(%v)", s.folder, device)
+	s.db.withNeed([]byte(s.folder), device[:], true, true, nativeFileIterator(fn))
 }
 
 func (s *FileSet) WithHave(device protocol.DeviceID, fn Iterator) {

+ 12 - 0
lib/db/structs.go

@@ -63,3 +63,15 @@ func (f FileInfoTruncated) FileName() string {
 func (f FileInfoTruncated) ModTime() time.Time {
 	return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
 }
+
+func (f FileInfoTruncated) ConvertToInvalidFileInfo(invalidatedBy protocol.ShortID) protocol.FileInfo {
+	return protocol.FileInfo{
+		Name:       f.Name,
+		Type:       f.Type,
+		ModifiedS:  f.ModifiedS,
+		ModifiedNs: f.ModifiedNs,
+		ModifiedBy: invalidatedBy,
+		Invalid:    true,
+		Version:    f.Version,
+	}
+}

+ 67 - 34
lib/db/structs.pb.go

@@ -1,6 +1,5 @@
-// Code generated by protoc-gen-gogo.
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
 // source: structs.proto
-// DO NOT EDIT!
 
 /*
 	Package db is a generated protocol buffer package.
@@ -39,6 +38,7 @@ const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
 type FileVersion struct {
 	Version protocol.Vector `protobuf:"bytes,1,opt,name=version" json:"version"`
 	Device  []byte          `protobuf:"bytes,2,opt,name=device,proto3" json:"device,omitempty"`
+	Invalid bool            `protobuf:"varint,3,opt,name=invalid,proto3" json:"invalid,omitempty"`
 }
 
 func (m *FileVersion) Reset()                    { *m = FileVersion{} }
@@ -109,6 +109,16 @@ func (m *FileVersion) MarshalTo(dAtA []byte) (int, error) {
 		i = encodeVarintStructs(dAtA, i, uint64(len(m.Device)))
 		i += copy(dAtA[i:], m.Device)
 	}
+	if m.Invalid {
+		dAtA[i] = 0x18
+		i++
+		if m.Invalid {
+			dAtA[i] = 1
+		} else {
+			dAtA[i] = 0
+		}
+		i++
+	}
 	return i, nil
 }
 
@@ -283,6 +293,9 @@ func (m *FileVersion) ProtoSize() (n int) {
 	if l > 0 {
 		n += 1 + l + sovStructs(uint64(l))
 	}
+	if m.Invalid {
+		n += 2
+	}
 	return n
 }
 
@@ -447,6 +460,26 @@ func (m *FileVersion) Unmarshal(dAtA []byte) error {
 				m.Device = []byte{}
 			}
 			iNdEx = postIndex
+		case 3:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Invalid", wireType)
+			}
+			var v int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				v |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			m.Invalid = bool(v != 0)
 		default:
 			iNdEx = preIndex
 			skippy, err := skipStructs(dAtA[iNdEx:])
@@ -988,36 +1021,36 @@ var (
 func init() { proto.RegisterFile("structs.proto", fileDescriptorStructs) }
 
 var fileDescriptorStructs = []byte{
-	// 483 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0x4f, 0x6b, 0xdb, 0x4e,
-	0x10, 0xf5, 0xc6, 0x8a, 0xff, 0xac, 0xe2, 0xfc, 0x7e, 0x59, 0x4a, 0x58, 0x0c, 0x95, 0x85, 0xa1,
-	0x20, 0x0a, 0x95, 0x5b, 0x87, 0x5e, 0xda, 0x9b, 0x29, 0x81, 0x40, 0x29, 0x45, 0x09, 0xe9, 0xa5,
-	0x60, 0x2c, 0x69, 0x2c, 0x2f, 0x95, 0x76, 0x55, 0xed, 0xca, 0xa0, 0x7e, 0x92, 0x1e, 0xf3, 0x71,
-	0x7c, 0xec, 0xb9, 0x87, 0xd0, 0xba, 0x9f, 0xa3, 0x50, 0xb4, 0x92, 0x15, 0x1d, 0xdb, 0xdb, 0xbc,
-	0xd9, 0xf7, 0xe6, 0xbd, 0x61, 0x16, 0x8f, 0xa4, 0xca, 0xf2, 0x40, 0x49, 0x37, 0xcd, 0x84, 0x12,
-	0xe4, 0x28, 0xf4, 0xc7, 0xcf, 0x22, 0xa6, 0x36, 0xb9, 0xef, 0x06, 0x22, 0x99, 0x45, 0x22, 0x12,
-	0x33, 0xfd, 0xe4, 0xe7, 0x6b, 0x8d, 0x34, 0xd0, 0x55, 0x25, 0x19, 0xbf, 0x6c, 0xd1, 0x65, 0xc1,
-	0x03, 0xb5, 0x61, 0x3c, 0x6a, 0x55, 0x31, 0xf3, 0xab, 0x09, 0x81, 0x88, 0x67, 0x3e, 0xa4, 0x95,
-	0x6c, 0xfa, 0x01, 0x9b, 0x97, 0x2c, 0x86, 0x5b, 0xc8, 0x24, 0x13, 0x9c, 0x3c, 0xc7, 0xfd, 0x6d,
-	0x55, 0x52, 0x64, 0x23, 0xc7, 0x9c, 0xff, 0xef, 0x1e, 0x44, 0xee, 0x2d, 0x04, 0x4a, 0x64, 0x0b,
-	0x63, 0x77, 0x3f, 0xe9, 0x78, 0x07, 0x1a, 0x39, 0xc7, 0xbd, 0x10, 0xb6, 0x2c, 0x00, 0x7a, 0x64,
-	0x23, 0xe7, 0xc4, 0xab, 0xd1, 0xf4, 0x12, 0x9b, 0xf5, 0xd0, 0xb7, 0x4c, 0x2a, 0xf2, 0x02, 0x0f,
-	0x6a, 0x85, 0xa4, 0xc8, 0xee, 0x3a, 0xe6, 0xfc, 0x3f, 0x37, 0xf4, 0xdd, 0x96, 0x77, 0x3d, 0xb8,
-	0xa1, 0xbd, 0x32, 0xbe, 0xde, 0x4d, 0x3a, 0xd3, 0xdf, 0x5d, 0x7c, 0x56, 0xb2, 0xae, 0xf8, 0x5a,
-	0xdc, 0x64, 0x39, 0x0f, 0x56, 0x0a, 0x42, 0x42, 0xb0, 0xc1, 0x57, 0x09, 0xe8, 0x90, 0x43, 0x4f,
-	0xd7, 0xe4, 0x29, 0x36, 0x54, 0x91, 0x56, 0x39, 0x4e, 0xe7, 0xe7, 0x0f, 0xc1, 0x1b, 0x79, 0x91,
-	0x82, 0xa7, 0x39, 0xa5, 0x5e, 0xb2, 0x2f, 0x40, 0xbb, 0x36, 0x72, 0xba, 0x9e, 0xae, 0x89, 0x8d,
-	0xcd, 0x14, 0xb2, 0x84, 0xc9, 0x2a, 0xa5, 0x61, 0x23, 0x67, 0xe4, 0xb5, 0x5b, 0xe4, 0x31, 0xc6,
-	0x89, 0x08, 0xd9, 0x9a, 0x41, 0xb8, 0x94, 0xf4, 0x58, 0x6b, 0x87, 0x87, 0xce, 0x35, 0xa1, 0xb8,
-	0x1f, 0x42, 0x0c, 0x0a, 0x42, 0xda, 0xb3, 0x91, 0x33, 0xf0, 0x0e, 0xb0, 0x7c, 0x61, 0x7c, 0xbb,
-	0x8a, 0x59, 0x48, 0xfb, 0xd5, 0x4b, 0x0d, 0xc9, 0x13, 0x7c, 0xca, 0xc5, 0xb2, 0xed, 0x3b, 0xd0,
-	0x84, 0x11, 0x17, 0xef, 0x5b, 0xce, 0xad, 0xbb, 0x0c, 0xff, 0xee, 0x2e, 0x63, 0x3c, 0x90, 0xf0,
-	0x39, 0x07, 0x1e, 0x00, 0xc5, 0x3a, 0x69, 0x83, 0xc9, 0x04, 0x9b, 0xcd, 0x1e, 0x5c, 0x52, 0xd3,
-	0x46, 0xce, 0xb1, 0xd7, 0xac, 0xf6, 0x4e, 0x92, 0x8f, 0x2d, 0x82, 0x5f, 0xd0, 0x13, 0x1b, 0x39,
-	0xc6, 0xe2, 0x75, 0x69, 0xf0, 0xfd, 0x7e, 0x72, 0xf1, 0x0f, 0x3f, 0xcd, 0xbd, 0xde, 0x88, 0x4c,
-	0x5d, 0xbd, 0x79, 0x98, 0xbe, 0x28, 0xca, 0x9d, 0x65, 0x91, 0xc4, 0x8c, 0x7f, 0x5a, 0xaa, 0x55,
-	0x16, 0x81, 0xa2, 0x67, 0xfa, 0x8c, 0xa3, 0xba, 0x7b, 0xa3, 0x9b, 0xd5, 0xfd, 0x17, 0x8f, 0x76,
-	0x3f, 0xad, 0xce, 0x6e, 0x6f, 0xa1, 0x6f, 0x7b, 0x0b, 0xfd, 0xd8, 0x5b, 0x9d, 0xbb, 0x5f, 0x16,
-	0xf2, 0x7b, 0xda, 0xe0, 0xe2, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x9e, 0xcd, 0xe3, 0xfd, 0x38,
-	0x03, 0x00, 0x00,
+	// 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,
 }

+ 1 - 0
lib/db/structs.proto

@@ -12,6 +12,7 @@ option (gogoproto.protosizer_all) = true;
 message FileVersion {
     protocol.Vector version = 1 [(gogoproto.nullable) = false];
     bytes           device  = 2;
+    bool            invalid = 3;
 }
 
 message VersionList {

+ 1 - 2
lib/discover/local.pb.go

@@ -1,6 +1,5 @@
-// Code generated by protoc-gen-gogo.
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
 // source: local.proto
-// DO NOT EDIT!
 
 /*
 	Package discover is a generated protocol buffer package.

+ 24 - 0
lib/fs/util.go

@@ -53,3 +53,27 @@ func getHomeDir() (string, error) {
 
 	return home, nil
 }
+
+var windowsDisallowedCharacters = string([]rune{
+	'<', '>', ':', '"', '|', '?', '*',
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+	11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+	21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+	31,
+})
+
+func WindowsInvalidFilename(name string) bool {
+	// None of the path components should end in space
+	for _, part := range strings.Split(name, `\`) {
+		if len(part) == 0 {
+			continue
+		}
+		if part[len(part)-1] == ' ' {
+			// Names ending in space are not valid.
+			return true
+		}
+	}
+
+	// The path must not contain any disallowed characters
+	return strings.ContainsAny(name, windowsDisallowedCharacters)
+}

+ 15 - 25
lib/model/model.go

@@ -595,7 +595,6 @@ type FolderCompletion struct {
 func (m *Model) Completion(device protocol.DeviceID, folder string) FolderCompletion {
 	m.fmut.RLock()
 	rf, ok := m.folderFiles[folder]
-	ignores := m.folderIgnores[folder]
 	m.fmut.RUnlock()
 	if !ok {
 		return FolderCompletion{} // Folder doesn't exist, so we hardly have any of it
@@ -615,10 +614,6 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
 
 	var need, fileNeed, downloaded, deletes int64
 	rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
-		if ignores.Match(f.FileName()).IsIgnored() {
-			return true
-		}
-
 		ft := f.(db.FileInfoTruncated)
 
 		// If the file is deleted, we account it only in the deleted column.
@@ -703,10 +698,9 @@ func (m *Model) NeedSize(folder string) db.Counts {
 
 	var result db.Counts
 	if rf, ok := m.folderFiles[folder]; ok {
-		ignores := m.folderIgnores[folder]
 		cfg := m.folderCfgs[folder]
 		rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
-			if shouldIgnore(f, ignores, cfg.IgnoreDelete) {
+			if cfg.IgnoreDelete && f.IsDeleted() {
 				return true
 			}
 
@@ -767,10 +761,9 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
 	}
 
 	rest = make([]db.FileInfoTruncated, 0, perpage)
-	ignores := m.folderIgnores[folder]
 	cfg := m.folderCfgs[folder]
 	rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
-		if shouldIgnore(f, ignores, cfg.IgnoreDelete) {
+		if cfg.IgnoreDelete && f.IsDeleted() {
 			return true
 		}
 
@@ -1721,6 +1714,13 @@ func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files [
 		objType := "file"
 		action := "modified"
 
+		switch {
+		case file.IsDeleted():
+			action = "deleted"
+
+		case file.Invalid:
+			action = "ignored" // invalidated seems not very user friendly
+
 		// If our local vector is version 1 AND it is the only version
 		// vector so far seen for this file then it is a new file.  Else if
 		// it is > 1 it's not new, and if it is 1 but another shortId
@@ -1728,16 +1728,13 @@ func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files [
 		// so the file is still not new but modified by us. Only if it is
 		// truly new do we change this to 'added', else we leave it as
 		// 'modified'.
-		if len(file.Version.Counters) == 1 && file.Version.Counters[0].Value == 1 {
+		case len(file.Version.Counters) == 1 && file.Version.Counters[0].Value == 1:
 			action = "added"
 		}
 
 		if file.IsDirectory() {
 			objType = "dir"
 		}
-		if file.IsDeleted() {
-			action = "deleted"
-		}
 
 		// Two different events can be fired here based on what EventType is passed into function
 		events.Default.Log(typeOfEvent, map[string]string{
@@ -1971,18 +1968,7 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
 			case !f.IsInvalid() && ignores.Match(f.Name).IsIgnored():
 				// File was valid at last pass but has been ignored. Set invalid bit.
 				l.Debugln("setting invalid bit on ignored", f)
-				nf := protocol.FileInfo{
-					Name:          f.Name,
-					Type:          f.Type,
-					Size:          f.Size,
-					ModifiedS:     f.ModifiedS,
-					ModifiedNs:    f.ModifiedNs,
-					ModifiedBy:    m.id.Short(),
-					Permissions:   f.Permissions,
-					NoPermissions: f.NoPermissions,
-					Invalid:       true,
-					Version:       f.Version, // The file is still the same, so don't bump version
-				}
+				nf := f.ConvertToInvalidFileInfo(m.id.Short())
 				batch = append(batch, nf)
 				batchSizeBytes += nf.ProtoSize()
 
@@ -2167,6 +2153,10 @@ func (m *Model) Override(folder string) {
 		}
 
 		have, ok := fs.Get(protocol.LocalDeviceID, need.Name)
+		// Don't override invalid (e.g. ignored) files
+		if ok && have.Invalid {
+			return true
+		}
 		if !ok || have.Name != need.Name {
 			// We are missing the file
 			need.Deleted = true

+ 34 - 29
lib/model/requests_test.go

@@ -26,10 +26,9 @@ func TestRequestSimple(t *testing.T) {
 	// Verify that the model performs a request and creates a file based on
 	// an incoming index update.
 
-	defer os.RemoveAll("_tmpfolder")
-
-	m, fc := setupModelWithConnection()
+	m, fc, tmpFolder := setupModelWithConnection()
 	defer m.Stop()
+	defer os.RemoveAll(tmpFolder)
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -52,7 +51,7 @@ func TestRequestSimple(t *testing.T) {
 	<-done
 
 	// Verify the contents
-	bs, err := ioutil.ReadFile("_tmpfolder/testfile")
+	bs, err := ioutil.ReadFile(filepath.Join(tmpFolder, "testfile"))
 	if err != nil {
 		t.Error("File did not sync correctly:", err)
 		return
@@ -70,10 +69,9 @@ func TestSymlinkTraversalRead(t *testing.T) {
 		return
 	}
 
-	defer os.RemoveAll("_tmpfolder")
-
-	m, fc := setupModelWithConnection()
+	m, fc, tmpFolder := setupModelWithConnection()
 	defer m.Stop()
+	defer os.RemoveAll(tmpFolder)
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -111,10 +109,9 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 		return
 	}
 
-	defer os.RemoveAll("_tmpfolder")
-
-	m, fc := setupModelWithConnection()
+	m, fc, tmpFolder := setupModelWithConnection()
 	defer m.Stop()
+	defer os.RemoveAll(tmpFolder)
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected names.
@@ -170,22 +167,25 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 }
 
 func TestRequestCreateTmpSymlink(t *testing.T) {
-	// Verify that the model performs a request and creates a file based on
-	// an incoming index update.
-
-	defer os.RemoveAll("_tmpfolder")
+	// Test that an update for a temporary file is invalidated
 
-	m, fc := setupModelWithConnection()
+	m, fc, tmpFolder := setupModelWithConnection()
 	defer m.Stop()
+	defer os.RemoveAll(tmpFolder)
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
-	badIdx := make(chan string)
+	goodIdx := make(chan struct{})
+	name := fs.TempName("testlink")
 	fc.mut.Lock()
 	fc.indexFn = func(folder string, fs []protocol.FileInfo) {
 		for _, f := range fs {
-			if f.Name == ".syncthing.testlink.tmp" {
-				badIdx <- f.Name
+			if f.Name == name {
+				if f.Invalid {
+					goodIdx <- struct{}{}
+				} else {
+					t.Fatal("Received index with non-invalid temporary file")
+				}
 				return
 			}
 		}
@@ -193,16 +193,13 @@ func TestRequestCreateTmpSymlink(t *testing.T) {
 	fc.mut.Unlock()
 
 	// Send an update for the test file, wait for it to sync and be reported back.
-	fc.addFile(".syncthing.testlink.tmp", 0644, protocol.FileInfoTypeSymlink, []byte(".."))
+	fc.addFile(name, 0644, protocol.FileInfoTypeSymlink, []byte(".."))
 	fc.sendIndexUpdate()
 
 	select {
-	case name := <-badIdx:
-		t.Fatal("Should not have sent the index entry for", name)
+	case <-goodIdx:
 	case <-time.After(3 * time.Second):
-		// Unfortunately not much else to trigger on here. The puller sleep
-		// interval is 1s so if we didn't get any requests within two
-		// iterations we should be fine.
+		t.Fatal("Timed out without index entry being sent")
 	}
 }
 
@@ -214,8 +211,12 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
 	// Sets up a folder with trashcan versioning and tries to use a
 	// deleted symlink to escape
 
+	tmpFolder, err := ioutil.TempDir(".", "_request-")
+	if err != nil {
+		panic("Failed to create temporary testing dir")
+	}
 	cfg := defaultConfig.RawCopy()
-	cfg.Folders[0] = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "_tmpfolder")
+	cfg.Folders[0] = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, tmpFolder)
 	cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
 		{DeviceID: device1},
 		{DeviceID: device2},
@@ -232,7 +233,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
 	m.StartFolder("default")
 	defer m.Stop()
 
-	defer os.RemoveAll("_tmpfolder")
+	defer os.RemoveAll(tmpFolder)
 
 	fc := addFakeConn(m, device2)
 	fc.folder = "default"
@@ -285,9 +286,13 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
 	}
 }
 
-func setupModelWithConnection() (*Model, *fakeConnection) {
+func setupModelWithConnection() (*Model, *fakeConnection, string) {
+	tmpFolder, err := ioutil.TempDir(".", "_request-")
+	if err != nil {
+		panic("Failed to create temporary testing dir")
+	}
 	cfg := defaultConfig.RawCopy()
-	cfg.Folders[0] = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, "_tmpfolder")
+	cfg.Folders[0] = config.NewFolderConfiguration("default", fs.FilesystemTypeBasic, tmpFolder)
 	cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
 		{DeviceID: device1},
 		{DeviceID: device2},
@@ -303,5 +308,5 @@ func setupModelWithConnection() (*Model, *fakeConnection) {
 	fc := addFakeConn(m, device2)
 	fc.folder = "default"
 
-	return m, fc
+	return m, fc, tmpFolder
 }

+ 38 - 58
lib/model/rwfolder.go

@@ -69,6 +69,7 @@ const (
 	dbUpdateDeleteFile
 	dbUpdateShortcutFile
 	dbUpdateHandleSymlink
+	dbUpdateInvalidate
 )
 
 const (
@@ -234,7 +235,9 @@ func (f *sendReceiveFolder) pull(prevSeq int64, prevIgnoreHash string) (curSeq i
 	f.model.fmut.RUnlock()
 
 	curSeq = prevSeq
-	if curIgnoreHash = curIgnores.Hash(); curIgnoreHash != prevIgnoreHash {
+	curIgnoreHash = curIgnores.Hash()
+	ignoresChanged := curIgnoreHash != prevIgnoreHash
+	if ignoresChanged {
 		// The ignore patterns have changed. We need to re-evaluate if
 		// there are files we need now that were ignored before.
 		l.Debugln(f, "ignore patterns have changed, resetting curSeq")
@@ -263,7 +266,7 @@ func (f *sendReceiveFolder) pull(prevSeq int64, prevIgnoreHash string) (curSeq i
 	for {
 		tries++
 
-		changed = f.pullerIteration(curIgnores)
+		changed := f.pullerIteration(curIgnores, ignoresChanged)
 		l.Debugln(f, "changed", changed)
 
 		if changed == 0 {
@@ -317,7 +320,7 @@ func (f *sendReceiveFolder) pull(prevSeq int64, prevIgnoreHash string) (curSeq i
 // returns the number items that should have been synced (even those that
 // might have failed). One puller iteration handles all files currently
 // flagged as needed in the folder.
-func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher) int {
+func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, ignoresChanged bool) int {
 	pullChan := make(chan pullBlockState)
 	copyChan := make(chan copyBlocksState)
 	finisherChan := make(chan *sharedPullerState)
@@ -374,15 +377,21 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher) int {
 	// (directories, symlinks and deletes) goes into the "process directly"
 	// pile.
 
-	folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
-		if shouldIgnore(intf, ignores, f.IgnoreDelete) {
+	// Don't iterate over invalid/ignored files unless ignores have changed
+	iterate := folderFiles.WithNeed
+	if ignoresChanged {
+		iterate = folderFiles.WithNeedOrInvalid
+	}
+
+	iterate(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
+		if f.IgnoreDelete && intf.IsDeleted() {
 			return true
 		}
 
-		if err := fileValid(intf); err != nil {
-			// The file isn't valid so we can't process it. Pretend that we
-			// tried and set the error for the file.
-			f.newError("need", intf.FileName(), err)
+		// If filename isn't valid, we can terminate early with an appropriate error.
+		// in case it is deleted, we don't care about the filename, so don't complain.
+		if !intf.IsDeleted() && runtime.GOOS == "windows" && fs.WindowsInvalidFilename(intf.FileName()) {
+			f.newError("need", intf.FileName(), fs.ErrInvalidFilename)
 			changed++
 			return true
 		}
@@ -390,6 +399,11 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher) int {
 		file := intf.(protocol.FileInfo)
 
 		switch {
+		case ignores.ShouldIgnore(file.Name):
+			file.Invalidate(f.model.id.Short())
+			l.Debugln(f, "Handling ignored file", file)
+			f.dbUpdates <- dbUpdateJob{file, dbUpdateInvalidate}
+
 		case file.IsDeleted():
 			processDirectly = append(processDirectly, file)
 			changed++
@@ -403,9 +417,15 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher) int {
 				if f.model.ConnectedTo(dev) {
 					f.queue.Push(file.Name, file.Size, file.ModTime())
 					changed++
-					break
+					return true
 				}
 			}
+			l.Debugln(f, "Needed file is unavailable", file)
+
+		case runtime.GOOS == "windows" && file.IsSymlink():
+			file.Invalidate(f.model.id.Short())
+			l.Debugln(f, "Invalidating symlink (unsupported)", file.Name)
+			f.dbUpdates <- dbUpdateJob{file, dbUpdateInvalidate}
 
 		default:
 			// Directories, symlinks
@@ -449,7 +469,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher) int {
 				// number, hence the deletion coming in again as part of
 				// WithNeed, furthermore, the file can simply be of the wrong
 				// type if we haven't yet managed to pull it.
-				if ok && !df.IsDeleted() && !df.IsSymlink() && !df.IsDirectory() {
+				if ok && !df.IsDeleted() && !df.IsSymlink() && !df.IsDirectory() && !df.IsInvalid() {
 					// Put files into buckets per first hash
 					key := string(df.Blocks[0].Hash)
 					buckets[key] = append(buckets[key], df)
@@ -457,11 +477,11 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher) int {
 			}
 
 		case fi.IsDirectory() && !fi.IsSymlink():
-			l.Debugln("Handling directory", fi.Name)
+			l.Debugln(f, "Handling directory", fi.Name)
 			f.handleDir(fi)
 
 		case fi.IsSymlink():
-			l.Debugln("Handling symlink", fi.Name)
+			l.Debugln(f, "Handling symlink", fi.Name)
 			f.handleSymlink(fi)
 
 		default:
@@ -566,13 +586,13 @@ nextFile:
 	doneWg.Wait()
 
 	for _, file := range fileDeletions {
-		l.Debugln("Deleting file", file.Name)
+		l.Debugln(f, "Deleting file", file.Name)
 		f.deleteFile(file)
 	}
 
 	for i := range dirDeletions {
 		dir := dirDeletions[len(dirDeletions)-i-1]
-		l.Debugln("Deleting dir", dir.Name)
+		l.Debugln(f, "Deleting dir", dir.Name)
 		f.deleteDir(dir, ignores)
 	}
 
@@ -1516,8 +1536,9 @@ func (f *sendReceiveFolder) dbUpdaterRoutine() {
 				changedDirs[filepath.Dir(job.file.Name)] = struct{}{}
 			case dbUpdateHandleDir:
 				changedDirs[job.file.Name] = struct{}{}
-			case dbUpdateHandleSymlink:
-				// fsyncing symlinks is only supported by MacOS, ignore
+			case dbUpdateHandleSymlink, dbUpdateInvalidate:
+				// fsyncing symlinks is only supported by MacOS
+				// and invalidated files are db only changes -> no sync
 			}
 
 			if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
@@ -1722,47 +1743,6 @@ func (l fileErrorList) Swap(a, b int) {
 	l[a], l[b] = l[b], l[a]
 }
 
-// fileValid returns nil when the file is valid for processing, or an error if it's not
-func fileValid(file db.FileIntf) error {
-	switch {
-	case file.IsDeleted():
-		// We don't care about file validity if we're not supposed to have it
-		return nil
-
-	case runtime.GOOS == "windows" && file.IsSymlink():
-		return errSymlinksUnsupported
-
-	case runtime.GOOS == "windows" && windowsInvalidFilename(file.FileName()):
-		return fs.ErrInvalidFilename
-	}
-
-	return nil
-}
-
-var windowsDisallowedCharacters = string([]rune{
-	'<', '>', ':', '"', '|', '?', '*',
-	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
-	11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
-	21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
-	31,
-})
-
-func windowsInvalidFilename(name string) bool {
-	// None of the path components should end in space
-	for _, part := range strings.Split(name, `\`) {
-		if len(part) == 0 {
-			continue
-		}
-		if part[len(part)-1] == ' ' {
-			// Names ending in space are not valid.
-			return true
-		}
-	}
-
-	// The path must not contain any disallowed characters
-	return strings.ContainsAny(name, windowsDisallowedCharacters)
-}
-
 // byComponentCount sorts by the number of path components in Name, that is
 // "x/y" sorts before "foo/bar/baz".
 type byComponentCount []protocol.FileInfo

+ 58 - 59
lib/protocol/bep.pb.go

@@ -1,6 +1,5 @@
-// Code generated by protoc-gen-gogo.
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
 // source: bep.proto
-// DO NOT EDIT!
 
 /*
 	Package protocol is a generated protocol buffer package.
@@ -307,7 +306,7 @@ type FileInfo struct {
 	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"`
 	Sequence      int64        `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
-	Blocks        []BlockInfo  `protobuf:"bytes,16,rep,name=Blocks" json:"Blocks"`
+	Blocks        []BlockInfo  `protobuf:"bytes,16,rep,name=Blocks,json=blocks" json:"Blocks"`
 	SymlinkTarget string       `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
 }
 
@@ -4218,61 +4217,61 @@ var fileDescriptorBep = []byte{
 	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,
 	0x4e, 0x7b, 0xb3, 0xa2, 0xb9, 0x44, 0x3f, 0x82, 0x42, 0xd3, 0xa1, 0xe3, 0x17, 0xb1, 0xd2, 0xef,
-	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,
+	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,
 	0x00,
 }

+ 7 - 0
lib/protocol/bep_extensions.go

@@ -113,6 +113,13 @@ func (f FileInfo) WinsConflict(other FileInfo) bool {
 	return f.Version.Compare(other.Version) == ConcurrentGreater
 }
 
+func (f *FileInfo) Invalidate(invalidatedBy ShortID) {
+	f.Invalid = true
+	f.ModifiedBy = invalidatedBy
+	f.Blocks = nil
+	f.Sequence = 0
+}
+
 func (b BlockInfo) String() string {
 	return fmt.Sprintf("Block{%d/%d/%d/%x}", b.Offset, b.Size, b.WeakHash, b.Hash)
 }

+ 1 - 2
lib/protocol/deviceid_test.pb.go

@@ -1,6 +1,5 @@
-// Code generated by protoc-gen-gogo.
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
 // source: deviceid_test.proto
-// DO NOT EDIT!
 
 /*
 Package protocol is a generated protocol buffer package.