Skip to content

Invalidate the discovery state when hidden files are enabled - #1029

Merged
TheOneRing merged 7 commits into
opencloud-eu:mainfrom
junkerderprovinz:fix/hidden-files-not-synced-on-enable
Sep 30, 2026
Merged

TheOneRing merged 7 commits into
opencloud-eu:mainfrom
junkerderprovinz:fix/hidden-files-not-synced-on-enable

Conversation

@junkerderprovinz

@junkerderprovinz junkerderprovinz commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1127. Part of #714.

Turning on "Sync hidden files" only changed the folder definition and scheduled nothing. When a sync did run, hidden items in the sync root were found, but hidden items inside folders that had already been synced were still skipped:

  • Remote (Client does not sync hidden files inside folders #1127): the folder's etag did not change, so discovery does not list it again. Force sync does not help, it only asks for a full local discovery.
  • Local: an incremental local discovery only looks at the paths the watcher reported and the folders above them, so hidden items elsewhere wait for the next full local discovery, up to about an hour by default.

Folder::setIgnoreHiddenFiles() now does what IgnoreListEditor::slotUpdateLocalIgnoreList() does after an ignore list change: forceRemoteDiscoveryNextSync() plus a full local discovery. When hidden files are turned on, FolderMan::setIgnoreHiddenFiles() also enqueues the affected folders. Turning the setting off does nothing extra, since there is nothing new to find.

What stays open from #714: on Linux, a hidden folder that existed when the watcher started has no inotify watch, so later edits inside it still wait for the next full local discovery or a restart. That needs a watcher change and is not part of this PR.

Tests:

  • TestFolderMan::testSetIgnoreHiddenFiles: turning hidden files on through FolderMan invalidates the stored folder etags. It fails without the forceRemoteDiscoveryNextSync() call.
  • TestSyncEngine::testRemoteHiddenFileInFolderNeedsRemoteDiscovery: at engine level, a remote hidden file in a synced folder is still missing after a full local discovery and arrives after forceRemoteDiscoveryNextSync().
  • TestLocalDiscovery::testHiddenFilesNeedFullLocalDiscovery: a local hidden folder in a folder the watcher did not report is only uploaded by a full local discovery.

Built on Linux with opencloudeu/desktop-client-build:ubuntu-24.04-qt6.10, all 35 tests pass (VFS off only).

A sync that is running while the setting changes can still commit etags after the invalidation. The ignore list editor has the same race, so that is left for a separate issue.


Assisted-by: Claude Code:claude-opus-5
Assisted-by: Claude Code:claude-opus-5-5

@TheOneRing

Copy link
Copy Markdown
Contributor

I mean it's obvious from the number of comments and the length of issue description, but please disclose extensive use of ai in the future.

Comment thread src/gui/folder.cpp Outdated
Comment on lines +337 to +346
if (!_engine) {
// the folder failed to set up, there is no state to invalidate
return;
}

// The engine caches the flag and is otherwise only updated when a sync starts. The folder
// watcher asks the engine whether a path is excluded, so without updating it right here
// changes to hidden files would keep being discarded.
_engine->setIgnoreHiddenFiles(ignore);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, the value is updated before a sync starts see:

_engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right for the sync path itself, since startSync() already refreshes this from _definition every run. Kept it though: the folder watcher reads the engine's cached flag live, any time, not just during a sync, so a file change between the toggle and the next sync needs this pushed immediately or the watcher still filters it out with the stale value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But changing the value here has no effect, as it's not propagated. If you insist that it is so essential to change the value immediately, I'd recommend that you restart the sync instead of just scheduling a new sync.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is propagated, just by live read instead of by push, and the readers sit outside the sync path.

SyncEngine::ignoreHiddenFiles() is a plain member read. SyncEngine::isExcluded() calls it on every invocation and passes the result straight into ExcludedFiles::isExcluded(), which takes excludeHidden as a per-call parameter and does not memoise it. So Folder::isFileExcludedAbsolute(), and with it FolderWatcher::pathIsIgnored(), sees the new value on the very next notification, with no sync involved.

Three readers use it that way:

1. FolderWatcher::addChanges() filters when the OS reports the change, not when a sync pops it:

if (pathIsIgnored(*it)) {
    it = paths.erase(it);
}
...
if (!paths.isEmpty()) {
    _changeSet.unite(paths);
    if (!_timer.isActive()) {
        _timer.start();
        Q_EMIT changesDetected();
    }
}

Erased paths never enter _changeSet, so the popChangeSet() in startSync() has nothing left to recover. That is precisely why the line you linked does not cover this case: it runs after that popChangeSet() call, and the event was already discarded before either of them. The drop also skips Q_EMIT changesDetected(), so no sync is scheduled for that change in the first place, which means there is no next sync to be rescued by.

2. On Linux, FolderWatcherPrivate::slotReceivedNotification():

if ((event->mask & (IN_MOVED_TO | IN_CREATE))
    && QFileInfo(p).isDir()
    && !_parent->pathIsIgnored(p)) {
    slotAddFolderRecursive(p);
}

slotAddFolderRecursive() has exactly two callers: the FolderWatcherPrivate constructor, which is FolderWatcher::init(), and this one. If the flag is stale when a hidden directory is created or moved in, that directory never gets a watch, and nothing re-registers it afterwards. Not startSync(), not a restarted sync, not a full local discovery. Only a new FolderWatcher. This assignment is what keeps newly created hidden directories out of the same permanent gap the pre-existing ones are already in.

3. SyncFileStatusTracker::fileStatus() calls _syncEngine->isExcluded() for the overlay icons, so without the immediate update they keep marking hidden files as excluded until a sync starts.

On Windows and macOS point 2 does not apply, because the watch is recursive from the sync root (ReadDirectoryChangesW with bWatchSubtree set to true, FSEvents on the root). Hidden directories do produce events there, so point 1 is the entire story on those platforms: without this assignment every one of those events is dropped.

The same invariant is already relied on in the Folder constructor at folder.cpp#L93, which performs the identical assignment. If the engine's copy only mattered from startSync() onwards, that line would be dead code too, since L913 sets it from the same _definition on every run. It is not dead: the watcher and the status tracker query the engine before the first sync ever runs. L348 just keeps that invariant across a change.

TestFolderMan::testSetIgnoreHiddenFiles pins the behaviour: immediately after the toggle it asserts !folder->isFileExcludedRelative(QStringLiteral(".hello/Text File.txt")), which is the exact call FolderWatcher::pathIsIgnored() makes. Dropping the assignment fails that test.

On restarting the sync instead: that would do strictly less here. SyncScheduler::terminateCurrentSync() aborts the engine and clears _currentSync, and the following startSync() then performs the identical _engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles), only later and after throwing away in-flight transfers. It never touches inotify registrations, so it does nothing for point 2. And "later" is not just a scheduling tick: SyncScheduler::enqueueFolder() starts nothing while another folder is syncing, startNext() returns early on _currentSync, so the stale window is the running sync plus whatever is queued ahead of this folder.

