Merge pull request #8792 from nextcloud/feature/8546-rich-logs

feat: Replaced Unified Logging System with Custom Solution.
This commit is contained in:
Camila Ayres 2025-09-24 14:05:00 +02:00 committed by GitHub
commit 1595798ace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 588 additions and 754 deletions

View File

@ -1,13 +1,13 @@
{
"originHash" : "7fd0674d455b084268d98aec4518c2ba8f46839c630e4e9f23da1b1241b16612",
"originHash" : "42f510a9baeebb4b09104db0837f6dd21eba28f9233a7e61bb67fb3885667f9c",
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3",
"version" : "1.6.1"
"revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b",
"version" : "1.5.1"
}
}
],

View File

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import OSLog
extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static let desktopClientConnection = Logger(
subsystem: subsystem, category: "desktopclientconnection")
static let fileProviderDomainDefaults = Logger(subsystem: subsystem, category: "fileProviderDomainDefaults")
static let fpUiExtensionService = Logger(subsystem: subsystem, category: "fpUiExtensionService")
static let fileProviderExtension = Logger(
subsystem: subsystem, category: "fileproviderextension")
static let keychain = Logger(subsystem: subsystem, category: "keychain")
static let shares = Logger(subsystem: subsystem, category: "shares")
static let logger = Logger(subsystem: subsystem, category: "logger")
@available(macOSApplicationExtension 12.0, *)
static func logEntries(interval: TimeInterval = -3600) -> (Array<String>?, Error?) {
do {
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
let timeDate = Date().addingTimeInterval(interval)
let logPosition = logStore.position(date: timeDate)
let entries = try logStore.getEntries(at: logPosition)
return (entries
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == Logger.subsystem }
.map { $0.composedMessage }, nil)
} catch let error {
Logger.logger.error("Could not acquire os log store: \(error)");
return (nil, error)
}
}
}

View File

@ -3,6 +3,7 @@
import FileProvider
import Foundation
import NextcloudFileProviderKit
import os
///
@ -24,8 +25,11 @@ struct FileProviderDomainDefaults {
///
let identifier: NSFileProviderDomainIdentifier
init(identifier: NSFileProviderDomainIdentifier) {
let logger: FileProviderLogger
init(identifier: NSFileProviderDomainIdentifier, log: any FileProviderLogging) {
self.identifier = identifier
self.logger = FileProviderLogger(category: "FileProviderDomainDefaults", log: log)
}
///
@ -58,10 +62,10 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if let value = internalConfig[ConfigKey.serverUrl.rawValue] as? String {
Logger.fileProviderDomainDefaults.debug("Returning existing value \"\(value)\" for \"serverUrl\" for file provider domain \"\(identifier)\".")
logger.debug("Returning existing value \"\(value)\" for \"serverUrl\" for file provider domain \"\(identifier)\".")
return value
} else {
Logger.fileProviderDomainDefaults.debug("No existing value for \"serverUrl\" for file provider domain \"\(identifier)\" found.")
logger.debug("No existing value for \"serverUrl\" for file provider domain \"\(identifier)\" found.")
return nil
}
}
@ -70,10 +74,10 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if newValue == nil {
Logger.fileProviderDomainDefaults.debug("Removing key \"serverUrl\" for file provider domain \"\(identifier)\" because the new value is nil.")
logger.debug("Removing key \"serverUrl\" for file provider domain \"\(identifier)\" because the new value is nil.")
internalConfig.removeValue(forKey: ConfigKey.serverUrl.rawValue)
} else if newValue == "" {
Logger.fileProviderDomainDefaults.error("Ignoring new value for \"serverUrl\" because it is an empty string for file provider domain \"\(identifier)\"!")
logger.error("Ignoring new value for \"serverUrl\" because it is an empty string for file provider domain \"\(identifier)\"!")
} else {
internalConfig[ConfigKey.serverUrl.rawValue] = newValue
}
@ -88,7 +92,7 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if let value = internalConfig[ConfigKey.trashDeletionEnabled.rawValue] as? Bool {
Logger.fileProviderDomainDefaults.debug("Returning existing value \"\(value)\" for \"trashDeletionEnabled\" for file provider domain \"\(identifier)\".")
logger.debug("Returning existing value \"\(value)\" for \"trashDeletionEnabled\" for file provider domain \"\(identifier)\".")
return value
} else {
return false
@ -98,7 +102,7 @@ struct FileProviderDomainDefaults {
set {
let identifier = self.identifier.rawValue
Logger.fileProviderDomainDefaults.error("Setting value \"\(newValue)\" for \"trashDeletionEnabled\" for file provider domain \"\(identifier)\".")
logger.error("Setting value \"\(newValue)\" for \"trashDeletionEnabled\" for file provider domain \"\(identifier)\".")
internalConfig[ConfigKey.trashDeletionEnabled.rawValue] = newValue
}
}
@ -111,10 +115,10 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if let value = internalConfig[ConfigKey.user.rawValue] as? String {
Logger.fileProviderDomainDefaults.debug("Returning existing value \"\(value)\" for \"user\" for file provider domain \"\(identifier)\".")
logger.debug("Returning existing value \"\(value)\" for \"user\" for file provider domain \"\(identifier)\".")
return value
} else {
Logger.fileProviderDomainDefaults.debug("No existing value for \"user\" for file provider domain \"\(identifier)\" found.")
logger.debug("No existing value for \"user\" for file provider domain \"\(identifier)\" found.")
return nil
}
}
@ -123,10 +127,10 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if newValue == nil {
Logger.fileProviderDomainDefaults.debug("Removing key \"user\" for file provider domain \"\(identifier)\" because the new value is nil.")
logger.debug("Removing key \"user\" for file provider domain \"\(identifier)\" because the new value is nil.")
internalConfig.removeValue(forKey: ConfigKey.user.rawValue)
} else if newValue == "" {
Logger.fileProviderDomainDefaults.error("Ignoring new value for \"user\" because it is an empty string for file provider domain \"\(identifier)\"!")
logger.error("Ignoring new value for \"user\" because it is an empty string for file provider domain \"\(identifier)\"!")
} else {
internalConfig[ConfigKey.user.rawValue] = newValue
}
@ -141,10 +145,10 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if let value = internalConfig[ConfigKey.userId.rawValue] as? String {
Logger.fileProviderDomainDefaults.debug("Returning existing value \"\(value)\" for \"userId\" for file provider domain \"\(identifier)\".")
logger.debug("Returning existing value \"\(value)\" for \"userId\" for file provider domain \"\(identifier)\".")
return value
} else {
Logger.fileProviderDomainDefaults.debug("No existing value for \"userId\" for file provider domain \"\(identifier)\" found.")
logger.debug("No existing value for \"userId\" for file provider domain \"\(identifier)\" found.")
return nil
}
}
@ -153,10 +157,10 @@ struct FileProviderDomainDefaults {
let identifier = self.identifier.rawValue
if newValue == nil {
Logger.fileProviderDomainDefaults.debug("Removing key \"userId\" for file provider domain \"\(identifier)\" because the new value is nil.")
logger.debug("Removing key \"userId\" for file provider domain \"\(identifier)\" because the new value is nil.")
internalConfig.removeValue(forKey: ConfigKey.userId.rawValue)
} else if newValue == "" {
Logger.fileProviderDomainDefaults.error("Ignoring new value for \"userId\" because it is an empty string for file provider domain \"\(identifier)\"!")
logger.error("Ignoring new value for \"userId\" because it is an empty string for file provider domain \"\(identifier)\"!")
} else {
internalConfig[ConfigKey.userId.rawValue] = newValue
}

View File

@ -33,7 +33,7 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
for itemIdentifier: NSFileProviderItemIdentifier,
completionHandler: @escaping ([NSFileProviderServiceSource]?, Error?) -> Void
) -> Progress {
Logger.desktopClientConnection.debug("Serving supported service sources")
logger.debug("Serving supported service sources")
let clientCommService = ClientCommunicationService(fpExtension: self)
let fpuiExtService = FPUIExtensionServiceSource(fpExtension: self)
let services: [NSFileProviderServiceSource] = [clientCommService, fpuiExtService]
@ -55,9 +55,7 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
private func signalEnumeratorAfterAccountSetup() {
guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error(
"Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup"
)
logger.error("Could not get file provider manager for domain \(self.domain.displayName), cannot notify after account setup")
return
}
@ -65,32 +63,24 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
if error != nil {
Logger.fileProviderExtension.error(
"Error resolving not authenticated, received error: \(error!.localizedDescription)"
)
self.logger.error("Error resolving not authenticated, received error: \(error!.localizedDescription)")
}
}
Logger.fileProviderExtension.debug(
"Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)"
)
logger.debug("Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl)")
notifyChange()
}
func notifyChange() {
guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error(
"Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify changes"
)
logger.error("Could not get file provider manager for domain \(self.domain.displayName), cannot notify changes")
return
}
fpManager.signalEnumerator(for: .workingSet) { error in
if error != nil {
Logger.fileProviderExtension.error(
"Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)"
)
self.logger.error("Error signalling enumerator for working set, received error: \(error!.localizedDescription)")
}
}
}
@ -109,34 +99,34 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
) {
let account = Account(user: user, id: userId, serverUrl: serverUrl, password: password)
Logger.fileProviderExtension.info("Setting up domain account for user: \(user, privacy: .public), userId: \(userId, privacy: .public), serverUrl: \(serverUrl, privacy: .public), password: \(password.isEmpty ? "<empty>" : "<not-empty>", privacy: .public), ncKitAccount: \(account.ncKitAccount, privacy: .public)")
logger.info("Setting up domain account for user: \(user), userId: \(userId), serverUrl: \(serverUrl), password: \(password.isEmpty ? "<empty>" : "<not-empty>"), ncKitAccount: \(account.ncKitAccount)")
guard account != ncAccount else {
Logger.fileProviderExtension.warning("Cancelling domain account setup because of receiving the same account information repeatedly!")
logger.info("Cancelling domain account setup because of receiving the same account information repeatedly!")
completionHandler?(NSError(.invalidCredentials))
return
}
guard password.isEmpty == false else {
Logger.fileProviderExtension.warning("Cancelling domain account setup because \"password\" is an empty string!")
logger.info("Cancelling domain account setup because \"password\" is an empty string!")
completionHandler?(NSError(.missingAccountInformation))
return
}
guard serverUrl.isEmpty == false else {
Logger.fileProviderExtension.warning("Cancelling domain account setup because \"serverUrl\" is an empty string!")
logger.info("Cancelling domain account setup because \"serverUrl\" is an empty string!")
completionHandler?(NSError(.missingAccountInformation))
return
}
guard user.isEmpty == false else {
Logger.fileProviderExtension.warning("Cancelling domain account setup because \"user\" is an empty string!")
logger.info("Cancelling domain account setup because \"user\" is an empty string!")
completionHandler?(NSError(.missingAccountInformation))
return
}
guard userId.isEmpty == false else {
Logger.fileProviderExtension.warning("Cancelling domain account setup because \"userId\" is an empty string!")
logger.info("Cancelling domain account setup because \"userId\" is an empty string!")
completionHandler?(NSError(.missingAccountInformation))
return
}
@ -145,7 +135,7 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
config.serverUrl = serverUrl
config.user = user
config.userId = userId
Keychain.savePassword(password, for: user, on: serverUrl)
keychain.savePassword(password, for: user, on: serverUrl)
NextcloudKit.clearAccountErrorState(for: account.ncKitAccount)
Task {
@ -156,7 +146,6 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
userId: userId,
password: password,
userAgent: userAgent,
nextcloudVersion: 25,
groupIdentifier: ""
)
@ -172,28 +161,28 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
break
}
Logger.fileProviderExtension.info("\(user, privacy: .public) authentication try timed out. Trying again soon.")
logger.info("\(user) authentication try timed out. Trying again soon.")
try? await Task.sleep(nanoseconds: authTimeout)
}
switch (authAttemptState) {
case .authenticationError:
Logger.fileProviderExtension.error("Authentication of \"\(user, privacy: .public)\" failed due to bad credentials, cancelling domain account setup!")
logger.error("Authentication of \"\(user)\" failed due to bad credentials, cancelling domain account setup!")
completionHandler?(NSError(.invalidCredentials))
return
case .connectionError:
// Despite multiple connection attempts we are still getting connection issues.
// Connection error should be provided
Logger.fileProviderExtension.error("Authentication of \"\(user, privacy: .public)\" try failed, no connection.")
logger.error("Authentication of \"\(user)\" try failed, no connection.")
completionHandler?(NSError(.connection))
return
case .success:
Logger.fileProviderExtension.info("Successfully authenticated! Nextcloud account set up in file provider extension. User: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)")
logger.info("Successfully authenticated! Nextcloud account set up in file provider extension. User: \(user) at server: \(serverUrl)")
}
Task { @MainActor in
ncAccount = account
dbManager = FilesDatabaseManager(account: account, fileProviderDomainIdentifier: domain.identifier)
dbManager = FilesDatabaseManager(account: account, fileProviderDomainIdentifier: domain.identifier, log: log)
if let changeObserver {
changeObserver.invalidate()
@ -205,10 +194,11 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
remoteInterface: ncKit,
changeNotificationInterface: self,
domain: domain,
dbManager: dbManager
dbManager: dbManager,
log: log
)
} else {
Logger.fileProviderExtension.error("Invalid db manager, cannot start RCO")
logger.error("Invalid db manager, cannot start RCO")
}
ncKit.setup(groupIdentifier: Bundle.main.bundleIdentifier!, delegate: changeObserver)
@ -219,9 +209,7 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
}
@objc func removeAccountConfig() {
Logger.fileProviderExtension.info(
"Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)"
)
logger.info("Received instruction to remove account data for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl)")
ncAccount = nil
dbManager = nil
}
@ -246,7 +234,7 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte
actionsLock.unlock()
guard let argument else { return }
Logger.fileProviderExtension.debug("Reporting sync \(argument)")
logger.debug("Reporting sync \(argument)")
let message = command + ":" + argument + "\n"
socketClient?.sendMessage(message)
}

