Browse Source

gui, lib: Fix tracking deleted locally-changed on encrypted (fixes #7715) (#7726)

Simon Frei 4 years ago
parent
commit
591e4d8af1

+ 0 - 6
gui/default/index.html

@@ -455,12 +455,6 @@
                           <a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
                         </td>
                       </tr>
-                      <tr ng-if="hasReceiveEncryptedItems(folder)">
-                        <th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Locally Changed Items</span></th>
-                        <td class="text-right">
-                          <a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{receiveEncryptedItemsCount(folder) | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
-                        </td>
-                      </tr>
                       <tr ng-if="folder.type != 'sendreceive'">
                         <th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folder Type</span></th>
                         <td class="text-right">

+ 4 - 19
gui/default/syncthing/core/syncthingController.js

@@ -931,9 +931,9 @@ angular.module('syncthing.core')
                 return 'faileditems';
             }
             if ($scope.hasReceiveOnlyChanged(folderCfg)) {
-                return 'localadditions';
-            }
-            if ($scope.hasReceiveEncryptedItems(folderCfg)) {
+                if (folderCfg.type === "receiveonly") {
+                    return 'localadditions';
+                }
                 return 'localunencrypted';
             }
             if (folderCfg.devices.length <= 1) {
@@ -2609,28 +2609,13 @@ angular.module('syncthing.core')
         };
 
         $scope.hasReceiveOnlyChanged = function (folderCfg) {
-            if (!folderCfg || folderCfg.type !== "receiveonly") {
+            if (!folderCfg || folderCfg.type !== ["receiveonly",  "receiveencrypted"].indexOf(folderCfg.type) === -1) {
                 return false;
             }
             var counts = $scope.model[folderCfg.id];
             return counts && counts.receiveOnlyTotalItems > 0;
         };
 
-        $scope.hasReceiveEncryptedItems = function (folderCfg) {
-            if (!folderCfg || folderCfg.type !== "receiveencrypted") {
-                return false;
-            }
-            return $scope.receiveEncryptedItemsCount(folderCfg) > 0;
-        };
-
-        $scope.receiveEncryptedItemsCount = function (folderCfg) {
-            var counts = $scope.model[folderCfg.id];
-            if (!counts) {
-                return 0;
-            }
-            return counts.receiveOnlyTotalItems - counts.receiveOnlyChangedDeletes;
-        }
-
         $scope.revertOverride = function () {
             $http.post(
                 urlbase + "/db/" + $scope.revertOverrideParams.operation +"?folder="

+ 91 - 26
lib/db/lowlevel.go

@@ -223,35 +223,10 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
 		blocksHashSame := ok && bytes.Equal(ef.BlocksHash, f.BlocksHash)
 
 		if ok {
-			if len(ef.Blocks) != 0 && !ef.IsInvalid() && ef.Size > 0 {
-				for _, block := range ef.Blocks {
-					keyBuf, err = db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
-					if err != nil {
-						return err
-					}
-					if err := t.Delete(keyBuf); err != nil {
-						return err
-					}
-				}
-				if !blocksHashSame {
-					keyBuf, err := db.keyer.GenerateBlockListMapKey(keyBuf, folder, ef.BlocksHash, name)
-					if err != nil {
-						return err
-					}
-					if err = t.Delete(keyBuf); err != nil {
-						return err
-					}
-				}
-			}
-
-			keyBuf, err = db.keyer.GenerateSequenceKey(keyBuf, folder, ef.SequenceNo())
+			keyBuf, err = db.removeLocalBlockAndSequenceInfo(keyBuf, folder, name, ef, !blocksHashSame, &t)
 			if err != nil {
 				return err
 			}
-			if err := t.Delete(keyBuf); err != nil {
-				return err
-			}
-			l.Debugf("removing sequence; folder=%q sequence=%v %v", folder, ef.SequenceNo(), ef.FileName())
 		}
 
 		f.Sequence = meta.nextLocalSeq()
@@ -314,6 +289,96 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
 	return t.Commit()
 }
 
+func (db *Lowlevel) removeLocalFiles(folder []byte, nameStrs []string, meta *metadataTracker) error {
+	db.gcMut.RLock()
+	defer db.gcMut.RUnlock()
+
+	t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
+	if err != nil {
+		return err
+	}
+	defer t.close()
+
+	var dk, gk, buf []byte
+	for _, nameStr := range nameStrs {
+		name := []byte(nameStr)
+		dk, err = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], name)
+		if err != nil {
+			return err
+		}
+
+		ef, ok, err := t.getFileByKey(dk)
+		if err != nil {
+			return err
+		}
+		if !ok {
+			l.Debugf("remove (local); folder=%q %v: file doesn't exist", folder, nameStr)
+			continue
+		}
+
+		buf, err = db.removeLocalBlockAndSequenceInfo(buf, folder, name, ef, true, &t)
+		if err != nil {
+			return err
+		}
+
+		meta.removeFile(protocol.LocalDeviceID, ef)
+
+		gk, err = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
+		if err != nil {
+			return err
+		}
+		buf, err = t.removeFromGlobal(gk, buf, folder, protocol.LocalDeviceID[:], name, meta)
+		if err != nil {
+			return err
+		}
+
+		err = t.Delete(dk)
+		if err != nil {
+			return err
+		}
+
+		if err := t.Checkpoint(); err != nil {
+			return err
+		}
+	}
+
+	return t.Commit()
+}
+
+func (db *Lowlevel) removeLocalBlockAndSequenceInfo(keyBuf, folder, name []byte, ef protocol.FileInfo, removeFromBlockListMap bool, t *readWriteTransaction) ([]byte, error) {
+	var err error
+	if len(ef.Blocks) != 0 && !ef.IsInvalid() && ef.Size > 0 {
+		for _, block := range ef.Blocks {
+			keyBuf, err = db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
+			if err != nil {
+				return nil, err
+			}
+			if err := t.Delete(keyBuf); err != nil {
+				return nil, err
+			}
+		}
+		if removeFromBlockListMap {
+			keyBuf, err := db.keyer.GenerateBlockListMapKey(keyBuf, folder, ef.BlocksHash, name)
+			if err != nil {
+				return nil, err
+			}
+			if err = t.Delete(keyBuf); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	keyBuf, err = db.keyer.GenerateSequenceKey(keyBuf, folder, ef.SequenceNo())
+	if err != nil {
+		return nil, err
+	}
+	if err := t.Delete(keyBuf); err != nil {
+		return nil, err
+	}
+	l.Debugf("removing sequence; folder=%q sequence=%v %v", folder, ef.SequenceNo(), ef.FileName())
+	return keyBuf, nil
+}
+
 func (db *Lowlevel) dropFolder(folder []byte) error {
 	db.gcMut.RLock()
 	defer db.gcMut.RUnlock()

+ 16 - 0
lib/db/set.go

@@ -144,6 +144,22 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
 	}
 }
 
+func (s *FileSet) RemoveLocalItems(items []string) {
+	opStr := fmt.Sprintf("%s RemoveLocalItems([%d])", s.folder, len(items))
+	l.Debugf(opStr)
+
+	s.updateMutex.Lock()
+	defer s.updateMutex.Unlock()
+
+	for i := range items {
+		items[i] = osutil.NormalizedFilename(items[i])
+	}
+
+	if err := s.db.removeLocalFiles([]byte(s.folder), items, s.meta); err != nil && !backend.IsClosed(err) {
+		fatalError(err, opStr, s.db)
+	}
+}
+
 type Snapshot struct {
 	folder     string
 	t          readOnlyTransaction

+ 1 - 0
lib/model/fakeconns_test.go

@@ -162,6 +162,7 @@ func (f *fakeConnection) sendIndexUpdate() {
 
 func addFakeConn(m *testModel, dev protocol.DeviceID, folderID string) *fakeConnection {
 	fc := newFakeConnection(dev, m)
+	fc.folder = folderID
 	m.AddConnection(fc, protocol.Hello{})
 
 	m.ClusterConfig(dev, protocol.ClusterConfig{

+ 56 - 28
lib/model/folder.go

@@ -532,16 +532,20 @@ func (f *folder) scanSubdirs(subDirs []string) error {
 	return nil
 }
 
+const maxToRemove = 1000
+
 type scanBatch struct {
-	*db.FileInfoBatch
-	f *folder
+	f           *folder
+	updateBatch *db.FileInfoBatch
+	toRemove    []string
 }
 
 func (f *folder) newScanBatch() *scanBatch {
 	b := &scanBatch{
-		f: f,
+		f:        f,
+		toRemove: make([]string, 0, maxToRemove),
 	}
-	b.FileInfoBatch = db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
+	b.updateBatch = db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
 		if err := b.f.getHealthErrorWithoutIgnores(); err != nil {
 			l.Debugf("Stopping scan of folder %s due to: %s", b.f.Description(), err)
 			return err
@@ -552,9 +556,32 @@ func (f *folder) newScanBatch() *scanBatch {
 	return b
 }
 
-// Append adds the fileinfo to the batch for updating, and does a few checks.
+func (b *scanBatch) Remove(item string) {
+	b.toRemove = append(b.toRemove, item)
+}
+
+func (b *scanBatch) flushToRemove() {
+	if len(b.toRemove) > 0 {
+		b.f.fset.RemoveLocalItems(b.toRemove)
+		b.toRemove = b.toRemove[:0]
+	}
+}
+
+func (b *scanBatch) Flush() error {
+	b.flushToRemove()
+	return b.updateBatch.Flush()
+}
+
+func (b *scanBatch) FlushIfFull() error {
+	if len(b.toRemove) >= maxToRemove {
+		b.flushToRemove()
+	}
+	return b.updateBatch.FlushIfFull()
+}
+
+// Update adds the fileinfo to the batch for updating, and does a few checks.
 // It returns false if the checks result in the file not going to be updated or removed.
-func (b *scanBatch) Append(fi protocol.FileInfo, snap *db.Snapshot) bool {
+func (b *scanBatch) Update(fi protocol.FileInfo, snap *db.Snapshot) bool {
 	// Check for a "virtual" parent directory of encrypted files. We don't track
 	// it, but check if anything still exists within and delete it otherwise.
 	if b.f.Type == config.FolderTypeReceiveEncrypted && fi.IsDirectory() && protocol.IsEncryptedParent(fs.PathComponents(fi.Name)) {
@@ -565,20 +592,21 @@ func (b *scanBatch) Append(fi protocol.FileInfo, snap *db.Snapshot) bool {
 	}
 	// Resolve receive-only items which are identical with the global state or
 	// the global item is our own receive-only item.
-	// Except if they are in a receive-encrypted folder and are locally added.
-	// Those must never be sent in index updates and thus must retain the flag.
 	switch gf, ok := snap.GetGlobal(fi.Name); {
 	case !ok:
 	case gf.IsReceiveOnlyChanged():
-		if b.f.Type == config.FolderTypeReceiveOnly && fi.Deleted {
-			l.Debugf("%v scanning: Marking deleted item as not locally changed", b.f, fi)
-			fi.LocalFlags &^= protocol.FlagLocalReceiveOnly
+		if fi.IsDeleted() {
+			// Our item is deleted and the global item is our own receive only
+			// file. No point in keeping track of that.
+			b.Remove(fi.Name)
+			return true
 		}
 	case gf.IsEquivalentOptional(fi, b.f.modTimeWindow, false, false, protocol.FlagLocalReceiveOnly):
-		l.Debugf("%v scanning: Replacing scanned file info with global as it's equivalent", b.f, fi)
+		// What we have locally is equivalent to the global file.
+		l.Debugf("%v scanning: Merging identical locally changed item with global", b.f, fi)
 		fi = gf
 	}
-	b.FileInfoBatch.Append(fi)
+	b.updateBatch.Append(fi)
 	return true
 }
 
@@ -634,7 +662,7 @@ func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (i
 			return changes, err
 		}
 
-		if batch.Append(res.File, snap) {
+		if batch.Update(res.File, snap) {
 			changes++
 		}
 
@@ -642,7 +670,7 @@ func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (i
 		case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
 		default:
 			if nf, ok := f.findRename(snap, res.File, alreadyUsedOrExisting); ok {
-				if batch.Append(nf, snap) {
+				if batch.Update(nf, snap) {
 					changes++
 				}
 			}
@@ -683,7 +711,7 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 				for _, file := range toIgnore {
 					l.Debugln("marking file as ignored", file)
 					nf := file.ConvertToIgnoredFileInfo()
-					if batch.Append(nf, snap) {
+					if batch.Update(nf, snap) {
 						changes++
 					}
 					if err := batch.FlushIfFull(); err != nil {
@@ -713,7 +741,7 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 
 				l.Debugln("marking file as ignored", file)
 				nf := file.ConvertToIgnoredFileInfo()
-				if batch.Append(nf, snap) {
+				if batch.Update(nf, snap) {
 					changes++
 				}
 
@@ -743,24 +771,24 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 					nf.Version = protocol.Vector{}
 				}
 				l.Debugln("marking file as deleted", nf)
-				if batch.Append(nf, snap) {
+				if batch.Update(nf, snap) {
 					changes++
 				}
 			case file.IsDeleted() && file.IsReceiveOnlyChanged():
 				switch f.Type {
 				case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
-					if gf, _ := snap.GetGlobal(file.Name); gf.IsDeleted() {
+					switch gf, ok := snap.GetGlobal(file.Name); {
+					case !ok:
+					case gf.IsReceiveOnlyChanged():
+						l.Debugln("removing deleted, receive-only item that is globally receive-only from db", file)
+						batch.Remove(file.Name)
+						changes++
+					case gf.IsDeleted():
 						// Our item is deleted and the global item is deleted too. We just
 						// pretend it is a normal deleted file (nobody cares about that).
-						// Except if this is a receive-encrypted folder and it
-						// is a locally added file. Those must never be sent
-						// in index updates and thus must retain the flag.
-						if f.Type == config.FolderTypeReceiveEncrypted && gf.IsReceiveOnlyChanged() {
-							return true
-						}
 						l.Debugf("%v scanning: Marking globally deleted item as not locally changed: %v", f, file.Name)
 						file.LocalFlags &^= protocol.FlagLocalReceiveOnly
-						if batch.Append(file.ConvertDeletedToFileInfo(), snap) {
+						if batch.Update(file.ConvertDeletedToFileInfo(), snap) {
 							changes++
 						}
 					}
@@ -769,7 +797,7 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 					// deleted and just the folder type/local flags changed.
 					file.LocalFlags &^= protocol.FlagLocalReceiveOnly
 					l.Debugln("removing receive-only flag on deleted item", file)
-					if batch.Append(file.ConvertDeletedToFileInfo(), snap) {
+					if batch.Update(file.ConvertDeletedToFileInfo(), snap) {
 						changes++
 					}
 				}
@@ -788,7 +816,7 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 			for _, file := range toIgnore {
 				l.Debugln("marking file as ignored", f)
 				nf := file.ConvertToIgnoredFileInfo()
-				if batch.Append(nf, snap) {
+				if batch.Update(nf, snap) {
 					changes++
 				}
 				if iterError = batch.FlushIfFull(); iterError != nil {

+ 11 - 26
lib/model/folder_recvonly_test.go

@@ -37,9 +37,9 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 	for _, dir := range []string{".stfolder", "ignDir", "unknownDir"} {
 		must(t, ffs.MkdirAll(dir, 0755))
 	}
-	must(t, writeFile(ffs, "ignDir/ignFile", []byte("hello\n"), 0644))
-	must(t, writeFile(ffs, "unknownDir/unknownFile", []byte("hello\n"), 0644))
-	must(t, writeFile(ffs, ".stignore", []byte("ignDir\n"), 0644))
+	writeFilePerm(t, ffs, "ignDir/ignFile", []byte("hello\n"), 0644)
+	writeFilePerm(t, ffs, "unknownDir/unknownFile", []byte("hello\n"), 0644)
+	writeFilePerm(t, ffs, ".stignore", []byte("ignDir\n"), 0644)
 
 	knownFiles := setupKnownFiles(t, ffs, []byte("hello\n"))
 
@@ -151,7 +151,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 	// Update the file.
 
 	newData := []byte("totally different data\n")
-	must(t, writeFile(ffs, "knownDir/knownFile", newData, 0644))
+	writeFilePerm(t, ffs, "knownDir/knownFile", newData, 0644)
 
 	// Rescan.
 
@@ -241,8 +241,8 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 	// Create a file and modify another
 
 	const file = "foo"
-	must(t, writeFile(ffs, file, []byte("hello\n"), 0644))
-	must(t, writeFile(ffs, "knownDir/knownFile", []byte("bye\n"), 0644))
+	writeFilePerm(t, ffs, file, []byte("hello\n"), 0644)
+	writeFilePerm(t, ffs, "knownDir/knownFile", []byte("bye\n"), 0644)
 
 	must(t, m.ScanFolder("ro"))
 
@@ -254,7 +254,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 	// Remove the file again and undo the modification
 
 	must(t, ffs.Remove(file))
-	must(t, writeFile(ffs, "knownDir/knownFile", oldData, 0644))
+	writeFilePerm(t, ffs, "knownDir/knownFile", oldData, 0644)
 	must(t, ffs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime()))
 
 	must(t, m.ScanFolder("ro"))
@@ -377,8 +377,8 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 
 	const file = "foo"
 	knownFile := filepath.Join("knownDir", "knownFile")
-	must(t, writeFile(ffs, file, []byte("hello\n"), 0644))
-	must(t, writeFile(ffs, knownFile, []byte("bye\n"), 0644))
+	writeFilePerm(t, ffs, file, []byte("hello\n"), 0644)
+	writeFilePerm(t, ffs, knownFile, []byte("bye\n"), 0644)
 
 	must(t, m.ScanFolder("ro"))
 
@@ -434,7 +434,7 @@ func TestRecvOnlyRevertOwnID(t *testing.T) {
 	must(t, ffs.MkdirAll(".stfolder", 0755))
 	data := []byte("hello\n")
 	name := "foo"
-	must(t, writeFile(ffs, name, data, 0644))
+	writeFilePerm(t, ffs, name, data, 0644)
 
 	// Make sure the file is scanned and locally changed
 	must(t, m.ScanFolder("ro"))
@@ -484,7 +484,7 @@ func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.Fi
 	t.Helper()
 
 	must(t, ffs.MkdirAll("knownDir", 0755))
-	must(t, writeFile(ffs, "knownDir/knownFile", data, 0644))
+	writeFilePerm(t, ffs, "knownDir/knownFile", data, 0644)
 
 	t0 := time.Now().Add(-1 * time.Minute)
 	must(t, ffs.Chtimes("knownDir/knownFile", t0, t0))
@@ -541,18 +541,3 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.Cancel
 
 	return m, f, cancel
 }
-
-func writeFile(fs fs.Filesystem, filename string, data []byte, perm fs.FileMode) error {
-	fd, err := fs.Create(filename)
-	if err != nil {
-		return err
-	}
-	_, err = fd.Write(data)
-	if err != nil {
-		return err
-	}
-	if err := fd.Close(); err != nil {
-		return err
-	}
-	return fs.Chmod(filename, perm)
-}

+ 7 - 9
lib/model/folder_sendrecv_test.go

@@ -77,12 +77,10 @@ func setupFile(filename string, blockNumbers []int) protocol.FileInfo {
 	}
 }
 
-func createFile(t *testing.T, name string, fs fs.Filesystem) protocol.FileInfo {
+func createEmptyFileInfo(t *testing.T, name string, fs fs.Filesystem) protocol.FileInfo {
 	t.Helper()
 
-	f, err := fs.Create(name)
-	must(t, err)
-	f.Close()
+	writeFile(t, fs, name, nil)
 	fi, err := fs.Stat(name)
 	must(t, err)
 	file, err := scanner.CreateFileInfo(fi, name, fs)
@@ -913,7 +911,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
 	name := "foo"
 
 	// create local file
-	file := createFile(t, name, ffs)
+	file := createEmptyFileInfo(t, name, ffs)
 	file.Version = protocol.Vector{}.Update(myID.Short())
 	f.updateLocalsFromScanning([]protocol.FileInfo{file})
 
@@ -945,7 +943,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
 	name := "foo"
 
 	// create local file
-	file := createFile(t, name, ffs)
+	file := createEmptyFileInfo(t, name, ffs)
 	file.Version = protocol.Vector{}.Update(myID.Short())
 	f.updateLocalsFromScanning([]protocol.FileInfo{file})
 
@@ -983,7 +981,7 @@ func TestDeleteBehindSymlink(t *testing.T) {
 	file := filepath.Join(link, "file")
 
 	must(t, ffs.MkdirAll(link, 0755))
-	fi := createFile(t, file, ffs)
+	fi := createEmptyFileInfo(t, file, ffs)
 	f.updateLocalsFromScanning([]protocol.FileInfo{fi})
 	must(t, osutil.RenameOrCopy(fs.CopyRangeMethodStandard, ffs, destFs, file, "file"))
 	must(t, ffs.RemoveAll(link))
@@ -1099,7 +1097,7 @@ func TestPullCaseOnlyPerformFinish(t *testing.T) {
 
 	name := "foo"
 	contents := []byte("contents")
-	must(t, writeFile(ffs, name, contents, 0644))
+	writeFile(t, ffs, name, contents)
 	must(t, f.scanSubdirs(nil))
 
 	var cur protocol.FileInfo
@@ -1122,7 +1120,7 @@ func TestPullCaseOnlyPerformFinish(t *testing.T) {
 	remote.Version = protocol.Vector{}.Update(device1.Short())
 	remote.Name = strings.ToUpper(cur.Name)
 	temp := fs.TempName(remote.Name)
-	must(t, writeFile(ffs, temp, contents, 0644))
+	writeFile(t, ffs, temp, contents)
 	scanChan := make(chan string, 1)
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 

+ 1 - 3
lib/model/model.go

@@ -1053,7 +1053,6 @@ func (m *model) RemoteNeedFolderFiles(folder string, device protocol.DeviceID, p
 func (m *model) LocalChangedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
 	m.fmut.RLock()
 	rf, ok := m.folderFiles[folder]
-	cfg := m.folderCfgs[folder]
 	m.fmut.RUnlock()
 
 	if !ok {
@@ -1071,11 +1070,10 @@ func (m *model) LocalChangedFolderFiles(folder string, page, perpage int) ([]db.
 	}
 
 	p := newPager(page, perpage)
-	recvEnc := cfg.Type == config.FolderTypeReceiveEncrypted
 	files := make([]db.FileInfoTruncated, 0, perpage)
 
 	snap.WithHaveTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
-		if !f.IsReceiveOnlyChanged() || (recvEnc && f.IsDeleted()) {
+		if !f.IsReceiveOnlyChanged() {
 			return true
 		}
 		if p.skip() {

+ 57 - 17
lib/model/model_test.go

@@ -299,7 +299,7 @@ func BenchmarkRequestInSingleFile(b *testing.B) {
 	mustRemove(b, defaultFs.RemoveAll("request"))
 	defer func() { mustRemove(b, defaultFs.RemoveAll("request")) }()
 	must(b, defaultFs.MkdirAll("request/for/a/file/in/a/couple/of/dirs", 0755))
-	writeFile(defaultFs, "request/for/a/file/in/a/couple/of/dirs/128k", buf, 0644)
+	writeFile(b, defaultFs, "request/for/a/file/in/a/couple/of/dirs/128k", buf)
 
 	b.ResetTimer()
 
@@ -1489,7 +1489,7 @@ func TestIgnores(t *testing.T) {
 	// Assure a clean start state
 	mustRemove(t, defaultFs.RemoveAll(config.DefaultMarkerName))
 	mustRemove(t, defaultFs.MkdirAll(config.DefaultMarkerName, 0644))
-	writeFile(defaultFs, ".stignore", []byte(".*\nquux\n"), 0644)
+	writeFile(t, defaultFs, ".stignore", []byte(".*\nquux\n"))
 
 	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
@@ -2030,8 +2030,8 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
 func TestIssue3028(t *testing.T) {
 	// Create two files that we'll delete, one with a name that is a prefix of the other.
 
-	must(t, writeFile(defaultFs, "testrm", []byte("Hello"), 0644))
-	must(t, writeFile(defaultFs, "testrm2", []byte("Hello"), 0644))
+	writeFile(t, defaultFs, "testrm", []byte("Hello"))
+	writeFile(t, defaultFs, "testrm2", []byte("Hello"))
 	defer func() {
 		mustRemove(t, defaultFs.Remove("testrm"))
 		mustRemove(t, defaultFs.Remove("testrm2"))
@@ -3403,7 +3403,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 	ffs := fcfg.Filesystem()
 	for i := 0; i < numFiles; i++ {
 		v := fmt.Sprintf("%d", i)
-		must(t, writeFile(ffs, v, []byte(v), 0644))
+		writeFile(t, ffs, v, []byte(v))
 	}
 
 	m.ScanFolders()
@@ -3426,7 +3426,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 			continue
 		}
 		v := fmt.Sprintf("%d", i)
-		must(t, writeFile(ffs, v, []byte(v+"-new"), 0644))
+		writeFile(t, ffs, v, []byte(v+"-new"))
 	}
 	// Rename
 	must(t, ffs.Rename("3", "17"))
@@ -3470,7 +3470,7 @@ func TestRenameSameFile(t *testing.T) {
 	defer cleanupModel(m)
 
 	ffs := fcfg.Filesystem()
-	must(t, writeFile(ffs, "file", []byte("file"), 0644))
+	writeFile(t, ffs, "file", []byte("file"))
 
 	m.ScanFolders()
 
@@ -3522,8 +3522,8 @@ func TestRenameEmptyFile(t *testing.T) {
 
 	ffs := fcfg.Filesystem()
 
-	must(t, writeFile(ffs, "file", []byte("data"), 0644))
-	must(t, writeFile(ffs, "empty", nil, 0644))
+	writeFile(t, ffs, "file", []byte("data"))
+	writeFile(t, ffs, "empty", nil)
 
 	m.ScanFolders()
 
@@ -3598,11 +3598,11 @@ func TestBlockListMap(t *testing.T) {
 	defer cleanupModel(m)
 
 	ffs := fcfg.Filesystem()
-	must(t, writeFile(ffs, "one", []byte("content"), 0644))
-	must(t, writeFile(ffs, "two", []byte("content"), 0644))
-	must(t, writeFile(ffs, "three", []byte("content"), 0644))
-	must(t, writeFile(ffs, "four", []byte("content"), 0644))
-	must(t, writeFile(ffs, "five", []byte("content"), 0644))
+	writeFile(t, ffs, "one", []byte("content"))
+	writeFile(t, ffs, "two", []byte("content"))
+	writeFile(t, ffs, "three", []byte("content"))
+	writeFile(t, ffs, "four", []byte("content"))
+	writeFile(t, ffs, "five", []byte("content"))
 
 	m.ScanFolders()
 
@@ -3631,7 +3631,7 @@ func TestBlockListMap(t *testing.T) {
 
 	// Modify
 	must(t, ffs.Remove("two"))
-	must(t, writeFile(ffs, "two", []byte("mew-content"), 0644))
+	writeFile(t, ffs, "two", []byte("mew-content"))
 
 	// Rename
 	must(t, ffs.Rename("three", "new-three"))
@@ -3667,7 +3667,7 @@ func TestScanRenameCaseOnly(t *testing.T) {
 
 	ffs := fcfg.Filesystem()
 	name := "foo"
-	must(t, writeFile(ffs, name, []byte("contents"), 0644))
+	writeFile(t, ffs, name, []byte("contents"))
 
 	m.ScanFolders()
 
@@ -3791,7 +3791,7 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
 
 	name := "foo"
 
-	must(t, writeFile(ffs, name, []byte(name), 0644))
+	writeFile(t, ffs, name, []byte(name))
 	m.ScanFolders()
 
 	file, ok := m.testCurrentFolderFile(fcfg.ID, name)
@@ -4255,6 +4255,46 @@ func TestPendingFolder(t *testing.T) {
 	}
 }
 
+func TestDeletedNotLocallyChangedReceiveOnly(t *testing.T) {
+	deletedNotLocallyChanged(t, config.FolderTypeReceiveOnly)
+}
+
+func TestDeletedNotLocallyChangedReceiveEncrypted(t *testing.T) {
+	deletedNotLocallyChanged(t, config.FolderTypeReceiveEncrypted)
+}
+
+func deletedNotLocallyChanged(t *testing.T, ft config.FolderType) {
+	w, fcfg, wCancel := tmpDefaultWrapper()
+	tfs := fcfg.Filesystem()
+	fcfg.Type = ft
+	setFolder(t, w, fcfg)
+	defer wCancel()
+	m := setupModel(t, w)
+	defer cleanupModelAndRemoveDir(m, tfs.URI())
+
+	name := "foo"
+	writeFile(t, tfs, name, nil)
+	must(t, m.ScanFolder(fcfg.ID))
+
+	fi, ok, err := m.CurrentFolderFile(fcfg.ID, name)
+	must(t, err)
+	if !ok {
+		t.Fatal("File hasn't been added")
+	}
+	if !fi.IsReceiveOnlyChanged() {
+		t.Fatal("File isn't receive-only-changed")
+	}
+
+	must(t, tfs.Remove(name))
+	must(t, m.ScanFolder(fcfg.ID))
+
+	_, ok, err = m.CurrentFolderFile(fcfg.ID, name)
+	must(t, err)
+	if ok {
+		t.Error("Expected file to be removed from db")
+	}
+}
+
 func equalStringsInAnyOrder(a, b []string) bool {
 	if len(a) != len(b) {
 		return false

+ 12 - 8
lib/model/requests_test.go

@@ -330,9 +330,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 	fc.deleteFile(invDel)
 	fc.addFile(ign, 0644, protocol.FileInfoTypeFile, contents)
 	fc.addFile(ignExisting, 0644, protocol.FileInfoTypeFile, contents)
-	if err := writeFile(fss, ignExisting, otherContents, 0644); err != nil {
-		panic(err)
-	}
+	writeFile(t, fss, ignExisting, otherContents)
 
 	done := make(chan struct{})
 	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
@@ -486,7 +484,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
 
 	payload := []byte("hello")
 
-	must(t, writeFile(tfs, "foo", payload, 0777))
+	writeFile(t, tfs, "foo", payload)
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
@@ -526,7 +524,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
 	payload = []byte("bye")
 	buf = make([]byte, len(payload))
 
-	must(t, writeFile(tfs, "foo", payload, 0777))
+	writeFile(t, tfs, "foo", payload)
 
 	_, err = m.Request(device1, "default", "foo", 0, int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
 	if err == nil {
@@ -1066,9 +1064,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 		return nil
 	})
 
-	if err := writeFile(fss, file, contents, 0644); err != nil {
-		panic(err)
-	}
+	writeFile(t, fss, file, contents)
 	m.ScanFolders()
 
 	select {
@@ -1430,6 +1426,14 @@ func TestRequestReceiveEncrypted(t *testing.T) {
 	// Simulate request from device that is untrusted too, i.e. with non-empty, but garbage hash
 	_, err := m.Request(device1, fcfg.ID, name, 0, 1064, 0, []byte("garbage"), 0, false)
 	must(t, err)
+
+	changed, err := m.LocalChangedFolderFiles(fcfg.ID, 1, 10)
+	must(t, err)
+	if l := len(changed); l != 1 {
+		t.Errorf("Expected one locally changed file, got %v", l)
+	} else if changed[0].Name != files[0].Name {
+		t.Errorf("Expected %v, got %v", files[0].Name, changed[0].Name)
+	}
 }
 
 func TestRequestGlobalInvalidToValid(t *testing.T) {

+ 15 - 0
lib/model/testutils_test.go

@@ -435,3 +435,18 @@ func addDevice2(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration)
 	must(t, err)
 	waiter.Wait()
 }
+
+func writeFile(t testing.TB, filesystem fs.Filesystem, name string, data []byte) {
+	t.Helper()
+	fd, err := filesystem.Create(name)
+	must(t, err)
+	defer fd.Close()
+	_, err = fd.Write(data)
+	must(t, err)
+}
+
+func writeFilePerm(t testing.TB, filesystem fs.Filesystem, name string, data []byte, perm fs.FileMode) {
+	t.Helper()
+	writeFile(t, filesystem, name, data)
+	must(t, filesystem.Chmod(name, perm))
+}