2 * Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
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.
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
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) {
22 let rootContainerDirectoryMetadata = NextcloudItemMetadataTable()
23 rootContainerDirectoryMetadata.directory = true
24 rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
26 // Create a serial dispatch queue
27 let dispatchQueue = DispatchQueue(label: "recursiveChangeEnumerationQueue", qos: .background)
30 let results = self.scanRecursively(rootContainerDirectoryMetadata, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly)
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
37 for updatedMetadata in results.updatedMetadatas {
38 guard let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: { $0.ocId == updatedMetadata.ocId } ) else {
42 checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx)
45 DispatchQueue.main.async {
46 completionHandler(results.metadatas, results.newMetadatas, results.updatedMetadatas, checkedDeletedMetadatas, results.error)
51 private func scanRecursively(_ directoryMetadata: NextcloudItemMetadataTable, ncAccount: NextcloudAccount, ncKit: NextcloudKit, scanChangesOnly: Bool) -> (metadatas: [NextcloudItemMetadataTable], newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], deletedMetadatas: [NextcloudItemMetadataTable], error: NKError?) {
53 if self.isInvalidated {
54 return ([], [], [], [], nil)
57 assert(directoryMetadata.directory, "Can only recursively scan a directory.")
59 var allMetadatas: [NextcloudItemMetadataTable] = []
60 var allNewMetadatas: [NextcloudItemMetadataTable] = []
61 var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
62 var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
64 let dbManager = NextcloudFilesDatabaseManager.shared
65 let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
69 var criticalError: NKError?
70 let itemServerUrl = directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue ?
71 ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
73 Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash))")
75 FileProviderEnumerator.readServerUrl(itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
78 let nkReadError = NKError(error: readError!)
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)")
85 if nkReadError.isNotFoundError {
86 Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting as deletion without error")
88 if let deletedMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: directoryMetadata.ocId) {
89 allDeletedMetadatas += deletedMetadatas
91 Logger.enumeration.error("An error occurred while trying to delete directory and children not found in recursive scan")
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.")
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
104 dispatchGroup.leave()
109 Logger.enumeration.info("Finished reading serverUrl: \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
111 if let metadatas = metadatas {
112 allMetadatas += metadatas
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))")
117 if let newMetadatas = newMetadatas {
118 allNewMetadatas += newMetadatas
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))")
123 if let updatedMetadatas = updatedMetadatas {
124 allUpdatedMetadatas += updatedMetadatas
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))")
129 if let deletedMetadatas = deletedMetadatas {
130 allDeletedMetadatas += deletedMetadatas
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))")
135 dispatchGroup.leave()
140 if criticalError != nil {
141 return ([], [], [], [], error: criticalError)
144 var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
145 var candidateMetadatas: [NextcloudItemMetadataTable]
148 candidateMetadatas = allUpdatedMetadatas
150 candidateMetadatas = allMetadatas
153 for candidateMetadata in candidateMetadatas {
154 if candidateMetadata.directory {
155 childDirectoriesToScan.append(candidateMetadata)
159 if childDirectoriesToScan.isEmpty {
160 return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
163 for childDirectory in childDirectoriesToScan {
164 let childScanResult = scanRecursively(childDirectory, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly)
166 allMetadatas += childScanResult.metadatas
167 allNewMetadatas += childScanResult.newMetadatas
168 allUpdatedMetadatas += childScanResult.updatedMetadatas
169 allDeletedMetadatas += childScanResult.deletedMetadatas
172 return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
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
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)")
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)
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)
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)
202 if stopAtMatchingEtags,
203 let directoryMetadata = dbManager.directoryMetadata(account: ncKitAccount, serverUrl: serverUrl) {
205 let directoryEtag = directoryMetadata.etag
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.")
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)
213 let metadatas = dbManager.itemMetadatas(account: account, serverUrl: serverUrl)
215 completionHandler(metadatas, nil, nil, nil, nkError.error)
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")
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)
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
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)
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.
245 let changedMetadatas = dbManager.updateItemMetadatas(account: ncKitAccount, serverUrl: serverUrl, updatedMetadatas: metadatas, updateDirectoryEtags: false)
247 completionHandler(metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas, changedMetadatas.deletedMetadatas, nil)