4dc6c6398d56a2f7fece9bbe18078a417ee9bc54
[nextcloud-desktop.git] /
1 /*
2  * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12  * for more details.
13  */
14
15 import Foundation
16 import RealmSwift
17 import FileProvider
18 import NextcloudKit
19 import OSLog
20
21 class NextcloudFilesDatabaseManager : NSObject {
22     static let shared = {
23         return NextcloudFilesDatabaseManager();
24     }()
25
26     let relativeDatabaseFolderPath = "Database/"
27     let databaseFilename = "fileproviderextdatabase.realm"
28     let relativeDatabaseFilePath: String
29     var databasePath: URL?
30
31     let schemaVersion: UInt64 = 100
32
33     override init() {
34         self.relativeDatabaseFilePath = self.relativeDatabaseFolderPath + self.databaseFilename
35
36         guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
37             super.init()
38             return
39         }
40
41         self.databasePath = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFilePath)
42
43         // Disable file protection for directory DB
44         // https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm
45         let dbFolder = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFolderPath)
46         let dbFolderPath = dbFolder.path
47         do {
48             try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
49             try FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: dbFolderPath)
50         } catch let error {
51             Logger.ncFilesDatabase.error("Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)")
52         }
53
54         let config = Realm.Configuration(
55             fileURL: self.databasePath,
56             schemaVersion: self.schemaVersion,
57             objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
58         )
59
60         Realm.Configuration.defaultConfiguration = config
61
62         do {
63             _ = try Realm()
64             Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
65         } catch let error as NSError {
66             Logger.ncFilesDatabase.error("Error opening Realm db: \(error.localizedDescription, privacy: .public)")
67         }
68
69         super.init()
70     }
71
72     private func ncDatabase() -> Realm {
73         let realm = try! Realm()
74         realm.refresh()
75         return realm
76     }
77
78     func anyItemMetadatasForAccount(_ account: String) -> Bool {
79         return !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account).isEmpty
80     }
81
82     func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
83         // Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db
84         // Let's therefore create a copy
85         if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first {
86             return NextcloudItemMetadataTable(value: itemMetadata)
87         }
88
89         return nil
90     }
91
92     private func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>) -> [NextcloudItemMetadataTable] {
93         let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
94         return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
95     }
96
97     func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
98         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
99         return sortedItemMetadatas(metadatas)
100     }
101
102     func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
103         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@", account, serverUrl)
104         return sortedItemMetadatas(metadatas)
105     }
106
107     func itemMetadatas(account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status) -> [NextcloudItemMetadataTable] {
108         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, status.rawValue)
109         return sortedItemMetadatas(metadatas)
110     }
111
112     func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> NextcloudItemMetadataTable? {
113         let ocId = identifier.rawValue
114         return itemMetadataFromOcId(ocId)
115     }
116
117     private func processItemMetadatasToDelete(existingMetadatas: Results<NextcloudItemMetadataTable>,
118                                               updatedMetadatas: [NextcloudItemMetadataTable]) -> [NextcloudItemMetadataTable] {
119
120         var deletedMetadatas: [NextcloudItemMetadataTable] = []
121
122         for existingMetadata in existingMetadatas {
123             guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
124                     let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId) else { continue }
125
126             deletedMetadatas.append(metadataToDelete)
127
128             Logger.ncFilesDatabase.debug("Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
129         }
130
131         return deletedMetadatas
132     }
133
134     private func processItemMetadatasToUpdate(existingMetadatas: Results<NextcloudItemMetadataTable>,
135                                               updatedMetadatas: [NextcloudItemMetadataTable],
136                                               updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], directoriesNeedingRename: [NextcloudItemMetadataTable]) {
137
138         var returningNewMetadatas: [NextcloudItemMetadataTable] = []
139         var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
140         var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
141
142         for updatedMetadata in updatedMetadatas {
143             if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) {
144
145                 if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue &&
146                     !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) {
147
148                     if updatedMetadata.directory {
149
150                         if updatedMetadata.serverUrl != existingMetadata.serverUrl || updatedMetadata.fileName != existingMetadata.fileName {
151
152                             directoriesNeedingRename.append(NextcloudItemMetadataTable(value: updatedMetadata))
153                             updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
154
155                         } else if !updateDirectoryEtags {
156                             updatedMetadata.etag = existingMetadata.etag
157                         }
158                     }
159
160                     returningUpdatedMetadatas.append(updatedMetadata)
161
162
163                     Logger.ncFilesDatabase.debug("Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
164                 } else {
165                     Logger.ncFilesDatabase.debug("Skipping item metadata update; same as existing, or still downloading/uploading. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
166                 }
167
168             } else { // This is a new metadata
169                 if !updateDirectoryEtags && updatedMetadata.directory {
170                     updatedMetadata.etag = ""
171                 }
172                 
173                 returningNewMetadatas.append(updatedMetadata)
174
175                 Logger.ncFilesDatabase.debug("Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
176             }
177         }
178
179         return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
180     }
181
182     func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) {
183         let database = ncDatabase()
184
185         do {
186             let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue)
187
188             let metadatasToDelete = processItemMetadatasToDelete(existingMetadatas: existingMetadatas,
189                                                                  updatedMetadatas: updatedMetadatas)
190
191             let metadatasToChange = processItemMetadatasToUpdate(existingMetadatas: existingMetadatas,
192                                                                  updatedMetadatas: updatedMetadatas,
193                                                                  updateDirectoryEtags: updateDirectoryEtags)
194
195             var metadatasToUpdate = metadatasToChange.updatedMetadatas
196             let metadatasToCreate = metadatasToChange.newMetadatas
197             let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
198
199             let metadatasToAdd = Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) }) +
200                                  Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
201
202             for metadata in directoriesNeedingRename {
203
204                 if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(ocId: metadata.ocId, newServerUrl: metadata.serverUrl, newFileName: metadata.fileName) {
205                     metadatasToUpdate += updatedDirectoryChildren
206                 }
207             }
208
209             try database.write {
210                 for metadata in metadatasToDelete {
211                     // Can't pass copies, we need the originals from the database
212                     database.delete(ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId))
213                 }
214
215                 for metadata in metadatasToAdd {
216                     database.add(metadata, update: .all)
217                 }
218
219             }
220
221             return (newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate, deletedMetadatas: metadatasToDelete)
222         } catch let error {
223             Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)")
224             return (nil, nil, nil)
225         }
226     }
227
228     func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) {
229         let database = ncDatabase()
230
231         do {
232             try database.write {
233                 guard let result = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId).first else {
234                     Logger.ncFilesDatabase.debug("Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)")
235                     return
236                 }
237
238                 result.status = status.rawValue
239                 database.add(result, update: .all)
240                 Logger.ncFilesDatabase.debug("Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
241
242                 completionHandler(NextcloudItemMetadataTable(value: result))
243             }
244         } catch let error {
245             Logger.ncFilesDatabase.error("Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
246             completionHandler(nil)
247         }
248     }
249
250     func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
251         let database = ncDatabase()
252
253         do {
254             try database.write {
255                 database.add(metadata, update: .all)
256                 Logger.ncFilesDatabase.debug("Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
257             }
258         } catch let error {
259             Logger.ncFilesDatabase.error("Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
260         }
261     }
262
263     @discardableResult func deleteItemMetadata(ocId: String) -> Bool {
264         let database = ncDatabase()
265
266         do {
267             try database.write {
268                 let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId)
269
270                 Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
271                 database.delete(results)
272             }
273
274             return true
275         } catch let error {
276             Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
277             return false
278         }
279     }
280
281     func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
282         let database = ncDatabase()
283
284         do {
285             try database.write {
286                 guard let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else {
287                     Logger.ncFilesDatabase.debug("Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash))")
288                     return
289                 }
290
291                 let oldFileName = itemMetadata.fileName
292                 let oldServerUrl = itemMetadata.serverUrl
293
294                 itemMetadata.fileName = newFileName
295                 itemMetadata.fileNameView = newFileName
296                 itemMetadata.serverUrl = newServerUrl
297
298                 database.add(itemMetadata, update: .all)
299
300                 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))")
301             }
302         } catch let error {
303             Logger.ncFilesDatabase.error("Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: OSLogPrivacy.auto(mask: .hash)) at proposed serverUrl \(newServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)), received error: \(error.localizedDescription, privacy: .public)")
304         }
305     }
306
307     func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? {
308         let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
309
310         if metadata.serverUrl == homeServerFilesUrl {
311             return .rootContainer
312         }
313
314         guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
315             Logger.ncFilesDatabase.error("Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
316             return nil
317         }
318
319         if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
320             return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
321         }
322
323         Logger.ncFilesDatabase.error("Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: OSLogPrivacy.auto(mask: .hash))")
324         return nil
325     }
326
327     func directoryMetadata(account: String, serverUrl: String) -> NextcloudItemMetadataTable? {
328         // We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com"
329         let problematicSeparator = "://"
330         let placeholderSeparator = "__TEMP_REPLACE__"
331         let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(of: problematicSeparator, with: placeholderSeparator)
332         var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
333         let directoryItemFileName = String(splitServerUrl.removeLast())
334         let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(of: placeholderSeparator, with: problematicSeparator)
335
336         if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true", account, directoryItemServerUrl, directoryItemFileName).first {
337             return NextcloudItemMetadataTable(value: metadata)
338         }
339
340         return nil
341     }
342
343     func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] {
344         let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
345         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@ AND ocId != %@ AND directory == true", directoryServerUrl, directoryMetadata.account)
346         return sortedItemMetadatas(metadatas)
347     }
348
349     func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudItemMetadataTable? {
350         return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
351     }
352
353     func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
354         if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first {
355             return NextcloudItemMetadataTable(value: metadata)
356         }
357
358         return nil
359     }
360
361     func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
362         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND directory == true", account)
363         return sortedItemMetadatas(metadatas)
364     }
365
366     func directoryMetadatas(account: String, parentDirectoryServerUrl: String) -> [NextcloudItemMetadataTable] {
367         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account, parentDirectoryServerUrl)
368         return sortedItemMetadatas(metadatas)
369     }
370
371     // Deletes all metadatas related to the info of the directory provided
372     func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? {
373         let database = ncDatabase()
374         guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
375             Logger.ncFilesDatabase.error("Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion")
376             return nil
377         }
378
379         let directoryMetadataCopy = NextcloudItemMetadataTable(value: directoryMetadata)
380         let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
381         let directoryAccount = directoryMetadata.account
382         let directoryEtag = directoryMetadata.etag
383
384         Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: OSLogPrivacy.auto(mask: .hash))")
385
386         guard deleteItemMetadata(ocId: directoryMetadata.ocId) else {
387             Logger.ncFilesDatabase.debug("Failure to delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: OSLogPrivacy.auto(mask: .hash))")
388             return nil
389         }
390
391         var deletedMetadatas: [NextcloudItemMetadataTable] = [directoryMetadata]
392
393         let results = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryAccount, directoryUrlPath)
394
395         for result in results {
396             let successfulItemMetadataDelete = deleteItemMetadata(ocId: result.ocId)
397             if (successfulItemMetadataDelete) {
398                 deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
399             }
400
401             if localFileMetadataFromOcId(result.ocId) != nil {
402                 deleteLocalFileMetadata(ocId: result.ocId)
403             }
404         }
405
406         Logger.ncFilesDatabase.debug("Completed deletions in directory recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: OSLogPrivacy.auto(mask: .hash))")
407
408         return deletedMetadatas
409     }
410
411     func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) -> [NextcloudItemMetadataTable]? {
412
413         let database = ncDatabase()
414
415         guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
416             Logger.ncFilesDatabase.error("Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming")
417             return nil
418         }
419
420         let oldItemServerUrl = directoryMetadata.serverUrl
421         let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName
422         let newDirectoryServerUrl = newServerUrl + "/" + newFileName
423         let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, oldDirectoryServerUrl)
424
425         renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
426         Logger.ncFilesDatabase.debug("Renamed root renaming directory")
427
428         do {
429             try database.write {
430                 for childItem in childItemResults {
431                     let oldServerUrl = childItem.serverUrl
432                     let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldDirectoryServerUrl, with: newDirectoryServerUrl)
433                     childItem.serverUrl = movedServerUrl
434                     database.add(childItem, update: .all)
435                     Logger.ncFilesDatabase.debug("Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
436                 }
437             }
438         } catch let error {
439             Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)")
440
441             return nil
442         }
443
444         let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, newDirectoryServerUrl)
445         return sortedItemMetadatas(updatedChildItemResults)
446     }
447
448     func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
449         if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first {
450             return NextcloudLocalFileMetadataTable(value: metadata)
451         }
452
453         return nil
454     }
455
456     func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
457         let database = ncDatabase()
458
459         do {
460             try database.write {
461                 let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
462
463                 newLocalFileMetadata.ocId = itemMetadata.ocId
464                 newLocalFileMetadata.fileName = itemMetadata.fileName
465                 newLocalFileMetadata.account = itemMetadata.account
466                 newLocalFileMetadata.etag = itemMetadata.etag
467                 newLocalFileMetadata.exifDate = Date()
468                 newLocalFileMetadata.exifLatitude = "-1"
469                 newLocalFileMetadata.exifLongitude = "-1"
470
471                 database.add(newLocalFileMetadata, update: .all)
472                 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))")
473             }
474         } catch let error {
475             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)")
476         }
477     }
478
479     func deleteLocalFileMetadata(ocId: String) {
480         let database = ncDatabase()
481
482         do {
483             try database.write {
484                 let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId)
485                 database.delete(results)
486             }
487         } catch let error {
488             Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
489         }
490     }
491
492     private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>) -> [NextcloudLocalFileMetadataTable] {
493         let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
494         return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
495     }
496
497     func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
498         let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account)
499         return sortedLocalFileMetadatas(results)
500     }
501
502     func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
503         let localFileMetadatas = localFileMetadatas(account: account)
504         let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId })
505
506         var itemMetadatas: [NextcloudItemMetadataTable] = []
507
508         for ocId in localFileMetadatasOcIds {
509             guard let itemMetadata = itemMetadataFromOcId(ocId) else {
510                 Logger.ncFilesDatabase.error("Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)")
511                 continue;
512             }
513
514             itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
515         }
516
517         return itemMetadatas
518     }
519 }