Skip to content

Commit d7a8b48

Browse files
authored
Merge pull request #87 from claucambra/bugfix/pagination-pt3
Further bugfixes for paginated server URL reads
2 parents 41168e0 + 9a0a733 commit d7a8b48

4 files changed

Lines changed: 227 additions & 8 deletions

File tree

Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,17 @@ extension Enumerator {
272272
// target item, hence why we always strip the target item out)
273273
let startIndex = pageIndex > 0 ? 0 : 1
274274
if pageIndex == 0 {
275-
guard let firstFile = files.first else {
276-
return (nil, .invalidResponseError)
275+
guard let firstFile = files.first else { return (nil, .invalidResponseError) }
276+
// Do not ingest metadata for the root container
277+
if !firstFile.fullUrlMatches(dbManager.account.davFilesUrl) {
278+
var metadata = firstFile.toItemMetadata()
279+
if metadata.directory,
280+
let existingMetadata = dbManager.itemMetadata(ocId: metadata.ocId)
281+
{
282+
metadata.downloaded = existingMetadata.downloaded
283+
}
284+
dbManager.addItemMetadata(metadata)
277285
}
278-
dbManager.addItemMetadata(firstFile.toItemMetadata())
279286
}
280287
let metadatas = files[startIndex..<files.count].map { $0.toItemMetadata() }
281288
metadatas.forEach { dbManager.addItemMetadata($0) }
@@ -455,7 +462,9 @@ extension Enumerator {
455462
// That is NOT the case for paginated results with offsets
456463
let isFollowUpPaginatedRequest = (pageSettings?.page != nil && pageSettings?.index ?? 0 > 0)
457464
if !isFollowUpPaginatedRequest {
458-
guard receivedFile.directory else {
465+
guard receivedFile.directory ||
466+
receivedFile.fullUrlMatches(dbManager.account.davFilesUrl)
467+
else {
459468
Self.logger.debug(
460469
"""
461470
Read item is a file.

Sources/NextcloudFileProviderKit/Extensions/NKFile+Extensions.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import NextcloudKit
1010
import RealmSwift
1111

1212
extension NKFile {
13+
func fullUrlMatches(_ urlString: String) -> Bool {
14+
var fileUrl = serverUrl + "/" + fileName
15+
if fileUrl.last == "/" { // This is likely the root container, as it has no filename
16+
fileUrl.removeLast()
17+
}
18+
return fileUrl == urlString
19+
}
20+
1321
func toItemMetadata(uploaded: Bool = true) -> SendableItemMetadata {
1422
let creationDate = creationDate ?? date
1523
let uploadDate = uploadDate ?? date

Tests/Interface/MockRemoteItem.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public class MockRemoteItem: Equatable {
6060
MockRemoteItem(
6161
identifier: NSFileProviderItemIdentifier.rootContainer.rawValue,
6262
versionIdentifier: "root",
63-
name: "root",
63+
name: "",
6464
remotePath: account.davFilesUrl,
6565
directory: true,
6666
account: account.ncKitAccount,
@@ -74,7 +74,7 @@ public class MockRemoteItem: Equatable {
7474
return MockRemoteItem(
7575
identifier: NSFileProviderItemIdentifier.trashContainer.rawValue,
7676
versionIdentifier: "root",
77-
name: "root",
77+
name: "",
7878
remotePath: account.trashUrl,
7979
directory: true,
8080
account: account.ncKitAccount,

Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

Lines changed: 204 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,6 @@ final class EnumeratorTests: XCTestCase {
398398
// as pages. This process continues until all directories have been enumerated.
399399
// We can verify that all items are discovered and stored in the database.
400400
let allItemIds = [
401-
rootItem.identifier,
402401
folder1.identifier,
403402
folder2.identifier,
404403
subfolder.identifier
@@ -407,8 +406,211 @@ final class EnumeratorTests: XCTestCase {
407406
+ subfolder.children.map(\.identifier)
408407

409408
for itemId in allItemIds {
410-
XCTAssertNotNil(Self.dbManager.itemMetadata(ocId: itemId), "Item with id \(itemId) should be in the database")
409+
XCTAssertNotNil(
410+
Self.dbManager.itemMetadata(ocId: itemId),
411+
"Item with id \(itemId) should be in the database"
412+
)
413+
}
414+
// Root should not be stored in database
415+
XCTAssertNil(Self.dbManager.itemMetadata(ocId: rootItem.identifier))
416+
}
417+
418+
func testReadServerUrlFollowUpPagination() async throws {
419+
// 1. Arrange: Setup a folder with enough children to require multiple pages.
420+
remoteFolder.children = []
421+
for i in 0..<10 {
422+
let childItem = MockRemoteItem(
423+
identifier: "folderChild\(i)",
424+
name: "folderChild\(i).txt",
425+
remotePath: Self.account.davFilesUrl + "/folder/folderChild\(i).txt",
426+
account: Self.account.ncKitAccount,
427+
username: Self.account.username,
428+
userId: Self.account.id,
429+
serverUrl: Self.account.serverUrl
430+
)
431+
childItem.parent = remoteFolder
432+
remoteFolder.children.append(childItem)
433+
}
434+
435+
let db = Self.dbManager.ncDatabase()
436+
debugPrint(db)
437+
let remoteInterface = MockRemoteInterface(rootItem: rootItem, pagination: true)
438+
439+
// Pre-populate the folder's metadata with an old etag to verify it gets updated
440+
// on the initial call.
441+
let oldEtag = "OLD_ETAG"
442+
var folderMetadata = remoteFolder.toItemMetadata(account: Self.account)
443+
folderMetadata.etag = oldEtag
444+
Self.dbManager.addItemMetadata(folderMetadata)
445+
446+
// --- Scenario A: Initial Paginated Request (isFollowUpPaginatedRequest == false) ---
447+
448+
// 2. Act: Call readServerUrl for the first page.
449+
let (
450+
initialMetadatas, _, _, _, initialNextPage, initialError
451+
) = await Enumerator.readServerUrl(
452+
remoteFolder.remotePath,
453+
pageSettings: (page: nil, index: 0, size: 5), // index is 0
454+
account: Self.account,
455+
remoteInterface: remoteInterface,
456+
dbManager: Self.dbManager
457+
)
458+
459+
// 3. Assert: Verify the initial request's behavior.
460+
XCTAssertNil(initialError)
461+
XCTAssertNotNil(initialNextPage, "Should receive a next page token for the initial request")
462+
// The first request for a folder returns the folder itself plus the first page of children.
463+
XCTAssertEqual(
464+
initialMetadatas?.count,
465+
4,
466+
"""
467+
Should get target + first page of children,
468+
but the target should not be included in the first page,
469+
so count is (4).
470+
"""
471+
)
472+
XCTAssertFalse(
473+
initialMetadatas?.contains(where: { $0.ocId == remoteFolder.identifier }) ?? false,
474+
"The folder itself should not be in the initial results."
475+
)
476+
477+
// The logic inside `if !isFollowUpPaginatedRequest` should have run,
478+
// updating the folder's metadata.
479+
let updatedFolderMetadata =
480+
try XCTUnwrap(Self.dbManager.itemMetadata(ocId: remoteFolder.identifier))
481+
XCTAssertNotEqual(
482+
updatedFolderMetadata.etag, oldEtag, "The folder's etag should have been updated."
483+
)
484+
XCTAssertEqual(updatedFolderMetadata.etag, remoteFolder.versionIdentifier)
485+
486+
// --- Scenario B: Follow-up Paginated Request (isFollowUpPaginatedRequest == true) ---
487+
488+
// 4. Act: Call readServerUrl for the second page using the received page token.
489+
let followUpPage = NSFileProviderPage(initialNextPage!.token.data(using: .utf8)!)
490+
let (
491+
followUpMetadatas, _, _, _, finalNextPage, followUpError
492+
) = await Enumerator.readServerUrl(
493+
remoteFolder.remotePath,
494+
pageSettings: (page: followUpPage, index: 1, size: 5), // index > 0 and page is non-nil
495+
account: Self.account,
496+
remoteInterface: remoteInterface,
497+
dbManager: Self.dbManager
498+
)
499+
500+
// 5. Assert: Verify the follow-up request's behavior.
501+
XCTAssertNil(followUpError)
502+
XCTAssertNotNil(finalNextPage, "Should receive a next page token for the second request.")
503+
// A follow-up request should *only* contain the children for that page.
504+
XCTAssertEqual(
505+
followUpMetadatas?.count, 5, "Should get only the second page of children (5)."
506+
)
507+
XCTAssertFalse(
508+
followUpMetadatas?.contains(where: { $0.ocId == remoteFolder.identifier }) ?? true,
509+
"The folder itself should NOT be in the follow-up page results."
510+
)
511+
512+
// This confirms the `if !isFollowUpPaginatedRequest` block was correctly skipped, as it'd
513+
// have processed the first item of the result array (`folderChild5`) as the parent folder,
514+
// which would lead to incorrect data or errors.
515+
let child5Metadata = Self.dbManager.itemMetadata(ocId: "folderChild5")
516+
XCTAssertNotNil(
517+
child5Metadata, "Metadata for the items on the second page should be in the database."
518+
)
519+
}
520+
521+
func testHandlePagedReadResults() throws {
522+
// 1. Arrange
523+
let dbManager = Self.dbManager
524+
let db = dbManager.ncDatabase()
525+
debugPrint(db)
526+
527+
let parentNKFile = remoteFolder.toNKFile()
528+
let childrenNKFiles = (0..<5).map { i in
529+
MockRemoteItem(
530+
identifier: "pagedChild\(i)",
531+
name: "pagedChild\(i).txt",
532+
remotePath: Self.account.davFilesUrl + "/folder/pagedChild\(i).txt",
533+
account: Self.account.ncKitAccount,
534+
username: Self.account.username,
535+
userId: Self.account.id,
536+
serverUrl: Self.account.serverUrl
537+
).toNKFile()
538+
}
539+
let followUpChildrenNKFiles = (5..<10).map { i in
540+
MockRemoteItem(
541+
identifier: "pagedChild\(i)",
542+
name: "pagedChild\(i).txt",
543+
remotePath: Self.account.davFilesUrl + "/folder/pagedChild\(i).txt",
544+
account: Self.account.ncKitAccount,
545+
username: Self.account.username,
546+
userId: Self.account.id,
547+
serverUrl: Self.account.serverUrl
548+
).toNKFile()
411549
}
550+
551+
// --- Scenario A: First Page (pageIndex == 0) ---
552+
// 2. Act
553+
let firstPageFiles = [parentNKFile] + childrenNKFiles
554+
let (firstPageResult, firstPageError) = Enumerator.handlePagedReadResults(
555+
files: firstPageFiles, pageIndex: 0, dbManager: dbManager
556+
)
557+
558+
// 3. Assert
559+
XCTAssertNil(firstPageError)
560+
// Result should only contain the children, not the parent.
561+
XCTAssertEqual(
562+
firstPageResult?.count, 5, "First page result should only contain children."
563+
)
564+
// The parent's metadata should be processed and saved to the DB.
565+
let parentMetadataFromDB = dbManager.itemMetadata(ocId: parentNKFile.ocId)
566+
XCTAssertNotNil(
567+
parentMetadataFromDB, "Parent folder metadata should be saved to DB from first page."
568+
)
569+
XCTAssertEqual(parentMetadataFromDB?.etag, parentNKFile.etag, "Parent etag should match.")
570+
// The children's metadata should also be saved.
571+
let childMetadataFromDB = dbManager.itemMetadata(ocId: "pagedChild0")
572+
XCTAssertNotNil(
573+
childMetadataFromDB, "Child metadata should be saved to DB from first page."
574+
)
575+
576+
// --- Scenario B: Follow-up Page (pageIndex > 0) ---
577+
// 4. Act
578+
let (followUpPageResult, followUpPageError) = Enumerator.handlePagedReadResults(
579+
files: followUpChildrenNKFiles, pageIndex: 1, dbManager: dbManager
580+
)
581+
582+
// 5. Assert
583+
XCTAssertNil(followUpPageError)
584+
// Result should contain all items passed in, as the parent is not included.
585+
XCTAssertEqual(
586+
followUpPageResult?.count, 5, "Follow-up page result should contain all its items."
587+
)
588+
let followUpChildMetadata = dbManager.itemMetadata(ocId: "pagedChild5")
589+
XCTAssertNotNil(
590+
followUpChildMetadata, "Child metadata should be saved to DB from follow-up page."
591+
)
592+
593+
// --- Scenario C: Root Folder (should not be ingested) ---
594+
// 6. Act
595+
var rootNKFile = MockRemoteItem.rootItem(account: Self.account).toNKFile()
596+
// The check is based on URL string matching.
597+
rootNKFile.path = Self.account.davFilesUrl
598+
599+
let (rootResult, rootError) = Enumerator.handlePagedReadResults(
600+
files: [rootNKFile], pageIndex: 0, dbManager: dbManager
601+
)
602+
603+
// 7. Assert
604+
XCTAssertNil(rootError)
605+
// The result should be empty as there are no children.
606+
XCTAssertEqual(
607+
rootResult?.count, 0, "Root folder enumeration should yield no children."
608+
)
609+
// No metadata should be saved for the root folder ID itself.
610+
XCTAssertNil(
611+
dbManager.itemMetadata(ocId: rootNKFile.ocId),
612+
"Metadata for the root folder itself should not be saved."
613+
)
412614
}
413615

414616
func testWorkingSetChangeEnumeration() async throws {

0 commit comments

Comments
 (0)