]> dgit.raspbian.org Git - nextcloud-desktop.git/blob
efc5ba69e95dac6db66146a1cdd7ff8668248c57
[nextcloud-desktop.git] /
1 /*
2  * Copyright (C) 2023 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 NextcloudKit
17 import OSLog
18
19 extension FileProviderEnumerator {
20     func fullRecursiveScan(ncAccount: NextcloudAccount, ncKit: NextcloudKit, scanChangesOnly: Bool, completionHandler: @escaping(_ metadatas: [NextcloudItemMetadataTable], _ newMetadatas: [NextcloudItemMetadataTable], _ updatedMetadatas: [NextcloudItemMetadataTable], _ deletedMetadatas: [NextcloudItemMetadataTable], _ error: NKError?) -> Void) {
21
22         let rootContainerDirectoryMetadata = NextcloudItemMetadataTable()
23         rootContainerDirectoryMetadata.directory = true
24         rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
25
26         // Create a serial dispatch queue
27         let dispatchQueue = DispatchQueue(label: "recursiveChangeEnumerationQueue", qos: .background)
28
29         dispatchQueue.async {
30             let results = self.scanRecursively(rootContainerDirectoryMetadata, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly)
31
32             // Run a check to ensure files deleted in one location are not updated in another (e.g. when moved)
33             // The recursive scan provides us with updated/deleted metadatas only on a folder by folder basis;
34             // so we need to check we are not simultaneously marking a moved file as deleted and updated
35             var checkedDeletedMetadatas = results.deletedMetadatas
36
37             for updatedMetadata in results.updatedMetadatas {
38                 guard let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: { $0.ocId == updatedMetadata.ocId } ) else {
39                     continue;
40                 }
41
42                 checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx)
43             }
44
45             DispatchQueue.main.async {
46                 completionHandler(results.metadatas, results.newMetadatas, results.updatedMetadatas, checkedDeletedMetadatas, results.error)
47             }
48         }
49     }
50
51     private func scanRecursively(_ directoryMetadata: NextcloudItemMetadataTable, ncAccount: NextcloudAccount, ncKit: NextcloudKit, scanChangesOnly: Bool) -> (metadatas: [NextcloudItemMetadataTable], newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], deletedMetadatas: [NextcloudItemMetadataTable], error: NKError?) {
52
53         if self.isInvalidated {
54             return ([], [], [], [], nil)
55         }
56
57         assert(directoryMetadata.directory, "Can only recursively scan a directory.")
58
59         var allMetadatas: [NextcloudItemMetadataTable] = []
60         var allNewMetadatas: [NextcloudItemMetadataTable] = []
61         var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
62         var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
63
64         let dbManager = NextcloudFilesDatabaseManager.shared
65         let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
66
67         dispatchGroup.enter()
68
69         var criticalError: NKError?
70         let itemServerUrl = directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue ?
71             ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
72
73         Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash))")
74
75         FileProviderEnumerator.readServerUrl(itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
76
77             if readError != nil {
78                 let nkReadError = NKError(error: readError!)
79
80                 // Is the error is that we have found matching etags on this item, then ignore it
81                 // if we are doing a full rescan
82                 guard nkReadError.isNoChangesError && scanChangesOnly else {
83                     Logger.enumeration.error("Finishing enumeration of changes at \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) with \(readError!.localizedDescription, privacy: .public)")
84
85                     if nkReadError.isNotFoundError {
86                         Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting as deletion without error")
87
88                         if let deletedMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: directoryMetadata.ocId) {
89                             allDeletedMetadatas += deletedMetadatas
90                         } else {
91                             Logger.enumeration.error("An error occurred while trying to delete directory and children not found in recursive scan")
92                         }
93
94                     } else if nkReadError.isNoChangesError { // All is well, just no changed etags
95                         Logger.enumeration.info("Error was to say no changed files -- not bad error. No need to check children.")
96
97                     } else if nkReadError.isUnauthenticatedError || nkReadError.isCouldntConnectError {
98                         // If it is a critical error then stop, if not then continue
99                         Logger.enumeration.error("Error will affect next enumerated items, so stopping enumeration.")
100                         criticalError = nkReadError
101
102                     }
103
104                     dispatchGroup.leave()
105                     return
106                 }
107             }
108
109             Logger.enumeration.info("Finished reading serverUrl: \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
110
111             if let metadatas = metadatas {
112                 allMetadatas += metadatas
113             } else {
114                 Logger.enumeration.warning("WARNING: Nil metadatas received for reading of changes at \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
115             }
116
117             if let newMetadatas = newMetadatas {
118                 allNewMetadatas += newMetadatas
119             } else {
120                 Logger.enumeration.warning("WARNING: Nil new metadatas received for reading of changes at \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
121             }
122
123             if let updatedMetadatas = updatedMetadatas {
124                 allUpdatedMetadatas += updatedMetadatas
125             } else {
126                 Logger.enumeration.warning("WARNING: Nil updated metadatas received for reading of changes at \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
127             }
128
129             if let deletedMetadatas = deletedMetadatas {
130                 allDeletedMetadatas += deletedMetadatas
131             } else {
132                 Logger.enumeration.warning("WARNING: Nil deleted metadatas received for reading of changes at \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
133             }
134
135             dispatchGroup.leave()
136         }
137
138         dispatchGroup.wait()
139
140         if criticalError != nil {
141             return ([], [], [], [], error: criticalError)
142         }
143
144         var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
145         var candidateMetadatas: [NextcloudItemMetadataTable]
146
147         if scanChangesOnly {
148             candidateMetadatas = allUpdatedMetadatas
149         } else {
150             candidateMetadatas = allMetadatas
151         }
152
153         for candidateMetadata in candidateMetadatas {
154             if candidateMetadata.directory {
155                 childDirectoriesToScan.append(candidateMetadata)
156             }
157         }
158
159         if childDirectoriesToScan.isEmpty {
160             return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
161         }
162
163         for childDirectory in childDirectoriesToScan {
164             let childScanResult = scanRecursively(childDirectory, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly)
165
166             allMetadatas += childScanResult.metadatas
167             allNewMetadatas += childScanResult.newMetadatas
168             allUpdatedMetadatas += childScanResult.updatedMetadatas
169             allDeletedMetadatas += childScanResult.deletedMetadatas
170         }
171
172         return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
173     }
174
175     static func readServerUrl(_ serverUrl: String, ncAccount: NextcloudAccount, ncKit: NextcloudKit, stopAtMatchingEtags: Bool = false, completionHandler: @escaping (_ metadatas: [NextcloudItemMetadataTable]?, _ newMetadatas: [NextcloudItemMetadataTable]?, _ updatedMetadatas: [NextcloudItemMetadataTable]?, _ deletedMetadatas: [NextcloudItemMetadataTable]?, _ readError: Error?) -> Void) {
176         let dbManager = NextcloudFilesDatabaseManager.shared
177         let ncKitAccount = ncAccount.ncKitAccount
178
179         Logger.enumeration.debug("Starting to read serverUrl: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash)) at depth 0. NCKit info: userId: \(ncKit.nkCommonInstance.user), password: \(ncKit.nkCommonInstance.password == "" ? "EMPTY PASSWORD" : "NOT EMPTY PASSWORD"), urlBase: \(ncKit.nkCommonInstance.urlBase), ncVersion: \(ncKit.nkCommonInstance.nextcloudVersion)")
180
181         ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: "0", showHiddenFiles: true) { account, files, _, error in
182             guard error == .success else {
183                 Logger.enumeration.error("0 depth readFileOrFolder of url: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) did not complete successfully, received error: \(error.errorDescription, privacy: .public)")
184                 completionHandler(nil, nil, nil, nil, error.error)
185                 return
186             }
187
188             guard let receivedItem = files.first else {
189                 Logger.enumeration.error("Received no items from readFileOrFolder of \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)), not much we can do...")
190                 completionHandler(nil, nil, nil, nil, error.error)
191                 return
192             }
193
194             guard receivedItem.directory else {
195                 Logger.enumeration.debug("Read item is a file. Converting NKfile for serverUrl: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
196                 let itemMetadata = dbManager.convertNKFileToItemMetadata(receivedItem, account: ncKitAccount)
197                 dbManager.addItemMetadata(itemMetadata) // TODO: Return some value when it is an update
198                 completionHandler([itemMetadata], nil, nil, nil, error.error)
199                 return
200             }
201
202             if stopAtMatchingEtags,
203                let directoryMetadata = dbManager.directoryMetadata(account: ncKitAccount, serverUrl: serverUrl) {
204
205                 let directoryEtag = directoryMetadata.etag
206
207                 guard directoryEtag == "" || directoryEtag != receivedItem.etag else {
208                     Logger.enumeration.debug("Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error.")
209
210                     let description = "Fetched directory etag is same as that stored locally. Not fetching child items."
211                     let nkError = NKError(errorCode: NKError.noChangesErrorCode, errorDescription: description)
212
213                     let metadatas = dbManager.itemMetadatas(account: account, serverUrl: serverUrl)
214
215                     completionHandler(metadatas, nil, nil, nil, nkError.error)
216                     return
217                 }
218             }
219
220             Logger.enumeration.debug("Starting to read serverUrl: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash)) at depth 1")
221
222             ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: "1", showHiddenFiles: true) { account, files, _, error in
223                 guard error == .success else {
224                     Logger.enumeration.error("1 depth readFileOrFolder of url: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) did not complete successfully, received error: \(error.errorDescription, privacy: .public)")
225                     completionHandler(nil, nil, nil, nil, error.error)
226                     return
227                 }
228
229                 Logger.enumeration.debug("Starting async conversion of NKFiles for serverUrl: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
230                 DispatchQueue.main.async {
231                     dbManager.convertNKFilesFromDirectoryReadToItemMetadatas(files, account: ncKitAccount) { directoryMetadata, childDirectoriesMetadata, metadatas in
232
233                         // STORE DATA FOR CURRENTLY SCANNED DIRECTORY
234                         // We have now scanned this directory's contents, so update with etag in order to not check again if not needed
235                         // unless it's the root container
236                         if serverUrl != ncAccount.davFilesUrl {
237                             dbManager.addItemMetadata(directoryMetadata)
238                         }
239
240                         // Don't update the etags for folders as we haven't checked their contents.
241                         // When we do a recursive check, if we update the etags now, we will think
242                         // that our local copies are up to date -- instead, leave them as the old.
243                         // They will get updated when they are the subject of a readServerUrl call.
244                         // (See above)
245                         let changedMetadatas = dbManager.updateItemMetadatas(account: ncKitAccount, serverUrl: serverUrl, updatedMetadatas: metadatas, updateDirectoryEtags: false)
246
247                         completionHandler(metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas, changedMetadatas.deletedMetadatas, nil)
248                     }
249                 }
250             }
251         }
252     }
253 }