bdd0a6b73a04108ffc9fdb3f0735f40b74e42016
[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                 returningNewMetadatas.append(updatedMetadata)
170
171                 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))")
172             }
173         }
174
175         return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
176     }
177
178     func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) {
179         let database = ncDatabase()
180
181         do {
182             let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue)
183
184             let metadatasToDelete = processItemMetadatasToDelete(existingMetadatas: existingMetadatas,
185                                                                  updatedMetadatas: updatedMetadatas)
186
187             let metadatasToChange = processItemMetadatasToUpdate(existingMetadatas: existingMetadatas,
188                                                                  updatedMetadatas: updatedMetadatas,
189                                                                  updateDirectoryEtags: updateDirectoryEtags)
190
191             var metadatasToUpdate = metadatasToChange.updatedMetadatas
192             let metadatasToCreate = metadatasToChange.newMetadatas
193             let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
194
195             let metadatasToAdd = Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) }) +
196                                  Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
197
198             try database.write {
199                 for metadata in metadatasToDelete {
200                     // Can't pass copies, we need the originals from the database
201                     database.delete(ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId))
202                 }
203
204                 for metadata in metadatasToAdd {
205                     database.add(metadata, update: .all)
206                 }
207
208             }
209
210             for metadata in directoriesNeedingRename {
211                 if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(ocId: metadata.ocId, newServerUrl: metadata.serverUrl, newFileName: metadata.fileName) {
212                     metadatasToUpdate += updatedDirectoryChildren
213                 }
214             }
215
216             return (newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate, deletedMetadatas: metadatasToDelete)
217         } catch let error {
218             Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)")
219             return (nil, nil, nil)
220         }
221     }
222
223     func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) {
224         let database = ncDatabase()
225
226         do {
227             try database.write {
228                 guard let result = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId).first else {
229                     Logger.ncFilesDatabase.debug("Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)")
230                     return
231                 }
232
233                 result.status = status.rawValue
234                 database.add(result, update: .all)
235                 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))")
236
237                 completionHandler(NextcloudItemMetadataTable(value: result))
238             }
239         } catch let error {
240             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)")
241             completionHandler(nil)
242         }
243     }
244
245     func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
246         let database = ncDatabase()
247
248         do {
249             try database.write {
250                 database.add(metadata, update: .all)
251                 Logger.ncFilesDatabase.debug("Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
252             }
253         } catch let error {
254             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)")
255         }
256     }
257
258     func deleteItemMetadata(ocId: String) {
259         let database = ncDatabase()
260
261         do {
262             try database.write {
263                 let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId)
264
265                 Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
266                 database.delete(results)
267             }
268         } catch let error {
269             Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
270         }
271     }
272
273     func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
274         let database = ncDatabase()
275
276         do {
277             try database.write {
278                 guard let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else {
279                     Logger.ncFilesDatabase.debug("Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash))")
280                     return
281                 }
282
283                 let oldFileName = itemMetadata.fileName
284                 let oldServerUrl = itemMetadata.serverUrl
285
286                 itemMetadata.fileName = newFileName
287                 itemMetadata.fileNameView = newFileName
288                 itemMetadata.serverUrl = newServerUrl
289
290                 database.add(itemMetadata, update: .all)
291
292                 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))")
293             }
294         } catch let error {
295             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)")
296         }
297     }
298
299     func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? {
300         let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
301
302         if metadata.serverUrl == homeServerFilesUrl {
303             return .rootContainer
304         }
305
306         guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
307             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))")
308             return nil
309         }
310
311         if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
312             return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
313         }
314
315         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))")
316         return nil
317     }
318
319     func directoryMetadata(account: String, serverUrl: String) -> NextcloudItemMetadataTable? {
320         // We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com"
321         let problematicSeparator = "://"
322         let placeholderSeparator = "__TEMP_REPLACE__"
323         let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(of: problematicSeparator, with: placeholderSeparator)
324         var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
325         let directoryItemFileName = String(splitServerUrl.removeLast())
326         let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(of: placeholderSeparator, with: problematicSeparator)
327
328         if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true", account, directoryItemServerUrl, directoryItemFileName).first {
329             return NextcloudItemMetadataTable(value: metadata)
330         }
331
332         return nil
333     }
334
335     func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] {
336         let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
337         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@ AND ocId != %@ AND directory == true", directoryServerUrl, directoryMetadata.account)
338         return sortedItemMetadatas(metadatas)
339     }
340
341     func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudItemMetadataTable? {
342         return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
343     }
344
345     func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
346         if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first {
347             return NextcloudItemMetadataTable(value: metadata)
348         }
349
350         return nil
351     }
352
353     func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
354         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND directory == true", account)
355         return sortedItemMetadatas(metadatas)
356     }
357
358     func directoryMetadatas(account: String, parentDirectoryServerUrl: String) -> [NextcloudItemMetadataTable] {
359         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account, parentDirectoryServerUrl)
360         return sortedItemMetadatas(metadatas)
361     }
362
363     // Deletes all metadatas related to the info of the directory provided
364     func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) {
365         let database = ncDatabase()
366         guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
367             Logger.ncFilesDatabase.error("Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion")
368             return
369         }
370
371         let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
372         let results = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, directoryUrlPath)
373
374         for result in results {
375             deleteItemMetadata(ocId: result.ocId)
376             deleteLocalFileMetadata(ocId: result.ocId)
377         }
378
379         do {
380             try database.write {
381                 Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryMetadata.etag, privacy: .public), serverUrl: \(directoryUrlPath)")
382                 database.delete(results)
383             }
384         } catch let error {
385             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)")
386         }
387     }
388
389     func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) -> [NextcloudItemMetadataTable]? {
390
391         let database = ncDatabase()
392
393         guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
394             Logger.ncFilesDatabase.error("Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming")
395             return nil
396         }
397
398         let oldServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
399         let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, oldServerUrl)
400
401         renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
402         Logger.ncFilesDatabase.debug("Renamed root renaming directory")
403
404         do {
405             try database.write {
406                 for childItem in childItemResults {
407                     let oldServerUrl = childItem.serverUrl
408                     let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldServerUrl, with: newServerUrl)
409                     childItem.serverUrl = movedServerUrl
410                     database.add(childItem, update: .all)
411                     Logger.ncFilesDatabase.debug("Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
412                 }
413             }
414         } catch let error {
415             Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)")
416
417             return nil
418         }
419
420         let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, newServerUrl)
421         return sortedItemMetadatas(updatedChildItemResults)
422     }
423
424     func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
425         if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first {
426             return NextcloudLocalFileMetadataTable(value: metadata)
427         }
428
429         return nil
430     }
431
432     func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
433         let database = ncDatabase()
434
435         do {
436             try database.write {
437                 let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
438
439                 newLocalFileMetadata.ocId = itemMetadata.ocId
440                 newLocalFileMetadata.fileName = itemMetadata.fileName
441                 newLocalFileMetadata.account = itemMetadata.account
442                 newLocalFileMetadata.etag = itemMetadata.etag
443                 newLocalFileMetadata.exifDate = Date()
444                 newLocalFileMetadata.exifLatitude = "-1"
445                 newLocalFileMetadata.exifLongitude = "-1"
446
447                 database.add(newLocalFileMetadata, update: .all)
448                 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))")
449             }
450         } catch let error {
451             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)")
452         }
453     }
454
455     func deleteLocalFileMetadata(ocId: String) {
456         let database = ncDatabase()
457
458         do {
459             try database.write {
460                 let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId)
461                 database.delete(results)
462             }
463         } catch let error {
464             Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
465         }
466     }
467
468     private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>) -> [NextcloudLocalFileMetadataTable] {
469         let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
470         return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
471     }
472
473     func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
474         let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account)
475         return sortedLocalFileMetadatas(results)
476     }
477
478     func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
479         let localFileMetadatas = localFileMetadatas(account: account)
480         let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId })
481
482         var itemMetadatas: [NextcloudItemMetadataTable] = []
483
484         for ocId in localFileMetadatasOcIds {
485             guard let itemMetadata = itemMetadataFromOcId(ocId) else {
486                 Logger.ncFilesDatabase.error("Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)")
487                 continue;
488             }
489
490             itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
491         }
492
493         return itemMetadatas
494     }
495
496     func convertNKFileToItemMetadata(_ file: NKFile, account: String) -> NextcloudItemMetadataTable {
497
498         let metadata = NextcloudItemMetadataTable()
499
500         metadata.account = account
501         metadata.checksums = file.checksums
502         metadata.commentsUnread = file.commentsUnread
503         metadata.contentType = file.contentType
504         if let date = file.creationDate {
505             metadata.creationDate = date as Date
506         } else {
507             metadata.creationDate = file.date as Date
508         }
509         metadata.dataFingerprint = file.dataFingerprint
510         metadata.date = file.date as Date
511         metadata.directory = file.directory
512         metadata.downloadURL = file.downloadURL
513         metadata.e2eEncrypted = file.e2eEncrypted
514         metadata.etag = file.etag
515         metadata.favorite = file.favorite
516         metadata.fileId = file.fileId
517         metadata.fileName = file.fileName
518         metadata.fileNameView = file.fileName
519         metadata.hasPreview = file.hasPreview
520         metadata.iconName = file.iconName
521         metadata.mountType = file.mountType
522         metadata.name = file.name
523         metadata.note = file.note
524         metadata.ocId = file.ocId
525         metadata.ownerId = file.ownerId
526         metadata.ownerDisplayName = file.ownerDisplayName
527         metadata.lock = file.lock
528         metadata.lockOwner = file.lockOwner
529         metadata.lockOwnerEditor = file.lockOwnerEditor
530         metadata.lockOwnerType = file.lockOwnerType
531         metadata.lockOwnerDisplayName = file.lockOwnerDisplayName
532         metadata.lockTime = file.lockTime
533         metadata.lockTimeOut = file.lockTimeOut
534         metadata.path = file.path
535         metadata.permissions = file.permissions
536         metadata.quotaUsedBytes = file.quotaUsedBytes
537         metadata.quotaAvailableBytes = file.quotaAvailableBytes
538         metadata.richWorkspace = file.richWorkspace
539         metadata.resourceType = file.resourceType
540         metadata.serverUrl = file.serverUrl
541         metadata.sharePermissionsCollaborationServices = file.sharePermissionsCollaborationServices
542         for element in file.sharePermissionsCloudMesh {
543             metadata.sharePermissionsCloudMesh.append(element)
544         }
545         for element in file.shareType {
546             metadata.shareType.append(element)
547         }
548         metadata.size = file.size
549         metadata.classFile = file.classFile
550         //FIXME: iOS 12.0,* don't detect UTI text/markdown, text/x-markdown
551         if (metadata.contentType == "text/markdown" || metadata.contentType == "text/x-markdown") && metadata.classFile == NKCommon.TypeClassFile.unknow.rawValue {
552             metadata.classFile = NKCommon.TypeClassFile.document.rawValue
553         }
554         if let date = file.uploadDate {
555             metadata.uploadDate = date as Date
556         } else {
557             metadata.uploadDate = file.date as Date
558         }
559         metadata.urlBase = file.urlBase
560         metadata.user = file.user
561         metadata.userId = file.userId
562
563         // Support for finding the correct filename for e2ee files should go here
564
565         return metadata
566     }
567
568     func convertNKFilesFromDirectoryReadToItemMetadatas(_ files: [NKFile], account: String, completionHandler: @escaping (_ directoryMetadata: NextcloudItemMetadataTable, _ childDirectoriesMetadatas: [NextcloudItemMetadataTable], _ metadatas: [NextcloudItemMetadataTable]) -> Void) {
569
570         var directoryMetadataSet = false
571         var directoryMetadata = NextcloudItemMetadataTable()
572         var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = []
573         var metadatas: [NextcloudItemMetadataTable] = []
574
575         for file in files {
576             let metadata = convertNKFileToItemMetadata(file, account: account)
577
578             if metadatas.isEmpty && !directoryMetadataSet {
579                 directoryMetadata = metadata;
580                 directoryMetadataSet = true;
581             } else {
582                 metadatas.append(metadata)
583                 if metadata.directory {
584                     childDirectoriesMetadatas.append(metadata)
585                 }
586             }
587         }
588
589         completionHandler(directoryMetadata, childDirectoriesMetadatas, metadatas)
590     }
591 }