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 @@ -225,6 +225,8 @@ extension Enumerator {
let updatedMetadatas = isNew ? [] : [metadata]
let newMetadatas = isNew ? [metadata] : []

metadata.lockToken = existing?.lockToken
metadata.visitedDirectory = existing?.visitedDirectory == true
metadata.downloaded = existing?.downloaded == true
metadata.keepDownloaded = existing?.keepDownloaded == true
dbManager.addItemMetadata(metadata)
Expand Down
Comment thread
camilasan marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -483,30 +483,25 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
examinedChildFilesAndDeletedItems.formUnion(metadatas[1...].filter { !$0.directory }.map(\.ocId))
}

// If the target is not in the updated metadatas then neither it, nor any of its kids have changed. So skip examining all of them.
if !allUpdatedMetadatas.contains(where: { $0.ocId == target.ocId }) {
logger.debug("Target has not changed. Skipping children.", [.url: itemRemoteUrl])
let materialisedChildren = materializedItems.filter { $0.serverUrl.hasPrefix(itemRemoteUrl) }.map(\.ocId)
examinedChildFilesAndDeletedItems.formUnion(materialisedChildren)
}

// OPTIMIZATION: For any child directories returned in this enumeration, if they haven't changed (etag matches database), mark them as examined so we don't enumerate them separately later.
// Only skip unchanged child directories with no materialized descendants.
// Lock changes don't propagate etags, so dirs with visible children must be enumerated.
if metadatas.count > 1 {
let childDirectories = metadatas[1...].filter(\.directory)

for childDir in childDirectories {
// Check if this directory is in our materialized items list
if let localItem = materializedItems.first(where: { $0.ocId == childDir.ocId }), localItem.etag == childDir.etag {
// Directory hasn't changed, mark as examined to skip separate enumeration.
logger.debug("Child directory etag unchanged, marking as examined.", [.name: childDir.fileName, .eTag: childDir.etag])
examinedChildFilesAndDeletedItems.insert(childDir.ocId)
guard let localItem = materializedItems.first(
where: { $0.ocId == childDir.ocId }
), localItem.isInSameDatabaseStoreableRemoteState(childDir) else {
continue
}

// Also mark any materialized children of this directory as examined.
let grandChildren = materializedItems.filter {
$0.serverUrl.hasPrefix(localItem.remotePath())
}
let hasMaterializedDescendants = materializedItems.contains {
$0.ocId != localItem.ocId
&& $0.serverUrl.hasPrefix(localItem.remotePath())
}
Comment thread
camilasan marked this conversation as resolved.

examinedChildFilesAndDeletedItems.formUnion(grandChildren.map(\.ocId))
if !hasMaterializedDescendants {
examinedChildFilesAndDeletedItems.insert(childDir.ocId)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,10 @@ public class MockRemoteItem: Equatable {
? NextcloudKit.shared.nkCommonInstance.rootFileName
: trashbinOriginalLocation?.split(separator: "/").last?.toString() ?? name
file.size = size
file.date = creationDate
file.creationDate = creationDate
file.date = modificationDate
file.directory = isRoot ? false : directory
file.permissions = "RGDNVW"
file.etag = versionIdentifier
file.ocId = identifier
file.fileId = identifier.replacingOccurrences(of: trashedItemIdSuffix, with: "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2022,4 +2022,87 @@ final class EnumeratorTests: NextcloudFileProviderKitTestCase {
)
}
}

func testLockChangeDetectedByRemoteStateComparison() {
var local = SendableItemMetadata(
ocId: "file1",
account: Self.account.ncKitAccount,
classFile: "",
contentType: "",
creationDate: Date(),
directory: false,
e2eEncrypted: false,
etag: "v1",
fileId: "file1",
fileName: "file.txt",
fileNameView: "file.txt",
ownerId: "",
ownerDisplayName: "",
path: "",
serverUrl: Self.account.davFilesUrl,
size: 0,
urlBase: Self.account.serverUrl,
user: Self.account.username,
userId: Self.account.id
)
var remote = local

XCTAssertTrue(local.isInSameDatabaseStoreableRemoteState(remote))

remote.lock = true
XCTAssertFalse(
local.isInSameDatabaseStoreableRemoteState(remote),
"A lock state change must be detected as a remote state difference"
)

local.lock = true
XCTAssertTrue(local.isInSameDatabaseStoreableRemoteState(remote))
}

func testLockTokenPreservedDuringTargetDepthRead() async throws {
let db = Self.dbManager.ncDatabase()
debugPrint(db)

let remoteFile = MockRemoteItem(
identifier: "lockTokenTestFile",
versionIdentifier: "V1",
name: "lockTokenTestFile.txt",
remotePath: Self.account.davFilesUrl + "/lockTokenTestFile.txt",
locked: true,
lockOwner: Self.account.username,
lockTimeOut: Date.now.advanced(by: 1_000_000),
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)

rootItem.children = [remoteFile]
remoteFile.parent = rootItem

let remoteInterface = MockRemoteInterface(account: Self.account, rootItem: rootItem)

var fileMetadata = remoteFile.toItemMetadata(account: Self.account)
fileMetadata.lockToken = "local-lock-token-123"
fileMetadata.downloaded = true
Self.dbManager.addItemMetadata(fileMetadata)

let (_, _, _, _, _, readError) = await Enumerator.readServerUrl(
Self.account.davFilesUrl + "/lockTokenTestFile.txt",
account: Self.account,
remoteInterface: remoteInterface,
dbManager: Self.dbManager,
depth: .target,
log: FileProviderLogMock()
)

XCTAssertNil(readError)

let postRead = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "lockTokenTestFile"))
XCTAssertEqual(
postRead.lockToken,
"local-lock-token-123",
"lockToken must be preserved across target-depth reads"
)
}
}
Loading