mirror of
https://github.com/nextcloud/desktop.git
synced 2025-10-26 11:17:43 +00:00
Trying to delete items from the trash with the "Allow deletion of items in Trash" setting unticked would previously result in the sync state being stuck in the "Syncing" state. Signed-off-by: Jyrki Gadinger <nilsding@nilsding.org>
585 lines
21 KiB
Swift
585 lines
21 KiB
Swift
/*
|
|
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
import FileProvider
|
|
import NCDesktopClientSocketKit
|
|
import NextcloudKit
|
|
import NextcloudFileProviderKit
|
|
import OSLog
|
|
|
|
@objc class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
|
let domain: NSFileProviderDomain
|
|
let ncKit = NextcloudKit.shared
|
|
let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
|
|
var ncAccount: Account?
|
|
var dbManager: FilesDatabaseManager?
|
|
var changeObserver: RemoteChangeObserver?
|
|
var ignoredFiles: IgnoredFilesMatcher?
|
|
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")
|
|
return nil;
|
|
}
|
|
|
|
let socketPath = containerUrl.appendingPathComponent(
|
|
".fileprovidersocket", conformingTo: .archive)
|
|
let lineProcessor = FileProviderSocketLineProcessor(delegate: self)
|
|
return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor)
|
|
}()
|
|
|
|
var syncActions = Set<UUID>()
|
|
var errorActions = Set<UUID>()
|
|
var actionsLock = NSLock()
|
|
|
|
// Whether or not we are going to recursively scan new folders when they are discovered.
|
|
// Apple's recommendation is that we should always scan the file hierarchy fully.
|
|
// This does lead to long load times when a file provider domain is initially configured.
|
|
// We can instead do a fast enumeration where we only scan folders as the user navigates through
|
|
// them, thereby avoiding this issue; the trade-off is that we will be unable to detect
|
|
// materialised file moves to unexplored folders, therefore deleting the item when we could have
|
|
// just moved it instead.
|
|
//
|
|
// 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 = FileProviderConfig(domainIdentifier: domain.identifier)
|
|
|
|
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.
|
|
self.domain = domain
|
|
super.init()
|
|
socketClient?.start()
|
|
}
|
|
|
|
func invalidate() {
|
|
// TODO: cleanup any resources
|
|
Logger.fileProviderExtension.debug(
|
|
"Extension for domain \(self.domain.displayName, privacy: .public) is being torn down"
|
|
)
|
|
}
|
|
|
|
func insertSyncAction(_ actionId: UUID) {
|
|
actionsLock.lock()
|
|
let oldActions = syncActions
|
|
syncActions.insert(actionId)
|
|
actionsLock.unlock()
|
|
updatedSyncStateReporting(oldActions: oldActions)
|
|
}
|
|
|
|
func insertErrorAction(_ actionId: UUID) {
|
|
actionsLock.lock()
|
|
let oldActions = syncActions
|
|
syncActions.remove(actionId)
|
|
errorActions.insert(actionId)
|
|
actionsLock.unlock()
|
|
updatedSyncStateReporting(oldActions: oldActions)
|
|
}
|
|
|
|
func removeSyncAction(_ actionId: UUID) {
|
|
actionsLock.lock()
|
|
let oldActions = syncActions
|
|
syncActions.remove(actionId)
|
|
errorActions.remove(actionId)
|
|
actionsLock.unlock()
|
|
updatedSyncStateReporting(oldActions: oldActions)
|
|
}
|
|
|
|
// MARK: - NSFileProviderReplicatedExtension protocol methods
|
|
|
|
func item(
|
|
for identifier: NSFileProviderItemIdentifier,
|
|
request _: NSFileProviderRequest,
|
|
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.
|
|
"""
|
|
)
|
|
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
|
|
"""
|
|
)
|
|
completionHandler(nil, NSFileProviderError(.cannotSynchronize))
|
|
return Progress()
|
|
}
|
|
|
|
let progress = Progress()
|
|
Task {
|
|
progress.totalUnitCount = 1
|
|
if let item = await Item.storedItem(
|
|
identifier: identifier,
|
|
account: ncAccount,
|
|
remoteInterface: ncKit,
|
|
dbManager: dbManager
|
|
) {
|
|
progress.completedUnitCount = 1
|
|
completionHandler(item, nil)
|
|
} else {
|
|
completionHandler(
|
|
nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)
|
|
)
|
|
}
|
|
}
|
|
return progress
|
|
}
|
|
|
|
func fetchContents(
|
|
for itemIdentifier: NSFileProviderItemIdentifier,
|
|
version requestedVersion: NSFileProviderItemVersion?,
|
|
request: NSFileProviderRequest,
|
|
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void
|
|
) -> Progress {
|
|
let actionId = UUID()
|
|
insertSyncAction(actionId)
|
|
|
|
Logger.fileProviderExtension.debug(
|
|
"Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
|
|
)
|
|
|
|
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."
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(
|
|
nil,
|
|
nil,
|
|
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
|
|
)
|
|
return Progress()
|
|
}
|
|
|
|
guard let ncAccount else {
|
|
Logger.fileProviderExtension.error(
|
|
"""
|
|
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
|
|
as account not set up yet.
|
|
"""
|
|
)
|
|
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
|
|
"""
|
|
)
|
|
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
|
|
return Progress()
|
|
}
|
|
|
|
|
|
let progress = Progress()
|
|
Task {
|
|
guard let item = await Item.storedItem(
|
|
identifier: itemIdentifier,
|
|
account: ncAccount,
|
|
remoteInterface: ncKit,
|
|
dbManager: dbManager
|
|
) else {
|
|
Logger.fileProviderExtension.error(
|
|
"""
|
|
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
|
|
as item not found.
|
|
"""
|
|
)
|
|
completionHandler(
|
|
nil,
|
|
nil,
|
|
NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier)
|
|
)
|
|
insertErrorAction(actionId)
|
|
return
|
|
}
|
|
|
|
let (localUrl, updatedItem, error) = await item.fetchContents(
|
|
domain: self.domain, progress: progress, dbManager: dbManager
|
|
)
|
|
removeSyncAction(actionId)
|
|
completionHandler(localUrl, updatedItem, error)
|
|
}
|
|
return progress
|
|
}
|
|
|
|
func createItem(
|
|
basedOn itemTemplate: NSFileProviderItem,
|
|
fields: NSFileProviderItemFields,
|
|
contents url: URL?,
|
|
options: NSFileProviderCreateItemOptions = [],
|
|
request: NSFileProviderRequest,
|
|
completionHandler: @escaping (
|
|
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
|
|
) -> Void
|
|
) -> Progress {
|
|
let actionId = UUID()
|
|
insertSyncAction(actionId)
|
|
|
|
let tempId = itemTemplate.itemIdentifier.rawValue
|
|
Logger.fileProviderExtension.debug(
|
|
"""
|
|
Received create item request for item with identifier: \(tempId, privacy: .public)
|
|
and filename: \(itemTemplate.filename, privacy: .public)
|
|
"""
|
|
)
|
|
|
|
guard let ncAccount else {
|
|
Logger.fileProviderExtension.error(
|
|
"""
|
|
Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public)
|
|
as account not set up yet
|
|
"""
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(itemTemplate, [], false, NSFileProviderError(.notAuthenticated))
|
|
return Progress()
|
|
}
|
|
|
|
guard let ignoredFiles else {
|
|
Logger.fileProviderExtension.error(
|
|
"""
|
|
Not creating item for identifier:
|
|
\(itemTemplate.itemIdentifier.rawValue, privacy: .public)
|
|
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
|
|
"""
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(itemTemplate, [], false, NSFileProviderError(.cannotSynchronize))
|
|
return Progress()
|
|
}
|
|
|
|
let progress = Progress()
|
|
Task {
|
|
let (item, error) = await Item.create(
|
|
basedOn: itemTemplate,
|
|
fields: fields,
|
|
contents: url,
|
|
request: request,
|
|
domain: self.domain,
|
|
account: ncAccount,
|
|
remoteInterface: ncKit,
|
|
ignoredFiles: ignoredFiles,
|
|
progress: progress,
|
|
dbManager: dbManager
|
|
)
|
|
|
|
if error != nil {
|
|
insertErrorAction(actionId)
|
|
signalEnumerator(completionHandler: { _ in })
|
|
} else {
|
|
removeSyncAction(actionId)
|
|
}
|
|
|
|
completionHandler(
|
|
item ?? itemTemplate,
|
|
NSFileProviderItemFields(),
|
|
false,
|
|
error
|
|
)
|
|
}
|
|
return progress
|
|
}
|
|
|
|
func modifyItem(
|
|
_ item: NSFileProviderItem,
|
|
baseVersion: NSFileProviderItemVersion,
|
|
changedFields: NSFileProviderItemFields,
|
|
contents newContents: URL?,
|
|
options: NSFileProviderModifyItemOptions = [],
|
|
request: NSFileProviderRequest,
|
|
completionHandler: @escaping (
|
|
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
|
|
) -> Void
|
|
) -> Progress {
|
|
// An item was modified on disk, process the item's modification
|
|
// TODO: Handle finder things like tags, other possible item changed fields
|
|
let actionId = UUID()
|
|
insertSyncAction(actionId)
|
|
|
|
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)
|
|
"""
|
|
)
|
|
|
|
guard let ncAccount else {
|
|
Logger.fileProviderExtension.error(
|
|
"Not modifying item: \(ocId, privacy: .public) 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."
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
|
|
return Progress()
|
|
}
|
|
|
|
|
|
guard let dbManager else {
|
|
Logger.fileProviderExtension.error(
|
|
"""
|
|
Not modifying item: \(ocId, privacy: .public)
|
|
with filename: \(item.filename, privacy: .public)
|
|
as database is unreachable
|
|
"""
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(item, [], false, NSFileProviderError(.cannotSynchronize))
|
|
return Progress()
|
|
}
|
|
|
|
let progress = Progress()
|
|
Task {
|
|
guard let existingItem = await Item.storedItem(
|
|
identifier: identifier,
|
|
account: ncAccount,
|
|
remoteInterface: ncKit,
|
|
dbManager: dbManager
|
|
) else {
|
|
Logger.fileProviderExtension.error(
|
|
"Not modifying item: \(ocId, privacy: .public) as item not found."
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(
|
|
item,
|
|
[],
|
|
false,
|
|
NSError.fileProviderErrorForNonExistentItem(withIdentifier: item.itemIdentifier)
|
|
)
|
|
return
|
|
}
|
|
let (modifiedItem, error) = await existingItem.modify(
|
|
itemTarget: item,
|
|
baseVersion: baseVersion,
|
|
changedFields: changedFields,
|
|
contents: newContents,
|
|
options: options,
|
|
request: request,
|
|
ignoredFiles: ignoredFiles,
|
|
domain: domain,
|
|
progress: progress,
|
|
dbManager: dbManager
|
|
)
|
|
|
|
if error != nil {
|
|
insertErrorAction(actionId)
|
|
signalEnumerator(completionHandler: { _ in })
|
|
} else {
|
|
removeSyncAction(actionId)
|
|
}
|
|
|
|
completionHandler(modifiedItem ?? item, [], false, error)
|
|
}
|
|
return progress
|
|
}
|
|
|
|
func deleteItem(
|
|
identifier: NSFileProviderItemIdentifier,
|
|
baseVersion _: NSFileProviderItemVersion,
|
|
options _: NSFileProviderDeleteItemOptions = [],
|
|
request _: NSFileProviderRequest,
|
|
completionHandler: @escaping (Error?) -> Void
|
|
) -> Progress {
|
|
let actionId = UUID()
|
|
insertSyncAction(actionId)
|
|
|
|
Logger.fileProviderExtension.debug(
|
|
"Received delete request for item: \(identifier.rawValue, privacy: .public)"
|
|
)
|
|
|
|
guard let ncAccount else {
|
|
Logger.fileProviderExtension.error(
|
|
"Not deleting item \(identifier.rawValue, privacy: .public), 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"
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(NSFileProviderError(.notAuthenticated))
|
|
return Progress()
|
|
}
|
|
|
|
guard let dbManager else {
|
|
Logger.fileProviderExtension.error(
|
|
"Not deleting item \(identifier.rawValue, privacy: .public), db manager unavailable"
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(NSFileProviderError(.cannotSynchronize))
|
|
return Progress()
|
|
}
|
|
|
|
let progress = Progress(totalUnitCount: 1)
|
|
Task {
|
|
guard let item = await Item.storedItem(
|
|
identifier: identifier,
|
|
account: ncAccount,
|
|
remoteInterface: ncKit,
|
|
dbManager: dbManager
|
|
) else {
|
|
Logger.fileProviderExtension.error(
|
|
"Not deleting item \(identifier.rawValue, privacy: .public), item not found"
|
|
)
|
|
insertErrorAction(actionId)
|
|
completionHandler(
|
|
NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)
|
|
)
|
|
return
|
|
}
|
|
|
|
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)
|
|
"""
|
|
)
|
|
removeSyncAction(actionId)
|
|
completionHandler(NSError.fileProviderErrorForRejectedDeletion(of: item))
|
|
return
|
|
}
|
|
let error = await item.delete(
|
|
domain: domain, ignoredFiles: ignoredFiles, dbManager: dbManager
|
|
)
|
|
if error != nil {
|
|
insertErrorAction(actionId)
|
|
signalEnumerator(completionHandler: { _ in })
|
|
} else {
|
|
removeSyncAction(actionId)
|
|
}
|
|
progress.completedUnitCount = 1
|
|
completionHandler(error)
|
|
}
|
|
return progress
|
|
}
|
|
|
|
func enumerator(
|
|
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
|
|
"""
|
|
)
|
|
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
|
|
"""
|
|
)
|
|
throw NSFileProviderError(.cannotSynchronize)
|
|
}
|
|
|
|
return Enumerator(
|
|
enumeratedItemIdentifier: containerItemIdentifier,
|
|
account: ncAccount,
|
|
remoteInterface: ncKit,
|
|
dbManager: dbManager,
|
|
domain: domain
|
|
)
|
|
}
|
|
|
|
func materializedItemsDidChange(completionHandler: @escaping () -> Void) {
|
|
guard let ncAccount else {
|
|
Logger.fileProviderExtension.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)
|
|
"""
|
|
)
|
|
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)"
|
|
)
|
|
completionHandler()
|
|
return
|
|
}
|
|
|
|
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
|
|
let materialisedObserver = MaterialisedEnumerationObserver(
|
|
ncKitAccount: ncAccount.ncKitAccount, dbManager: dbManager
|
|
) { _, _ in
|
|
completionHandler()
|
|
}
|
|
let startingPage = NSFileProviderPage(NSFileProviderPage.initialPageSortedByName as Data)
|
|
|
|
materialisedEnumerator.enumerateItems(for: materialisedObserver, startingAt: startingPage)
|
|
}
|
|
|
|
// MARK: - Helper functions
|
|
|
|
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."
|
|
)
|
|
return
|
|
}
|
|
|
|
fpManager.signalEnumerator(for: .workingSet, completionHandler: completionHandler)
|
|
}
|
|
}
|