View File

@ -27,7 +27,7 @@ extension FileProviderExtension: NSFileProviderCustomAction {
completionHandler: completionHandler
)
default:
Logger.fileProviderExtension.error("Unsupported action: \(actionIdentifier.rawValue)")
logger.error("Unsupported action: \(actionIdentifier.rawValue)")
completionHandler(NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError))
return Progress()
}
@ -39,16 +39,12 @@ extension FileProviderExtension: NSFileProviderCustomAction {
completionHandler: @escaping ((any Error)?) -> Void
) -> Progress {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not setting keep offline for items, account not set up yet."
)
logger.error("Not setting keep offline for items, account not set up yet.")
completionHandler(NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"Not setting keep offline for items as database is unreachable."
)
logger.error("Not setting keep offline for items as database is unreachable.")
completionHandler(NSFileProviderError(.cannotSynchronize))
return Progress()
}
@ -57,7 +53,7 @@ extension FileProviderExtension: NSFileProviderCustomAction {
// If there are no items, complete successfully immediately.
if itemIdentifiers.isEmpty {
Logger.fileProviderExtension.info("No items to process for keepDownloaded action.")
logger.info("No items to process for keepDownloaded action.")
completionHandler(nil)
return progress
}
@ -78,7 +74,8 @@ extension FileProviderExtension: NSFileProviderCustomAction {
identifier: identifier,
account: ncAccount,
remoteInterface: localNcKit,
dbManager: dbManager
dbManager: dbManager,
log: self.log
) else {
throw NSError.fileProviderErrorForNonExistentItem(
withIdentifier: identifier
@ -92,20 +89,10 @@ extension FileProviderExtension: NSFileProviderCustomAction {
progress.completedUnitCount = 1
}
}
Logger.fileProviderExtension.info(
"""
All items successfully processed for
keepDownloaded=\(keepDownloaded, privacy: .public)
"""
)
logger.info("All items successfully processed for keepDownloaded=\(keepDownloaded)")
completionHandler(nil)
} catch let error {
Logger.fileProviderExtension.error(
"""
Error during keepDownloaded=\(keepDownloaded, privacy: .public)
action: \(error.localizedDescription, privacy: .public)
"""
)
logger.error("Error during keepDownloaded=\(keepDownloaded) action: \(error.localizedDescription)")
completionHandler(error)
}
}

View File

@ -36,6 +36,7 @@ extension FileProviderExtension: NSFileProviderThumbnailing {
usingRemoteInterface: self.ncKit,
andDatabase: dbManager,
perThumbnailCompletionHandler: perThumbnailCompletionHandler,
log: log,
completionHandler: completionHandler
)
}

View File

