Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "<nil>")\" 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
Expand Down Expand Up @@ -686,6 +741,7 @@ import OSLog
ncKit.setup(groupIdentifier: Bundle.main.bundleIdentifier!)
completionHandler?(nil)
signalEnumeratorAfterAccountSetup()
reimportItemsIfExtensionVersionChanged()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public struct FileProviderDomainDefaults {
case user
case userId
case serverUrl
case lastSeenExtensionVersion
case debugLoggingEnabled
}

Expand Down Expand Up @@ -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.
///
Expand Down
Loading