Skip to content
Draft
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ You are an experienced engineer specialized on C++ and Qt and familiar with the
The Nextcloud Desktop Client is a tool to synchronize files from Nextcloud Server with your computer.
Qt, C++, CMake and KDE Craft are the key technologies used for building the app on Windows, macOS and Linux.
Beyond that, there are platform-specific extensions of the multi-platform app in the `./shell_integration` directory.
Other platforms like iOS and Android are irrelevant for this project.

## Project Structure: AI Agent Handling Guidelines

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,45 @@
// Do not pass in the NSFileProviderPage default pages, these are not valid Nextcloud
// pagination tokens
var pageTotal: Int? = nil
var pageIndex = 0
var parsedPage: NSFileProviderPage? = nil

if page != NSFileProviderPage.initialPageSortedByName as NSFileProviderPage, page != NSFileProviderPage.initialPageSortedByDate as NSFileProviderPage {
if let enumPageResponse = try? JSONDecoder().decode(EnumeratorPageResponse.self, from: page.rawValue) {
if let total = enumPageResponse.total {
pageTotal = total
}
pageIndex = enumPageResponse.index
parsedPage = page
} else {
logger.error("Could not parse page")
}
}

// Check server version to determine if pagination should be enabled.
// Pagination was fixed in Nextcloud 31 (server bug: https://github.com/nextcloud/server/issues/53674)
// For older servers, we fall back to non-paginated requests.
// Note: currentCapabilities uses RetrievedCapabilitiesActor which caches capabilities
// for 30 minutes, so this call is efficient and doesn't make a network request on every enumeration.
let (_, capabilities, _, _) = await remoteInterface.currentCapabilities(
account: account,
options: .init(),
taskHandler: { _ in }
)

Check warning on line 158 in shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View workflow job for this annotation

GitHub Actions / Lint

Remove trailing space at end of a line. (trailingSpace)
let serverMajorVersion = capabilities?.major ?? 0
let supportsPagination = serverMajorVersion >= 31

Check warning on line 161 in shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View workflow job for this annotation

GitHub Actions / Lint

Remove trailing space at end of a line. (trailingSpace)
// Enable pagination by passing page settings if server supports it
let pageSettings: (page: NSFileProviderPage?, index: Int, size: Int)? = supportsPagination ? (
page: parsedPage,
index: pageIndex,
size: pageItemCount
) : nil

let readResult = await Self.readServerUrl(
serverUrl,
pageSettings: nil,
pageSettings: pageSettings,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
Expand Down Expand Up @@ -180,7 +205,8 @@
nextPage = nil
}

nextPage = nil
// Note: Removed unconditional `nextPage = nil` that was disabling pagination
// This enables proper pagination for large folders (1500+ files) when server supports it

logger.info(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1742,4 +1742,230 @@
XCTAssertTrue(workingSetIds2.contains(notVisitedFolder.ocId),
"Newly visited folder should now be in working set")
}

// MARK: - Pagination Tests (Server Version Detection)