There is one real thing your suggestion points at, though, and I would rather name it than skip past it. If the toggle lands while a sync is running, that sync can commit fresh directory etags after forceRemoteDiscoveryNextSync() has already written _invalid_, which silently undoes the invalidation. That is pre-existing and identical in IgnoreListEditor::slotUpdateLocalIgnoreList(), which this change mirrors. I am happy to handle it either here, by terminating a running sync for the affected folder before invalidating, or in a separate PR that fixes both call sites. Tell me which you prefer and I will do it.

What did change: the comment above the assignment was misleading. It described the flag as a sync-time thing, which is exactly the reading that makes the line look inert. It now names the actual live readers. The outdated block below it is rewritten as well, in 43652aa.

Comment thread src/gui/folder.cpp Outdated
@TheOneRing

Copy link
Copy Markdown
Contributor

I mean it's obvious from the number of comments and the length of issue description, but please disclose extensive use of ai in the future.

https://github.com/opencloud-eu/.github/blob/main/profile/AI_GUIDELINE.md

@junkerderprovinz

junkerderprovinz commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

You're right, and I should have flagged this upfront. I used Claude Code throughout this work: investigating the stale discovery-state bug, drafting the fix, and writing the tests.

I reviewed and understand every part of the change. The design call was mine: the ignore-hidden-files flag goes stale in three separate places (SyncEngine's cached copy, the folder watcher not watching hidden directories, and local discovery's cached exclude state). Each of those needs to be invalidated and refreshed, not just papered over with a full resync.

