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