From c9236b1adc5368d46c020d2df1b2698745a7dcf8 Mon Sep 17 00:00:00 2001 From: Simon Frei Date: Mon, 22 Jun 2026 20:32:03 +0200 Subject: [PATCH] chore: stop treating dirs as having size 128 (#10750) Resp. directories for the database, and both dirs and symlinks for `FileInfo.FileSize`. Instead handle it in the UI progress percentage. We even already have special cases there for deletions, might as well handle directories just like any other zero-sized needed item there. This went through the very thorough testing of running it on my laptop, the migration was applied and it seemed to be working fine after. --------- Signed-off-by: Simon Frei --- .../syncthing/core/syncthingController.js | 16 ++++++++-------- internal/db/sqlite/db_test.go | 7 +++---- internal/db/sqlite/folderdb_update.go | 4 ---- .../sql/migrations/folder/06-zero-size-dirs.sql | 12 ++++++++++++ internal/db/sqlite/util.go | 4 ++++ lib/api/api.go | 2 +- lib/model/folder_recvonly_test.go | 9 ++++----- lib/model/model_test.go | 6 ++++-- lib/protocol/bep_fileinfo.go | 10 ---------- lib/protocol/protocol.go | 5 +++-- lib/protocol/protocol_test.go | 4 ++-- lib/scanner/walk_test.go | 6 +++--- 12 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 internal/db/sqlite/sql/migrations/folder/06-zero-size-dirs.sql diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index f8b388065..d232cf62f 100644 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -1139,20 +1139,20 @@ angular.module('syncthing.core') }; $scope.syncPercentage = function (folder) { - if (typeof $scope.model[folder] === 'undefined') { + var model = $scope.model[folder]; + if (typeof model === 'undefined') { return 100; } - if ($scope.model[folder].needTotalItems === 0) { + if (model.needTotalItems === 0) { return 100; } - if (($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) || $scope.model[folder].globalBytes == 0) { - // We don't need any data, but we have deletes that we need - // to do. Drop down the completion percentage to indicate - // that we have stuff to do. - // Do the same thing in case we only have zero byte files to sync. + if (model.needBytes == 0 && model.needTotalItems > 0) { + // We don't need any data, but we have deletes, directories, + // symlinks or zero byte files that we need to do. Drop down the + // completion percentage to indicate that we have stuff to do. return 95; } - return progressIntegerPercentage($scope.model[folder].inSyncBytes, $scope.model[folder].globalBytes); + return progressIntegerPercentage(model.inSyncBytes, model.globalBytes); }; $scope.scanPercentage = function (folder) { diff --git a/internal/db/sqlite/db_test.go b/internal/db/sqlite/db_test.go index bc85ae7e7..fdb592b38 100644 --- a/internal/db/sqlite/db_test.go +++ b/internal/db/sqlite/db_test.go @@ -29,7 +29,6 @@ import ( const ( folderID = "test" blockSize = 128 << 10 - dirSize = 128 ) func TestBasics(t *testing.T) { @@ -72,11 +71,11 @@ func TestBasics(t *testing.T) { t.Fatal(err) } const ( - localSize = (1+2+3)*blockSize + dirSize + localSize = (1+2+3)*blockSize remoteSize = (3 + 4 + 5) * blockSize - globalSize = (2+3+3+4+5)*blockSize + dirSize + globalSize = (2+3+3+4+5)*blockSize needSizeLocal = remoteSize - needSizeRemote = (2+3)*blockSize + dirSize + needSizeRemote = (2+3)*blockSize ) t.Run("SchemaVersion", func(t *testing.T) { diff --git a/internal/db/sqlite/folderdb_update.go b/internal/db/sqlite/folderdb_update.go index 56ff37216..b6f9e53bc 100644 --- a/internal/db/sqlite/folderdb_update.go +++ b/internal/db/sqlite/folderdb_update.go @@ -111,10 +111,6 @@ func (s *folderDB) Update(device protocol.DeviceID, fs []protocol.FileInfo, opti f.BlocksHash = nil } - if f.Type == protocol.FileInfoTypeDirectory { - f.Size = 128 // synthetic directory size - } - // Insert the file. // // If it is a remote file, set remote_sequence otherwise leave it at diff --git a/internal/db/sqlite/sql/migrations/folder/06-zero-size-dirs.sql b/internal/db/sqlite/sql/migrations/folder/06-zero-size-dirs.sql new file mode 100644 index 000000000..38122c0cf --- /dev/null +++ b/internal/db/sqlite/sql/migrations/folder/06-zero-size-dirs.sql @@ -0,0 +1,12 @@ +-- Copyright (C) 2026 The Syncthing Authors. +-- +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at https://mozilla.org/MPL/2.0/. + +-- All non-file entries should have size zero. +-- Directories were previously stored with a "synthetic" size of 128. +UPDATE files + SET size = 0 + WHERE type != 0 +; diff --git a/internal/db/sqlite/util.go b/internal/db/sqlite/util.go index dfed2857c..3c5cb91b9 100644 --- a/internal/db/sqlite/util.go +++ b/internal/db/sqlite/util.go @@ -106,6 +106,10 @@ func (i indirectFI) FileInfo() (protocol.FileInfo, error) { fi.Blocks = bl.Blocks } fi.Name = osutil.NativeFilename(fi.Name) + // Undo earlier behaviour of setting a synthetic size on dirs. + if fi.Type != protocol.FileInfoTypeFile && fi.Size > 0 { + fi.Size = 0 + } return protocol.FileInfoFromDB(&fi), nil } diff --git a/lib/api/api.go b/lib/api/api.go index 046ad33f2..da34ec481 100644 --- a/lib/api/api.go +++ b/lib/api/api.go @@ -1774,7 +1774,7 @@ func fileIntfJSONMap(f protocol.FileInfo) map[string]interface{} { out := map[string]interface{}{ "name": f.FileName(), "type": f.FileType().String(), - "size": f.FileSize(), + "size": f.Size, "deleted": f.IsDeleted(), "invalid": f.IsInvalid(), "ignored": f.IsIgnored(), diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go index e84d0c49d..12916127f 100644 --- a/lib/model/folder_recvonly_test.go +++ b/lib/model/folder_recvonly_test.go @@ -163,12 +163,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) { // We now have a newer file than the rest of the cluster. Global state should reflect this. size = mustV(m.GlobalSize("ro")) - const sizeOfDir = 128 - if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) { + if size.Files != 1 || size.Bytes != int64(len(oldData)) { t.Fatalf("Global: expected no change due to the new file: %+v", size) } size = mustV(m.LocalSize("ro", protocol.LocalDeviceID)) - if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) { + if size.Files != 1 || size.Bytes != int64(len(newData)) { t.Fatalf("Local: expected the new file to be reflected: %+v", size) } size = mustV(m.NeedSize("ro", protocol.LocalDeviceID)) @@ -185,11 +184,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) { m.Revert("ro") size = mustV(m.GlobalSize("ro")) - if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) { + if size.Files != 1 || size.Bytes != int64(len(oldData)) { t.Fatalf("Global: expected the global size to revert: %+v", size) } size = mustV(m.LocalSize("ro", protocol.LocalDeviceID)) - if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) { + if size.Files != 1 || size.Bytes != int64(len(newData)) { t.Fatalf("Local: expected the local size to remain: %+v", size) } size = mustV(m.NeedSize("ro", protocol.LocalDeviceID)) diff --git a/lib/model/model_test.go b/lib/model/model_test.go index 3eb65fd25..54da73943 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -1787,10 +1787,12 @@ func TestGlobalDirectoryTree(t *testing.T) { b := func(isfile bool, path ...string) protocol.FileInfo { typ := protocol.FileInfoTypeDirectory var blocks []protocol.BlockInfo + var size int64 if isfile { typ = protocol.FileInfoTypeFile blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}} + size = 0xa } seq++ return protocol.FileInfo{ @@ -1798,7 +1800,7 @@ func TestGlobalDirectoryTree(t *testing.T) { Type: typ, ModifiedS: 0x666, Blocks: blocks, - Size: 0xa, + Size: size, Sequence: seq, } } @@ -1814,7 +1816,7 @@ func TestGlobalDirectoryTree(t *testing.T) { return &TreeEntry{ Name: name, ModTime: time.Unix(0x666, 0), - Size: 128, + Size: 0, Type: protocol.FileInfoTypeDirectory.String(), Children: entries, } diff --git a/lib/protocol/bep_fileinfo.go b/lib/protocol/bep_fileinfo.go index 3f6b5e088..329ce5a58 100644 --- a/lib/protocol/bep_fileinfo.go +++ b/lib/protocol/bep_fileinfo.go @@ -389,16 +389,6 @@ func (f FileInfo) HasPermissionBits() bool { return !f.NoPermissions } -func (f FileInfo) FileSize() int64 { - if f.Deleted { - return 0 - } - if f.IsDirectory() || f.IsSymlink() { - return SyntheticDirectorySize - } - return f.Size -} - func (f FileInfo) BlockSize() int { if f.RawBlockSize < MinBlockSize { return MinBlockSize diff --git a/lib/protocol/protocol.go b/lib/protocol/protocol.go index 9204ef35b..c28d7d5ce 100644 --- a/lib/protocol/protocol.go +++ b/lib/protocol/protocol.go @@ -56,7 +56,8 @@ const ( // DesiredPerFileBlocks is the number of blocks we aim for per file DesiredPerFileBlocks = 2000 - SyntheticDirectorySize = 128 + // We used to send this size for directories and still accept that for compat. + deprecatedSyntheticDirectorySize = 128 // don't bother compressing messages smaller than this many bytes compressionThreshold = 128 @@ -640,7 +641,7 @@ func checkFileInfoConsistency(f FileInfo) error { // Only files should have blocks return errNonFileHasBlocks - case f.IsDirectory() && f.Size != 0 && f.Size != SyntheticDirectorySize: + case f.IsDirectory() && f.Size != 0 && f.Size != deprecatedSyntheticDirectorySize: // Directories should be size zero or the synthetic directory size return errNonFileHasSize diff --git a/lib/protocol/protocol_test.go b/lib/protocol/protocol_test.go index 1bba358b3..6ac5175c2 100644 --- a/lib/protocol/protocol_test.go +++ b/lib/protocol/protocol_test.go @@ -464,7 +464,7 @@ func TestCheckConsistency(t *testing.T) { fi: FileInfo{ Name: "foo", Type: FileInfoTypeDirectory, - Size: SyntheticDirectorySize, + Size: deprecatedSyntheticDirectorySize, }, ok: true, }, @@ -492,7 +492,7 @@ func TestCheckConsistency(t *testing.T) { Name: "foo", Type: FileInfoTypeSymlink, SymlinkTarget: []byte("bar"), - Size: SyntheticDirectorySize, + Size: deprecatedSyntheticDirectorySize, }, ok: false, }, diff --git a/lib/scanner/walk_test.go b/lib/scanner/walk_test.go index d75dddaf3..699772f97 100644 --- a/lib/scanner/walk_test.go +++ b/lib/scanner/walk_test.go @@ -41,9 +41,9 @@ type testfileList []testfile var testdata = testfileList{ {"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"}, - {"dir1", 128, ""}, + {"dir1", 0, ""}, {filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"}, - {"dir2", 128, ""}, + {"dir2", 0, ""}, {filepath.Join("dir2", "cfile"), 4, "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"}, {"excludes", 37, "df90b52f0c55dba7a7a940affe482571563b1ac57bd5be4d8a0291e7de928e06"}, {"further-excludes", 5, "7eb0a548094fa6295f7fd9200d69973e5f5ec5c04f2a86d998080ac43ecf89f1"}, @@ -601,7 +601,7 @@ func (l fileList) testfiles() testfileList { if len(f.Blocks) > 1 { panic("simple test case stuff only supports a single block per file") } - testfiles[i] = testfile{name: f.Name, length: f.FileSize()} + testfiles[i] = testfile{name: f.Name, length: f.Size} if len(f.Blocks) == 1 { testfiles[i].hash = fmt.Sprintf("%x", f.Blocks[0].Hash) }