Assisted-by: Claude Code:claude-opus-5

I've added that line to the PR description too, and I'll disclose this on opencloud-eu contributions going forward without waiting to be asked.

Comment thread src/gui/folder.cpp Outdated
Comment on lines +337 to +346
if (!_engine) {
// the folder failed to set up, there is no state to invalidate
return;
}

// The engine caches the flag and is otherwise only updated when a sync starts. The folder
// watcher asks the engine whether a path is excluded, so without updating it right here
// changes to hidden files would keep being discarded.
_engine->setIgnoreHiddenFiles(ignore);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But changing the value here has no effect, as it's not propagated. If you insist that it is so essential to change the value immediately, I'd recommend that you restart the sync instead of just scheduling a new sync.

Comment thread src/gui/folder.cpp Outdated
Comment on lines +354 to +362
// On Linux the watcher registers one watch per directory and skipped the hidden ones,
// so directories that already existed while hidden files were ignored stay unwatched
// until the app restarts or something else re-creates the watcher. That gap is real
// but is not fixed here: FolderWatcherPrivate has no destructor on Linux, so calling
// init() again on an existing instance leaks the old inotify fd and every watch on it.
// The full local discovery above still catches up on existing hidden content; only
// live changes inside previously-unwatched hidden directories need the app restart in
// the meantime.
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outdated comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that block still described the FolderWatcher::init() call that was dropped in 66c56f7. Rewritten in 43652aa so it describes the state as it actually is: on Linux, directories that were already hidden when the watcher was set up have no inotify watch, because slotAddFolderRecursive() skipped them and it runs again only from FolderWatcher::init(). The full local discovery above picks up their content, and live changes below them stay invisible until the client is restarted, since re-creating the watcher here would leak the old inotify fd and every watch on it while FolderWatcherPrivate has no destructor on Linux.

The comment above _engine->setIgnoreHiddenFiles() was misleading in the same way and is reworded in the same commit. Details in the other thread.

Comment thread src/gui/folder.cpp Outdated
_journal.forceRemoteDiscoveryNextSync();
slotNextSyncFullLocalDiscovery();

// On Linux only: directories that were already hidden when the watcher was set up have no

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still outdated. please check what you ai is doing...

Comment thread src/gui/folder.cpp Outdated
Comment on lines +342 to +347
// The engine holds the flag the folder watcher and the status tracker read, and they read it
// outside of a sync: FolderWatcher::addChanges() drops ignored paths the moment the OS reports
// them, before they ever reach the change set startSync() pops, and on Linux
// FolderWatcherPrivate::slotReceivedNotification() decides right there whether a newly created
// directory is given a watch at all. Refreshing the engine only in startSync() would discard
// every hidden-file change until then, without even scheduling the sync that would fix it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bullshit

Comment thread src/gui/folder.cpp Outdated
// FolderWatcherPrivate::slotReceivedNotification() decides right there whether a newly created
// directory is given a watch at all. Refreshing the engine only in startSync() would discard
// every hidden-file change until then, without even scheduling the sync that would fix it.
_engine->setIgnoreHiddenFiles(ignore);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the value is only evaluated when the sync run is started as it is passed to steps deeper down in the sync.

and please stop responding with ai generated answers

@TheOneRing

Copy link
Copy Markdown
Contributor

PS: Your quite focused on the folder watcher, it has no effect on running syncs, only on triggering syncs...

@junkerderprovinz

Copy link
Copy Markdown
Contributor Author

You're right, dropped it along with both comments. setIgnoreHiddenFiles() now only invalidates the remote etags and requests a full local discovery. The test assertions that checked for the engine flag are gone too. Sorry about the AI walls of text, that stops.

@TheOneRing TheOneRing left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry forgot to send the comment...
I think #1127 is also related to the changes in here.

