]> dgit.raspbian.org Git - nextcloud-desktop.git/blob
445662285eadf03c8d9243d810fe03e54c7949a6
[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, NextcloudDirectoryMetadataTable.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]) -> (newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable]) {
143
144         assert(databaseToWriteTo.isInWriteTransaction)
145
146         var returningNewMetadatas: [NextcloudItemMetadataTable] = []
147         var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
148
149         for updatedMetadata in updatedMetadatas {
150             if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) {
151
152                 if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue &&
153                     !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) {
154
155                     returningUpdatedMetadatas.append(NextcloudItemMetadataTable(value: updatedMetadata))
156                     databaseToWriteTo.add(updatedMetadata, update: .all)
157
158                     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))")
159                 } else {
160                     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))")
161                 }
162
163             } else { // This is a new metadata
164                 returningNewMetadatas.append(NextcloudItemMetadataTable(value: updatedMetadata))
165                 databaseToWriteTo.add(updatedMetadata, update: .all)
166
167                 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))")
168             }
169         }
170
171         return (returningNewMetadatas, returningUpdatedMetadatas)
172     }
173
174     func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], completionHandler: @escaping(_ newMetadatas: [NextcloudItemMetadataTable]?, _ updatedMetadatas: [NextcloudItemMetadataTable]?, _ deletedMetadatas: [NextcloudItemMetadataTable]?) -> Void) {
175         let database = ncDatabase()
176
177         do {
178             try database.write {
179                 let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue)
180
181                 let deletedMetadatas = processItemMetadatasToDelete(databaseToWriteTo: database,
182                                                                     existingMetadatas: existingMetadatas,
183                                                                     updatedMetadatas: updatedMetadatas)
184
185                 let metadatasFromUpdate = processItemMetadatasToUpdate(databaseToWriteTo: database,
186                                                                        existingMetadatas: existingMetadatas,
187                                                                        updatedMetadatas: updatedMetadatas)
188
189                 completionHandler(metadatasFromUpdate.newMetadatas, metadatasFromUpdate.updatedMetadatas, deletedMetadatas)
190             }
191         } catch let error {
192             Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)")
193             completionHandler(nil, nil, nil)
194         }
195     }
196
197     func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) {
198         let database = ncDatabase()
199
200         do {
201             try database.write {
202                 guard let result = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId).first else {
203                     Logger.ncFilesDatabase.debug("Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)")
204                     return
205                 }
206
207                 result.status = status.rawValue
208                 database.add(result, update: .all)
209                 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))")
210
211                 completionHandler(NextcloudItemMetadataTable(value: result))
212             }
213         } catch let error {
214             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)")
215             completionHandler(nil)
216         }
217     }
218
219     func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
220         let database = ncDatabase()
221
222         do {
223             try database.write {
224                 database.add(metadata, update: .all)
225                 Logger.ncFilesDatabase.debug("Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
226             }
227         } catch let error {
228             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)")
229         }
230     }
231
232     func deleteItemMetadata(ocId: String) {
233         let database = ncDatabase()
234
235         do {
236             try database.write {
237                 let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId)
238
239                 Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
240                 database.delete(results)
241             }
242         } catch let error {
243             Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
244         }
245     }
246
247     func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
248         let database = ncDatabase()
249
250         do {
251             try database.write {
252                 guard let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else {
253                     Logger.ncFilesDatabase.debug("Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash))")
254                     return
255                 }
256
257                 let oldFileName = itemMetadata.fileName
258                 let oldServerUrl = itemMetadata.serverUrl
259
260                 itemMetadata.fileName = newFileName
261                 itemMetadata.fileNameView = newFileName
262                 itemMetadata.serverUrl = newServerUrl
263
264                 database.add(itemMetadata, update: .all)
265
266                 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))")
267             }
268         } catch let error {
269             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)")
270         }
271     }
272
273     func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? {
274         let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
275
276         if metadata.serverUrl == homeServerFilesUrl {
277             return .rootContainer
278         }
279
280         guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
281             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))")
282             return nil
283         }
284
285         if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
286             return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
287         }
288
289         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))")
290         return nil
291     }
292
293     func directoryMetadata(account: String, serverUrl: String) -> NextcloudDirectoryMetadataTable? {
294         if let metadata = ncDatabase().objects(NextcloudDirectoryMetadataTable.self).filter("account == %@ AND serverUrl == %@", account, serverUrl).first {
295             return NextcloudDirectoryMetadataTable(value: metadata)
296         }
297
298         return nil
299     }
300
301     func directoryMetadata(ocId: String) -> NextcloudDirectoryMetadataTable? {
302         if let metadata = ncDatabase().objects(NextcloudDirectoryMetadataTable.self).filter("ocId == %@", ocId).first {
303             return NextcloudDirectoryMetadataTable(value: metadata)
304         }
305
306         return nil
307     }
308
309     private func sortedDirectoryMetadatas(_ metadatas: Results<NextcloudDirectoryMetadataTable>) -> [NextcloudDirectoryMetadataTable] {
310         let sortedMetadatas = metadatas.sorted(byKeyPath: "serverUrl", ascending: true)
311         return Array(sortedMetadatas.map { NextcloudDirectoryMetadataTable(value: $0) })
312     }
313
314     func childDirectoriesForDirectory(_ directoryMetadata: NextcloudDirectoryMetadataTable) -> [NextcloudDirectoryMetadataTable] {
315         let metadatas = ncDatabase().objects(NextcloudDirectoryMetadataTable.self).filter("serverUrl BEGINSWITH %@ AND ocId != %@", directoryMetadata.serverUrl, directoryMetadata.account)
316         return sortedDirectoryMetadatas(metadatas)
317     }
318
319     func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudDirectoryMetadataTable? {
320         return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
321     }
322
323     func directoryMetadatas(account: String) -> [NextcloudDirectoryMetadataTable] {
324         let metadatas = ncDatabase().objects(NextcloudDirectoryMetadataTable.self).filter("account == %@", account)
325         return sortedDirectoryMetadatas(metadatas)
326     }
327
328     func directoryMetadatas(account: String, parentDirectoryServerUrl: String) -> [NextcloudDirectoryMetadataTable] {
329         let metadatas = ncDatabase().objects(NextcloudDirectoryMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@", account, parentDirectoryServerUrl)
330         return sortedDirectoryMetadatas(metadatas)
331     }
332
333     private func processDirectoryMetadatasToDelete(databaseToWriteTo: Realm,
334                                            existingDirectoryMetadatas: Results<NextcloudDirectoryMetadataTable>,
335                                            updatedDirectoryMetadatas: [NextcloudDirectoryMetadataTable]) {
336
337         for existingMetadata in existingDirectoryMetadatas {
338             guard !updatedDirectoryMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
339                   let metadataToDelete = directoryMetadata(ocId: existingMetadata.ocId) else { continue }
340
341             Logger.ncFilesDatabase.debug("Deleting directory metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), serverUrl: \(existingMetadata.serverUrl)")
342
343             self.deleteDirectoryAndSubdirectoriesMetadata(ocId: metadataToDelete.ocId)
344         }
345     }
346
347     private func processDirectoryMetadatasToUpdate(databaseToWriteTo: Realm,
348                                            existingDirectoryMetadatas: Results<NextcloudDirectoryMetadataTable>,
349                                            updatedDirectoryMetadatas: [NextcloudDirectoryMetadataTable]) {
350
351         assert(databaseToWriteTo.isInWriteTransaction)
352
353         for updatedMetadata in updatedDirectoryMetadatas {
354             if let existingMetadata = existingDirectoryMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) {
355
356                 if !existingMetadata.isInSameRemoteState(updatedMetadata) {
357
358                     databaseToWriteTo.add(NextcloudDirectoryMetadataTable(value: updatedMetadata), update: .all)
359                     Logger.ncFilesDatabase.debug("Updated existing directory metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), serverUrl: \(updatedMetadata.serverUrl)")
360                 }
361                 // Don't update under other circumstances in which the metadata already exists
362
363             } else { // This is a new metadata
364                 databaseToWriteTo.add(NextcloudDirectoryMetadataTable(value: updatedMetadata), update: .all)
365                 Logger.ncFilesDatabase.debug("Created new directory metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), serverUrl: \(updatedMetadata.serverUrl)")
366             }
367         }
368     }
369
370     func updateDirectoryMetadatas(account: String, parentDirectoryServerUrl: String, updatedDirectoryMetadatas: [NextcloudDirectoryMetadataTable]) {
371         let database = ncDatabase()
372
373         let existingDirectoryMetadatas = ncDatabase().objects(NextcloudDirectoryMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@", account, parentDirectoryServerUrl)
374
375         // Actual db writing handled internally
376         processDirectoryMetadatasToDelete(databaseToWriteTo: database,
377                                           existingDirectoryMetadatas: existingDirectoryMetadatas,
378                                           updatedDirectoryMetadatas: updatedDirectoryMetadatas)
379
380         do {
381             try database.write {
382
383                 processDirectoryMetadatasToUpdate(databaseToWriteTo: database,
384                                                   existingDirectoryMetadatas: existingDirectoryMetadatas,
385                                                   updatedDirectoryMetadatas: updatedDirectoryMetadatas)
386             }
387         } catch let error {
388             Logger.ncFilesDatabase.error("Could not update directory metadatas, received error: \(error.localizedDescription)")
389         }
390     }
391
392     func directoryMetadataFromItemMetadata(directoryItemMetadata: NextcloudItemMetadataTable, recordEtag: Bool = false) -> NextcloudDirectoryMetadataTable {
393         var newDirectoryMetadata = NextcloudDirectoryMetadataTable()
394         let directoryOcId = directoryItemMetadata.ocId
395
396         if let existingDirectoryMetadata = directoryMetadata(ocId: directoryOcId) {
397             newDirectoryMetadata = existingDirectoryMetadata
398         }
399
400         if recordEtag {
401             newDirectoryMetadata.etag = directoryItemMetadata.etag
402         }
403
404         newDirectoryMetadata.ocId = directoryOcId
405         newDirectoryMetadata.fileId = directoryItemMetadata.fileId
406         newDirectoryMetadata.parentDirectoryServerUrl = directoryItemMetadata.serverUrl
407         newDirectoryMetadata.serverUrl = directoryItemMetadata.serverUrl + "/" + directoryItemMetadata.fileNameView
408         newDirectoryMetadata.account = directoryItemMetadata.account
409         newDirectoryMetadata.e2eEncrypted = directoryItemMetadata.e2eEncrypted
410         newDirectoryMetadata.favorite = directoryItemMetadata.favorite
411         newDirectoryMetadata.permissions = directoryItemMetadata.permissions
412
413         return newDirectoryMetadata
414     }
415
416     func updateDirectoryMetadatasFromItemMetadatas(account: String, parentDirectoryServerUrl: String, updatedDirectoryItemMetadatas: [NextcloudItemMetadataTable], recordEtag: Bool = false) {
417
418         var updatedDirMetadatas: [NextcloudDirectoryMetadataTable] = []
419
420         for directoryItemMetadata in updatedDirectoryItemMetadatas {
421             let newDirectoryMetadata = directoryMetadataFromItemMetadata(directoryItemMetadata: directoryItemMetadata, recordEtag: recordEtag)
422             updatedDirMetadatas.append(newDirectoryMetadata)
423         }
424
425         updateDirectoryMetadatas(account: account, parentDirectoryServerUrl: parentDirectoryServerUrl, updatedDirectoryMetadatas: updatedDirMetadatas)
426     }
427
428     func addDirectoryMetadata(_ metadata: NextcloudDirectoryMetadataTable) {
429         let database = ncDatabase()
430
431         do {
432             try database.write {
433                 database.add(metadata, update: .all)
434                 Logger.ncFilesDatabase.debug("Added new directory metadata. ocId: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), serverUrl: \(metadata.serverUrl)")
435             }
436         } catch let error {
437             Logger.ncFilesDatabase.error("Could not add new directory metadata. ocId: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), serverUrl: \(metadata.serverUrl), received error: \(error.localizedDescription, privacy: .public)")
438         }
439     }
440
441     // Deletes all metadatas related to the info of the directory provided
442     func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) {
443         let database = ncDatabase()
444         guard let directoryMetadata = database.objects(NextcloudDirectoryMetadataTable.self).filter("ocId == %@", ocId).first else {
445             Logger.ncFilesDatabase.error("Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion")
446             return
447         }
448
449         let results = database.objects(NextcloudDirectoryMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, directoryMetadata.serverUrl)
450
451         for result in results {
452             deleteItemMetadata(ocId: result.ocId)
453             deleteLocalFileMetadata(ocId: result.ocId)
454         }
455
456         do {
457             try database.write {
458                 Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryMetadata.etag, privacy: .public), serverUrl: \(directoryMetadata.serverUrl)")
459                 database.delete(results)
460             }
461         } catch let error {
462             Logger.ncFilesDatabase.error("Could not delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryMetadata.etag, privacy: .public), serverUrl: \(directoryMetadata.serverUrl), received error: \(error.localizedDescription, privacy: .public)")
463         }
464     }
465
466     func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) {
467
468         let database = ncDatabase()
469
470         do {
471             try database.write {
472                 guard let directoryTableResult = database.objects(NextcloudDirectoryMetadataTable.self).filter("ocId == %@", ocId).first,
473                       let directoryItemResult = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else {
474                     Logger.ncFilesDatabase.error("Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming")
475                     return
476                 }
477
478                 let oldServerUrl = directoryTableResult.serverUrl
479
480                 let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryTableResult.account, oldServerUrl)
481                 let childDirectoryResults = database.objects(NextcloudDirectoryMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryTableResult.account, oldServerUrl)
482
483                 directoryTableResult.serverUrl = newServerUrl
484                 database.add(directoryTableResult, update: .all)
485                 directoryItemResult.fileName = newFileName
486                 directoryItemResult.fileNameView = newFileName
487                 database.add(directoryItemResult, update: .all)
488                 Logger.ncFilesDatabase.debug("Renamed root renaming directory at \(oldServerUrl) to \(newServerUrl)")
489
490                 for childItem in childItemResults {
491                     let oldServerUrl = childItem.serverUrl
492                     let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldServerUrl, with: newServerUrl)
493                     childItem.serverUrl = movedServerUrl
494                     database.add(childItem, update: .all)
495                     Logger.ncFilesDatabase.debug("Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
496                 }
497
498                 for childDirectory in childDirectoryResults {
499                     let oldServerUrl = childDirectory.serverUrl
500                     let oldParentServerUrl = childDirectory.parentDirectoryServerUrl
501                     let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldServerUrl, with: newServerUrl)
502                     let movedParentServerUrl = oldServerUrl.replacingOccurrences(of: oldParentServerUrl, with: newServerUrl)
503                     childDirectory.serverUrl = movedServerUrl
504                     childDirectory.parentDirectoryServerUrl = movedParentServerUrl
505                     database.add(childDirectory, update: .all)
506                     Logger.ncFilesDatabase.debug("Moved childDirectory at \(oldServerUrl) to \(movedServerUrl)")
507                 }
508             }
509         } catch let error {
510             Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)")
511         }
512     }
513
514     func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
515         if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first {
516             return NextcloudLocalFileMetadataTable(value: metadata)
517         }
518
519         return nil
520     }
521
522     func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
523         let database = ncDatabase()
524
525         do {
526             try database.write {
527                 let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
528
529                 newLocalFileMetadata.ocId = itemMetadata.ocId
530                 newLocalFileMetadata.fileName = itemMetadata.fileName
531                 newLocalFileMetadata.account = itemMetadata.account
532                 newLocalFileMetadata.etag = itemMetadata.etag
533                 newLocalFileMetadata.exifDate = Date()
534                 newLocalFileMetadata.exifLatitude = "-1"
535                 newLocalFileMetadata.exifLongitude = "-1"
536
537                 database.add(newLocalFileMetadata, update: .all)
538                 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))")
539             }
540         } catch let error {
541             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)")
542         }
543     }
544
545     func deleteLocalFileMetadata(ocId: String) {
546         let database = ncDatabase()
547
548         do {
549             try database.write {
550                 let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId)
551                 database.delete(results)
552             }
553         } catch let error {
554             Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
555         }
556     }
557
558     private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>) -> [NextcloudLocalFileMetadataTable] {
559         let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
560         return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
561     }
562
563     func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
564         let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account)
565         return sortedLocalFileMetadatas(results)
566     }
567
568     func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
569         let localFileMetadatas = localFileMetadatas(account: account)
570         let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId })
571
572         var itemMetadatas: [NextcloudItemMetadataTable] = []
573
574         for ocId in localFileMetadatasOcIds {
575             guard let itemMetadata = itemMetadataFromOcId(ocId) else {
576                 Logger.ncFilesDatabase.error("Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)")
577                 continue;
578             }
579
580             itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
581         }
582
583         return itemMetadatas
584     }
585
586     func convertNKFileToItemMetadata(_ file: NKFile, account: String) -> NextcloudItemMetadataTable {
587
588         let metadata = NextcloudItemMetadataTable()
589
590         metadata.account = account
591         metadata.checksums = file.checksums
592         metadata.commentsUnread = file.commentsUnread
593         metadata.contentType = file.contentType
594         if let date = file.creationDate {
595             metadata.creationDate = date as Date
596         } else {
597             metadata.creationDate = file.date as Date
598         }
599         metadata.dataFingerprint = file.dataFingerprint
600         metadata.date = file.date as Date
601         metadata.directory = file.directory
602         metadata.downloadURL = file.downloadURL
603         metadata.e2eEncrypted = file.e2eEncrypted
604         metadata.etag = file.etag
605         metadata.favorite = file.favorite
606         metadata.fileId = file.fileId
607         metadata.fileName = file.fileName
608         metadata.fileNameView = file.fileName
609         metadata.hasPreview = file.hasPreview
610         metadata.iconName = file.iconName
611         metadata.mountType = file.mountType
612         metadata.name = file.name
613         metadata.note = file.note
614         metadata.ocId = file.ocId
615         metadata.ownerId = file.ownerId
616         metadata.ownerDisplayName = file.ownerDisplayName
617         metadata.lock = file.lock
618         metadata.lockOwner = file.lockOwner
619         metadata.lockOwnerEditor = file.lockOwnerEditor
620         metadata.lockOwnerType = file.lockOwnerType
621         metadata.lockOwnerDisplayName = file.lockOwnerDisplayName
622         metadata.lockTime = file.lockTime
623         metadata.lockTimeOut = file.lockTimeOut
624         metadata.path = file.path
625         metadata.permissions = file.permissions
626         metadata.quotaUsedBytes = file.quotaUsedBytes
627         metadata.quotaAvailableBytes = file.quotaAvailableBytes
628         metadata.richWorkspace = file.richWorkspace
629         metadata.resourceType = file.resourceType
630         metadata.serverUrl = file.serverUrl
631         metadata.sharePermissionsCollaborationServices = file.sharePermissionsCollaborationServices
632         for element in file.sharePermissionsCloudMesh {
633             metadata.sharePermissionsCloudMesh.append(element)
634         }
635         for element in file.shareType {
636             metadata.shareType.append(element)
637         }
638         metadata.size = file.size
639         metadata.classFile = file.classFile
640         //FIXME: iOS 12.0,* don't detect UTI text/markdown, text/x-markdown
641         if (metadata.contentType == "text/markdown" || metadata.contentType == "text/x-markdown") && metadata.classFile == NKCommon.TypeClassFile.unknow.rawValue {
642             metadata.classFile = NKCommon.TypeClassFile.document.rawValue
643         }
644         if let date = file.uploadDate {
645             metadata.uploadDate = date as Date
646         } else {
647             metadata.uploadDate = file.date as Date
648         }
649         metadata.urlBase = file.urlBase
650         metadata.user = file.user
651         metadata.userId = file.userId
652
653         // Support for finding the correct filename for e2ee files should go here
654
655         return metadata
656     }
657
658     func convertNKFilesFromDirectoryReadToItemMetadatas(_ files: [NKFile], account: String, completionHandler: @escaping (_ directoryMetadata: NextcloudItemMetadataTable, _ childDirectoriesMetadatas: [NextcloudItemMetadataTable], _ metadatas: [NextcloudItemMetadataTable]) -> Void) {
659
660         var directoryMetadataSet = false
661         var directoryMetadata = NextcloudItemMetadataTable()
662         var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = []
663         var metadatas: [NextcloudItemMetadataTable] = []
664
665         for file in files {
666             let metadata = convertNKFileToItemMetadata(file, account: account)
667
668             if metadatas.isEmpty && !directoryMetadataSet {
669                 directoryMetadata = metadata;
670                 directoryMetadataSet = true;
671             } else {
672                 metadatas.append(metadata)
673                 if metadata.directory {
674                     childDirectoriesMetadatas.append(metadata)
675                 }
676             }
677         }
678
679         completionHandler(directoryMetadata, childDirectoriesMetadatas, metadatas)
680     }
681 }