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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public final class FilesDatabaseManager: Sendable {
)
}

private static let schemaVersion = SchemaVersion.addedIsLockFileOfLocalOriginToRealmItemMetadata
private static let schemaVersion = SchemaVersion.addedWasTrashedLocallyToRealmItemMetadata
let logger: FileProviderLogger
let account: Account

Expand Down Expand Up @@ -86,6 +86,15 @@ public final class FilesDatabaseManager: Sendable {
}
}

if oldSchemaVersion == SchemaVersion.addedIsLockFileOfLocalOriginToRealmItemMetadata.rawValue {
migration.enumerateObjects(ofType: RealmItemMetadata.className()) { _, newObject in
guard let newObject else {
return
}

newObject["wasTrashedLocally"] = false
}
}
},
objectTypes: [RealmItemMetadata.self, RemoteFileChunk.self]
)
Expand Down Expand Up @@ -430,7 +439,7 @@ public final class FilesDatabaseManager: Sendable {
do {
try database.write {
database.add(RealmItemMetadata(value: metadata), update: .all)
logger.debug("Added item metadata.", [.item: metadata.ocId, .name: metadata.name, .url: metadata.serverUrl])
logger.debug("Added item metadata.", [.item: metadata.ocId, .name: metadata.fileName, .url: metadata.serverUrl])
}
} catch {
logger.error("Failed to add item metadata.", [.item: metadata.ocId, .name: metadata.name, .url: metadata.serverUrl, .error: error])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ enum SchemaVersion: UInt64 {
case deletedLocalFileMetadata = 200
case addedLockTokenPropertyToRealmItemMetadata = 201
case addedIsLockFileOfLocalOriginToRealmItemMetadata = 202
case addedWasTrashedLocallyToRealmItemMetadata = 203
}
107 changes: 20 additions & 87 deletions Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,29 @@
import NextcloudKit

extension Enumerator {
static func completeEnumerationObserver(
_ observer: NSFileProviderEnumerationObserver,
account: Account,
remoteInterface: RemoteInterface,
dbManager: FilesDatabaseManager,
numPage: Int,
trashItems: [NKTrash],
log: any FileProviderLogging
) {
var metadatas = [SendableItemMetadata]()
for trashItem in trashItems {
let metadata = trashItem.toItemMetadata(account: account)
dbManager.addItemMetadata(metadata)
metadatas.append(metadata)
}

Task { [metadatas] in
let logger = FileProviderLogger(category: "Enumerator", log: log)

do {
let items = try await metadatas.toFileProviderItems(account: account, remoteInterface: remoteInterface, dbManager: dbManager, log: log)

Task { @MainActor in
observer.didEnumerate(items)
logger.info("Did enumerate \(items.count) trash items.")
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
}
} catch {
logger.error("Finishing enumeration with error.")
Task { @MainActor in observer.finishEnumeratingWithError(error) }
}
}
}

static func completeChangesObserver(
_ observer: NSFileProviderChangeObserver,
anchor: NSFileProviderSyncAnchor,
account: Account,
remoteInterface: RemoteInterface,
dbManager: FilesDatabaseManager,
trashItems: [NKTrash],
log: any FileProviderLogging
) async {
///
/// Change enumeration completion.
///
/// `NKTrash` items do not have an ETag. We assume they cannot be modified while they are in the trash. So we will just check by their `ocId`.
/// Newly added items by deletion on the server side or another client are not of interested and we do not want to display them in the local trash.
/// In the end, only the remotely and permanently deleted items are of interest.
///
static func completeChangesObserver(_ observer: NSFileProviderChangeObserver, anchor: NSFileProviderSyncAnchor, account: Account, dbManager: FilesDatabaseManager, remoteTrashItems: [NKTrash], log: any FileProviderLogging) async {
let logger = FileProviderLogger(category: "Enumerator", log: log)
var newTrashedItems = [NSFileProviderItem]()

// NKTrash items do not have an etag ; we assume they cannot be modified while they are in
// the trash, so we will just check by ocId
var existingTrashedItems = dbManager.trashedItemMetadatas(account: account)

for trashItem in trashItems {
if let existingTrashItemIndex = existingTrashedItems.firstIndex(
where: { $0.ocId == trashItem.ocId }
) {
existingTrashedItems.remove(at: existingTrashItemIndex)
continue
}

let metadata = trashItem.toItemMetadata(account: account)
dbManager.addItemMetadata(metadata)

let item = await Item(
metadata: metadata,
parentItemIdentifier: .trashContainer,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
)
newTrashedItems.append(item)

logger.debug("Will enumerate changed trash item.", [.item: metadata.ocId, .name: metadata.fileName])
}

let deletedTrashedItemsIdentifiers = existingTrashedItems.map {
NSFileProviderItemIdentifier($0.ocId)
}
if !deletedTrashedItemsIdentifiers.isEmpty {
for itemIdentifier in deletedTrashedItemsIdentifiers {
dbManager.deleteItemMetadata(ocId: itemIdentifier.rawValue)
}

logger.debug("Will enumerate deleted trashed items: \(deletedTrashedItemsIdentifiers)")
observer.didDeleteItems(withIdentifiers: deletedTrashedItemsIdentifiers)
let localIdentifiers = dbManager.trashedItemMetadatas(account: account).map(\.ocId)
let localSet = Set(localIdentifiers)
let remoteIdentifiers = remoteTrashItems.map(\.ocId)
let remoteSet = Set(remoteIdentifiers)
let orphanedSet = localSet.subtracting(remoteSet)
let orphanedIdentifiers = orphanedSet.map { NSFileProviderItemIdentifier($0) }

for identifier in orphanedSet {
logger.info("Permanently deleting remote trash item which could not be matched with a local one.", [.item: identifier])
dbManager.deleteItemMetadata(ocId: identifier)
}

if !newTrashedItems.isEmpty {
observer.didUpdate(newTrashedItems)
}
observer.didDeleteItems(withIdentifiers: orphanedIdentifiers)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
logger.debug("Finished enumerating remote changes in trash.")
}
}
57 changes: 6 additions & 51 deletions Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,8 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
public func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
logger.info("Received enumerate items request for enumerator with user", [.account: account.ncKitAccount, .url: serverUrl])

/*
- inspect the page to determine whether this is an initial or a follow-up request (TODO)

If this is an enumerator for a directory, the root container or all directories:
- perform a server request to fetch directory contents
If this is an enumerator for the working set:
- perform a server request to update your local database
- fetch the working set from your local database

- inform the observer about the items returned by the server (possibly multiple times)
- inform the observer that you are finished with this page
*/

if enumeratedItemIdentifier == .trashContainer {
logger.info("Enumerating trash.", [.account: account.ncKitAccount, .url: serverUrl])
logger.info("Enumerating items in trash.", [.account: account.ncKitAccount, .url: serverUrl])

Task { [weak self] in
guard let self else {
Expand All @@ -111,42 +98,11 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
return
}

let domain = domain
let enumeratedItemIdentifier = enumeratedItemIdentifier

let (_, trashedItems, _, trashReadError) = await remoteInterface.listingTrashAsync(
filename: nil,
showHiddenFiles: true,
account: account.ncKitAccount,
options: .init(),
taskHandler: { task in
if let domain {
NSFileProviderManager(for: domain)?.register(
task,
forItemWithIdentifier: enumeratedItemIdentifier,
completionHandler: { _ in }
)
}
}
)

guard trashReadError == .success else {
let error = trashReadError.fileProviderError(handlingNoSuchItemErrorUsingItemIdentifier: enumeratedItemIdentifier) ?? NSFileProviderError(.cannotSynchronize)
observer.finishEnumeratingWithError(error)
return
}

Self.completeEnumerationObserver(
observer,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
numPage: 1,
trashItems: trashedItems ?? [],
log: logger.log
)
// We only want to list items deleted on the local device.
// That cannot happen before the initial content enumeration for a file provider domain because the latter does not exist yet.
// Hence the initial trash content enumeration can be finished with an empty set.
observer.finishEnumerating(upTo: Self.fileProviderPageforNumPage(1))
}

return
}

Expand Down Expand Up @@ -334,9 +290,8 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
observer,
anchor: anchor,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
trashItems: trashedItems ?? [],
remoteTrashItems: trashedItems ?? [],
log: logger.log
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension NKFile {
return fileUrl == urlString
}

func toItemMetadata(uploaded: Bool = true) -> SendableItemMetadata {
func toItemMetadata(uploaded: Bool = true, wasTrashedLocally: Bool = false) -> SendableItemMetadata {
let creationDate = creationDate ?? date
let uploadDate = uploadDate ?? date

Expand Down Expand Up @@ -88,7 +88,8 @@ extension NKFile {
uploadDate: uploadDate as Date,
urlBase: urlBase,
user: user,
userId: userId
userId: userId,
wasTrashedLocally: wasTrashedLocally
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import Foundation
import NextcloudKit

extension NKTrash {
func toItemMetadata(account: Account) -> SendableItemMetadata {
///
/// Convert a trashed item representation into sendable item metadata.
///
func toItemMetadata(account: Account, wasTrashedLocally: Bool = false) -> SendableItemMetadata {
SendableItemMetadata(
ocId: ocId,
account: account.ncKitAccount,
Expand Down Expand Up @@ -35,7 +38,8 @@ extension NKTrash {
trashbinDeletionTime: trashbinDeletionTime,
urlBase: account.serverUrl,
user: account.username,
userId: account.id
userId: account.id,
wasTrashedLocally: wasTrashedLocally
)
}
}
3 changes: 2 additions & 1 deletion Sources/NextcloudFileProviderKit/Item/Item+Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ public extension Item {
uploaded: true,
urlBase: account.serverUrl,
user: account.username,
userId: account.id
userId: account.id,
wasTrashedLocally: false
)

dbManager.addItemMetadata(newMetadata)
Expand Down
3 changes: 2 additions & 1 deletion Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ extension Item {
uploaded: false,
urlBase: account.serverUrl,
user: account.username,
userId: account.id
userId: account.id,
wasTrashedLocally: false
)

dbManager.addItemMetadata(metadata)
Expand Down
3 changes: 2 additions & 1 deletion Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ extension Item {
uploaded: false,
urlBase: account.serverUrl,
user: account.username,
userId: account.id
userId: account.id,
wasTrashedLocally: false
)

dbManager.addItemMetadata(metadata)
Expand Down
27 changes: 11 additions & 16 deletions Sources/NextcloudFileProviderKit/Item/Item+Modify.swift
Original file line number Diff line number Diff line change
Expand Up @@ -656,13 +656,13 @@ public extension Item {

return (modifiedItem, nil)
} else if changedFields.contains(.parentItemIdentifier) && newParentItemIdentifier == .trashContainer {
let (_, capabilities, _, error) = await remoteInterface.currentCapabilities(
account: account, options: .init(), taskHandler: { _ in }
)
let (_, capabilities, _, error) = await remoteInterface.currentCapabilities(account: account, options: .init(), taskHandler: { _ in })

guard let capabilities, error == .success else {
logger.error("Could not acquire capabilities during item move to trash, won't proceed.", [.item: modifiedItem, .error: error])
return (nil, error.fileProviderError)
}

guard capabilities.files?.undelete == true else {
logger.error("Cannot delete item as server does not support trashing.", [.item: modifiedItem])
return (nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError))
Expand All @@ -672,15 +672,8 @@ public extension Item {
// Rename the item if necessary before doing the trashing procedures
if changedFields.contains(.filename) {
let currentParentItemRemotePath = modifiedItem.metadata.serverUrl
let preTrashingRenamedRemotePath =
currentParentItemRemotePath + "/" + itemTarget.filename
let (renameModifiedItem, renameError) = await modifiedItem.move(
newFileName: itemTarget.filename,
newRemotePath: preTrashingRenamedRemotePath,
newParentItemIdentifier: modifiedItem.parentItemIdentifier,
newParentItemRemotePath: currentParentItemRemotePath,
dbManager: dbManager
)
let preTrashingRenamedRemotePath = currentParentItemRemotePath + "/" + itemTarget.filename
let (renameModifiedItem, renameError) = await modifiedItem.move(newFileName: itemTarget.filename, newRemotePath: preTrashingRenamedRemotePath, newParentItemIdentifier: modifiedItem.parentItemIdentifier, newParentItemRemotePath: currentParentItemRemotePath, dbManager: dbManager)

guard renameError == nil, let renameModifiedItem else {
logger.error("Could not rename pre-trash item.", [.item: modifiedItem.itemIdentifier, .error: error])
Expand All @@ -690,10 +683,12 @@ public extension Item {
modifiedItem = renameModifiedItem
}

let (trashedItem, trashingError) = await Self.trash(
modifiedItem, account: account, dbManager: dbManager, domain: domain, log: logger.log
)
guard trashingError == nil else { return (modifiedItem, trashingError) }
let (trashedItem, trashingError) = await Self.trash(modifiedItem, account: account, dbManager: dbManager, domain: domain, log: logger.log)

guard trashingError == nil else {
return (modifiedItem, trashingError)
}

modifiedItem = trashedItem
} else if changedFields.contains(.filename) || changedFields.contains(.parentItemIdentifier) {
// Recover the item first
Expand Down
Loading