Your pr made me aware of forceRemoteDiscoveryNextSync and that I really don't like how it works and that it might get called during a running sync.
But I guess we should handle that in a different issue.

Comment thread src/gui/folder.h Outdated
Comment on lines +162 to +165
* When hidden files start to be synced, the discovery state that was built while they were
* ignored has to be invalidated, otherwise the items that were skipped so far would only
* show up after a restart of the client.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description doesn't make much sense, does it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it didn't. Reworded in 02dddf2.

@junkerderprovinz

Copy link
Copy Markdown
Contributor Author

#1127 is the remote side of the same bug. Force sync only asks for a full local discovery and leaves the remote etags alone, so f1 isn't listed again. The root is listed on every sync, which is why .hidden-file.txt does arrive. This PR calls forceRemoteDiscoveryNextSync() when hidden files get turned on, the same call the ignore list editor makes, so it should fix #1127 as well. I added a sync engine test for that case.

This time I built the client and ran the tests. One of my tests from August was wrong and failed: it put the hidden folder at the root, and an incremental discovery walks the root on its way to any reported path. It uses a subfolder now. All 35 tests pass on Linux, and I shortened the description. It also says now that the Linux watcher gap from #714 stays open.

Handling the running-sync case separately sounds good to me.

junkerderprovinz added 7 commits September 30, 2026 10:58
Toggling "sync hidden files" only wrote the new value into the folder
definition. The sync engine keeps its own copy of the flag that is
refreshed when a sync starts, and the state that was built up while the
hidden items were ignored was never invalidated:

* The folder watcher asks the sync engine whether a changed path is
  excluded, so changes to hidden files kept being discarded until a sync
  run started. On Linux the watcher also registers one inotify watch per
  directory and skipped the hidden ones, so changes below them were not
  reported at all until the client was restarted.
* Hidden items are not in the local database, so an incremental local
  discovery cannot see them. The next full local discovery only happens
  after fullLocalDiscoveryInterval, one hour by default.
* Hidden items on the server were skipped without changing the parent
  etags, so remote discovery would not look at them again either.

Folder::setIgnoreHiddenFiles() now updates the sync engine right away
and, when hidden files start to be synced, forces a full local and
remote re-discovery and re-initialises the folder watcher, the same way
a change of the ignore list already does. FolderMan enqueues the
affected folders so the items that were hidden so far are picked up
without waiting for the next scheduled run.

Fixes opencloud-eu#714
FolderWatcherPrivate has no destructor on Linux, so calling init() again
leaks the old inotify fd and every watch registered on it. Reported by
TheOneRing in review. Full local discovery still catches up on existing
hidden content; only live changes inside directories that stayed unwatched
need a restart in the meantime, matching prior behavior.
…es toggle

The old comment implied the engine's copy of the flag only matters for the
sync path, which makes the assignment read as dead code. Name the live
consumers instead: FolderWatcher::addChanges() filters paths at the moment
the OS reports them, so anything dropped there never reaches the change set
that startSync() pops, and on Linux slotReceivedNotification() decides right
then whether a newly created directory is given a watch at all.

Also replace the stale note about the removed FolderWatcher::init() call
with a description of the remaining Linux-only watch gap as it stands.
The engine reads the flag when a sync run starts, and the folder watcher only
triggers syncs rather than affecting a running one, so pushing the value here
bought nothing the forced full local discovery does not already cover. The
tests that asserted the engine flag go with it; they were checking the
constructor rather than the toggle.
An incremental local discovery walks the sync root on its way to every
reported path, so a hidden folder there was picked up without the full
discovery the test is about.
@TheOneRing
TheOneRing force-pushed the fix/hidden-files-not-synced-on-enable branch from e684a3d to e4d6411 Compare September 30, 2026 08:58
@TheOneRing

Copy link
Copy Markdown
Contributor

Thank you <3

@TheOneRing
TheOneRing merged commit 7098d58 into opencloud-eu:main Sep 30, 2026
14 of 15 checks passed
@openclouders openclouders mentioned this pull request Sep 30, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client does not sync hidden files inside folders

2 participants