@ -12,6 +12,10 @@ import OSLog
@objc class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
let domain: NSFileProviderDomain
let keychain: Keychain
let log: any FileProviderLogging
let logger: FileProviderLogger
///
/// NextcloudKit instance used by this file provider extension object.
///
@ -25,13 +29,13 @@ import OSLog
lazy var ncKitBackground = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)
lazy var socketClient: LocalSocketClient? = {
guard let containerUrl = pathForAppGroupContainer() else {
Logger.fileProviderExtension.critical("Won't start socket client, no container url")
logger.fault("Won't start socket client, no container URL available!")
return nil;
}
let socketPath = containerUrl.appendingPathComponent(
".fileprovidersocket", conformingTo: .archive)
let lineProcessor = FileProviderSocketLineProcessor(delegate: self)
let lineProcessor = FileProviderSocketLineProcessor(delegate: self, log: log)
return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor)
}()
@ -50,31 +54,32 @@ import OSLog
// Since it's not desirable to cancel a long recursive enumeration half-way through, we do the
// fast enumeration by default. We prompt the user on the client side to run a proper, full
// enumeration if they want for safety.
lazy var config = FileProviderDomainDefaults(identifier: domain.identifier)
lazy var config = FileProviderDomainDefaults(identifier: domain.identifier, log: log)
required init(domain: NSFileProviderDomain) {
// The containing application must create a domain using
// `NSFileProviderManager.add(_:, completionHandler:)`. The system will then launch the
// application extension process, call `FileProviderExtension.init(domain:)` to instantiate
// the extension for that domain, and call methods on the instance.
Logger.fileProviderExtension.debug("Initializing with domain identifier: \(domain.identifier.rawValue)")
self.domain = domain
// Set up logging.
self.log = FileProviderLog(fileProviderDomainIdentifier: domain.identifier)
self.logger = FileProviderLogger(category: "FileProviderExtension", log: log)
logger.debug("Initializing with domain identifier: \(domain.identifier.rawValue)")
// Set up NextcloudKit.
self.ncKit = NextcloudKit.shared
if let logDirectory = FileManager.default.fileProviderDomainSupportDirectory(for: domain.identifier) {
Logger.fileProviderExtension.info("NextcloudKit log file directory: \(logDirectory.path)")
#if DEBUG
NKLogFileManager.configure(logLevel: .verbose)
#else
NKLogFileManager.configure(logLevel: .normal)
#endif
#if DEBUG
let nextcloudKitLogLevel = 2
#else
let nextcloudKitLogLevel = 1
#endif
logger.info("Current NextcloudKit log file URL: \(NKLogFileManager.shared.currentLogFileURL().absoluteString)")
Logger.fileProviderExtension.info("NextcloudKit log level: \(nextcloudKitLogLevel)")
ncKit.setupLog(pathLog: logDirectory.path, levelLog: nextcloudKitLogLevel, copyLogToDocumentDirectory: true)
}
self.keychain = Keychain(log: log)
super.init()
socketClient?.start()
@ -82,7 +87,7 @@ import OSLog
func invalidate() {
// TODO: cleanup any resources
Logger.fileProviderExtension.debug("Extension for domain \(self.domain.displayName, privacy: .public) is being torn down")
logger.debug("Extension for domain \(self.domain.displayName) is being torn down")
}
func insertSyncAction(_ actionId: UUID) {
@ -119,22 +124,12 @@ import OSLog
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"""
Not fetching item for identifier: \(identifier.rawValue, privacy: .public)
as account not set up yet.
"""
)
logger.error("Not fetching item because account not set up yet.", [.item: identifier])
completionHandler(nil, NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"""
Not fetching item for identifier: \(identifier.rawValue, privacy: .public)
as database is unreachable
"""
)
logger.error("Not fetching item because database is unavailable.", [.item: identifier])
completionHandler(nil, NSFileProviderError(.cannotSynchronize))
return Progress()
}
@ -146,7 +141,8 @@ import OSLog
identifier: identifier,
account: ncAccount,
remoteInterface: ncKit,
dbManager: dbManager
dbManager: dbManager,
log: log
) {
progress.completedUnitCount = 1
completionHandler(item, nil)
@ -168,15 +164,11 @@ import OSLog
let actionId = UUID()
insertSyncAction(actionId)
Logger.fileProviderExtension.debug(
"Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
)
logger.info("Received request to fetch contents of item.", [.item: itemIdentifier])
guard requestedVersion == nil else {
// TODO: Add proper support for file versioning
Logger.fileProviderExtension.error(
"Can't return contents for a specific version as this is not supported."
)
logger.error("Can't return contents for a specific version as this is not supported.", [.item: itemIdentifier])
insertErrorAction(actionId)
completionHandler(
nil,
@ -187,23 +179,14 @@ import OSLog
}
guard let ncAccount else {
Logger.fileProviderExtension.error(
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as account not set up yet.
"""
)
logger.error("Not fetching contents for item because account not set up yet.", [.item: itemIdentifier])
insertErrorAction(actionId)
completionHandler(nil, nil, NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as database is unreachable
"""
)
logger.error("Not fetching contents for item because database is unavailable.", [.item: itemIdentifier])
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
return Progress()
}
@ -215,14 +198,11 @@ import OSLog
identifier: itemIdentifier,
account: ncAccount,
remoteInterface: ncKit,
dbManager: dbManager
dbManager: dbManager,
log: log
) else {
Logger.fileProviderExtension.error(
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as item not found.
"""
)
logger.error("Not fetching contents for item because item was not found.", [.item: itemIdentifier])
completionHandler(
nil,
nil,
@ -255,17 +235,17 @@ import OSLog
insertSyncAction(actionId)
let tempId = itemTemplate.itemIdentifier.rawValue
Logger.fileProviderExtension.debug(
logger.debug(
"""
Received create item request for item with identifier: \(tempId, privacy: .public)
and filename: \(itemTemplate.filename, privacy: .public)
Received create item request for item with identifier: \(tempId)
and filename: \(itemTemplate.filename)
"""
)
guard let ncAccount else {
Logger.fileProviderExtension.error(
logger.error(
"""
Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public)
Not creating item: \(itemTemplate.itemIdentifier.rawValue)
as account not set up yet
"""
)
@ -275,25 +255,14 @@ import OSLog
}
guard let ignoredFiles else {
Logger.fileProviderExtension.error(
"""
Not creating item for identifier:
\(itemTemplate.itemIdentifier.rawValue, privacy: .public)
as ignore list not set up yet.
"""
)
logger.error("Not creating item for identifier: \(itemTemplate.itemIdentifier.rawValue) as ignore list not set up yet.")
insertErrorAction(actionId)
completionHandler(itemTemplate, [], false, NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"""
Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public)
as database is unreachable
"""
)
logger.error("Not creating item because database is unavailable.", [.item: itemTemplate.itemIdentifier])
insertErrorAction(actionId)
completionHandler(itemTemplate, [], false, NSFileProviderError(.cannotSynchronize))
return Progress()
@ -311,7 +280,8 @@ import OSLog
remoteInterface: ncKit,
ignoredFiles: ignoredFiles,
progress: progress,
dbManager: dbManager
dbManager: dbManager,
log: log
)
if error != nil {
@ -349,26 +319,17 @@ import OSLog
let identifier = item.itemIdentifier
let ocId = identifier.rawValue
Logger.fileProviderExtension.debug(
"""
Received modify item request for item with identifier: \(ocId, privacy: .public)
and filename: \(item.filename, privacy: .public)
"""
)
logger.debug("Received modify item request for item with identifier: \(ocId) and filename: \(item.filename)")
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not modifying item: \(ocId, privacy: .public) as account not set up yet."
)
logger.error("Not modifying item: \(ocId) as account not set up yet.")
insertErrorAction(actionId)
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let ignoredFiles else {
Logger.fileProviderExtension.error(
"Not modifying item: \(ocId, privacy: .public) as ignore list not set up yet."
)
logger.error("Not modifying item: \(ocId) as ignore list not set up yet.")
insertErrorAction(actionId)
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
return Progress()
@ -376,13 +337,7 @@ import OSLog
guard let dbManager else {
Logger.fileProviderExtension.error(
"""
Not modifying item: \(ocId, privacy: .public)
with filename: \(item.filename, privacy: .public)
as database is unreachable
"""
)
logger.error("Not modifying item because database is unavailable.")
insertErrorAction(actionId)
completionHandler(item, [], false, NSFileProviderError(.cannotSynchronize))
return Progress()
@ -394,11 +349,10 @@ import OSLog
identifier: identifier,
account: ncAccount,
remoteInterface: ncKit,
dbManager: dbManager
dbManager: dbManager,
log: log
) else {
Logger.fileProviderExtension.error(
"Not modifying item: \(ocId, privacy: .public) as item not found."
)
logger.error("Not modifying item: \(ocId) as item not found.")
insertErrorAction(actionId)
completionHandler(
item,
@ -443,32 +397,24 @@ import OSLog
let actionId = UUID()
insertSyncAction(actionId)
Logger.fileProviderExtension.debug(
"Received delete request for item: \(identifier.rawValue, privacy: .public)"
)
logger.debug("Received delete request for item: \(identifier.rawValue)")
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not deleting item \(identifier.rawValue, privacy: .public), account not set up yet"
)
logger.error("Not deleting item \(identifier.rawValue), account not set up yet")
insertErrorAction(actionId)
completionHandler(NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let ignoredFiles else {
Logger.fileProviderExtension.error(
"Not deleting \(identifier.rawValue, privacy: .public), ignore list not received"
)
logger.error("Not deleting \(identifier.rawValue), ignore list not received")
insertErrorAction(actionId)
completionHandler(NSFileProviderError(.notAuthenticated))
return Progress()
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"Not deleting item \(identifier.rawValue, privacy: .public), db manager unavailable"
)
logger.error("Not deleting item \(identifier.rawValue), db manager unavailable")
insertErrorAction(actionId)
completionHandler(NSFileProviderError(.cannotSynchronize))
return Progress()
@ -480,11 +426,10 @@ import OSLog
identifier: identifier,
account: ncAccount,
remoteInterface: ncKit,
dbManager: dbManager
dbManager: dbManager,
log: log
) else {
Logger.fileProviderExtension.error(
"Not deleting item \(identifier.rawValue, privacy: .public), item not found"
)
logger.error("Not deleting item \(identifier.rawValue), item not found")
insertErrorAction(actionId)
completionHandler(
NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)
@ -493,12 +438,7 @@ import OSLog
}
guard config.trashDeletionEnabled || item.parentItemIdentifier != .trashContainer else {
Logger.fileProviderExtension.warning(
"""
System requested deletion of item in trash, but deleting trash items is disabled.
item: \(item.filename, privacy: .public)
"""
)
logger.info("System requested deletion of item in trash, but deleting trash items is disabled. item: \(item.filename)")
removeSyncAction(actionId)
completionHandler(NSError.fileProviderErrorForRejectedDeletion(of: item))
return
@ -522,24 +462,12 @@ import OSLog
for containerItemIdentifier: NSFileProviderItemIdentifier, request _: NSFileProviderRequest
) throws -> NSFileProviderEnumerator {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"""
Not providing enumerator for container
with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet
as account not set up
"""
)
logger.error("Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue) yet as account not set up")
throw NSFileProviderError(.notAuthenticated)
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"""
Not providing enumerator for container
with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet
as db manager is unavailable
"""
)
logger.error("Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue) yet as db manager is unavailable")
throw NSFileProviderError(.cannotSynchronize)
}
@ -548,41 +476,32 @@ import OSLog
account: ncAccount,
remoteInterface: ncKit,
dbManager: dbManager,
domain: domain
domain: domain,
log: log
)
}
func materializedItemsDidChange(completionHandler: @escaping () -> Void) {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not purging stale local file metadatas, account not set up")
logger.error("Not purging stale local file metadatas, account not set up")
completionHandler()
return
}
guard let dbManager else {
Logger.fileProviderExtension.error(
"""
Not purging stale local file metadatas.
db manager unabilable for domain: \(self.domain.displayName, privacy: .public)
"""
)
logger.error("Not purging stale local file metadatas. db manager unabilable for domain: \(self.domain.displayName)")
completionHandler()
return
}
guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error(
"Could not get file provider manager for domain: \(self.domain.displayName, privacy: .public)"
)
logger.error("Could not get file provider manager for domain: \(self.domain.displayName)")
completionHandler()
return
}
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
let materialisedObserver = MaterialisedEnumerationObserver(
ncKitAccount: ncAccount.ncKitAccount, dbManager: dbManager
) { _, _ in
let materialisedObserver = MaterialisedEnumerationObserver(ncKitAccount: ncAccount.ncKitAccount, dbManager: dbManager, log: log) { _, _ in
completionHandler()
}
let startingPage = NSFileProviderPage(NSFileProviderPage.initialPageSortedByName as Data)
@ -594,9 +513,7 @@ import OSLog
func signalEnumerator(completionHandler: @escaping (_ error: Error?) -> Void) {
guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error(
"Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts."
)
logger.error("Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts.")
return
}

View File

@ -10,35 +10,35 @@ import OSLog
class FileProviderSocketLineProcessor: NSObject, LineProcessor {
var delegate: FileProviderExtension
let logger: FileProviderLogger
required init(delegate: FileProviderExtension) {
required init(delegate: FileProviderExtension, log: any FileProviderLogging) {
self.delegate = delegate
self.logger = FileProviderLogger(category: "FileProviderSocketLineProcessor", log: log)
}
func process(_ line: String) {
if line.contains("~") { // We use this as the separator specifically in ACCOUNT_DETAILS
Logger.desktopClientConnection.debug(
"Processing file provider line with potentially sensitive user data")
logger.debug("Processing file provider line with potentially sensitive user data")
} else {
Logger.desktopClientConnection.debug(
"Processing file provider line: \(line, privacy: .public)")
logger.debug("Processing file provider line: \(line)")
}
let splitLine = line.split(separator: ":", maxSplits: 1)
guard let commandSubsequence = splitLine.first else {
Logger.desktopClientConnection.error("Input line did not have a first element")
logger.error("Input line did not have a first element")
return
}
let command = String(commandSubsequence)
Logger.desktopClientConnection.debug("Received command: \(command, privacy: .public)")
logger.debug("Received command: \(command)")
if command == "SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER" {
delegate.sendFileProviderDomainIdentifier()
} else if command == "ACCOUNT_NOT_AUTHENTICATED" {
delegate.removeAccountConfig()
} else if command == "ACCOUNT_DETAILS" {
guard let accountDetailsSubsequence = splitLine.last else {
Logger.desktopClientConnection.error("Account details did not have a first element")
logger.error("Account details did not have a first element")
return
}
let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 4)
@ -58,13 +58,11 @@ class FileProviderSocketLineProcessor: NSObject, LineProcessor {
)
} else if command == "IGNORE_LIST" {
guard let ignoreListSubsequence = splitLine.last else {
Logger.desktopClientConnection.error("Ignore list missing contents!")
logger.error("Ignore list missing contents!")
return
}
let ignoreList = ignoreListSubsequence.components(separatedBy: "_~IL$~_")
Logger.desktopClientConnection.debug(
"Applying \(ignoreList.count, privacy: .public) ignore file patterns"
)
logger.debug("Applying \(ignoreList.count) ignore file patterns")
delegate.ignoredFiles = IgnoredFilesMatcher(ignoreList: ignoreList)
}
}

View File

@ -2,19 +2,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import Foundation
import NextcloudFileProviderKit
import os
///
/// macOS keychain abstraction to fetch account passwords.
///
struct Keychain {
let logger: FileProviderLogger
init(log: any FileProviderLogging) {
self.logger = FileProviderLogger(category: "Keychain", log: log)
}
///
/// Lookup a generic password for the given account on the given server.
///
/// - Returns: `nil` in case of any error or the password not being found.
///
static func getPassword(for account: String, on server: String) -> String? {
Logger.keychain.debug("Looking for password of \"\(account)\" on \"\(server)\" in keychain...")
func getPassword(for account: String, on server: String) -> String? {
logger.debug("Looking for password of \"\(account)\" on \"\(server)\" in keychain...")
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
@ -29,32 +36,32 @@ struct Keychain {
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
Logger.keychain.error("Item not found!")
logger.error("Item not found!")
return nil
}
guard status == errSecSuccess else {
Logger.keychain.error("Keychain status: \(status)")
logger.error("Keychain status: \(status)")
return nil
}
guard let existingItem = item as? [String : Any], let passwordData = existingItem[kSecValueData as String] as? Data, let password = String(data: passwordData, encoding: String.Encoding.utf8) else {
Logger.keychain.error("Unexpected password data!")
logger.error("Unexpected password data!")
return nil
}
Logger.keychain.debug("Found \(password.isEmpty ? "empty" : "non-empty") password for \"\(account)\" on \"\(server)\" in keychain.")
logger.debug("Found \(password.isEmpty ? "empty" : "non-empty") password for \"\(account)\" on \"\(server)\" in keychain.")
return password
}
static func savePassword(_ password: String, for account: String, on server: String) {
func savePassword(_ password: String, for account: String, on server: String) {
guard password.isEmpty == false else {
Logger.keychain.error("Not saving password password for \"\(account)\" on \"\(server)\" because it is empty!")
logger.error("Not saving password password for \"\(account)\" on \"\(server)\" because it is empty!")
return
}
Logger.keychain.debug("Saving password for \"\(account)\" on \"\(server)\"...")
logger.debug("Saving password for \"\(account)\" on \"\(server)\"...")
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
@ -74,9 +81,9 @@ struct Keychain {
let updateStatus = SecItemUpdate(query as CFDictionary, updateAttributes as CFDictionary)
if updateStatus == errSecSuccess {
Logger.keychain.debug("Succeeded to update password for \"\(account)\" on \"\(server)\" in keychain.")
logger.debug("Succeeded to update password for \"\(account)\" on \"\(server)\" in keychain.")
} else {
Logger.keychain.error("Failed to update password for \"\(account)\" on \"\(server)\" in keychain. Status: \(updateStatus)")
logger.error("Failed to update password for \"\(account)\" on \"\(server)\" in keychain. Status: \(updateStatus)")
}
} else if status == errSecItemNotFound {
// Item doesn't exist, add a new one
@ -90,12 +97,12 @@ struct Keychain {
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
if addStatus == errSecSuccess {
Logger.keychain.debug("Succeeded to add password for \"\(account)\" on \"\(server)\" in keychain.")
logger.debug("Succeeded to add password for \"\(account)\" on \"\(server)\" in keychain.")
} else {
Logger.keychain.error("Failed to add password for \"\(account)\" on \"\(server)\" in keychain. Status: \(addStatus)")
logger.error("Failed to add password for \"\(account)\" on \"\(server)\" in keychain. Status: \(addStatus)")
}
} else {
Logger.keychain.error("Failed to check for existing password for \"\(account)\" on \"\(server)\" in keychain. Status: \(status)")
logger.error("Failed to check for existing password for \"\(account)\" on \"\(server)\" in keychain. Status: \(status)")
}
}
}

View File

@ -21,7 +21,6 @@
password:(NSString *)password
userAgent:(NSString *)userAgent;
- (void)removeAccountConfig;
- (void)createDebugLogStringWithCompletionHandler:(void(^)(NSString *debugLogString, NSError *error))completionHandler;
- (void)getTrashDeletionEnabledStateWithCompletionHandler:(void(^)(BOOL enabled, BOOL set))completionHandler;
- (void)setTrashDeletionEnabled:(BOOL)enabled;
- (void)setIgnoreList:(NSArray<NSString *> *)ignoreList;

View File

@ -10,11 +10,13 @@ import OSLog
class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, ClientCommunicationProtocol {
let listener = NSXPCListener.anonymous()
let logger: FileProviderLogger
let serviceName = NSFileProviderServiceName("com.nextcloud.desktopclient.ClientCommunicationService")
let fpExtension: FileProviderExtension
init(fpExtension: FileProviderExtension) {
Logger.desktopClientConnection.debug("Instantiating client communication service")
self.logger = FileProviderLogger(category: "ClientCommunicationService", log: fpExtension.log)
logger.debug("Instantiating client communication service")
self.fpExtension = fpExtension
super.init()
}
@ -36,7 +38,7 @@ class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCLi
func getFileProviderDomainIdentifier(completionHandler: @escaping (String?, Error?) -> Void) {
let identifier = self.fpExtension.domain.identifier.rawValue
Logger.desktopClientConnection.info("Returning file provider domain identifier \(identifier, privacy: .public)")
logger.info("Returning file provider domain identifier \(identifier)")
completionHandler(identifier, nil)
}
@ -47,7 +49,7 @@ class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCLi
password: String,
userAgent: String
) {
Logger.desktopClientConnection.info("Received configure account information over client communication service")
logger.info("Received configure account information over client communication service")
self.fpExtension.setupDomainAccount(
user: user,
userId: userId,
@ -61,23 +63,6 @@ class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCLi
self.fpExtension.removeAccountConfig()
}
func createDebugLogString(completionHandler: ((String?, Error?) -> Void)!) {
if #available(macOSApplicationExtension 12.0, *) {
let (logs, error) = Logger.logEntries()
guard error == nil else {
Logger.logger.error("Cannot create debug archive, received error: \(error, privacy: .public)")
completionHandler(nil, error)
return
}
guard let logs = logs else {
Logger.logger.error("Canot create debug archive with nil logs.")
completionHandler(nil, nil)
return
}
completionHandler(logs.joined(separator: "\n"), nil)
}
}
func getTrashDeletionEnabledState(completionHandler: @escaping (Bool, Bool) -> Void) {
let enabled = fpExtension.config.trashDeletionEnabled
let set = fpExtension.config.trashDeletionSet
@ -86,13 +71,11 @@ class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCLi
func setTrashDeletionEnabled(_ enabled: Bool) {
fpExtension.config.trashDeletionEnabled = enabled
Logger.fileProviderExtension.info(
"Trash deletion setting changed to: \(enabled, privacy: .public)"
)
logger.info("Trash deletion setting changed to: \(enabled)")
}
func setIgnoreList(_ ignoreList: [String]) {
self.fpExtension.ignoredFiles = IgnoredFilesMatcher(ignoreList: ignoreList)
Logger.fileProviderExtension.info("Ignore list updated.")
logger.info("Ignore list updated.")
}
}

View File

@ -13,12 +13,16 @@ import NextcloudFileProviderKit
import OSLog
class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, FPUIExtensionService {
let keychain: Keychain
let listener = NSXPCListener.anonymous()
let logger: FileProviderLogger
let serviceName = fpUiExtensionServiceName
let fpExtension: FileProviderExtension
init(fpExtension: FileProviderExtension) {
Logger.fpUiExtensionService.debug("Instantiating FPUIExtensionService service")
keychain = Keychain(log: fpExtension.log)
logger = FileProviderLogger(category: "FPUIExtensionServiceSource", log: fpExtension.log)
logger.debug("Instantiating FPUIExtensionService service")
self.fpExtension = fpExtension
super.init()
}
@ -42,10 +46,10 @@ class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCLi
//MARK: - FPUIExtensionService protocol methods
func authenticate() async -> NSError? {
Logger.fpUiExtensionService.info("Authenticating...")
logger.info("Authenticating...")
guard let user = fpExtension.config.user, let userId = fpExtension.config.userId, let serverUrl = fpExtension.config.serverUrl, let password = Keychain.getPassword(for: user, on: serverUrl) else {
Logger.fpUiExtensionService.error("Missing account information, cannot authenticate!")
guard let user = fpExtension.config.user, let userId = fpExtension.config.userId, let serverUrl = fpExtension.config.serverUrl, let password = keychain.getPassword(for: user, on: serverUrl) else {
logger.error("Missing account information, cannot authenticate!")
return NSError(.missingAccountInformation)
}
@ -61,7 +65,7 @@ class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCLi
return nil
}
let nkSession = fpExtension.ncKit.getSession(account: account)
let nkSession = fpExtension.ncKit.nkCommonInstance.nksessions.session(forAccount: account)
return nkSession?.userAgent as NSString?
}
@ -71,23 +75,23 @@ class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCLi
func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString? {
let rawIdentifier = identifier.rawValue
Logger.shares.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
logger.info("Fetching shares for item \(rawIdentifier)")
guard let baseUrl = fpExtension.ncAccount?.davFilesUrl else {
Logger.shares.error("Could not fetch shares as ncAccount on parent extension is nil")
logger.error("Could not fetch shares as ncAccount on parent extension is nil")
return nil
}
guard let account = fpExtension.ncAccount?.ncKitAccount else {
Logger.shares.error("Could not fetch ncKitAccount on parent extension")
logger.error("Could not fetch ncKitAccount on parent extension")
return nil
}
guard let dbManager = fpExtension.dbManager else {
Logger.shares.error("Could not get db manager for \(account, privacy: .public)")
logger.error("Could not get db manager for \(account)")
return nil
}
guard let item = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier) else {
Logger.shares.error("No item \(rawIdentifier, privacy: .public) in db, no shares.")
logger.error("No item \(rawIdentifier) in db, no shares.")
return nil
}

View File

@ -3,6 +3,7 @@
import AppKit
import FileProviderUI
import NextcloudFileProviderKit
import os
///
@ -10,6 +11,17 @@ import os
///
class AuthenticationViewController: NSViewController {
private var authenticationError: Error?
private var logger: FileProviderLogger!
var log: (any FileProviderLogging)? {
didSet {
guard let log else {
return
}
logger = FileProviderLogger(category: "AuthenticationViewController", log: log)
}
}
@IBOutlet var activityDescription: NSTextField!
@IBOutlet var cancellationButton: NSButton!
@ -18,6 +30,11 @@ class AuthenticationViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let domainIdentifier = extensionContext.domainIdentifier else {
fatalError("Domain identifier is not provided by the extension context!")
return
}
activityDescription.stringValue = String(localized: "Authenticating…")
cancellationButton.title = String(localized: "Cancel")
}
@ -80,16 +97,16 @@ class AuthenticationViewController: NSViewController {
let url = try await manager.getUserVisibleURL(for: .rootContainer)
let connection = try await serviceConnection(url: url, interruptionHandler: {
Logger.authenticationViewController.error("Service connection interrupted")
self.logger.error("Service connection interrupted")
})
if let error = await connection.authenticate() {
Logger.authenticationViewController.error("An error was returned from the authentication call: \(error.localizedDescription, privacy: .public)")
logger.error("An error was returned from the authentication call: \(error.localizedDescription)")
updateViewsWithError(error)
return
}
Logger.authenticationViewController.info("Apparently, the authentication was successful.")
logger.info("Apparently, the authentication was successful.")
extensionContext.completeRequest()
}

View File

@ -1,12 +1,8 @@
//
// DocumentActionViewController.swift
// FileProviderUIExt
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
import FileProviderUI
import NextcloudFileProviderKit
import OSLog
class DocumentActionViewController: FPUIActionExtensionViewController {
@ -20,7 +16,32 @@ class DocumentActionViewController: FPUIActionExtensionViewController {
)
}
///
/// To be passed down in the hierarchy to all subordinate code.
///
var log: (any FileProviderLogging)!
///
/// To be used by this view controller only.
///
/// Child view controllers must set up their own for clarity.
///
var logger: FileProviderLogger!
// MARK: - Lifecycle
func setUpLogger() {
if log == nil {
log = FileProviderLog(fileProviderDomainIdentifier: domain.identifier)
}
if logger == nil, let log {
logger = FileProviderLogger(category: "DocumentActionViewController", log: log)
}
}
func prepare(childViewController: NSViewController) {
setUpLogger()
addChild(childViewController)
view.addSubview(childViewController.view)
@ -32,31 +53,32 @@ class DocumentActionViewController: FPUIActionExtensionViewController {
])
}
override func prepare(
forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]
) {
Logger.actionViewController.info("Preparing action: \(actionIdentifier, privacy: .public)")
override func prepare(forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]) {
setUpLogger()
logger?.info("Preparing action: \(actionIdentifier)")
switch (actionIdentifier) {
case "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction":
prepare(childViewController: ShareViewController(itemIdentifiers))
case "com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction":
prepare(childViewController: LockViewController(itemIdentifiers, locking: true))
case "com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction":
prepare(childViewController: LockViewController(itemIdentifiers, locking: false))
case "com.nextcloud.desktopclient.FileProviderUIExt.EvictAction":
evict(itemsWithIdentifiers: itemIdentifiers, inDomain: domain);
extensionContext.completeRequest();
default:
return
case "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction":
prepare(childViewController: ShareViewController(itemIdentifiers, log: log))
case "com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction":
prepare(childViewController: LockViewController(itemIdentifiers, locking: true, log: log))
case "com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction":
prepare(childViewController: LockViewController(itemIdentifiers, locking: false, log: log))
case "com.nextcloud.desktopclient.FileProviderUIExt.EvictAction":
evict(itemsWithIdentifiers: itemIdentifiers, inDomain: domain);
extensionContext.completeRequest();
default:
return
}
}
override func prepare(forError error: Error) {
Logger.actionViewController.info("Preparing for error: \(error.localizedDescription, privacy: .public)")
setUpLogger()
logger?.info("Preparing for error.", [.error: error])
let storyboard = NSStoryboard(name: "Authentication", bundle: Bundle(for: type(of: self)))
let viewController = storyboard.instantiateInitialController() as! NSViewController
let viewController = storyboard.instantiateInitialController() as! AuthenticationViewController
viewController.log = log
prepare(childViewController: viewController)
}
@ -64,4 +86,40 @@ class DocumentActionViewController: FPUIActionExtensionViewController {
override public func loadView() {
self.view = NSView()
}
// MARK: - Eviction
///
/// Use a file provider domain manager to evict all items identified by the given array.
///
func evict(itemsWithIdentifiers identifiers: [NSFileProviderItemIdentifier], inDomain domain: NSFileProviderDomain) async {
logger?.debug("Starting eviction process…")
guard let manager = NSFileProviderManager(for: domain) else {
logger?.error("Could not get file provider domain manager.", [.domain: domain.identifier])
return;
}
do {
for itemIdentifier in identifiers {
logger?.error("Evicting item: \(itemIdentifier.rawValue)")
try await manager.evictItem(identifier: itemIdentifier)
}
} catch let error {
logger?.error("Error evicting item: \(error.localizedDescription)")
}
}
///
/// Synchronous wrapper of ``evict(itemsWithIdentifiers:inDomain:)-67w8c``.
///
func evict(itemsWithIdentifiers identifiers: [NSFileProviderItemIdentifier], inDomain domain: NSFileProviderDomain) {
let semaphore = DispatchSemaphore(value: 0)
Task {
await evict(itemsWithIdentifiers: identifiers, inDomain: domain)
semaphore.signal()
}
semaphore.wait()
}
}

View File

@ -1,48 +0,0 @@
//
// Eviction.swift
// FileProviderUIExt
//
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
import FileProvider
import Foundation
import OSLog
func evict(
itemsWithIdentifiers identifiers: [NSFileProviderItemIdentifier],
inDomain domain: NSFileProviderDomain
) async {
Logger.eviction.debug("Starting eviction process…")
guard let manager = NSFileProviderManager(for: domain) else {
Logger.eviction.error(
"Could not get manager for domain: \(domain.identifier.rawValue, privacy: .public)"
)
return;
}
do {
for itemIdentifier in identifiers {
Logger.eviction.error(
"Evicting item: \(itemIdentifier.rawValue, privacy: .public)"
)
try await manager.evictItem(identifier: itemIdentifier)
}
} catch let error {
Logger.eviction.error(
"Error evicting item: \(error.localizedDescription, privacy: .public)"
)
}
}
func evict(
itemsWithIdentifiers identifiers: [NSFileProviderItemIdentifier],
inDomain domain: NSFileProviderDomain
) {
let semaphore = DispatchSemaphore(value: 0)
Task {
await evict(itemsWithIdentifiers: identifiers, inDomain: domain)
semaphore.signal()
}
semaphore.wait()
}

View File

@ -1,26 +0,0 @@
//
// Logger+Extensions.swift
// FileProviderUIExt
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
import OSLog
extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static let actionViewController = Logger(subsystem: subsystem, category: "actionViewController")
static let authenticationViewController = Logger(subsystem: subsystem, category: "authenticationViewController")
static let eviction = Logger(subsystem: subsystem, category: "eviction")
static let lockViewController = Logger(subsystem: subsystem, category: "lockViewController")
static let metadataProvider = Logger(subsystem: subsystem, category: "metadataProvider")
static let shareCapabilities = Logger(subsystem: subsystem, category: "shareCapabilities")
static let shareController = Logger(subsystem: subsystem, category: "shareController")
static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource")
static let sharesDataSource = Logger(subsystem: subsystem, category: "sharesDataSource")
static let shareOptionsView = Logger(subsystem: subsystem, category: "shareOptionsView")
static let shareViewController = Logger(subsystem: subsystem, category: "shareViewController")
}

View File

@ -17,6 +17,8 @@ import QuickLookThumbnailing
class LockViewController: NSViewController {
let itemIdentifiers: [NSFileProviderItemIdentifier]
let locking: Bool
let log: any FileProviderLogging
let logger: FileProviderLogger
@IBOutlet weak var fileNameIcon: NSImageView!
@IBOutlet weak var fileNameLabel: NSTextField!
@ -33,9 +35,11 @@ class LockViewController: NSViewController {
return parent as? DocumentActionViewController
}
init(_ itemIdentifiers: [NSFileProviderItemIdentifier], locking: Bool) {
init(_ itemIdentifiers: [NSFileProviderItemIdentifier], locking: Bool, log: any FileProviderLogging) {
self.itemIdentifiers = itemIdentifiers
self.locking = locking
self.log = log
self.logger = FileProviderLogger(category: "LockViewController", log: log)
super.init(nibName: nil, bundle: nil)
}
@ -45,17 +49,12 @@ class LockViewController: NSViewController {
override func viewDidLoad() {
guard let firstItem = itemIdentifiers.first else {
Logger.shareViewController.error("called without items")
logger.error("called without items")
closeAction(self)
return
}
Logger.lockViewController.info(
"""
Locking \(self.locking ? "enabled" : "disabled", privacy: .public) for items:
\(firstItem.rawValue, privacy: .public)
"""
)
logger.info("Locking \(self.locking ? "enabled" : "disabled") for items: \(firstItem.rawValue)")
Task {
await processItemIdentifier(firstItem)
@ -75,20 +74,21 @@ class LockViewController: NSViewController {
}
private func presentError(_ error: String) {
Logger.lockViewController.error("Error: \(error, privacy: .public)")
logger.error("Error: \(error)")
descriptionLabel.stringValue = "Error: \(error)"
stopIndicatingLoading()
}
private func fetchCapabilities(account: Account, kit: NextcloudKit) async -> Capabilities? {
return await withCheckedContinuation { continuation in
kit.getCapabilities(account: account.ncKitAccount) { account, data, error in
kit.getCapabilities(account: account.ncKitAccount) { account, _, data, error in
guard error == .success, let capabilitiesJson = data?.data else {
self.presentError("Error getting server caps: \(error.errorDescription)")
continuation.resume(returning: nil)
return
}
Logger.lockViewController.info("Successfully retrieved server share capabilities")
self.logger.info("Successfully retrieved server share capabilities")
continuation.resume(returning: Capabilities(data: capabilitiesJson))
}
}
@ -102,7 +102,7 @@ class LockViewController: NSViewController {
do {
let itemUrl = try await manager.getUserVisibleURL(for: itemIdentifier)
guard itemUrl.startAccessingSecurityScopedResource() else {
Logger.lockViewController.error("Could not access scoped resource for item url!")
logger.error("Could not access scoped resource for item url!")
return
}
await updateFileDetailsDisplay(itemUrl: itemUrl)
@ -110,7 +110,7 @@ class LockViewController: NSViewController {
await lockOrUnlockFile(localItemUrl: itemUrl)
} catch let error {
let errorString = "Error processing item: \(error)"
Logger.lockViewController.error("\(errorString, privacy: .public)")
logger.error("\(errorString)")
fileNameLabel.stringValue = String(localized: "Could not lock unknown item…")
descriptionLabel.stringValue = error.localizedDescription
}
@ -129,9 +129,7 @@ class LockViewController: NSViewController {
let fileThumbnail = await withCheckedContinuation { continuation in
generator.generateRepresentations(for: request) { thumbnail, type, error in
if thumbnail == nil || error != nil {
Logger.lockViewController.error(
"Could not get thumbnail: \(error, privacy: .public)"
)
self.logger.error("Could not get thumbnail.", [.error: error])
}
continuation.resume(returning: thumbnail)
}
@ -161,7 +159,7 @@ class LockViewController: NSViewController {
do {
let connection = try await serviceConnection(url: localItemUrl, interruptionHandler: {
Logger.lockViewController.error("Service connection interrupted")
self.logger.error("Service connection interrupted")
})
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
let credentials = await connection.credentials() as? Dictionary<String, String>,
@ -180,7 +178,6 @@ class LockViewController: NSViewController {
userId: account.id,
password: account.password,
userAgent: "Nextcloud-macOS/FileProviderUIExt",
nextcloudVersion: 25,
groupIdentifier: ""
)
guard let capabilities = await fetchCapabilities(account: account, kit: kit),
@ -213,12 +210,7 @@ class LockViewController: NSViewController {
let serverUrlFileName = itemMetadata.serverUrl + "/" + itemMetadata.fileName
Logger.lockViewController.info(
"""
Locking file: \(serverUrlFileName, privacy: .public)
\(self.locking ? "locking" : "unlocking", privacy: .public)
"""
)
logger.info("Locking file: \(serverUrlFileName) \(self.locking ? "locking" : "unlocking")")
let error = await withCheckedContinuation { continuation in
kit.lockUnlockFile(

View File

@ -1,32 +1,27 @@
//
// MetadataProvider.swift
// FileProviderUIExt
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
import Foundation
import NextcloudFileProviderKit
import NextcloudKit
import OSLog
func fetchItemMetadata(
itemRelativePath: String, account: Account, kit: NextcloudKit
) async -> NKFile? {
func fetchItemMetadata(itemRelativePath: String, account: Account, kit: NextcloudKit) async -> NKFile? {
func slashlessPath(_ string: String) -> String {
var strCopy = string
if strCopy.hasPrefix("/") {
strCopy.removeFirst()
}
if strCopy.hasSuffix("/") {
strCopy.removeLast()
}
return strCopy
}
guard let nksession = kit.getSession(account: account.ncKitAccount) else {
Logger.metadataProvider.error("Could not get nksession for \(account.ncKitAccount)")
guard let nksession = kit.nkCommonInstance.nksessions.session(forAccount: account.ncKitAccount) else {
return nil
}
@ -34,21 +29,15 @@ func fetchItemMetadata(
let davSuffix = slashlessPath(nksession.dav)
let userId = nksession.userId
let itemRelPath = slashlessPath(itemRelativePath)
let itemFullServerPath = "\(urlBase)/\(davSuffix)/files/\(userId)/\(itemRelPath)"
return await withCheckedContinuation { continuation in
kit.readFileOrFolder(
serverUrlFileName: itemFullServerPath, depth: "0", account: account.ncKitAccount
) {
account, files, data, error in
kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0", account: account.ncKitAccount) { account, files, data, error in
guard error == .success else {
Logger.metadataProvider.error(
"Error getting item metadata: \(error.errorDescription)"
)
continuation.resume(returning: nil)
return
}
Logger.metadataProvider.info("Successfully retrieved item metadata")
continuation.resume(returning: files?.first)
}
}

View File

@ -16,6 +16,8 @@ class ShareController: ObservableObject {
@Published private(set) var share: NKShare
private let kit: NextcloudKit
private let account: Account
let log: any FileProviderLogging
let logger: FileProviderLogger
static func create(
account: Account,
@ -33,33 +35,24 @@ class ShareController: ObservableObject {
attributes: String? = nil,
options: NKRequestOptions = NKRequestOptions()
) async -> NKError? {
Logger.shareController.info("Creating share: \(itemServerRelativePath)")
return await withCheckedContinuation { continuation in
if shareType == .publicLink {
kit.createShareLink(
kit.createShare(
path: itemServerRelativePath,
hideDownload: hideDownload,
shareType: ShareType.publicLink.rawValue,
shareWith: nil,
publicUpload: publicUpload,
hideDownload: hideDownload,
password: password,
permissions: permissions,
account: account.ncKitAccount,
options: options
) { account, share, data, error in
defer { continuation.resume(returning: error) }
guard error == .success else {
Logger.shareController.error(
"""
Error creating link share: \(error.errorDescription, privacy: .public)
"""
)
return
}
continuation.resume(returning: error)
}
} else {
guard let shareWith = shareWith else {
let errorString = "No recipient for share!"
Logger.shareController.error("\(errorString, privacy: .public)")
let error = NKError(statusCode: 0, fallbackDescription: errorString)
let error = NKError(statusCode: 0, fallbackDescription: "No recipient for share!")
continuation.resume(returning: error)
return
}
@ -73,24 +66,18 @@ class ShareController: ObservableObject {
attributes: attributes,
account: account.ncKitAccount
) { account, share, data, error in
defer { continuation.resume(returning: error) }
guard error == .success else {
Logger.shareController.error(
"""
Error creating share: \(error.errorDescription, privacy: .public)
"""
)
return
}
continuation.resume(returning: error)
}
}
}
}
init(share: NKShare, account: Account, kit: NextcloudKit) {
init(share: NKShare, account: Account, kit: NextcloudKit, log: any FileProviderLogging) {
self.account = account
self.share = share
self.kit = kit
self.log = log
self.logger = FileProviderLogger(category: "ShareController", log: log)
}
func save(
@ -104,7 +91,8 @@ class ShareController: ObservableObject {
attributes: String? = nil,
options: NKRequestOptions = NKRequestOptions()
) async -> NKError? {
Logger.shareController.info("Saving share: \(self.share.url, privacy: .public)")
logger.info("Saving share.", [.url: self.share.url])
return await withCheckedContinuation { continuation in
kit.updateShare(
idShare: share.idShare,
@ -119,43 +107,36 @@ class ShareController: ObservableObject {
account: account.ncKitAccount,
options: options
) { account, share, data, error in
Logger.shareController.info(
"""
Received update response: \(share?.url ?? "", privacy: .public)
"""
)
defer { continuation.resume(returning: error) }
self.logger.info("Received update response: \(share?.url ?? "")")
defer {
continuation.resume(returning: error)
}
guard error == .success, let share = share else {
Logger.shareController.error(
"""
Error updating save: \(error.errorDescription, privacy: .public)
"""
)
self.logger.error("Error updating save.", [.error: error])
return
}
self.share = share
}
}
}
func delete() async -> NKError? {
Logger.shareController.info("Deleting share: \(self.share.url, privacy: .public)")
logger.info("Deleting share: \(self.share.url)")
return await withCheckedContinuation { continuation in
kit.deleteShare(
idShare: share.idShare, account: account.ncKitAccount
) { account, _, error in
Logger.shareController.info(
"""
Received delete response: \(self.share.url, privacy: .public)
"""
)
defer { continuation.resume(returning: error) }
kit.deleteShare(idShare: share.idShare, account: account.ncKitAccount) { account, _, error in
self.logger.info("Received delete response: \(self.share.url)")
defer {
continuation.resume(returning: error)
}
guard error == .success else {
Logger.shareController.error(
"""
Error deleting save: \(error.errorDescription, privacy: .public)
"""
)
self.logger.error("Error deleting save: \(error.errorDescription)")
return
}
}

View File

@ -14,6 +14,8 @@ import OSLog
import SuggestionsTextFieldKit
class ShareOptionsView: NSView {
var logger: FileProviderLogger?
@IBOutlet private weak var optionsTitleTextField: NSTextField!
@IBOutlet private weak var shareRecipientTextField: NSTextField! // Hide if public link share
@IBOutlet private weak var labelTextField: NSTextField!
@ -41,20 +43,28 @@ class ShareOptionsView: NSView {
let kit = NextcloudKit.shared
var account: Account? {
didSet {
Logger.shareOptionsView.info("Setting up account.")
logger?.info("Setting up account.")
guard let account else {
Logger.shareOptionsView.error("Could not configure suggestions data source.")
logger?.error("Could not configure suggestions data source.")
return
}
suggestionsTextFieldDelegate.suggestionsDataSource = ShareeSuggestionsDataSource(
account: account, kit: kit
)
suggestionsTextFieldDelegate.confirmationHandler = { suggestion in
guard let sharee = suggestion?.data as? NKSharee else { return }
self.shareRecipientTextField.stringValue = sharee.shareWith
Logger.shareOptionsView.debug("Chose sharee \(sharee.shareWith, privacy: .public)")
guard let controller else {
return
}
suggestionsTextFieldDelegate.suggestionsDataSource = ShareeSuggestionsDataSource(account: account, kit: kit, log: controller.log)
suggestionsTextFieldDelegate.confirmationHandler = { suggestion in
guard let sharee = suggestion?.data as? NKSharee else {
return
}
self.shareRecipientTextField.stringValue = sharee.shareWith
self.logger?.debug("Chose sharee \(sharee.shareWith)")
}
suggestionsTextFieldDelegate.targetTextField = shareRecipientTextField
}
}
@ -74,7 +84,7 @@ class ShareOptionsView: NSView {
}
var createMode = false {
didSet {
Logger.shareOptionsView.info("Create mode set: \(self.createMode, privacy: .public)")
logger?.info("Create mode set: \(self.createMode)")
shareTypePicker.isHidden = !createMode
shareRecipientTextField.isHidden = !createMode
labelTextField.isHidden = createMode // Cannot set label on create API call
@ -97,7 +107,7 @@ class ShareOptionsView: NSView {
private var suggestionsTextFieldDelegate = SuggestionsTextFieldDelegate()
private func update() {
guard let share = controller?.share else {
guard let controller else {
reset()
setAllFields(enabled: false)
saveButton.isEnabled = false
@ -105,6 +115,9 @@ class ShareOptionsView: NSView {
return
}
logger = FileProviderLogger(category: "ShareOptionsView", log: controller.log)
let share = controller.share
// Programmatically update localizable texts.
publicLinkShareMenuItem.title = String(localized: "Public link share")
userShareMenuItem.title = String(localized: "User share")
@ -278,18 +291,13 @@ class ShareOptionsView: NSView {
let uploadAndEdit = uploadEditPermissionCheckbox.state == .on
guard !createMode else {
Logger.shareOptionsView.info("Creating new share!")
logger?.info("Creating new share!")
guard let dataSource,
let account,
let itemServerRelativePath = dataSource.itemServerRelativePath
else {
Logger.shareOptionsView.error("Cannot create new share due to missing data.")
Logger.shareOptionsView.error("dataSource: \(self.dataSource, privacy: .public)")
Logger.shareOptionsView.error("account: \(self.account != nil, privacy: .public)")
Logger.shareOptionsView.error(
"path: \(self.dataSource?.itemServerRelativePath ?? "", privacy: .public)"
)
logger?.error("Cannot create new share due to missing data. dataSource: \(String(describing: self.dataSource)) account: \(self.account != nil) path: \(self.dataSource?.itemServerRelativePath ?? "")")
return
}
@ -327,10 +335,10 @@ class ShareOptionsView: NSView {
return
}
Logger.shareOptionsView.info("Editing existing share!")
logger?.info("Editing existing share!")
guard let controller = controller else {
Logger.shareOptionsView.error("No valid share controller, cannot edit share.")
logger?.error("No valid share controller, cannot edit share.")
return
}
let share = controller.share
@ -378,3 +386,4 @@ class ShareOptionsView: NSView {
}
}
}

View File

@ -19,6 +19,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
private let reattemptInterval: TimeInterval = 3.0
let kit = NextcloudKit.shared
let logger: FileProviderLogger
var uiDelegate: ShareViewDataSourceUIDelegate?
var sharesTableView: NSTableView? {
@ -48,12 +49,15 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
userId: account.username,
password: account.password,
userAgent: userAgent,
nextcloudVersion: 25,
groupIdentifier: ""
)
}
}
init(log: any FileProviderLogging) {
self.logger = FileProviderLogger(category: "ShareTableViewDataSource", log: log)
}
func loadItem(url: URL) {
itemServerRelativePath = nil
itemURL = url
@ -93,7 +97,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
do {
let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
Logger.sharesDataSource.error("Service connection interrupted")
self.logger.error("Service connection interrupted")
})
if let acquiredUserAgent = await connection.userAgent() {
userAgent = acquiredUserAgent as String
@ -148,7 +152,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
let rawIdentifier = itemIdentifier.rawValue
Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
logger.info("Fetching shares for item \(rawIdentifier)")
guard let account else {
self.presentError(String(localized: "NextcloudKit instance or account is unavailable, cannot fetch shares!"))
@ -162,7 +166,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
parameters: parameter, account: account.ncKitAccount
) { account, shares, data, error in
let shareCount = shares?.count ?? 0
Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
self.logger.info("Received \(shareCount) shares")
defer { continuation.resume(returning: shares ?? []) }
guard error == .success else {
self.presentError(String(localized: "Error fetching shares: \(error.errorDescription)"))
@ -190,20 +194,21 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
}
return await withCheckedContinuation { continuation in
kit.getCapabilities(account: account.ncKitAccount) { account, data, error in
kit.getCapabilities(account: account.ncKitAccount) { account, _, data, error in
guard error == .success, let capabilitiesJson = data?.data else {
self.presentError(String(localized: "Error getting server caps: \(error.errorDescription)"))
continuation.resume(returning: nil)
return
}
Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
self.logger.info("Successfully retrieved server share capabilities")
continuation.resume(returning: Capabilities(data: capabilitiesJson))
}
}
}
private func presentError(_ errorString: String) {
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
logger.error("\(errorString)")
Task { @MainActor in self.uiDelegate?.showError(errorString) }
}
@ -222,7 +227,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
guard let view = tableView.makeView(
withIdentifier: shareItemViewIdentifier, owner: self
) as? ShareTableItemView else {
Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
logger.error("Acquired item view from table is not a share item view!")
return nil
}
view.share = share

View File

@ -1,20 +1,18 @@
//
// ShareViewController.swift
// FileProviderUIExt
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
import AppKit
import FileProvider
import NextcloudFileProviderKit
import NextcloudKit
import OSLog
import QuickLookThumbnailing
class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
let shareDataSource = ShareTableViewDataSource()
let shareDataSource: ShareTableViewDataSource
let itemIdentifiers: [NSFileProviderItemIdentifier]
let log: any FileProviderLogging
let logger: FileProviderLogger
@IBOutlet weak var fileNameIcon: NSImageView!
@IBOutlet weak var fileNameLabel: NSTextField!
@ -39,22 +37,20 @@ class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
return parent as? DocumentActionViewController
}
init(_ itemIdentifiers: [NSFileProviderItemIdentifier]) {
init(_ itemIdentifiers: [NSFileProviderItemIdentifier], log: any FileProviderLogging) {
self.itemIdentifiers = itemIdentifiers
self.log = log
self.logger = FileProviderLogger(category: "ShareViewController", log: log)
self.shareDataSource = ShareTableViewDataSource(log: log)
super.init(nibName: nil, bundle: nil)
guard let firstItem = itemIdentifiers.first else {
Logger.shareViewController.error("called without items")
logger.error("called without items")
closeAction(self)
return
}
Logger.shareViewController.info(
"""
Instantiated with itemIdentifiers:
\(itemIdentifiers.map { $0.rawValue }, privacy: .public)
"""
)
logger.info("Instantiated with itemIdentifiers: \(itemIdentifiers.map { $0.rawValue })")
Task {
await processItemIdentifier(firstItem)
@ -83,9 +79,10 @@ class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
do {
let itemUrl = try await manager.getUserVisibleURL(for: itemIdentifier)
guard itemUrl.startAccessingSecurityScopedResource() else {
Logger.shareViewController.error("Could not access scoped resource for item url!")
logger.error("Could not access scoped resource for item url!")
return
}
await updateDisplay(itemUrl: itemUrl)
shareDataSource.uiDelegate = self
shareDataSource.sharesTableView = tableView
@ -94,7 +91,7 @@ class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
itemUrl.stopAccessingSecurityScopedResource()
} catch let error {
let errorString = "Error processing item: \(error)"
Logger.shareViewController.error("\(errorString, privacy: .public)")
logger.error("\(errorString)")
fileNameLabel.stringValue = String(localized: "Unknown item")
descriptionLabel.stringValue = errorString
}
@ -114,12 +111,9 @@ class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
let fileThumbnail = await withCheckedContinuation { continuation in
generator.generateRepresentations(for: request) { thumbnail, type, error in
if thumbnail == nil || error != nil {
Logger.shareViewController.error(
"""
Could not get thumbnail: \(error, privacy: .public)
"""
)
self.logger.error("Could not get thumbnail.", [.error: error])
}
continuation.resume(returning: thumbnail)
}
}
@ -183,9 +177,8 @@ class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
func showOptions(share: NKShare) {
guard let account = shareDataSource.account, share.canEdit || share.canDelete else { return }
optionsView.account = account
optionsView.controller = ShareController(
share: share, account: account, kit: shareDataSource.kit
)
optionsView.controller = ShareController(share: share, account: account, kit: shareDataSource.kit, log: log)
if !splitView.arrangedSubviews.contains(optionsView) {
splitView.addArrangedSubview(optionsView)
optionsView.isHidden = false

View File

@ -14,26 +14,33 @@ import SuggestionsTextFieldKit
class ShareeSuggestionsDataSource: SuggestionsDataSource {
let kit: NextcloudKit
let logger: FileProviderLogger
let account: Account
var suggestions: [Suggestion] = []
var inputString: String = "" {
didSet { Task { await updateSuggestions() } }
didSet {
Task {
await updateSuggestions()
}
}
}
init(account: Account, kit: NextcloudKit) {
init(account: Account, kit: NextcloudKit, log: any FileProviderLogging) {
self.account = account
self.kit = kit
self.logger = FileProviderLogger(category: "ShareeSuggestionsDataSource", log: log)
}
private func updateSuggestions() async {
let sharees = await fetchSharees(search: inputString)
Logger.shareeDataSource.info("Fetched \(sharees.count, privacy: .public) sharees.")
logger.info("Fetched \(sharees.count) sharees.")
suggestions = suggestionsFromSharees(sharees)
NotificationCenter.default.post(name: SuggestionsChangedNotificationName, object: self)
}
private func fetchSharees(search: String) async -> [NKSharee] {
Logger.shareeDataSource.debug("Searching sharees with: \(search, privacy: .public)")
logger.debug("Searching sharees with: \(search)")
return await withCheckedContinuation { continuation in
kit.searchSharees(
search: inputString,
@ -41,11 +48,12 @@ class ShareeSuggestionsDataSource: SuggestionsDataSource {
perPage: 20,
account: account.ncKitAccount,
completion: { account, sharees, data, error in
defer { continuation.resume(returning: sharees ?? []) }
defer {
continuation.resume(returning: sharees ?? [])
}
guard error == .success else {
Logger.shareeDataSource.error(
"Error fetching sharees: \(error.errorDescription, privacy: .public)"
)
self.logger.error("Error fetching sharees: \(error.errorDescription)")
return
}
}

View File

@ -8,19 +8,14 @@
/* Begin PBXBuildFile section */
530429982DD44235004BB598 /* FileProviderExtension+CustomActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 530429972DD44226004BB598 /* FileProviderExtension+CustomActions.swift */; };
5307A6E62965C6FA001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E52965C6FA001E0C6A /* NextcloudKit */; };
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E72965DAD8001E0C6A /* NextcloudKit */; };
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 531522812B8E01C6002E31BE /* ShareTableItemView.xib */; };
531EDE572D897B4F00FD91F9 /* Eviction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531EDE562D897B4F00FD91F9 /* Eviction.swift */; };
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */; };
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */; };
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */; };
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */; };
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */; };
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */; };
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */; };
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5374FD432B95EE1400C78D54 /* ShareController.swift */; };
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */; };
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 537630902B85F4980026BFAB /* ShareViewController.xib */; };
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630922B85F4B00026BFAB /* ShareViewController.swift */; };
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; };
@ -50,7 +45,6 @@
53D666612B70C9A70042C03D /* FileProviderDomainDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D666602B70C9A70042C03D /* FileProviderDomainDefaults.swift */; };
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */; };
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */; };
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53FE14532B8E1219006C4193 /* NextcloudKit */; };
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */; };
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */; };
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */; };
@ -162,16 +156,13 @@
/* Begin PBXFileReference section */
530429972DD44226004BB598 /* FileProviderExtension+CustomActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+CustomActions.swift"; sourceTree = "<group>"; };
531522812B8E01C6002E31BE /* ShareTableItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableItemView.xib; sourceTree = "<group>"; };
531EDE562D897B4F00FD91F9 /* Eviction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Eviction.swift; sourceTree = "<group>"; };
5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClientCommunicationProtocol.h; sourceTree = "<group>"; };
5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCommunicationService.swift; sourceTree = "<group>"; };
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FileProviderExt-Bridging-Header.h"; sourceTree = "<group>"; };
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+Thumbnailing.swift"; sourceTree = "<group>"; };
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareeSuggestionsDataSource.swift; sourceTree = "<group>"; };
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderSocketLineProcessor.swift; sourceTree = "<group>"; };
5374FD432B95EE1400C78D54 /* ShareController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareController.swift; sourceTree = "<group>"; };
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
537630902B85F4980026BFAB /* ShareViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareViewController.xib; sourceTree = "<group>"; };
537630922B85F4B00026BFAB /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionServiceSource.swift; sourceTree = "<group>"; };
@ -231,7 +222,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */,
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */,
53903D302956173F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */,
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */,
@ -252,7 +242,6 @@
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */,
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */,
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */,
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -260,7 +249,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5307A6E62965C6FA001E0C6A /* NextcloudKit in Frameworks */,
53903D212956164F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -276,14 +264,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
531EDE542D88569400FD91F9 /* Evicting */ = {
isa = PBXGroup;
children = (
531EDE562D897B4F00FD91F9 /* Eviction.swift */,
);
path = Evicting;
sourceTree = "<group>";
};
5350E4C72B0C368B00F276CB /* Services */ = {
isa = PBXGroup;
children = (
@ -299,7 +279,6 @@
isa = PBXGroup;
children = (
AA9987852E72B6DB00B2C428 /* NextcloudKit+clearAccountErrorState.swift */,
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */,
AA7F17E62E7038340000E928 /* NSError+FileProviderErrorCode.swift */,
);
path = Extensions;
@ -308,7 +287,6 @@
5376307B2B85E2E00026BFAB /* Extensions */ = {
isa = PBXGroup;
children = (
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */,
53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */,
);
path = Extensions;
@ -382,7 +360,6 @@
isa = PBXGroup;
children = (
AA7F17DF2E70171A0000E928 /* Authentication */,
531EDE542D88569400FD91F9 /* Evicting */,
5376307B2B85E2E00026BFAB /* Extensions */,
537BD6782C58D0FC00446ED0 /* Locking */,
537BD6772C58D0C400446ED0 /* Sharing */,
@ -506,7 +483,6 @@
);
name = FileProviderExt;
packageProductDependencies = (
5307A6E72965DAD8001E0C6A /* NextcloudKit */,
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */,
);
productName = FileProviderExt;
@ -542,11 +518,9 @@
buildRules = (
);
dependencies = (
53FE14522B8E1213006C4193 /* PBXTargetDependency */,
);
name = FileProviderUIExt;
packageProductDependencies = (
53FE14532B8E1219006C4193 /* NextcloudKit */,
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */,
53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */,
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */,
@ -574,7 +548,6 @@
);
name = desktopclient;
packageProductDependencies = (
5307A6E52965C6FA001E0C6A /* NextcloudKit */,
);
productName = desktopclient;
productReference = C2B573B11B1CD91E00303B36 /* desktopclient.app */;
@ -753,7 +726,6 @@
);
mainGroup = C2B573941B1CD88000303B36;
packageReferences = (
5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */,
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */,
53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */,
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */,
@ -851,7 +823,6 @@
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */,
AA02B2AB2E7048C800C72B34 /* Keychain.swift in Sources */,
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */,
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */,
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */,
530429982DD44235004BB598 /* FileProviderExtension+CustomActions.swift in Sources */,
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */,
@ -872,7 +843,6 @@
buildActionMask = 2147483647;
files = (
537BD6822C58F72E00446ED0 /* MetadataProvider.swift in Sources */,
531EDE572D897B4F00FD91F9 /* Eviction.swift in Sources */,
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */,
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */,
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */,
@ -882,7 +852,6 @@
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */,
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */,
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */,
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */,
AA7F17E32E70173E0000E928 /* AuthenticationViewController.swift in Sources */,
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */,
537BD6802C58F01B00446ED0 /* FileProviderCommunication.swift in Sources */,
@ -930,10 +899,6 @@
target = 53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */;
targetProxy = 53903D322956173F00D0B308 /* PBXContainerItemProxy */;
};
53FE14522B8E1213006C4193 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 53FE14512B8E1213006C4193 /* NextcloudKit */;
};
C2B573E01B1CD9CE00303B36 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C2B573D61B1CD9CE00303B36 /* FinderSyncExt */;
@ -1649,14 +1614,6 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nextcloud/NextcloudKit.git";
requirement = {
kind = exactVersion;
version = 6.0.9;
};
};
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/claucambra/NextcloudCapabilitiesKit.git";
@ -1684,16 +1641,6 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
5307A6E52965C6FA001E0C6A /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
5307A6E72965DAD8001E0C6A /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */;
@ -1714,16 +1661,6 @@
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
productName = NextcloudFileProviderKit;
};
53FE14512B8E1213006C4193 /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
53FE14532B8E1219006C4193 /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = C2B573951B1CD88000303B36 /* Project object */;

View File

@ -1,5 +1,5 @@
{
"originHash" : "b1fcfe4980e7dccc02e64ff6b1167e865e1b0fd2839c1f05e315987946e210b5",
"originHash" : "af7b30e0399513f38616bd75821c834208fc3c22e3f5ec7d1765b1dbb03a46cd",
"pins" : [
{
"identity" : "alamofire",
@ -25,16 +25,16 @@
"location" : "https://github.com/nextcloud/NextcloudFileProviderKit.git",
"state" : {
"branch" : "main",
"revision" : "628d78e08a72a9927775d50c71f6af0e8c0f8df3"
"revision" : "8add813aceb22a11309496e3249b36bd80c5db3d"
}
},
{
"identity" : "nextcloudkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nextcloud/NextcloudKit.git",
"location" : "https://github.com/nextcloud/NextcloudKit",
"state" : {
"revision" : "8ac6704234f529d09b0651e4d94fdc14f8820333",
"version" : "6.0.9"
"revision" : "3353eacfb44fb981ae2e14236f0bcd7e11ea2707",
"version" : "7.1.4"
}
},
{
@ -51,8 +51,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-swift.git",
"state" : {
"revision" : "bae6c4be7df169fdb047d0ad63f902c1e2665e83",
"version" : "20.0.1"
"revision" : "6260534683132eb981338c7c39fd5e69205876e6",
"version" : "20.0.3"
}
},
{

View File

@ -25,6 +25,7 @@
#endif
#ifdef BUILD_FILE_PROVIDER_MODULE
#include "macOS/fileproviderutils.h"
#include "macOS/fileprovider.h"
#include "macOS/fileprovidersettingscontroller.h"
#endif
@ -39,6 +40,7 @@
#include <QMessageBox>
#include <QNetworkProxy>
#include <QDir>
#include <QDirIterator>
#include <QScopedValueRollback>
#include <QMessageBox>
@ -139,23 +141,27 @@ bool createDebugArchive(const QString &filename)
}
#ifdef BUILD_FILE_PROVIDER_MODULE
const auto fileProvider = OCC::Mac::FileProvider::instance();
if (fileProvider && fileProvider->fileProviderAvailable()) {
const auto tempDir = QTemporaryDir();
const auto xpc = fileProvider->xpc();
const auto vfsAccounts = OCC::Mac::FileProviderSettingsController::instance()->vfsEnabledAccounts();
for (const auto &accountUserIdAtHost : vfsAccounts) {
const auto accountState = OCC::AccountManager::instance()->accountFromUserId(accountUserIdAtHost);
if (!accountState) {
qWarning() << "Could not find account for" << accountUserIdAtHost;
continue;
}
const auto account = accountState->account();
const auto vfsLogFilename = QStringLiteral("macOS_vfs_%1.log").arg(account->davUser());
const auto vfsLogPath = tempDir.filePath(vfsLogFilename);
xpc->createDebugArchiveForFileProviderDomain(accountUserIdAtHost, vfsLogPath);
zip.addLocalFile(vfsLogPath, vfsLogFilename);
qDebug() << "Trying to add file provider domain log files...";
const auto fileProviderExtensionLogDirectory = OCC::Mac::FileProviderUtils::fileProviderExtensionLogDirectory();
if (fileProviderExtensionLogDirectory.exists()) {
// Recursively add all files from the container log directory
QDirIterator it(fileProviderExtensionLogDirectory.path(), QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
while (it.hasNext()) {
const auto logFilePath = it.next();
const auto logFileInfo = QFileInfo(logFilePath);
// Calculate relative path from the base container log directory
const auto relativePath = fileProviderExtensionLogDirectory.relativeFilePath(logFilePath);
const auto zipPath = QStringLiteral("File Provider Domains/%1").arg(relativePath);
zip.addLocalFile(logFilePath, zipPath);
}
qDebug() << "Added file provider domain log files from" << fileProviderExtensionLogDirectory.path();
} else {
qWarning() << "file provider domain container log directory not found at" << fileProviderExtensionLogDirectory.path();
}
#endif
@ -182,7 +188,7 @@ GeneralSettings::GeneralSettings(QWidget *parent)
_ui->setupUi(this);
updatePollIntervalVisibility();
connect(_ui->serverNotificationsCheckBox, &QAbstractButton::toggled,
this, &GeneralSettings::slotToggleOptionalServerNotifications);
_ui->serverNotificationsCheckBox->setToolTip(tr("Server notifications that require attention."));
@ -211,7 +217,7 @@ GeneralSettings::GeneralSettings(QWidget *parent)
_ui->autostartCheckBox->setChecked(hasSystemAutoStart);
_ui->autostartCheckBox->setDisabled(hasSystemAutoStart);
_ui->autostartCheckBox->setToolTip(tr("You cannot disable autostart because system-wide autostart is enabled."));
} else {
} else {
connect(_ui->autostartCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleLaunchOnStartup);
_ui->autostartCheckBox->setChecked(ConfigFile().launchOnSystemStartup());
}
@ -239,7 +245,7 @@ GeneralSettings::GeneralSettings(QWidget *parent)
connect(_ui->newExternalStorage, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings);
connect(_ui->moveFilesToTrashCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings);
connect(_ui->remotePollIntervalSpinBox, &QSpinBox::valueChanged, this, &GeneralSettings::slotRemotePollIntervalChanged);
// Hide on non-Windows, or WindowsVersion < 10.
// The condition should match the default value of ConfigFile::showInExplorerNavigationPane.
#ifdef Q_OS_WIN
@ -318,9 +324,9 @@ void GeneralSettings::loadMiscSettings()
_ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage());
_ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons());
const auto interval = cfgFile.remotePollInterval();
const auto interval = cfgFile.remotePollInterval();
_ui->remotePollIntervalSpinBox->setValue(static_cast<int>(interval.count() / 1000));
updatePollIntervalVisibility();
updatePollIntervalVisibility();
}
#if defined(BUILD_UPDATER)
@ -680,7 +686,7 @@ void GeneralSettings::customizeStyle()
#endif
}
void GeneralSettings::slotRemotePollIntervalChanged(int seconds)
void GeneralSettings::slotRemotePollIntervalChanged(int seconds)
{
if (_currentlyLoading) {
return;
@ -691,7 +697,7 @@ void GeneralSettings::slotRemotePollIntervalChanged(int seconds)
cfgFile.setRemotePollInterval(interval);
}
void GeneralSettings::updatePollIntervalVisibility()
void GeneralSettings::updatePollIntervalVisibility()
{
const auto accounts = AccountManager::instance()->accounts();
const auto pushAvailable = std::any_of(accounts.cbegin(), accounts.cend(), [](const AccountStatePtr &accountState) -> bool {

View File

@ -33,25 +33,25 @@ QString uuidDomainIdentifierForAccount(const OCC::Account * const account)
{
Q_ASSERT(account);
const auto accountId = account->userIdAtHostWithPort();
if (accountId.isEmpty()) {
qCWarning(OCC::lcMacFileProviderDomainManager) << "Cannot generate UUID for account with empty userIdAtHostWithPort";
return {};
}
// Try to get existing UUID mapping first
OCC::ConfigFile cfg;
const QString existingUuid = cfg.fileProviderDomainUuidFromAccountId(accountId);
if (!existingUuid.isEmpty()) {
qCDebug(OCC::lcMacFileProviderDomainManager) << "Using existing UUID for account:"
qCDebug(OCC::lcMacFileProviderDomainManager) << "Using existing UUID for account:"
<< accountId
<< "UUID:"
<< existingUuid;
return existingUuid;
}
// Generate new UUID for this account
const QString newUuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
@ -85,7 +85,7 @@ inline QString accountIdFromDomainId(const QString &domainId)
if (domainId.isEmpty()) {
return {};
}
// Check if this is a UUID-based domain identifier
if (QUuid::fromString(domainId).isNull() == false) {
// This is a UUID, look up the account ID from the mapping
@ -107,7 +107,7 @@ inline QString accountIdFromDomainId(const QString &domainId)
return {};
}
// This is a legacy account-based domain identifier
qCDebug(OCC::lcMacFileProviderDomainManager) << "Using legacy account-based domain ID:"
<< domainId;
@ -120,13 +120,13 @@ QString accountIdFromDomainId(NSString * const domainId)
if (!domainId) {
return {};
}
auto qDomainId = QString::fromNSString(domainId);
if (qDomainId.isEmpty()) {
return {};
}
// Check if this is a UUID-based domain identifier
if (QUuid::fromString(qDomainId).isNull() == false) {
// This is a UUID, look up the account ID from the mapping
@ -143,7 +143,7 @@ QString accountIdFromDomainId(NSString * const domainId)
return {};
}
// This is a legacy account-based domain identifier - handle the old logic
qCDebug(OCC::lcMacFileProviderDomainManager) << "Processing legacy account-based domain ID from NSString:" << qDomainId;
@ -231,21 +231,24 @@ public:
<< "and display name:"
<< domain.displayName
<< "removing and recreating";
[NSFileProviderManager removeDomain:domain completionHandler:^(NSError * const error) {
if (error) {
qCWarning(lcMacFileProviderDomainManager) << "Error removing file provider domain with illegal domain identifier: "
<< error.code
<< error.localizedDescription;
} else {
qCInfo(lcMacFileProviderDomainManager) << "Successfully removed file provider domain with illegal domain identifier: "
<< domain.identifier;
}
removeFileProviderDomainData(domain.identifier);
[domain release];
}];
return;
}
_registeredDomains.insert(accountId, domain);
NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:domain];
@ -276,6 +279,8 @@ public:
<< error.code
<< error.localizedDescription;
}
removeFileProviderDomainData(domain.identifier);
}];
}
}
@ -341,6 +346,38 @@ public:
}
}
void removeFileProviderDomainData(NSString * const domainIdentifier)
{
const auto qDomainIdentifier = QString::fromNSString(domainIdentifier);
// Remove logs.
auto logDirectory = OCC::Mac::FileProviderUtils::fileProviderDomainLogDirectory(qDomainIdentifier);
if (logDirectory.exists()) {
qCInfo(lcMacFileProviderDomainManager) << "Removing log directory at" << logDirectory.path();
logDirectory.removeRecursively();
} else {
qCInfo(lcMacFileProviderDomainManager) << "Due to lack of existence, not removing log directory at" << logDirectory.path();
}
// Remove support data.
auto supportDirectory = OCC::Mac::FileProviderUtils::fileProviderDomainSupportDirectory(qDomainIdentifier);
if (supportDirectory.exists()) {
qCInfo(lcMacFileProviderDomainManager) << "Removing support directory at" << supportDirectory.path();
supportDirectory.removeRecursively();
} else {
qCInfo(lcMacFileProviderDomainManager) << "Due to lack of existence, not removing support directory at" << supportDirectory.path();
}
// Remove configuration leftovers.
OCC::ConfigFile cfg;
cfg.removeFileProviderDomainMappingByDomainIdentifier(qDomainIdentifier);
}
void removeFileProviderDomain(const AccountState * const accountState)
{
if (@available(macOS 11.0, *)) {
@ -367,6 +404,7 @@ public:
<< error.localizedDescription;
}
removeFileProviderDomainData(fileProviderDomain.identifier);
NSFileProviderDomain * const domain = _registeredDomains.take(accountId);
[domain release];
@ -392,16 +430,15 @@ public:
const auto registeredDomainPtrs = _registeredDomains.values();
const auto accountIds = _registeredDomains.keys();
for (NSFileProviderDomain * const domain : registeredDomainPtrs) {
removeFileProviderDomainData(domain.identifier);
if (domain != nil) {
[domain release];
}
}
// Clean up UUID mappings for all accounts
OCC::ConfigFile cfg;
for (const QString &accountId : accountIds) {
cfg.removeFileProviderDomainUuidMapping(accountId);
}
_registeredDomains.clear();
}];
}
@ -432,15 +469,13 @@ public:
return;
}
removeFileProviderDomainData(domain.identifier);
const QString accountId = accountIdFromDomainId(domain.identifier);
NSFileProviderDomain * const registeredDomainPtr = _registeredDomains.take(accountId);
if (registeredDomainPtr != nil) {
[domain release];
// Clean up UUID mapping when wiping domain
if (!accountId.isEmpty()) {
OCC::ConfigFile cfg;
cfg.removeFileProviderDomainUuidMapping(accountId);
}
}
}];
}
@ -510,6 +545,7 @@ public:
Q_ASSERT(fileProviderDomain != nil);
NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:fileProviderDomain];
[fpManager reconnectWithCompletionHandler:^(NSError * const error) {
if (error) {
qCWarning(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: "
@ -587,6 +623,7 @@ void FileProviderDomainManager::start()
// shutdowns.
connect(AccountManager::instance(), &AccountManager::accountSyncConnectionRemoved,
this, &FileProviderDomainManager::removeFileProviderDomainForAccount);
connect(AccountManager::instance(), &AccountManager::accountRemoved,
this, [this](const AccountState * const accountState) {
const auto trReason = tr("%1 application has been closed. Reopen to reconnect.").arg(APPLICATION_NAME);

View File

@ -49,7 +49,6 @@ public slots:
void createEvictionWindowForAccount(const QString &userIdAtHost);
void refreshMaterialisedItemsForAccount(const QString &userIdAtHost);
void signalFileProviderDomain(const QString &userIdAtHost);
void createDebugArchive(const QString &userIdAtHost);
signals:
void vfsEnabledAccountsChanged();

View File

@ -509,24 +509,6 @@ void FileProviderSettingsController::signalFileProviderDomain(const QString &use
d->signalFileProviderDomain(userIdAtHost);
}
void FileProviderSettingsController::createDebugArchive(const QString &userIdAtHost)
{
const auto filename = QFileDialog::getSaveFileName(nullptr,
tr("Create Debug Archive"),
QStandardPaths::writableLocation(QStandardPaths::StandardLocation::DocumentsLocation),
tr("Text files") + " (*.txt)");
if (filename.isEmpty()) {
return;
}
const auto xpc = FileProvider::instance()->xpc();
if (!xpc) {
qCWarning(lcFileProviderSettingsController) << "Could not create debug archive, FileProviderXPC is not available.";
return;
}
xpc->createDebugArchiveForFileProviderDomain(userIdAtHost, filename);
}
FileProviderDomainSyncStatus *FileProviderSettingsController::domainSyncStatusForAccount(const QString &userIdAtHost) const
{
return d->domainSyncStatusForAccount(userIdAtHost);

View File

@ -9,8 +9,17 @@
class QString;
// Forward declarations for Objective-C types using conditional compilation
#ifdef __OBJC__
@class NSFileProviderDomain;
@class NSFileProviderManager;
@class NSString;
#else
// In C++ context, use opaque pointers
struct NSFileProviderDomain;
struct NSFileProviderManager;
struct NSString;
#endif
/**
* This file contains the FileProviderUtils namespace, which contains
@ -94,6 +103,21 @@ QString domainIdentifierForAccountIdentifier(const NSString *accountId);
*/
bool illegalDomainIdentifier(const QString &domainId);
/**
* @brief Find the logs directory of the file provider extension for all the domains.
*/
QDir fileProviderExtensionLogDirectory();
/**
* @brief Find the logs directory of the file provider domain with the given identifier.
*/
QDir fileProviderDomainLogDirectory(const QString domainIdentifier);
/**
* @brief Find the application support directory of the file provider domain with the given identifier.
*/
QDir fileProviderDomainSupportDirectory(const QString domainIdentifier);
/**
* @brief Synchronously retrieves an NSFileProviderManager for the given domain identifier.
*

View File

@ -7,6 +7,8 @@
#include "fileproviderutils.h"
#include "account.h"
#include <QCoreApplication>
#include <QDir>
#include <QLoggingCategory>
#include <QRegularExpression>
#include <QString>
@ -134,6 +136,49 @@ QString domainIdentifierForAccount(const OCC::AccountPtr account)
return domainIdentifierForAccount(account.get());
}
QDir fileProviderExtensionContainer()
{
const auto baseBundleId = QCoreApplication::organizationDomain();
const auto extensionBundleId = baseBundleId + QStringLiteral(".FileProviderExt");
auto dir = QDir::home();
dir.cd("Library");
dir.cd("Containers");
dir.cd(extensionBundleId);
return dir;
}
QDir fileProviderExtensionLogDirectory()
{
auto dir = fileProviderExtensionContainer();
dir.cd("Data");
dir.cd("Library");
dir.cd("Logs");
return dir;
}
QDir fileProviderDomainLogDirectory(const QString domainIdentifier)
{
auto dir = fileProviderExtensionLogDirectory();
dir.cd(domainIdentifier);
return dir;
}
QDir fileProviderDomainSupportDirectory(const QString domainIdentifier)
{
auto dir = fileProviderExtensionContainer();
dir.cd("Data");
dir.cd("Library");
dir.cd("Application Support");
dir.cd("File Provider Domains");
dir.cd(domainIdentifier);
return dir;
}
NSFileProviderManager *managerForDomainIdentifier(const QString &domainIdentifier)
{
NSFileProviderDomain * const domain = domainForIdentifier(domainIdentifier);

View File

@ -35,7 +35,6 @@ public slots:
void authenticateFileProviderDomains();
void authenticateFileProviderDomain(const QString &fileProviderDomainIdentifier) const;
void unauthenticateFileProviderDomain(const QString &fileProviderDomainIdentifier) const;
void createDebugArchiveForFileProviderDomain(const QString &fileProviderDomainIdentifier, const QString &filename);
void setIgnoreList() const;
void setTrashDeletionEnabledForFileProviderDomain(const QString &fileProviderDomainIdentifier, bool enabled) const;

View File

@ -124,48 +124,6 @@ void FileProviderXPC::slotAccountStateChanged(const AccountState::State state) c
break;
}
}
void FileProviderXPC::createDebugArchiveForFileProviderDomain(const QString &fileProviderDomainIdentifier, const QString &filename)
{
qCInfo(lcFileProviderXPC) << "Creating debug archive for extension" << fileProviderDomainIdentifier << "at" << filename;
if (!fileProviderDomainReachable(fileProviderDomainIdentifier)) {
qCWarning(lcFileProviderXPC) << "Extension is not reachable. Cannot create debug archive";
return;
}
// You need to fetch the contents from the extension and then create the archive from the client side.
// The extension is not allowed to ask for permission to write into the file system as it is not a user facing process.
const auto clientCommService = (NSObject<ClientCommunicationProtocol> *)_clientCommServices.value(fileProviderDomainIdentifier);
const auto group = dispatch_group_create();
__block NSString *rcvdDebugLogString;
dispatch_group_enter(group);
[clientCommService createDebugLogStringWithCompletionHandler:^(NSString *const debugLogString, NSError *const error) {
if (error != nil) {
qCWarning(lcFileProviderXPC) << "Error getting debug log string" << error.localizedDescription;
dispatch_group_leave(group);
return;
} else if (debugLogString == nil) {
qCWarning(lcFileProviderXPC) << "Debug log string is nil";
dispatch_group_leave(group);
return;
}
rcvdDebugLogString = [NSString stringWithString:debugLogString];
[rcvdDebugLogString retain];
dispatch_group_leave(group);
}];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
QFile debugLogFile(filename);
if (debugLogFile.open(QIODevice::WriteOnly)) {
debugLogFile.write(rcvdDebugLogString.UTF8String);
debugLogFile.close();
qCInfo(lcFileProviderXPC) << "Debug log file written to" << filename;
} else {
qCWarning(lcFileProviderXPC) << "Could not open debug log file" << filename;
}
[rcvdDebugLogString release];
}
bool FileProviderXPC::fileProviderDomainReachable(const QString &fileProviderDomainIdentifier, const bool retry, const bool reconfigureOnFail)
{

View File

@ -1330,6 +1330,7 @@ void ConfigFile::setFileProviderDomainUuidForAccountId(const QString &accountId,
if (accountId.isEmpty() || domainUuid.isEmpty()) {
return;
}
storeData(QStringLiteral("FileProviderDomainUuids"), accountId, domainUuid);
storeData(QStringLiteral("FileProviderAccountIds"), domainUuid, accountId);
}
@ -1339,6 +1340,7 @@ QString ConfigFile::accountIdFromFileProviderDomainUuid(const QString &domainUui
if (domainUuid.isEmpty()) {
return {};
}
return retrieveData(QStringLiteral("FileProviderAccountIds"), domainUuid).toString();
}
@ -1347,11 +1349,29 @@ void ConfigFile::removeFileProviderDomainUuidMapping(const QString &accountId)
if (accountId.isEmpty()) {
return;
}
const QString domainUuid = fileProviderDomainUuidFromAccountId(accountId);
if (!domainUuid.isEmpty()) {
removeData(QStringLiteral("FileProviderAccountIds"), domainUuid);
}
removeData(QStringLiteral("FileProviderDomainUuids"), accountId);
}
void ConfigFile::removeFileProviderDomainMappingByDomainIdentifier(const QString domainIdentifier)
{
if (domainIdentifier.isEmpty()) {
return;
}
removeData(QStringLiteral("FileProviderAccountIds"), domainIdentifier);
const QString accountIdentifier = accountIdFromFileProviderDomainUuid(domainIdentifier);
if (!accountIdentifier.isEmpty()) {
removeData(QStringLiteral("FileProviderDomainUuids"), accountIdentifier);
}
}
}

View File

@ -254,6 +254,7 @@ public:
void setFileProviderDomainUuidForAccountId(const QString &accountId, const QString &domainUuid);
[[nodiscard]] QString accountIdFromFileProviderDomainUuid(const QString &domainUuid) const;
void removeFileProviderDomainUuidMapping(const QString &accountId);
void removeFileProviderDomainMappingByDomainIdentifier(const QString domainIdentifier);
static constexpr char isVfsEnabledC[] = "isVfsEnabled";
static constexpr char launchOnSystemStartupC[] = "launchOnSystemStartup";