]> dgit.raspbian.org Git - nextcloud-desktop.git/blob
977ece03175efd300d32b7ca389d2ec844c7ff9f
[nextcloud-desktop.git] /
1 /*
2  * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
3  *
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.
8  *
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
12  * for more details.
13  */
14
15 import Foundation
16 import RealmSwift
17 import FileProvider
18 import NextcloudKit
19 import OSLog
20
21 class NextcloudFilesDatabaseManager : NSObject {
22     static let shared = {
23         return NextcloudFilesDatabaseManager();
24     }()
25
26     let relativeDatabaseFolderPath = "Database/"
27     let databaseFilename = "fileproviderextdatabase.realm"
28     let relativeDatabaseFilePath: String
29     var databasePath: URL?
30
31     let schemaVersion: UInt64 = 100
32
33     override init() {
34         self.relativeDatabaseFilePath = self.relativeDatabaseFolderPath + self.databaseFilename
35
36         guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
37             super.init()
38             return
39         }
40
41         self.databasePath = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFilePath)
42
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
47         do {
48             try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
49             try FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: dbFolderPath)
50         } catch let error {
51             Logger.ncFilesDatabase.error("Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)")
52         }
53
54         let config = Realm.Configuration(
55             fileURL: self.databasePath,
56             schemaVersion: self.schemaVersion,
57             objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
58         )
59
60         Realm.Configuration.defaultConfiguration = config
61
62         do {
63             _ = try Realm()
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)")
67         }
68
69         super.init()
70     }
71
72     private func ncDatabase() -> Realm {
73         let realm = try! Realm()
74         realm.refresh()
75         return realm
76     }
77
78     func anyItemMetadatasForAccount(_ account: String) -> Bool {
79         return !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account).isEmpty
80     }
81
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)
87         }
88
89         return nil
90     }
91
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) })
95     }
96
97     func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
98         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
99         return sortedItemMetadatas(metadatas)
100     }
101
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)
105     }
106
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)
110     }
111
112     func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> NextcloudItemMetadataTable? {
113         let ocId = identifier.rawValue
114         return itemMetadataFromOcId(ocId)
115     }
116
117     private func processItemMetadatasToDelete(existingMetadatas: Results<NextcloudItemMetadataTable>,
118                                               updatedMetadatas: [NextcloudItemMetadataTable]) -> [NextcloudItemMetadataTable] {
119
120         var deletedMetadatas: [NextcloudItemMetadataTable] = []
121
122         for existingMetadata in existingMetadatas {
123             guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
124                     let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId) else { continue }
125
126             deletedMetadatas.append(metadataToDelete)
127
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))")
129         }
130
131         return deletedMetadatas
132     }
133
134     private func processItemMetadatasToUpdate(existingMetadatas: Results<NextcloudItemMetadataTable>,
135                                               updatedMetadatas: [NextcloudItemMetadataTable],
136                                               updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], directoriesNeedingRename: [NextcloudItemMetadataTable]) {
137
138         var returningNewMetadatas: [NextcloudItemMetadataTable] = []
139         var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
140         var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
141
142         for updatedMetadata in updatedMetadatas {
143             if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) {
144
145                 if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue &&
146                     !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) {
147
148                     if updatedMetadata.directory {
149
150                         if updatedMetadata.serverUrl != existingMetadata.serverUrl || updatedMetadata.fileName != existingMetadata.fileName {
151
152                             directoriesNeedingRename.append(NextcloudItemMetadataTable(value: updatedMetadata))
153                             updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
154
155                         } else if !updateDirectoryEtags {
156                             updatedMetadata.etag = existingMetadata.etag
157                         }
158                     }
159
160                     returningUpdatedMetadatas.append(updatedMetadata)
161
162
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))")
164                 } else {
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))")
166                 }
167
168             } else { // This is a new metadata
169                 if !updateDirectoryEtags && updatedMetadata.directory {
170                     updatedMetadata.etag = ""
171                 }
172                 
173                 returningNewMetadatas.append(updatedMetadata)
174
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))")
176             }
177         }
178
179         return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
180     }
181
182     func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) {
183         let database = ncDatabase()
184
185         do {
186             let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue)
187
188             let metadatasToDelete = processItemMetadatasToDelete(existingMetadatas: existingMetadatas,
189                                                                  updatedMetadatas: updatedMetadatas)
190
191             let metadatasToChange = processItemMetadatasToUpdate(existingMetadatas: existingMetadatas,
192                                                                  updatedMetadatas: updatedMetadatas,
193                                                                  updateDirectoryEtags: updateDirectoryEtags)
194
195             var metadatasToUpdate = metadatasToChange.updatedMetadatas
196             let metadatasToCreate = metadatasToChange.newMetadatas
197             let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
198
199             let metadatasToAdd = Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) }) +
200                                  Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
201
202             for metadata in directoriesNeedingRename {
203
204                 if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(ocId: metadata.ocId, newServerUrl: metadata.serverUrl, newFileName: metadata.fileName) {
205                     metadatasToUpdate += updatedDirectoryChildren
206                 }
207             }
208
209             try database.write {
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))
213                 }
214
215                 for metadata in metadatasToAdd {
216                     database.add(metadata, update: .all)
217                 }
218
219             }
220
221             return (newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate, deletedMetadatas: metadatasToDelete)
222         } catch let error {
223             Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)")
224             return (nil, nil, nil)
225         }
226     }
227
228     func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) {
229         let database = ncDatabase()
230
231         do {
232             try database.write {
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)")
235                     return
236                 }
237
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))")
241
242                 completionHandler(NextcloudItemMetadataTable(value: result))
243             }
244         } catch let error {
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)
247         }
248     }
249
250     func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
251         let database = ncDatabase()
252
253         do {
254             try database.write {
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))")
257             }
258         } catch let error {
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)")
260         }
261     }
262
263     func deleteItemMetadata(ocId: String) {
264         let database = ncDatabase()
265
266         do {
267             try database.write {
268                 let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId)
269
270                 Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
271                 database.delete(results)
272             }
273         } catch let error {
274             Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
275         }
276     }
277
278     func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
279         let database = ncDatabase()
280
281         do {
282             try database.write {
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))")
285                     return
286                 }
287
288                 let oldFileName = itemMetadata.fileName
289                 let oldServerUrl = itemMetadata.serverUrl
290
291                 itemMetadata.fileName = newFileName
292                 itemMetadata.fileNameView = newFileName
293                 itemMetadata.serverUrl = newServerUrl
294
295                 database.add(itemMetadata, update: .all)
296
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))")
298             }
299         } catch let error {
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)")
301         }
302     }
303
304     func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? {
305         let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
306
307         if metadata.serverUrl == homeServerFilesUrl {
308             return .rootContainer
309         }
310
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))")
313             return nil
314         }
315
316         if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
317             return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
318         }
319
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))")
321         return nil
322     }
323
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)
332
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)
335         }
336
337         return nil
338     }
339
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)
344     }
345
346     func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudItemMetadataTable? {
347         return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
348     }
349
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)
353         }
354
355         return nil
356     }
357
358     func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
359         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND directory == true", account)
360         return sortedItemMetadatas(metadatas)
361     }
362
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)
366     }
367
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")
373             return nil
374         }
375
376         var deletedMetadatas: [NextcloudItemMetadataTable] = []
377
378         let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
379         let results = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, directoryUrlPath)
380
381         for result in results {
382             deleteItemMetadata(ocId: result.ocId)
383             deleteLocalFileMetadata(ocId: result.ocId)
384
385             deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
386         }
387
388         do {
389             try database.write {
390                 Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryMetadata.etag, privacy: .public), serverUrl: \(directoryUrlPath)")
391
392                 database.delete(results)
393
394                 return deletedMetadatas
395             }
396         } catch let error {
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)")
398         }
399
400         return nil
401     }
402
403     func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) -> [NextcloudItemMetadataTable]? {
404
405         let database = ncDatabase()
406
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")
409             return nil
410         }
411
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)
416
417         renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
418         Logger.ncFilesDatabase.debug("Renamed root renaming directory")
419
420         do {
421             try database.write {
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)")
428                 }
429             }
430         } catch let error {
431             Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)")
432
433             return nil
434         }
435
436         let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, newDirectoryServerUrl)
437         return sortedItemMetadatas(updatedChildItemResults)
438     }
439
440     func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
441         if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first {
442             return NextcloudLocalFileMetadataTable(value: metadata)
443         }
444
445         return nil
446     }
447
448     func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
449         let database = ncDatabase()
450
451         do {
452             try database.write {
453                 let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
454
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"
462
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))")
465             }
466         } catch let error {
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)")
468         }
469     }
470
471     func deleteLocalFileMetadata(ocId: String) {
472         let database = ncDatabase()
473
474         do {
475             try database.write {
476                 let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId)
477                 database.delete(results)
478             }
479         } catch let error {
480             Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
481         }
482     }
483
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) })
487     }
488
489     func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
490         let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account)
491         return sortedLocalFileMetadatas(results)
492     }
493
494     func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
495         let localFileMetadatas = localFileMetadatas(account: account)
496         let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId })
497
498         var itemMetadatas: [NextcloudItemMetadataTable] = []
499
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)")
503                 continue;
504             }
505
506             itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
507         }
508
509         return itemMetadatas
510     }
511 }