Skip to content

Commit ca08237

Browse files
i2h3backportbot[bot]
authored andcommitted
fix(file-provider): Working Set Change Enumeration (fixes #9602)
- Removed obsolete account argument from FilesDatabaseManager.pendingWorkingSetChanges() - Removed obsolete account argument from FilesDatabaseManager.managedMaterialisedItemMetadatas() - Introduced hard delete for items reported as deleted - Refactored FilesDatabaseManager.pendingWorkingSetChanges() for clarity - Initializer of Enumerator now throws .noSuchItem if it is initialized for a non-system container and inexistent identifier - Excluding softly deleted items from materialized item enumeration - Improved code formatting Signed-off-by: Iva Horn <iva.horn@nextcloud.com>
1 parent e067132 commit ca08237

File tree

6 files changed

+159
-128
lines changed

6 files changed

+159
-128
lines changed

shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift

Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -447,21 +447,53 @@ public final class FilesDatabaseManager: Sendable {
447447
}
448448
}
449449

450+
///
451+
/// Mark an item as deleted.
452+
///
453+
/// This is a soft delete and does not actually delete data for which there is ``removeItemMetadata(ocId:)``.
454+
///
455+
/// - Parameters:
456+
/// - ocId: The unique identifier of the item.
457+
///
450458
@discardableResult public func deleteItemMetadata(ocId: String) -> Bool {
451459
do {
452460
let results = itemMetadatas.where { $0.ocId == ocId }
453461
let database = ncDatabase()
462+
454463
try database.write {
455-
logger.debug("Deleting item metadata. \(ocId)")
456464
results.forEach { $0.deleted = true }
465+
logger.debug("Marked item as deleted.", [.item: ocId])
457466
}
467+
458468
return true
459469
} catch {
460-
logger.error("Could not delete item metadata with ocId \(ocId).", [.error: error])
470+
logger.error("Could not mark item as deleted.", [.item: ocId, .error: error])
461471
return false
462472
}
463473
}
464474

