Skip to content

Commit 4cd885d

Browse files
authored
Merge pull request #110 from nextcloud/bugfix/retain-keep-dl
Ensure KeepDownloaded property value is retained on item update
2 parents 1c7d2c8 + 02ed1cf commit 4cd885d

4 files changed

Lines changed: 293 additions & 1 deletion

File tree

Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ public final class FilesDatabaseManager: Sendable {
284284
updatedMetadata.downloaded = existingMetadata.downloaded
285285
}
286286
updatedMetadata.visitedDirectory = existingMetadata.visitedDirectory
287+
updatedMetadata.keepDownloaded = existingMetadata.keepDownloaded
287288

288289
returningUpdatedMetadatas.append(updatedMetadata)
289290

@@ -332,6 +333,7 @@ public final class FilesDatabaseManager: Sendable {
332333
downloaded: \(updatedMetadata.downloaded, privacy: .public)
333334
uploaded: \(updatedMetadata.uploaded, privacy: .public)
334335
visitedDirectory: \(updatedMetadata.visitedDirectory, privacy: .public)
336+
keepDownloaded: \(updatedMetadata.keepDownloaded, privacy: .public)
335337
deleted: \(updatedMetadata.deleted, privacy: .public)
336338
syncTime: \(updatedMetadata.syncTime, privacy: .public)
337339
"""
@@ -422,7 +424,7 @@ public final class FilesDatabaseManager: Sendable {
422424
if readTargetMetadata.directory {
423425
readTargetMetadata.visitedDirectory = true
424426
}
425-
427+
426428
if let existing = itemMetadata(ocId: readTargetMetadata.ocId) {
427429
if existing.status == Status.normal.rawValue,
428430
!existing.isInSameDatabaseStoreableRemoteState(readTargetMetadata)
@@ -431,6 +433,7 @@ public final class FilesDatabaseManager: Sendable {
431433
if keepExistingDownloadState {
432434
readTargetMetadata.downloaded = existing.downloaded
433435
}
436+
readTargetMetadata.keepDownloaded = existing.keepDownloaded
434437
metadatasToUpdate.insert(readTargetMetadata, at: 0)
435438
}
436439
} else {
@@ -539,6 +542,7 @@ public final class FilesDatabaseManager: Sendable {
539542
downloaded: \(metadata.downloaded, privacy: .public)
540543
uploaded: \(metadata.uploaded, privacy: .public)
541544
visitedDirectory: \(metadata.visitedDirectory, privacy: .public)
545+
keepDownloaded: \(metadata.keepDownloaded, privacy: .public)
542546
deleted: \(metadata.deleted, privacy: .public)
543547
syncTime: \(metadata.syncTime, privacy: .public)
544548
"""

Sources/NextcloudFileProviderKit/Enumeration/Enumerator+SyncEngine.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension Enumerator {
3131
metadata.visitedDirectory = true
3232
if let existingMetadata = dbManager.itemMetadata(ocId: metadata.ocId) {
3333
metadata.downloaded = existingMetadata.downloaded
34+
metadata.keepDownloaded = existingMetadata.keepDownloaded
3435
}
3536
}
3637
dbManager.addItemMetadata(metadata)
@@ -80,6 +81,7 @@ extension Enumerator {
8081
assert(directoryMetadata.directory)
8182
if let existingMetadata = dbManager.itemMetadata(ocId: directoryMetadata.ocId) {
8283
directoryMetadata.downloaded = existingMetadata.downloaded
84+
directoryMetadata.keepDownloaded = existingMetadata.keepDownloaded
8385
}
8486
directoryMetadata.visitedDirectory = true
8587

@@ -237,6 +239,7 @@ extension Enumerator {
237239
let newItems: [SendableItemMetadata] = isNew ? [metadata] : []
238240
let updatedItems: [SendableItemMetadata] = isNew ? [] : [metadata]
239241
metadata.downloaded = existing?.downloaded == true
242+
metadata.keepDownloaded = existing?.keepDownloaded == true
240243
dbManager.addItemMetadata(metadata)
241244
return ([metadata], newItems, updatedItems, nil, nextPage, nil)
242245
}
@@ -250,6 +253,7 @@ extension Enumerator {
250253
let newMetadatas = isNew ? [metadata] : []
251254

252255
metadata.downloaded = existing?.downloaded == true
256+
metadata.keepDownloaded = existing?.keepDownloaded == true
253257
dbManager.addItemMetadata(metadata)
254258

255259
return ([metadata], newMetadatas, updatedMetadatas, nil, nextPage, nil)

Tests/NextcloudFileProviderKitTests/EnumeratorTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,42 @@ final class EnumeratorTests: XCTestCase {
11281128
}
11291129
}
11301130

1131+
func testKeepDownloadedRetainedDuringEnumeration() async throws {
1132+
let db = Self.dbManager.ncDatabase()
1133+
debugPrint(db)
1134+
let remoteInterface = MockRemoteInterface(rootItem: rootItem)
1135+
1136+
let existingFolder = remoteFolder.toItemMetadata(account: Self.account)
1137+
Self.dbManager.addItemMetadata(existingFolder)
1138+
1139+
// Setup existing item with keepDownloaded = true
1140+
var existingItem = remoteItemA.toItemMetadata(account: Self.account)
1141+
existingItem.keepDownloaded = true
1142+
existingItem.downloaded = true
1143+
Self.dbManager.addItemMetadata(existingItem)
1144+
1145+
// Simulate server response with updated etag but no keepDownloaded
1146+
remoteFolder.versionIdentifier = "NEW"
1147+
remoteItemA.versionIdentifier = "NEW_ETAG"
1148+
1149+
let enumerator = Enumerator(
1150+
enumeratedItemIdentifier: .init(remoteFolder.identifier),
1151+
account: Self.account,
1152+
remoteInterface: remoteInterface,
1153+
dbManager: Self.dbManager
1154+
)
1155+
let observer = MockChangeObserver(enumerator: enumerator)
1156+
try await observer.enumerateChanges()
1157+
1158+
// Verify the updated metadata
1159+
let updatedMetadata = try XCTUnwrap(
1160+
Self.dbManager.itemMetadata(ocId: remoteItemA.identifier)
1161+
)
1162+
XCTAssertTrue(updatedMetadata.keepDownloaded, "keepDownloaded should remain true after enumeration")
1163+
XCTAssertEqual(updatedMetadata.etag, "NEW_ETAG", "Etag should be updated")
1164+
XCTAssertTrue(updatedMetadata.downloaded, "Downloaded state should be preserved")
1165+
}
1166+
11311167
func testTrashChangeEnumerationFailWhenNoTrashInCapabilities() async {
11321168
let remoteInterface = MockRemoteInterface(rootItem: rootItem, rootTrashItem: rootTrashItem)
11331169
XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))

Tests/NextcloudFileProviderKitTests/FilesDatabaseManagerTests.swift

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,211 @@ final class FilesDatabaseManagerTests: XCTestCase {
10731073
XCTAssertFalse(finalMetadata.keepDownloaded)
10741074
}
10751075

1076+
func testKeepDownloadedRetainedDuringDepth1ReadUpdate() throws {
1077+
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
1078+
1079+
// Create initial metadata with keepDownloaded = true
1080+
var initialMetadata = SendableItemMetadata(ocId: "test-keep-downloaded", fileName: "test.txt", account: account)
1081+
initialMetadata.downloaded = true
1082+
initialMetadata.uploaded = true
1083+
initialMetadata.keepDownloaded = true
1084+
initialMetadata.etag = "old-etag"
1085+
1086+
Self.dbManager.addItemMetadata(initialMetadata)
1087+
1088+
// Verify initial state
1089+
let storedMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
1090+
XCTAssertTrue(storedMetadata.keepDownloaded, "Initial keepDownloaded should be true")
1091+
XCTAssertTrue(storedMetadata.downloaded, "Initial downloaded should be true")
1092+
1093+
// Update metadata with new etag (simulating server update)
1094+
var updatedMetadata = initialMetadata
1095+
updatedMetadata.etag = "new-etag"
1096+
updatedMetadata.keepDownloaded = false // This would be the case when converting from NKFile
1097+
1098+
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
1099+
account: account.ncKitAccount,
1100+
serverUrl: account.davFilesUrl,
1101+
updatedMetadatas: [updatedMetadata],
1102+
keepExistingDownloadState: true
1103+
)
1104+
1105+
XCTAssertEqual(result.newMetadatas?.isEmpty, true, "Should create no new metadatas")
1106+
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false, "Should update existing metadata")
1107+
1108+
// Verify keepDownloaded is retained
1109+
let finalMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
1110+
XCTAssertTrue(finalMetadata.keepDownloaded, "keepDownloaded should be retained during update")
1111+
XCTAssertTrue(finalMetadata.downloaded, "downloaded should be retained during update")
1112+
XCTAssertEqual(finalMetadata.etag, "new-etag", "etag should be updated")
1113+
1114+
// Verify in database
1115+
let dbMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
1116+
XCTAssertTrue(dbMetadata.keepDownloaded, "keepDownloaded should be retained in database")
1117+
}
1118+
1119+
func testKeepDownloadedRetainedWithKeepExistingDownloadStateFalse() throws {
1120+
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
1121+
1122+
// Create initial metadata with keepDownloaded = true
1123+
var initialMetadata = SendableItemMetadata(ocId: "test-keep-downloaded-false", fileName: "test.txt", account: account)
1124+
initialMetadata.downloaded = true
1125+
initialMetadata.uploaded = true
1126+
initialMetadata.keepDownloaded = true
1127+
initialMetadata.etag = "old-etag"
1128+
1129+
Self.dbManager.addItemMetadata(initialMetadata)
1130+
1131+
// Update metadata with new etag
1132+
var updatedMetadata = initialMetadata
1133+
updatedMetadata.etag = "new-etag"
1134+
updatedMetadata.keepDownloaded = false // This would be the case when converting from NKFile
1135+
updatedMetadata.downloaded = false // Set to false to test keepDownloaded retention
1136+
1137+
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
1138+
account: account.ncKitAccount,
1139+
serverUrl: account.davFilesUrl,
1140+
updatedMetadatas: [updatedMetadata],
1141+
keepExistingDownloadState: false // Even when not keeping download state
1142+
)
1143+
1144+
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false, "Should update existing metadata")
1145+
1146+
// Verify keepDownloaded is still retained even when keepExistingDownloadState is false
1147+
let finalMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
1148+
XCTAssertTrue(finalMetadata.keepDownloaded, "keepDownloaded should be retained regardless of keepExistingDownloadState")
1149+
XCTAssertEqual(finalMetadata.downloaded, false, "downloaded should not be retained when keepExistingDownloadState is false")
1150+
}
1151+
1152+
func testKeepDownloadedRetainedInReadTargetMetadata() throws {
1153+
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
1154+
1155+
// Create existing metadata with keepDownloaded = true
1156+
var existingMetadata = SendableItemMetadata(ocId: "read-target-test", fileName: "target.txt", account: account)
1157+
existingMetadata.keepDownloaded = true
1158+
existingMetadata.downloaded = true
1159+
existingMetadata.status = Status.normal.rawValue
1160+
1161+
Self.dbManager.addItemMetadata(existingMetadata)
1162+
1163+
// Create new read target metadata (simulating reading from server)
1164+
var readTargetMetadata = SendableItemMetadata(ocId: "read-target-test", fileName: "target.txt", account: account)
1165+
readTargetMetadata.etag = "new-etag"
1166+
readTargetMetadata.keepDownloaded = false // Would be false when created from NKFile
1167+
readTargetMetadata.downloaded = false
1168+
1169+
// This simulates the path in depth1ReadUpdateItemMetadatas where readTargetMetadata
1170+
// is processed and existing properties should be retained
1171+
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
1172+
account: account.ncKitAccount,
1173+
serverUrl: account.davFilesUrl,
1174+
updatedMetadatas: [readTargetMetadata],
1175+
keepExistingDownloadState: true
1176+
)
1177+
1178+
let updatedMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
1179+
XCTAssertTrue(updatedMetadata.keepDownloaded, "keepDownloaded should be retained in read target metadata")
1180+
XCTAssertTrue(updatedMetadata.downloaded, "downloaded should be retained when keepExistingDownloadState is true")
1181+
}
1182+
1183+
func testKeepDownloadedNotSetForNewMetadata() throws {
1184+
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
1185+
1186+
// Create completely new metadata (not existing in database)
1187+
var newMetadata = SendableItemMetadata(ocId: "new-item", fileName: "new.txt", account: account)
1188+
newMetadata.etag = "initial-etag"
1189+
newMetadata.keepDownloaded = false // Should remain false for new items
1190+
1191+
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
1192+
account: account.ncKitAccount,
1193+
serverUrl: account.davFilesUrl,
1194+
updatedMetadatas: [newMetadata],
1195+
keepExistingDownloadState: true
1196+
)
1197+
1198+
XCTAssertEqual(result.newMetadatas?.isEmpty, false, "Should create new metadata")
1199+
XCTAssertEqual(result.updatedMetadatas?.isEmpty, true, "Should not update any metadata")
1200+
1201+
let createdMetadata = try XCTUnwrap(result.newMetadatas?.first)
1202+
XCTAssertFalse(createdMetadata.keepDownloaded, "keepDownloaded should remain false for new items")
1203+
1204+
// Verify in database
1205+
let dbMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "new-item"))
1206+
XCTAssertFalse(dbMetadata.keepDownloaded, "keepDownloaded should be false in database for new items")
1207+
}
1208+
1209+
func testKeepDownloadedRetainedWithMultipleItems() throws {
1210+
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
1211+
1212+
var parentFolder = SendableItemMetadata(ocId: "pf", fileName: "pf", account: account)
1213+
parentFolder.uploaded = true
1214+
parentFolder.etag = "old-pf"
1215+
1216+
// Create multiple items with different keepDownloaded states
1217+
var item1 = SendableItemMetadata(ocId: "multi-1", fileName: "file1.txt", account: account)
1218+
item1.keepDownloaded = true
1219+
item1.downloaded = true
1220+
item1.uploaded = true
1221+
item1.etag = "old-1"
1222+
item1.serverUrl = account.davFilesUrl.appending("/pf")
1223+
1224+
var item2 = SendableItemMetadata(ocId: "multi-2", fileName: "file2.txt", account: account)
1225+
item2.keepDownloaded = false
1226+
item2.downloaded = false
1227+
item2.uploaded = true
1228+
item2.etag = "old-2"
1229+
item2.serverUrl = account.davFilesUrl.appending("/pf")
1230+
1231+
var item3 = SendableItemMetadata(ocId: "multi-3", fileName: "file3.txt", account: account)
1232+
item3.keepDownloaded = true
1233+
item3.downloaded = false
1234+
item3.uploaded = true
1235+
item3.etag = "old-3"
1236+
item3.serverUrl = account.davFilesUrl.appending("/pf")
1237+
1238+
Self.dbManager.addItemMetadata(parentFolder)
1239+
Self.dbManager.addItemMetadata(item1)
1240+
Self.dbManager.addItemMetadata(item2)
1241+
Self.dbManager.addItemMetadata(item3)
1242+
1243+
// Update all items with new etags
1244+
var updatedParentFolder = parentFolder
1245+
updatedParentFolder.etag = "new-pf"
1246+
1247+
var updatedItem1 = item1
1248+
updatedItem1.etag = "new-1"
1249+
updatedItem1.keepDownloaded = false // Would be reset when converting from NKFile
1250+
1251+
var updatedItem2 = item2
1252+
updatedItem2.etag = "new-2"
1253+
updatedItem2.keepDownloaded = false
1254+
1255+
var updatedItem3 = item3
1256+
updatedItem3.etag = "new-3"
1257+
updatedItem3.keepDownloaded = false
1258+
1259+
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
1260+
account: account.ncKitAccount,
1261+
serverUrl: account.davFilesUrl.appending("/pf"),
1262+
updatedMetadatas: [updatedParentFolder, updatedItem1, updatedItem2, updatedItem3],
1263+
keepExistingDownloadState: true
1264+
)
1265+
1266+
XCTAssertEqual(result.updatedMetadatas?.count, 4, "Should update all four items")
1267+
1268+
// Verify each item's keepDownloaded state is correctly retained
1269+
let updatedMetadatas = try XCTUnwrap(result.updatedMetadatas)
1270+
1271+
let finalItem1 = try XCTUnwrap(updatedMetadatas.first { $0.ocId == "multi-1" })
1272+
XCTAssertTrue(finalItem1.keepDownloaded, "Item 1 should retain keepDownloaded = true")
1273+
1274+
let finalItem2 = try XCTUnwrap(updatedMetadatas.first { $0.ocId == "multi-2" })
1275+
XCTAssertFalse(finalItem2.keepDownloaded, "Item 2 should retain keepDownloaded = false")
1276+
1277+
let finalItem3 = try XCTUnwrap(updatedMetadatas.first { $0.ocId == "multi-3" })
1278+
XCTAssertTrue(finalItem3.keepDownloaded, "Item 3 should retain keepDownloaded = true")
1279+
}
1280+
10761281
func testParentItemIdentifierWithRemoteFallback() async throws {
10771282
let rootItem = MockRemoteItem.rootItem(account: Self.account)
10781283
let remoteFolder = MockRemoteItem(
@@ -1197,6 +1402,49 @@ final class FilesDatabaseManagerTests: XCTestCase {
11971402
XCTAssertTrue(materialisedOcIds.contains(sDirD.ocId))
11981403
}
11991404

1405+
func testKeepDownloadedRetainedDuringUpdate() throws {
1406+
let account = Account(user: "test", id: "t", serverUrl: "https://example.com", password: "")
1407+
1408+
// Create initial metadata with keepDownloaded = true
1409+
var initialMetadata = SendableItemMetadata(ocId: "test-keep-downloaded", fileName: "test.txt", account: account)
1410+
initialMetadata.downloaded = true
1411+
initialMetadata.uploaded = true
1412+
initialMetadata.keepDownloaded = true
1413+
initialMetadata.etag = "old-etag"
1414+
1415+
Self.dbManager.addItemMetadata(initialMetadata)
1416+
1417+
// Verify initial state
1418+
let storedMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
1419+
XCTAssertTrue(storedMetadata.keepDownloaded, "Initial keepDownloaded should be true")
1420+
XCTAssertTrue(storedMetadata.downloaded, "Initial downloaded should be true")
1421+
1422+
// Update metadata with new etag (simulating server update)
1423+
var updatedMetadata = initialMetadata
1424+
updatedMetadata.etag = "new-etag"
1425+
updatedMetadata.keepDownloaded = false // This would be the case when converting from NKFile
1426+
1427+
let result = Self.dbManager.depth1ReadUpdateItemMetadatas(
1428+
account: account.ncKitAccount,
1429+
serverUrl: account.davFilesUrl,
1430+
updatedMetadatas: [updatedMetadata],
1431+
keepExistingDownloadState: true
1432+
)
1433+
1434+
XCTAssertEqual(result.newMetadatas?.isEmpty, true, "Should create no new metadatas")
1435+
XCTAssertEqual(result.updatedMetadatas?.isEmpty, false, "Should update existing metadata")
1436+
1437+
// Verify keepDownloaded is retained
1438+
let finalMetadata = try XCTUnwrap(result.updatedMetadatas?.first)
1439+
XCTAssertTrue(finalMetadata.keepDownloaded, "keepDownloaded should be retained during update")
1440+
XCTAssertTrue(finalMetadata.downloaded, "downloaded should be retained during update")
1441+
XCTAssertEqual(finalMetadata.etag, "new-etag", "etag should be updated")
1442+
1443+
// Verify in database
1444+
let dbMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "test-keep-downloaded"))
1445+
XCTAssertTrue(dbMetadata.keepDownloaded, "keepDownloaded should be retained in database")
1446+
}
1447+
12001448
func testPendingWorkingSetChanges() {
12011449
// 1. Arrange
12021450
let anchorDate = Date().addingTimeInterval(-300) // 5 minutes ago

0 commit comments

Comments
 (0)