From 5da8fd150fc59b17c8be933f529bdf0cbe2e820a Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Wed, 20 May 2026 15:57:50 +0200 Subject: [PATCH] fix(file-provider): Re-import items after updates Fixes #10065. Signed-off-by: Iva Horn --- .../Extension/FileProviderExtension.swift | 56 +++++++++++++++++++ .../FileProviderDomainDefaults.swift | 36 ++++++++++++ 2 files changed, 92 insertions(+) diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Extension/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Extension/FileProviderExtension.swift index d9366f71f2685..743d8c5af14aa 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Extension/FileProviderExtension.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Extension/FileProviderExtension.swift @@ -529,6 +529,61 @@ import OSLog notifyChange() } + /// + /// Trigger a one-shot reimport of all items below the root container when the extension + /// bundle's version differs from the value last persisted for this domain. + /// + /// The file provider framework caches the `NSFileProviderItem` snapshot returned at + /// enumeration time and does not re-query items it already holds when the extension's view + /// of them changes between app versions (new `userInfo` keys, fixed predicates, corrected + /// derivations of `contentPolicy`, …). `signalEnumerator(for: .workingSet, …)` only triggers + /// incremental `enumerateChanges(from:)` against the last sync anchor and does not refresh + /// those snapshots; `NSFileProviderManager.reimportItems(below:)` is the explicit API for + /// that and refreshes every freshly computed `NSFileProviderItem` property. + /// + /// The bundle's `CFBundleShortVersionString` is compared against + /// ``FileProviderDomainDefaults/lastSeenExtensionVersion``. On any difference — including the + /// initial `nil` for a fresh install — the reimport is requested. The persisted value is + /// only updated after the framework acknowledges success so a failed launch retries on the + /// next start. See nextcloud/desktop#10065. + /// + private func reimportItemsIfExtensionVersionChanged() { + guard let manager else { + logger.error("Cannot trigger item reimport because the file provider manager is not available.") + return + } + + let bundle = Bundle(for: type(of: self)) + + guard let currentVersion = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, currentVersion.isEmpty == false else { + logger.error("Cannot trigger item reimport because the extension bundle has no CFBundleShortVersionString.") + return + } + + let lastSeenVersion = config.lastSeenExtensionVersion + + guard currentVersion != lastSeenVersion else { + logger.debug("Extension version \"\(currentVersion)\" unchanged since last launch. Skipping item reimport.") + return + } + + logger.info("Extension version changed from \"\(lastSeenVersion ?? "")\" to \"\(currentVersion)\". Reimporting items below the root container.") + + manager.reimportItems(below: .rootContainer) { [weak self] error in + guard let self else { + return + } + + if let error { + logger.error("Failed to reimport items below the root container after extension version bump.", [.error: error.localizedDescription]) + return + } + + logger.info("Reimport of items below the root container completed successfully after extension version bump.") + config.lastSeenExtensionVersion = currentVersion + } + } + /// /// Concurrent invocations of this method are serialized: at most one `performSetup(…)` runs /// at a time. Duplicate invocations — by value equality on ``Account`` — received while a @@ -686,6 +741,7 @@ import OSLog ncKit.setup(groupIdentifier: Bundle.main.bundleIdentifier!) completionHandler?(nil) signalEnumeratorAfterAccountSetup() + reimportItemsIfExtensionVersionChanged() } } diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/FileProviderDomainDefaults.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/FileProviderDomainDefaults.swift index e193e4449f984..a76f4f72def72 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/FileProviderDomainDefaults.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/FileProviderDomainDefaults.swift @@ -20,6 +20,7 @@ public struct FileProviderDomainDefaults { case user case userId case serverUrl + case lastSeenExtensionVersion case debugLoggingEnabled } @@ -147,6 +148,41 @@ public struct FileProviderDomainDefaults { } } + /// + /// The extension bundle's `CFBundleShortVersionString` as last seen for this domain. + /// + /// Used by ``FileProviderExtension`` to detect app updates: when the value differs from the extension bundle's current version on launch, a one-shot reimport of all items below the root container is requested so the framework refreshes its cached `NSFileProviderItem` snapshots (`userInfo`, `contentPolicy`, …) for previously enumerated items. + /// The value is only persisted after the framework acknowledges the reimport, so a failed launch retries on the next start. + /// + /// See nextcloud/desktop#10065. + /// + public var lastSeenExtensionVersion: String? { + get { + let identifier = identifier.rawValue + + if let value = internalConfig[ConfigKey.lastSeenExtensionVersion.rawValue] as? String { + logger.debug("Returning existing value \"\(value)\" for \"lastSeenExtensionVersion\" for file provider domain \"\(identifier)\".") + return value + } else { + logger.debug("No existing value for \"lastSeenExtensionVersion\" for file provider domain \"\(identifier)\" found.") + return nil + } + } + + set { + let identifier = identifier.rawValue + + if newValue == nil { + logger.debug("Removing key \"lastSeenExtensionVersion\" for file provider domain \"\(identifier)\" because the new value is nil.") + internalConfig.removeValue(forKey: ConfigKey.lastSeenExtensionVersion.rawValue) + } else if newValue == "" { + logger.error("Ignoring new value for \"lastSeenExtensionVersion\" because it is an empty string for file provider domain \"\(identifier)\"!") + } else { + internalConfig[ConfigKey.lastSeenExtensionVersion.rawValue] = newValue + } + } + } + /// /// Controls whether `.debug`-level messages are included in logging output. ///