]> dgit.raspbian.org Git - nextcloud-desktop.git/blob
06bdc266fac98f0db4b88c22f3ef4ce9f5ff0da9
[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 FileProvider
16 import Foundation
17 import NextcloudKit
18 import OSLog
19 import RealmSwift
20
21 class NextcloudFilesDatabaseManager {
22     static let shared = NextcloudFilesDatabaseManager()!
23
24     private static let relativeDatabaseFolderPath = "Database/"
25     private static let databaseFilename = "fileproviderextdatabase.realm"
26     private static let schemaVersion: UInt64 = 100
27
28     init(realmConfig: Realm.Configuration = Realm.Configuration.defaultConfiguration) {
29         Realm.Configuration.defaultConfiguration = realmConfig
30
31         do {
32             _ = try Realm()
33             Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
34         } catch let error {
35             Logger.ncFilesDatabase.error("Error opening Realm db: \(error, privacy: .public)")
36         }
37     }
38
39     convenience init?() {
40         let relativeDatabaseFilePath = Self.relativeDatabaseFolderPath + Self.databaseFilename
41         guard let fileProviderDataDirUrl = pathForFileProviderExtData() else { return nil }
42         let databasePath = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFilePath)
43
44         // Disable file protection for directory DB
45         // https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/
46         let dbFolder = fileProviderDataDirUrl.appendingPathComponent(Self.relativeDatabaseFolderPath)
47         let dbFolderPath = dbFolder.path
48         do {
49             try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
50             try FileManager.default.setAttributes(
51                 [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
52                 ofItemAtPath: dbFolderPath
53             )
54         } catch {
55             Logger.ncFilesDatabase.error(
56                 "Could not set permission level for db folder: \(error, privacy: .public)"
57             )
58         }
59
60         let config = Realm.Configuration(
61             fileURL: databasePath,
62             schemaVersion: Self.schemaVersion,
63             objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
64         )
65         self.init(realmConfig: config)
66     }
67
68     func ncDatabase() -> Realm {
69         let realm = try! Realm()
70         realm.refresh()
71         return realm
72     }
73
74     func anyItemMetadatasForAccount(_ account: String) -> Bool {
75         !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
76             .isEmpty
77     }
78
79     func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
80         // Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db
81         // Let's therefore create a copy
82         if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
83             "ocId == %@", ocId
84         ).first {
85             return NextcloudItemMetadataTable(value: itemMetadata)
86         }
87
88         return nil
89     }
90
91     func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>)
92         -> [NextcloudItemMetadataTable]
93     {
94         let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
95         return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
96     }
97
98     func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
99         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
100             "account == %@", account)
101         return sortedItemMetadatas(metadatas)
102     }
103
104     func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
105         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
106             "account == %@ AND serverUrl == %@", account, serverUrl)
107         return sortedItemMetadatas(metadatas)
108     }
109
110     func itemMetadatas(
111         account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status
112     )
113         -> [NextcloudItemMetadataTable]
114     {
115         let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
116             "account == %@ AND serverUrl == %@ AND status == %@", 
117             account,
118             serverUrl,
119             status.rawValue)
120         return sortedItemMetadatas(metadatas)
121     }
122
123     func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier)
124         -> NextcloudItemMetadataTable?
125     {
126         let ocId = identifier.rawValue
127         return itemMetadataFromOcId(ocId)
128     }
129
130     private func processItemMetadatasToDelete(
131         existingMetadatas: Results<NextcloudItemMetadataTable>,
132         updatedMetadatas: [NextcloudItemMetadataTable]
133     ) -> [NextcloudItemMetadataTable] {
134         var deletedMetadatas: [NextcloudItemMetadataTable] = []
135
136         for existingMetadata in existingMetadatas {
137             guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
138                 let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId)
139             else { continue }
140
141             deletedMetadatas.append(metadataToDelete)
142
143             Logger.ncFilesDatabase.debug(
144                 "Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: .public)"
145             )
146         }
147
148         return deletedMetadatas
149     }
150
151     private func processItemMetadatasToUpdate(
152         existingMetadatas: Results<NextcloudItemMetadataTable>,
153         updatedMetadatas: [NextcloudItemMetadataTable],
154         updateDirectoryEtags: Bool
155     ) -> (
156         newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable],
157         directoriesNeedingRename: [NextcloudItemMetadataTable]
158     ) {
159         var returningNewMetadatas: [NextcloudItemMetadataTable] = []
160         var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
161         var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
162
163         for updatedMetadata in updatedMetadatas {
164             if let existingMetadata = existingMetadatas.first(where: {
165                 $0.ocId == updatedMetadata.ocId
166             }) {
167                 if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue,
168                     !existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata)
169                 {
170                     if updatedMetadata.directory {
171                         if updatedMetadata.serverUrl != existingMetadata.serverUrl
172                             || updatedMetadata.fileName != existingMetadata.fileName
173                         {
174                             directoriesNeedingRename.append(
175                                 NextcloudItemMetadataTable(value: updatedMetadata))
176                             updatedMetadata.etag = ""  // Renaming doesn't change the etag so reset manually
177
178                         } else if !updateDirectoryEtags {
179                             updatedMetadata.etag = existingMetadata.etag
180                         }
181                     }
182
183                     returningUpdatedMetadatas.append(updatedMetadata)
184
185                     Logger.ncFilesDatabase.debug(
186                         "Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
187                     )
188                 } else {
189                     Logger.ncFilesDatabase.debug(
190                         "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: .public)"
191                     )
192                 }
193
194             } else {  // This is a new metadata
195                 if !updateDirectoryEtags, updatedMetadata.directory {
196                     updatedMetadata.etag = ""
197                 }
198
199                 returningNewMetadatas.append(updatedMetadata)
200
201                 Logger.ncFilesDatabase.debug(
202                     "Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
203                 )
204             }
205         }
206
207         return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
208     }
209
210     func updateItemMetadatas(
211         account: String, 
212         serverUrl: String,
213         updatedMetadatas: [NextcloudItemMetadataTable],
214         updateDirectoryEtags: Bool
215     ) -> (
216         newMetadatas: [NextcloudItemMetadataTable]?,
217         updatedMetadatas: [NextcloudItemMetadataTable]?,
218         deletedMetadatas: [NextcloudItemMetadataTable]?
219     ) {
220         let database = ncDatabase()
221
222         do {
223             let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter(
224                 "account == %@ AND serverUrl == %@ AND status == %@", 
225                 account,
226                 serverUrl,
227                 NextcloudItemMetadataTable.Status.normal.rawValue)
228
229             let metadatasToDelete = processItemMetadatasToDelete(
230                 existingMetadatas: existingMetadatas,
231                 updatedMetadatas: updatedMetadatas)
232
233             let metadatasToChange = processItemMetadatasToUpdate(
234                 existingMetadatas: existingMetadatas,
235                 updatedMetadatas: updatedMetadatas,
236                 updateDirectoryEtags: updateDirectoryEtags)
237
238             var metadatasToUpdate = metadatasToChange.updatedMetadatas
239             let metadatasToCreate = metadatasToChange.newMetadatas
240             let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
241
242             let metadatasToAdd =
243                 Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) })
244                 + Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
245
246             for metadata in directoriesNeedingRename {
247                 if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(
248                     ocId: metadata.ocId, 
249                     newServerUrl: metadata.serverUrl,
250                     newFileName: metadata.fileName)
251                 {
252                     metadatasToUpdate += updatedDirectoryChildren
253                 }
254             }
255
256             try database.write {
257                 for metadata in metadatasToDelete {
258                     // Can't pass copies, we need the originals from the database
259                     database.delete(
260                         ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
261                             "ocId == %@", metadata.ocId))
262                 }
263
264                 for metadata in metadatasToAdd {
265                     database.add(metadata, update: .all)
266                 }
267             }
268
269             return (
270                 newMetadatas: metadatasToCreate, 
271                 updatedMetadatas: metadatasToUpdate,
272                 deletedMetadatas: metadatasToDelete
273             )
274         } catch {
275             Logger.ncFilesDatabase.error(
276                 "Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)"
277             )
278             return (nil, nil, nil)
279         }
280     }
281
282     func setStatusForItemMetadata(
283         _ metadata: NextcloudItemMetadataTable, 
284         status: NextcloudItemMetadataTable.Status,
285         completionHandler: @escaping (_ updatedMetadata: NextcloudItemMetadataTable?) -> Void
286     ) {
287         let database = ncDatabase()
288
289         do {
290             try database.write {
291                 guard
292                     let result = database.objects(NextcloudItemMetadataTable.self).filter(
293                         "ocId == %@", metadata.ocId
294                     ).first
295                 else {
296                     Logger.ncFilesDatabase.debug(
297                         "Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)"
298                     )
299                     return
300                 }
301
302                 result.status = status.rawValue
303                 database.add(result, update: .all)
304                 Logger.ncFilesDatabase.debug(
305                     "Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
306                 )
307
308                 completionHandler(NextcloudItemMetadataTable(value: result))
309             }
310         } catch {
311             Logger.ncFilesDatabase.error(
312                 "Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
313             )
314             completionHandler(nil)
315         }
316     }
317
318     func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
319         let database = ncDatabase()
320
321         do {
322             try database.write {
323                 database.add(metadata, update: .all)
324                 Logger.ncFilesDatabase.debug(
325                     "Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
326                 )
327             }
328         } catch {
329             Logger.ncFilesDatabase.error(
330                 "Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
331             )
332         }
333     }
334
335     @discardableResult func deleteItemMetadata(ocId: String) -> Bool {
336         let database = ncDatabase()
337
338         do {
339             try database.write {
340                 let results = database.objects(NextcloudItemMetadataTable.self).filter(
341                     "ocId == %@", ocId)
342
343                 Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
344                 database.delete(results)
345             }
346
347             return true
348         } catch {
349             Logger.ncFilesDatabase.error(
350                 "Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
351             )
352             return false
353         }
354     }
355
356     func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
357         let database = ncDatabase()
358
359         do {
360             try database.write {
361                 guard
362                     let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
363                         "ocId == %@", ocId
364                     ).first
365                 else {
366                     Logger.ncFilesDatabase.debug(
367                         "Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: .public)"
368                     )
369                     return
370                 }
371
372                 let oldFileName = itemMetadata.fileName
373                 let oldServerUrl = itemMetadata.serverUrl
374
375                 itemMetadata.fileName = newFileName
376                 itemMetadata.fileNameView = newFileName
377                 itemMetadata.serverUrl = newServerUrl
378
379                 database.add(itemMetadata, update: .all)
380
381                 Logger.ncFilesDatabase.debug(
382                     "Renamed item \(oldFileName, privacy: .public) to \(newFileName, privacy: .public), moved from serverUrl: \(oldServerUrl, privacy: .public) to serverUrl: \(newServerUrl, privacy: .public)"
383                 )
384             }
385         } catch {
386             Logger.ncFilesDatabase.error(
387                 "Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: .public) at proposed serverUrl \(newServerUrl, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
388             )
389         }
390     }
391
392     func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable)
393         -> NSFileProviderItemIdentifier?
394     {
395         let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
396
397         if metadata.serverUrl == homeServerFilesUrl {
398             return .rootContainer
399         }
400
401         guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
402             Logger.ncFilesDatabase.error(
403                 "Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
404             )
405             return nil
406         }
407
408         if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
409             return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
410         }
411
412         Logger.ncFilesDatabase.error(
413             "Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
414         )
415         return nil
416     }
417 }