2 * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21 class NextcloudFilesDatabaseManager : NSObject {
23 return NextcloudFilesDatabaseManager();
26 let relativeDatabaseFolderPath = "Database/"
27 let databaseFilename = "fileproviderextdatabase.realm"
28 let relativeDatabaseFilePath: String
29 var databasePath: URL?
31 let schemaVersion: UInt64 = 100
34 self.relativeDatabaseFilePath = self.relativeDatabaseFolderPath + self.databaseFilename
36 guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
41 self.databasePath = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFilePath)
43 // Disable file protection for directory DB
44 // https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm
45 let dbFolder = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFolderPath)
46 let dbFolderPath = dbFolder.path
48 try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
49 try FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: dbFolderPath)
51 Logger.ncFilesDatabase.error("Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)")
54 let config = Realm.Configuration(
55 fileURL: self.databasePath,
56 schemaVersion: self.schemaVersion,
57 objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
60 Realm.Configuration.defaultConfiguration = config
64 Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
65 } catch let error as NSError {
66 Logger.ncFilesDatabase.error("Error opening Realm db: \(error.localizedDescription, privacy: .public)")
72 private func ncDatabase() -> Realm {
73 let realm = try! Realm()
78 func anyItemMetadatasForAccount(_ account: String) -> Bool {
79 return !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account).isEmpty
82 func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
83 // Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db
84 // Let's therefore create a copy
85 if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first {
86 return NextcloudItemMetadataTable(value: itemMetadata)
92 private func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>) -> [NextcloudItemMetadataTable] {
93 let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
94 return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
97 func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
98 let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
99 return sortedItemMetadatas(metadatas)
102 func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
103 let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@", account, serverUrl)
104 return sortedItemMetadatas(metadatas)
107 func itemMetadatas(account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status) -> [NextcloudItemMetadataTable] {
108 let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, status.rawValue)
109 return sortedItemMetadatas(metadatas)
112 func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> NextcloudItemMetadataTable? {
113 let ocId = identifier.rawValue
114 return itemMetadataFromOcId(ocId)
117 private func processItemMetadatasToDelete(existingMetadatas: Results<NextcloudItemMetadataTable>,
118 updatedMetadatas: [NextcloudItemMetadataTable]) -> [NextcloudItemMetadataTable] {
120 var deletedMetadatas: [NextcloudItemMetadataTable] = []
122 for existingMetadata in existingMetadatas {
123 guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
124 let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId) else { continue }
126 deletedMetadatas.append(metadataToDelete)
128 Logger.ncFilesDatabase.debug("Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
131 return deletedMetadatas
134 private func processItemMetadatasToUpdate(existingMetadatas: Results<NextcloudItemMetadataTable>,
135 updatedMetadatas: [NextcloudItemMetadataTable],
136 updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], directoriesNeedingRename: [NextcloudItemMetadataTable]) {
138 var returningNewMetadatas: [NextcloudItemMetadataTable] = []
139 var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
140 var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
142 for updatedMetadata in updatedMetadatas {
143 if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) {
145 if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue &&
146 !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) {
148 if updatedMetadata.directory {
150 if updatedMetadata.serverUrl != existingMetadata.serverUrl || updatedMetadata.fileName != existingMetadata.fileName {
152 directoriesNeedingRename.append(NextcloudItemMetadataTable(value: updatedMetadata))
153 updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
155 } else if !updateDirectoryEtags {
156 updatedMetadata.etag = existingMetadata.etag
160 returningUpdatedMetadatas.append(updatedMetadata)
163 Logger.ncFilesDatabase.debug("Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
165 Logger.ncFilesDatabase.debug("Skipping item metadata update; same as existing, or still downloading/uploading. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
168 } else { // This is a new metadata
169 if !updateDirectoryEtags && updatedMetadata.directory {
170 updatedMetadata.etag = ""
173 returningNewMetadatas.append(updatedMetadata)
175 Logger.ncFilesDatabase.debug("Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
179 return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
182 func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) {
183 let database = ncDatabase()
186 let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue)
188 let metadatasToDelete = processItemMetadatasToDelete(existingMetadatas: existingMetadatas,
189 updatedMetadatas: updatedMetadatas)
191 let metadatasToChange = processItemMetadatasToUpdate(existingMetadatas: existingMetadatas,
192 updatedMetadatas: updatedMetadatas,
193 updateDirectoryEtags: updateDirectoryEtags)
195 var metadatasToUpdate = metadatasToChange.updatedMetadatas
196 let metadatasToCreate = metadatasToChange.newMetadatas
197 let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
199 let metadatasToAdd = Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) }) +
200 Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
202 for metadata in directoriesNeedingRename {
204 if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(ocId: metadata.ocId, newServerUrl: metadata.serverUrl, newFileName: metadata.fileName) {
205 metadatasToUpdate += updatedDirectoryChildren
210 for metadata in metadatasToDelete {
211 // Can't pass copies, we need the originals from the database
212 database.delete(ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId))
215 for metadata in metadatasToAdd {
216 database.add(metadata, update: .all)
221 return (newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate, deletedMetadatas: metadatasToDelete)
223 Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)")
224 return (nil, nil, nil)
228 func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) {
229 let database = ncDatabase()
233 guard let result = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId).first else {
234 Logger.ncFilesDatabase.debug("Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)")
238 result.status = status.rawValue
239 database.add(result, update: .all)
240 Logger.ncFilesDatabase.debug("Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
242 completionHandler(NextcloudItemMetadataTable(value: result))
245 Logger.ncFilesDatabase.error("Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
246 completionHandler(nil)
250 func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
251 let database = ncDatabase()
255 database.add(metadata, update: .all)
256 Logger.ncFilesDatabase.debug("Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
259 Logger.ncFilesDatabase.error("Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
263 func deleteItemMetadata(ocId: String) {
264 let database = ncDatabase()
268 let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId)
270 Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
271 database.delete(results)
274 Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
278 func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
279 let database = ncDatabase()
283 guard let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else {
284 Logger.ncFilesDatabase.debug("Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash))")
288 let oldFileName = itemMetadata.fileName
289 let oldServerUrl = itemMetadata.serverUrl
291 itemMetadata.fileName = newFileName
292 itemMetadata.fileNameView = newFileName
293 itemMetadata.serverUrl = newServerUrl
295 database.add(itemMetadata, update: .all)
297 Logger.ncFilesDatabase.debug("Renamed item \(oldFileName, privacy: OSLogPrivacy.auto(mask: .hash)) to \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash)), moved from serverUrl: \(oldServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) to serverUrl: \(newServerUrl, privacy: OSLogPrivacy.auto(mask: .hash))")
300 Logger.ncFilesDatabase.error("Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash)) at proposed serverUrl \(newServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
304 func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? {
305 let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
307 if metadata.serverUrl == homeServerFilesUrl {
308 return .rootContainer
311 guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
312 Logger.ncFilesDatabase.error("Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
316 if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
317 return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
320 Logger.ncFilesDatabase.error("Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
324 func directoryMetadata(account: String, serverUrl: String) -> NextcloudItemMetadataTable? {
325 // We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com"
326 let problematicSeparator = "://"
327 let placeholderSeparator = "__TEMP_REPLACE__"
328 let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(of: problematicSeparator, with: placeholderSeparator)
329 var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
330 let directoryItemFileName = String(splitServerUrl.removeLast())
331 let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(of: placeholderSeparator, with: problematicSeparator)
333 if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true", account, directoryItemServerUrl, directoryItemFileName).first {
334 return NextcloudItemMetadataTable(value: metadata)
340 func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] {
341 let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
342 let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@ AND ocId != %@ AND directory == true", directoryServerUrl, directoryMetadata.account)
343 return sortedItemMetadatas(metadatas)
346 func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudItemMetadataTable? {
347 return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
350 func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
351 if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first {
352 return NextcloudItemMetadataTable(value: metadata)
358 func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
359 let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND directory == true", account)
360 return sortedItemMetadatas(metadatas)
363 func directoryMetadatas(account: String, parentDirectoryServerUrl: String) -> [NextcloudItemMetadataTable] {
364 let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account, parentDirectoryServerUrl)
365 return sortedItemMetadatas(metadatas)
368 // Deletes all metadatas related to the info of the directory provided
369 func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? {
370 let database = ncDatabase()
371 guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
372 Logger.ncFilesDatabase.error("Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion")
376 var deletedMetadatas: [NextcloudItemMetadataTable] = []
378 let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
379 let results = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, directoryUrlPath)
381 for result in results {
382 deleteItemMetadata(ocId: result.ocId)
383 deleteLocalFileMetadata(ocId: result.ocId)
385 deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
390 Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryMetadata.etag, privacy: .public), serverUrl: \(directoryUrlPath)")
392 database.delete(results)
394 return deletedMetadatas
397 Logger.ncFilesDatabase.error("Could not delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryMetadata.etag, privacy: .public), serverUrl: \(directoryUrlPath), received error: \(error.localizedDescription, privacy: .public)")
403 func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) -> [NextcloudItemMetadataTable]? {
405 let database = ncDatabase()
407 guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
408 Logger.ncFilesDatabase.error("Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming")
412 let oldItemServerUrl = directoryMetadata.serverUrl
413 let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName
414 let newDirectoryServerUrl = newServerUrl + "/" + newFileName
415 let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, oldDirectoryServerUrl)
417 renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
418 Logger.ncFilesDatabase.debug("Renamed root renaming directory")
422 for childItem in childItemResults {
423 let oldServerUrl = childItem.serverUrl
424 let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldDirectoryServerUrl, with: newDirectoryServerUrl)
425 childItem.serverUrl = movedServerUrl
426 database.add(childItem, update: .all)
427 Logger.ncFilesDatabase.debug("Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
431 Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)")
436 let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, newDirectoryServerUrl)
437 return sortedItemMetadatas(updatedChildItemResults)
440 func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
441 if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first {
442 return NextcloudLocalFileMetadataTable(value: metadata)
448 func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
449 let database = ncDatabase()
453 let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
455 newLocalFileMetadata.ocId = itemMetadata.ocId
456 newLocalFileMetadata.fileName = itemMetadata.fileName
457 newLocalFileMetadata.account = itemMetadata.account
458 newLocalFileMetadata.etag = itemMetadata.etag
459 newLocalFileMetadata.exifDate = Date()
460 newLocalFileMetadata.exifLatitude = "-1"
461 newLocalFileMetadata.exifLongitude = "-1"
463 database.add(newLocalFileMetadata, update: .all)
464 Logger.ncFilesDatabase.debug("Added local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
467 Logger.ncFilesDatabase.error("Could not add local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
471 func deleteLocalFileMetadata(ocId: String) {
472 let database = ncDatabase()
476 let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId)
477 database.delete(results)
480 Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
484 private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>) -> [NextcloudLocalFileMetadataTable] {
485 let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
486 return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
489 func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
490 let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account)
491 return sortedLocalFileMetadatas(results)
494 func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
495 let localFileMetadatas = localFileMetadatas(account: account)
496 let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId })
498 var itemMetadatas: [NextcloudItemMetadataTable] = []
500 for ocId in localFileMetadatasOcIds {
501 guard let itemMetadata = itemMetadataFromOcId(ocId) else {
502 Logger.ncFilesDatabase.error("Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)")
506 itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))