475+
///
476+
/// Hard delete an item.
477+
///
478+
/// Unlike ``deleteItemMetadata(ocId:)``, this actually deletes a data record.
479+
///
480+
/// - Parameters:
481+
/// - ocId: The unique identifier of the item.
482+
///
483+
public func removeItemMetadata(ocId: String) {
484+
do {
485+
let database = ncDatabase()
486+
let results = itemMetadatas.where { $0.ocId == ocId }
487+
488+
try database.write {
489+
database.delete(results)
490+
logger.debug("Removed item metadata from database.", [.item: ocId])
491+
}
492+
} catch {
493+
logger.error("Could not remove item metadata.", [.item: ocId, .error: error])
494+
}
495+
}
496+
465497
public func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
466498
guard let itemMetadata = itemMetadatas.where({ $0.ocId == ocId }).first else {
467499
logger.error("Could not find an item with ocID \(ocId) to rename to \(newFileName)")
@@ -538,13 +570,12 @@ public final class FilesDatabaseManager: Sendable {
538570
return NSFileProviderItemIdentifier(parentMetadata.ocId)
539571
}
540572

541-
private func managedMaterialisedItemMetadatas(account: String) -> Results<RealmItemMetadata> {
573+
private func managedMaterialisedItemMetadatas() -> Results<RealmItemMetadata> {
542574
itemMetadatas.where { candidate in
543-
let belongsToAccount = candidate.account == account
544575
let isVisitedDirectory = candidate.directory && candidate.visitedDirectory
545576
let isDownloadedFile = candidate.directory == false && candidate.downloaded
546577

547-
return belongsToAccount && (isVisitedDirectory || isDownloadedFile)
578+
return isVisitedDirectory || isDownloadedFile
548579
}
549580
}
550581

@@ -556,67 +587,97 @@ public final class FilesDatabaseManager: Sendable {
556587
///
557588
/// - Returns: An array of sendable metadata objects.
558589
///
559-
public func materialisedItemMetadatas(account: String) -> [SendableItemMetadata] {
560-
managedMaterialisedItemMetadatas(account: account).toUnmanagedResults()
590+
public func materialisedItemMetadatas(account _: String) -> [SendableItemMetadata] {
591+
managedMaterialisedItemMetadatas().toUnmanagedResults()
561592
}
562593

563-
public func pendingWorkingSetChanges(account: Account, since date: Date) -> (updated: [SendableItemMetadata], deleted: [SendableItemMetadata]) {
564-
let accId = account.ncKitAccount
565-
let pending = managedMaterialisedItemMetadatas(account: accId).where { $0.syncTime > date }
566-
var updated = pending.where { !$0.deleted }.toUnmanagedResults()
567-
var deleted = pending.where { $0.deleted }.toUnmanagedResults()
568-
var handledUpdateOcIds = Set(updated.map(\.ocId))
594+
///
595+
/// Look up the not yet synchronized changes and deletions in the materialized items since the last given synchronization time.
596+
///
597+
/// - Parameters:
598+
/// - date: All items with a synchronization time later than this are considered.
599+
///
600+
/// - Returns: Locally changed items in the working set grouped by "updated" and "deleted".
601+
///
602+
public func pendingWorkingSetChanges(since date: Date) -> (updated: [SendableItemMetadata], deleted: [SendableItemMetadata]) {
603+
logger.debug("Gathering pending working set changes...")
604+
let pendingChanges = managedMaterialisedItemMetadatas().where { $0.syncTime > date }
605+
var updatedItems = pendingChanges.where { !$0.deleted }.toUnmanagedResults()
606+
var deletedItems = pendingChanges.where { $0.deleted }.toUnmanagedResults()
607+
608+
for item in updatedItems {
609+
logger.debug("Found updated item.", [.item: item.ocId, .name: item.fileName])
610+
}
569611

570-
updated
571-
.map { $0.remotePath() }
572-
.forEach { serverUrl in
573-
logger.debug("Checking updated item...", [.url: serverUrl])
612+
for item in deletedItems {
613+
logger.debug("Found deleted item.", [.item: item.ocId, .name: item.fileName])
614+
}
574615

616+
var updatedItemIdentifiers = Set(updatedItems.map(\.ocId))
617+
var deletedItemIdentifiers = Set(deletedItems.map(\.ocId))
618+
619+
updatedItems // Look for changed children
620+
.filter {
621+
$0.directory // files do not have any children to look for
622+
}
623+
.map {
624+
$0.remotePath()
625+
}
626+
.forEach { serverUrl in
575627
itemMetadatas
576-
.where { $0.serverUrl == serverUrl && $0.syncTime > date }
577-
.forEach { metadata in
578-
guard !handledUpdateOcIds.contains(metadata.ocId) else {
579-
return
580-
}
628+
.where {
629+
$0.serverUrl == serverUrl && $0.syncTime > date
630+
}
631+
.forEach { child in
632+
let sendableMetadata = SendableItemMetadata(value: child)
581633

582-
handledUpdateOcIds.insert(metadata.ocId)
583-
let sendableMetadata = SendableItemMetadata(value: metadata)
634+
if child.deleted {
635+
guard deletedItemIdentifiers.contains(child.ocId) == false else {
636+
return
637+
}
584638

585-
if metadata.deleted {
586-
deleted.append(sendableMetadata)
587-
logger.debug("Appended deleted item to working set changes.", [.item: metadata.ocId, .url: serverUrl])
639+
deletedItemIdentifiers.insert(child.ocId)
640+
deletedItems.append(sendableMetadata)
641+
logger.debug("Appended deleted item to working set changes.", [.item: child.ocId, .url: serverUrl])
588642
} else {
589-
updated.append(sendableMetadata)
590-
logger.debug("Appended updated item to working set changes.", [.item: metadata.ocId, .url: serverUrl])
643+
guard updatedItemIdentifiers.contains(child.ocId) == false else {
644+
return
645+
}
646+
647+
updatedItemIdentifiers.insert(child.ocId)
648+
updatedItems.append(sendableMetadata)
649+
logger.debug("Appended updated item to working set changes.", [.item: child.ocId, .url: serverUrl])
591650
}
592651
}
593652
}
594653

595-
let handledDeleteOcIds = Set(deleted.map(\.ocId))
596-
597-
deleted
598-
.map { $0.remotePath() }
654+
deletedItems // Look for deleted children recursively
655+
.filter {
656+
$0.directory // files do not have any children to look for
657+
}
658+
.map {
659+
$0.remotePath()
660+
}
599661
.forEach { serverUrl in
600-
logger.debug("Verifying deleted item...", [.url: serverUrl])
601-
602662
itemMetadatas.where {
603663
$0.serverUrl.starts(with: serverUrl) && $0.syncTime > date
604-
}.forEach { metadata in
605-
guard metadata.isLockFileOfLocalOrigin == false else {
606-
logger.info("Excluding item from deletion because it is a lock file from local origin.", [.item: metadata.ocId])
664+
}.forEach { child in
665+
guard child.isLockFileOfLocalOrigin == false else {
666+
logger.info("Excluding item from deletion because it is a lock file from local origin.", [.item: child.ocId, .name: child.fileName])
607667
return
608668
}
609669

610-
guard !handledDeleteOcIds.contains(metadata.ocId) else {
670+
guard !deletedItemIdentifiers.contains(child.ocId) else {
611671
return
612672
}
613673

614-
deleted.append(SendableItemMetadata(value: metadata))
615-
logger.debug("Appended deleted item to working set changes.", [.item: metadata.ocId, .url: serverUrl])
674+
deletedItemIdentifiers.insert(child.ocId)
675+
deletedItems.append(SendableItemMetadata(value: child))
676+
logger.debug("Appended deleted item to working set changes.", [.item: child.ocId, .url: serverUrl])
616677
}
617678
}
618679

619-
return (updated, deleted)
680+
return (updatedItems, deletedItems)
620681
}
621682

622683
public func itemsMetadataByFileNameSuffix(suffix: String) -> [SendableItemMetadata] {

shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
3737
domain: NSFileProviderDomain? = nil,
3838
pageSize: Int = 1000,
3939
log: any FileProviderLogging
40-
) {
40+
) throws {
4141
self.enumeratedItemIdentifier = enumeratedItemIdentifier
4242
self.remoteInterface = remoteInterface
4343
self.account = account
@@ -51,25 +51,23 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
5151
serverUrl = account.davFilesUrl
5252
enumeratedItemMetadata = nil
5353
} else {
54-
logger.debug("Providing enumerator for item with identifier.", [.item: enumeratedItemIdentifier])
55-
enumeratedItemMetadata = dbManager.itemMetadata(
56-
enumeratedItemIdentifier
57-
)
54+
enumeratedItemMetadata = dbManager.itemMetadata(enumeratedItemIdentifier)
5855

59-
if let enumeratedItemMetadata {
60-
serverUrl = enumeratedItemMetadata.serverUrl + "/" + enumeratedItemMetadata.fileName
61-
} else {
62-
serverUrl = ""
63-
logger.error("Could not find itemMetadata for file with identifier.", [.item: enumeratedItemIdentifier])
56+
guard let enumeratedItemMetadata, enumeratedItemMetadata.deleted == false else {
57+
logger.error("Could not find item with identifier.", [.item: enumeratedItemIdentifier])
58+
throw NSFileProviderError(.noSuchItem)
6459
}
60+
61+
logger.debug("Providing enumerator for item with identifier.", [.item: enumeratedItemIdentifier, .name: enumeratedItemMetadata.fileName])
62+
serverUrl = enumeratedItemMetadata.serverUrl + "/" + enumeratedItemMetadata.fileName
6563
}
6664

6765
logger.info("Set up enumerator.", [.account: self.account.ncKitAccount, .url: serverUrl])
6866
super.init()
6967
}
7068

7169
public func invalidate() {
72-
logger.debug("Enumerator is being invalidated.", [.item: enumeratedItemIdentifier])
70+
logger.debug("Enumerator is being invalidated.", [.item: enumeratedItemIdentifier, .name: enumeratedItemMetadata?.fileName])
7371
}
7472

7573
// MARK: - Protocol methods
@@ -112,6 +110,7 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
112110
let ncKitAccount = account.ncKitAccount
113111
// Visited folders and downloaded files
114112
let materialisedItems = dbManager.materialisedItemMetadatas(account: ncKitAccount)
113+
.filter { !$0.deleted }
115114
completeEnumerationObserver(observer, nextPage: nil, itemMetadatas: materialisedItems)
116115
return
117116
}
@@ -248,7 +247,7 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
248247

249248
Task {
250249
await checkMaterializedItemsOnServer()
251-
let pendingLocalChanges = dbManager.pendingWorkingSetChanges(account: account, since: date)
250+
let pendingLocalChanges = dbManager.pendingWorkingSetChanges(since: date)
252251

253252
completeChangesObserver(
254253
observer,
@@ -434,6 +433,7 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
434433
// This way we ensure we visit parent folders before their children.
435434
let materializedItems = dbManager
436435
.materialisedItemMetadatas(account: account.ncKitAccount)
436+
.filter { !$0.deleted }
437437
.sorted { $0.remotePath().count < $1.remotePath().count }
438438

439439
var allNewMetadatas = [SendableItemMetadata]()
@@ -639,15 +639,15 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
639639
}
640640

641641
for metadata in newMetadatas {
642-
logger.debug("Got added metadata.", [.item: metadata.ocId, .name: metadata.fileName])
642+
logger.debug("Got added metadata to report.", [.item: metadata.ocId, .name: metadata.fileName])
643643
}
644644

645645
for metadata in updatedMetadatas {
646-
logger.debug("Got updated metadata.", [.item: metadata.ocId, .name: metadata.fileName])
646+
logger.debug("Got updated metadata to report.", [.item: metadata.ocId, .name: metadata.fileName])
647647
}
648648

649649
for metadata in deletedMetadatas {
650-
logger.debug("Got deleted metadata.", [.item: metadata.ocId, .name: metadata.fileName])
650+
logger.debug("Got deleted metadata to report.", [.item: metadata.ocId, .name: metadata.fileName])
651651
}
652652

653653
// The file provider framework does not differentiate between newly added and updated items, hence the collections are merged.
@@ -671,6 +671,10 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
671671
}
672672

673673
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
674+
675+
for metadata in deletedMetadatas {
676+
dbManager.removeItemMetadata(ocId: metadata.ocId)
677+
}
674678
}
675679
} catch let error as NSError { // This error can only mean a missing parent item identifier
676680
guard handleInvalidParent else {

shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
340340
super.init()
341341
}
342342

343-
public static func storedItem(
344-
identifier: NSFileProviderItemIdentifier,
345-
account: Account,
346-
remoteInterface: RemoteInterface,
347-
dbManager: FilesDatabaseManager,
348-
log: any FileProviderLogging
349-
) async -> Item? {
343+
public static func storedItem(identifier: NSFileProviderItemIdentifier, account: Account, remoteInterface: RemoteInterface, dbManager: FilesDatabaseManager, log: any FileProviderLogging) async -> Item? {
350344
// resolve the given identifier to a record in the model
351345

352346
let remoteSupportsTrash = await remoteInterface.supportsTrash(account: account)

0 commit comments

Comments
 (0)