Skip to content

Commit f08095f

Browse files
authored
Merge pull request #74 from claucambra/bugfix/trash-capabilities
Check for trash in server capabilities before attempting trash-related operations
2 parents a4892bb + 17dcecf commit f08095f

11 files changed

Lines changed: 202 additions & 5 deletions

File tree

Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ public class Enumerator: NSObject, NSFileProviderEnumerator {
144144
)
145145

146146
Task {
147+
let (_, capabilities, _, error) = await remoteInterface.currentCapabilities(
148+
account: account, options: .init(), taskHandler: { _ in }
149+
)
150+
guard let capabilities, error == .success else {
151+
Self.logger.error(
152+
"""
153+
Could not acquire capabilities, cannot check trash.
154+
Error: \(error, privacy: .public)
155+
""")
156+
observer.finishEnumeratingWithError(NSFileProviderError(.serverUnreachable))
157+
return
158+
}
159+
guard capabilities.files?.undelete == true else {
160+
Self.logger.error("Trash is unsupported on server, cannot enumerate items.")
161+
observer.finishEnumeratingWithError(
162+
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
163+
)
164+
return
165+
}
166+
147167
let (_, trashedItems, _, trashReadError) = await remoteInterface.trashedItems(
148168
account: account,
149169
options: .init(),
@@ -388,6 +408,26 @@ public class Enumerator: NSObject, NSFileProviderEnumerator {
388408
)
389409

390410
Task {
411+
let (_, capabilities, _, error) = await remoteInterface.currentCapabilities(
412+
account: account, options: .init(), taskHandler: { _ in }
413+
)
414+
guard let capabilities, error == .success else {
415+
Self.logger.error(
416+
"""
417+
Could not acquire capabilities, cannot check trash.
418+
Error: \(error, privacy: .public)
419+
""")
420+
observer.finishEnumeratingWithError(NSFileProviderError(.serverUnreachable))
421+
return
422+
}
423+
guard capabilities.files?.undelete == true else {
424+
Self.logger.error("Trash is unsupported on server, cannot enumerate changes.")
425+
observer.finishEnumeratingWithError(
426+
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
427+
)
428+
return
429+
}
430+
391431
let (_, trashedItems, _, trashReadError) = await remoteInterface.trashedItems(
392432
account: account,
393433
options: .init(),

Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import FileProvider
1010
import Foundation
1111
import NextcloudCapabilitiesKit
1212
import NextcloudKit
13+
import OSLog
1314

1415
fileprivate let CapabilitiesFetchInterval: TimeInterval = 30 * 60 // 30mins
1516

@@ -430,6 +431,23 @@ extension NextcloudKit: RemoteInterface {
430431
return (account.ncKitAccount, lastRetrieval.capabilities, nil, .success)
431432
}
432433

434+
public func currentCapabilitiesSync(account: Account) -> Capabilities? {
435+
let semaphore = DispatchSemaphore(value: 0)
436+
var capabilities: Capabilities?
437+
Task {
438+
let (_, fetchedCapabilities, _, error) = await currentCapabilities(account: account)
439+
if error != .success {
440+
Logger
441+
.init(subsystem: Logger.subsystem, category: "NextcloudKitRemoteInterface")
442+
.error("Error during sync capabilities fetch: \(error, privacy: .public)")
443+
}
444+
capabilities = fetchedCapabilities
445+
semaphore.signal()
446+
}
447+
semaphore.wait()
448+
return capabilities
449+
}
450+
433451
public func fetchUserProfile(
434452
account: Account,
435453
options: NKRequestOptions = .init(),

Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ public protocol RemoteInterface {
165165
taskHandler: @escaping (_ task: URLSessionTask) -> Void
166166
) async -> (account: String, capabilities: Capabilities?, data: Data?, error: NKError)
167167

168+
func currentCapabilitiesSync(account: Account) -> Capabilities?
169+
168170
func fetchUserProfile(
169171
account: Account,
170172
options: NKRequestOptions,

Sources/NextcloudFileProviderKit/Item/Item+Modify.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,26 @@ public extension Item {
709709
)
710710
return (modifiedItem, nil)
711711
} else if changedFields.contains(.parentItemIdentifier) && newParentItemIdentifier == .trashContainer {
712+
let (_, capabilities, _, error) = await remoteInterface.currentCapabilities(
713+
account: account, options: .init(), taskHandler: { _ in }
714+
)
715+
guard let capabilities, error == .success else {
716+
Self.logger.error(
717+
"""
718+
Could not acquire capabilities during item move to trash, won't proceed.
719+
Error: \(error, privacy: .public)
720+
Item: \(modifiedItem.filename)
721+
"""
722+
)
723+
return (nil, error.fileProviderError)
724+
}
725+
guard capabilities.files?.undelete == true else {
726+
Self.logger.error(
727+
"Cannot delete \(modifiedItem.filename) as server does not support trashing."
728+
)
729+
return (nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError))
730+
}
731+
712732
// We can't just move files into the trash, we need to issue a deletion; let's handle it
713733
// Rename the item if necessary before doing the trashing procedures
714734
if (changedFields.contains(.filename)) {

Sources/NextcloudFileProviderKit/Item/Item.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ public class Item: NSObject, NSFileProviderItem {
4141
.allowsReading,
4242
.allowsDeleting,
4343
.allowsReparenting,
44-
.allowsRenaming,
45-
.allowsTrashing
44+
.allowsRenaming
4645
]
4746

4847
#if os(macOS)
@@ -55,6 +54,11 @@ public class Item: NSObject, NSFileProviderItem {
5554
if #unavailable(macOS 13.0) {
5655
directoryCapabilities.insert(.allowsEvicting)
5756
}
57+
58+
if remoteInterface.currentCapabilitiesSync(account: account)?.files?.undelete == true {
59+
directoryCapabilities.insert(.allowsTrashing)
60+
}
61+
5862
return directoryCapabilities
5963
}
6064
guard !metadata.lock else {
@@ -69,7 +73,9 @@ public class Item: NSObject, NSFileProviderItem {
6973
.allowsReparenting,
7074
.allowsEvicting,
7175
]
72-
if !isLockFileName(filename) {
76+
if remoteInterface.currentCapabilitiesSync(account: account)?.files?.undelete == true,
77+
!isLockFileName(filename)
78+
{
7379
itemCapabilities.insert(.allowsTrashing)
7480
}
7581
return itemCapabilities

Tests/Interface/MockChangeObserver.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class MockChangeObserver: NSObject, NSFileProviderChangeObserver {
3333

3434
public func finishEnumeratingWithError(_ error: Error) {
3535
self.error = error
36+
isComplete = true
3637
}
3738

3839
public func enumerateChanges() async throws {

Tests/Interface/MockEnumerationObserver.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class MockEnumerationObserver: NSObject, NSFileProviderEnumerationObserve
2828

2929
public func finishEnumeratingWithError(_ error: Error) {
3030
self.error = error
31+
isComplete = true
3132
}
3233

3334
public func enumerateItems() async throws {

Tests/Interface/MockRemoteInterface.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,12 +1144,24 @@ public class MockRemoteInterface: RemoteInterface {
11441144

11451145
public func currentCapabilities(
11461146
account: Account,
1147-
options: NKRequestOptions,
1148-
taskHandler: @escaping (URLSessionTask) -> Void
1147+
options: NKRequestOptions = .init(),
1148+
taskHandler: @escaping (URLSessionTask) -> Void = { _ in }
11491149
) async -> (account: String, capabilities: Capabilities?, data: Data?, error: NKError) {
11501150
return await fetchCapabilities(account: account, options: options, taskHandler: taskHandler)
11511151
}
11521152

1153+
public func currentCapabilitiesSync(account: Account) -> Capabilities? {
1154+
let semaphore = DispatchSemaphore(value: 0)
1155+
var capabilities: Capabilities?
1156+
Task {
1157+
let result = await currentCapabilities(account: account)
1158+
capabilities = result.capabilities
1159+
semaphore.signal()
1160+
}
1161+
semaphore.wait()
1162+
return capabilities
1163+
}
1164+
11531165
public func fetchUserProfile(
11541166
account: Account,
11551167
options: NKRequestOptions = .init(),

Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,4 +867,50 @@ final class EnumeratorTests: XCTestCase {
867867
XCTAssertNotNil(Self.dbManager.itemMetadata(ocId: remoteTrashItemB.identifier))
868868
XCTAssertNotNil(Self.dbManager.itemMetadata(ocId: remoteTrashItemC.identifier))
869869
}
870+
871+
func testTrashItemEnumerationFailWhenNoTrashInCapabilities() async {
872+
let remoteInterface = MockRemoteInterface(rootItem: rootItem, rootTrashItem: rootTrashItem)
873+
XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))
874+
remoteInterface.capabilities =
875+
remoteInterface.capabilities.replacingOccurrences(of: ##""undelete": true,"##, with: "")
876+
877+
let db = Self.dbManager.ncDatabase() // Strong ref for in memory test db
878+
debugPrint(db) // Avoid build-time warning about unused variable, ensure compiler won't free
879+
let enumerator = Enumerator(
880+
enumeratedItemIdentifier: .trashContainer,
881+
account: Self.account,
882+
remoteInterface: remoteInterface,
883+
dbManager: Self.dbManager
884+
)
885+
let observer = MockEnumerationObserver(enumerator: enumerator)
886+
do {
887+
try await observer.enumerateItems()
888+
XCTFail("Item enumeration should have failed!")
889+
} catch let error {
890+
XCTAssertEqual((error as NSError?)?.code, NSFeatureUnsupportedError)
891+
}
892+
}
893+
894+
func testTrashChangeEnumerationFailWhenNoTrashInCapabilities() async {
895+
let remoteInterface = MockRemoteInterface(rootItem: rootItem, rootTrashItem: rootTrashItem)
896+
XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))
897+
remoteInterface.capabilities =
898+
remoteInterface.capabilities.replacingOccurrences(of: ##""undelete": true,"##, with: "")
899+
900+
let db = Self.dbManager.ncDatabase() // Strong ref for in memory test db
901+
debugPrint(db) // Avoid build-time warning about unused variable, ensure compiler won't free
902+
let enumerator = Enumerator(
903+
enumeratedItemIdentifier: .trashContainer,
904+
account: Self.account,
905+
remoteInterface: remoteInterface,
906+
dbManager: Self.dbManager
907+
)
908+
let observer = MockChangeObserver(enumerator: enumerator)
909+
do {
910+
try await observer.enumerateChanges()
911+
XCTFail("Item enumeration should have failed!")
912+
} catch let error {
913+
XCTAssertEqual((error as NSError?)?.code, NSFeatureUnsupportedError)
914+
}
915+
}
870916
}

Tests/NextcloudFileProviderKitTests/ItemModifyTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,4 +1540,38 @@ final class ItemModifyTests: XCTestCase {
15401540

15411541
XCTAssertNotEqual(modifiedItem?.metadata.classFile, "lock")
15421542
}
1543+
1544+
func testMoveToTrashFailsWhenNoTrashInCapabilities() async throws {
1545+
let remoteInterface = MockRemoteInterface(rootItem: rootItem, rootTrashItem: rootTrashItem)
1546+
XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))
1547+
remoteInterface.capabilities =
1548+
remoteInterface.capabilities.replacingOccurrences(of: ##""undelete": true,"##, with: "")
1549+
1550+
let itemMetadata = remoteItem.toItemMetadata(account: Self.account)
1551+
Self.dbManager.addItemMetadata(itemMetadata)
1552+
1553+
let item = Item(
1554+
metadata: itemMetadata,
1555+
parentItemIdentifier: .rootContainer,
1556+
account: Self.account,
1557+
remoteInterface: remoteInterface,
1558+
dbManager: Self.dbManager
1559+
)
1560+
let trashItem = Item(
1561+
metadata: itemMetadata,
1562+
parentItemIdentifier: .trashContainer,
1563+
account: Self.account,
1564+
remoteInterface: remoteInterface,
1565+
dbManager: Self.dbManager
1566+
)
1567+
1568+
let (_, error) = await item.modify(
1569+
itemTarget: trashItem,
1570+
changedFields: [.parentItemIdentifier],
1571+
contents: nil,
1572+
dbManager: Self.dbManager
1573+
)
1574+
XCTAssertNotNil(error)
1575+
XCTAssertEqual((error as NSError?)?.code, NSFeatureUnsupportedError)
1576+
}
15431577
}

0 commit comments

Comments
 (0)