/// Test that pagination is enabled when server is Nextcloud 31 or newer
func testPaginationEnabledForNC31Plus() async throws {
let db = Self.dbManager.ncDatabase()
debugPrint(db)

// Setup a folder with many children to trigger pagination
remoteFolder.children = []
for i in 0..<25 {

Check warning on line 1755 in shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Add or remove space around operators or delimiters. (spaceAroundOperators)
let childItem = MockRemoteItem(
identifier: "paginatedChild\(i)",
name: "file_\(i).pdf",
remotePath: Self.account.davFilesUrl + "/folder/file_\(i).pdf",
data: Data(repeating: UInt8(i % 256), count: 100),
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
childItem.parent = remoteFolder
remoteFolder.children.append(childItem)
}

// Create remote interface with pagination enabled and NC31+ capabilities
let remoteInterface = MockRemoteInterface(
account: Self.account, rootItem: rootItem, pagination: true
)
// Override capabilities to simulate NC31+
remoteInterface.capabilities = ##"""
{
"ocs": {
"data": {
"version": {
"major": 31,
"minor": 0,
"micro": 0,
"string": "31.0.0"
}
}
}
}
"""##

Self.dbManager.addItemMetadata(remoteFolder.toItemMetadata(account: Self.account))

let enumerator = Enumerator(
enumeratedItemIdentifier: .init(remoteFolder.identifier),
account: Self.account,
remoteInterface: remoteInterface,
dbManager: Self.dbManager,
pageSize: 5, // Small page size to force multiple pages
log: FileProviderLogMock()
)
let observer = MockEnumerationObserver(enumerator: enumerator)
try await observer.enumerateItems()

// With pagination enabled, all items should be enumerated
XCTAssertEqual(
observer.items.count,
26, // folder + 25 children
"Pagination should enumerate all items for NC31+"
)

// Verify all items are in database
for i in 0..<25 {

Check warning on line 1811 in shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Add or remove space around operators or delimiters. (spaceAroundOperators)
XCTAssertNotNil(
Self.dbManager.itemMetadata(ocId: "paginatedChild\(i)"),
"Child item paginatedChild\(i) should be in DB with pagination enabled"
)
}
}

/// Test that pagination is disabled when server is older than Nextcloud 31
func testPaginationDisabledForOldServers() async throws {
let db = Self.dbManager.ncDatabase()
debugPrint(db)

// Setup a folder with children
remoteFolder.children = []
for i in 0..<10 {

Check warning on line 1826 in shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Add or remove space around operators or delimiters. (spaceAroundOperators)
let childItem = MockRemoteItem(
identifier: "oldServerChild\(i)",
name: "file_\(i).txt",
remotePath: Self.account.davFilesUrl + "/folder/file_\(i).txt",
data: Data(repeating: UInt8(i % 256), count: 50),
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
childItem.parent = remoteFolder
remoteFolder.children.append(childItem)
}

// Create remote interface with pagination NOT enabled (simulates old server)
let remoteInterface = MockRemoteInterface(
account: Self.account, rootItem: rootItem, pagination: false
)
// Override capabilities to simulate NC30
remoteInterface.capabilities = ##"""
{
"ocs": {
"data": {
"version": {
"major": 30,
"minor": 0,
"micro": 5,
"string": "30.0.5"
}
}
}
}
"""##

Self.dbManager.addItemMetadata(remoteFolder.toItemMetadata(account: Self.account))

let enumerator = Enumerator(
enumeratedItemIdentifier: .init(remoteFolder.identifier),
account: Self.account,
remoteInterface: remoteInterface,
dbManager: Self.dbManager,
pageSize: 5, // Page size doesn't matter when pagination is disabled
log: FileProviderLogMock()
)
let observer = MockEnumerationObserver(enumerator: enumerator)
try await observer.enumerateItems()

// With pagination disabled, all items should still be enumerated (in single request)
XCTAssertEqual(
observer.items.count,
11, // folder + 10 children
"Non-paginated enumeration should work for older servers"
)

// Verify all items are in database
for i in 0..<10 {

Check warning on line 1882 in shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Add or remove space around operators or delimiters. (spaceAroundOperators)
XCTAssertNotNil(
Self.dbManager.itemMetadata(ocId: "oldServerChild\(i)"),
"Child item oldServerChild\(i) should be in DB even without pagination"
)
}
}

/// Test that pagination works correctly for large folders on NC31+
func testLargeFolderPaginationOnNC31() async throws {
let db = Self.dbManager.ncDatabase()
debugPrint(db)

// Setup a folder with 50 children to test pagination across multiple pages
remoteFolder.children = []
for i in 0..<50 {

Check warning on line 1897 in shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Add or remove space around operators or delimiters. (spaceAroundOperators)
let childItem = MockRemoteItem(
identifier: "largeChild\(i)",
name: "document_\(String(format: "%03d", i)).pdf",
remotePath: Self.account.davFilesUrl + "/folder/document_\(String(format: "%03d", i)).pdf",
data: Data(repeating: UInt8(i % 256), count: 100),
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
childItem.parent = remoteFolder
remoteFolder.children.append(childItem)
}

// Create remote interface with pagination enabled and NC31+ capabilities
let remoteInterface = MockRemoteInterface(
account: Self.account, rootItem: rootItem, pagination: true
)
remoteInterface.capabilities = ##"""
{
"ocs": {
"data": {
"version": {
"major": 31,
"minor": 0,
"micro": 0,
"string": "31.0.0"
}
}
}
}
"""##

Self.dbManager.addItemMetadata(remoteFolder.toItemMetadata(account: Self.account))

let enumerator = Enumerator(
enumeratedItemIdentifier: .init(remoteFolder.identifier),
account: Self.account,
remoteInterface: remoteInterface,
dbManager: Self.dbManager,
pageSize: 10, // 50 items = 5 pages
log: FileProviderLogMock()
)
let observer = MockEnumerationObserver(enumerator: enumerator)
try await observer.enumerateItems()

// All 50 children + folder should be enumerated
XCTAssertEqual(
observer.items.count,
51,
"Large folder with 50 items should be fully enumerated with pagination"
)

// Verify multiple pages were used
XCTAssertGreaterThan(
observer.observedPages.count,
1,
"Large folder enumeration should use multiple pages"
)

// Verify all items are in database
var itemsInDb = 0
for i in 0..<50 {

Check warning on line 1960 in shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Add or remove space around operators or delimiters. (spaceAroundOperators)
if Self.dbManager.itemMetadata(ocId: "largeChild\(i)") != nil {
itemsInDb += 1
}
}
XCTAssertEqual(
itemsInDb,
50,
"All 50 items should be saved to database with pagination"
)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading