fd5cc0cdc8017dd1d1e08f2cd4ffb5e5d563fb71
[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     static 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 = 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 static func scanRecursively(_ directoryMetadata: NextcloudItemMetadataTable, ncAccount: NextcloudAccount, ncKit: NextcloudKit, scanChangesOnly: Bool) -> (metadatas: [NextcloudItemMetadataTable], newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], deletedMetadatas: [NextcloudItemMetadataTable], error: NKError?) {
52
53         assert(directoryMetadata.directory, "Can only recursively scan a directory.")
54
55         var allMetadatas: [NextcloudItemMetadataTable] = []
56         var allNewMetadatas: [NextcloudItemMetadataTable] = []
57         var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
58         var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
59
60         let dbManager = NextcloudFilesDatabaseManager.shared
61         let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
62
63         dispatchGroup.enter()
64
65         var criticalError: NKError?
66         let itemServerUrl = directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue ?
67             ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
68
69         Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash))")
70
71         FileProviderEnumerator.readServerUrl(itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
72
73             if readError != nil {
74                 let nkReadError = NKError(error: readError!)
75
76                 // Is the error is that we have found matching etags on this item, then ignore it
77                 // if we are doing a full rescan
78                 guard nkReadError.isNoChangesError && scanChangesOnly else {
79                     Logger.enumeration.error("Finishing enumeration of changes at \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) with \(readError!.localizedDescription, privacy: .public)")
80
81                     if nkReadError.isNotFoundError {
82                         Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting as deletion without error")
83
84                         if let deletedMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: directoryMetadata.ocId) {
85                             allDeletedMetadatas += deletedMetadatas
86                         } else {
87                             Logger.enumeration.error("An error occurred while trying to delete directory and children not found in recursive scan")
88                         }
89
90                     } else if nkReadError.isNoChangesError { // All is well, just no changed etags
91                         Logger.enumeration.info("Error was to say no changed files -- not bad error. No need to check children.")
92
93                     } else if nkReadError.isUnauthenticatedError || nkReadError.isCouldntConnectError {
94                         // If it is a critical error then stop, if not then continue
95                         Logger.enumeration.error("Error will affect next enumerated items, so stopping enumeration.")
96                         criticalError = nkReadError
97
98                     }
99
100                     dispatchGroup.leave()
101                     return
102                 }
103             }
104
105             Logger.enumeration.info("Finished reading serverUrl: \(itemServerUrl, privacy: OSLogPrivacy.auto(mask: .hash)) for user: \(ncAccount.ncKitAccount, privacy: OSLogPrivacy.auto(mask: .hash))")
106
107             if let metadatas = metadatas {
108                 allMetadatas += metadatas
109             } else {
110                 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))")
111             }
112
113             if let newMetadatas = newMetadatas {
114                 allNewMetadatas += newMetadatas
115             } else {
116                 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))")
117             }
118
119             if let updatedMetadatas = updatedMetadatas {
120                 allUpdatedMetadatas += updatedMetadatas
121             } else {
122                 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))")
123             }
124
125             if let deletedMetadatas = deletedMetadatas {
126                 allDeletedMetadatas += deletedMetadatas
127             } else {
128                 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))")
129             }
130
131             dispatchGroup.leave()
132         }
133
134         dispatchGroup.wait()
135
136         if criticalError != nil {
137             return ([], [], [], [], error: criticalError)
138         }
139
140         var updatedDirectories: [NextcloudItemMetadataTable] = []
141         for updatedMetadata in allUpdatedMetadatas {
142             if updatedMetadata.directory {
143                 updatedDirectories.append(updatedMetadata)
144             }
145         }
146
147         if updatedDirectories.isEmpty {
148             return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
149         }
150
151         for childDirectory in updatedDirectories {
152             let childScanResult = scanRecursively(childDirectory, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly)
153
154             allMetadatas += childScanResult.metadatas
155             allNewMetadatas += childScanResult.newMetadatas
156             allUpdatedMetadatas += childScanResult.updatedMetadatas
157             allDeletedMetadatas += childScanResult.deletedMetadatas
158         }
159
160         return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
161     }
162
163     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) {
164         let dbManager = NextcloudFilesDatabaseManager.shared
165         let ncKitAccount = ncAccount.ncKitAccount
166
167         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)")
168
169         ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: "0", showHiddenFiles: true) { account, files, _, error in
170             guard error == .success else {
171                 Logger.enumeration.error("0 depth readFileOrFolder of url: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) did not complete successfully, received error: \(error.errorDescription, privacy: .public)")
172                 completionHandler(nil, nil, nil, nil, error.error)
173                 return
174             }
175
176             guard let receivedItem = files.first else {
177                 Logger.enumeration.error("Received no items from readFileOrFolder of \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)), not much we can do...")
178                 completionHandler(nil, nil, nil, nil, error.error)
179                 return
180             }
181
182             guard receivedItem.directory else {
183                 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))")
184                 let itemMetadata = dbManager.convertNKFileToItemMetadata(receivedItem, account: ncKitAccount)
185                 dbManager.addItemMetadata(itemMetadata) // TODO: Return some value when it is an update
186                 completionHandler([itemMetadata], nil, nil, nil, error.error)
187                 return
188             }
189
190             if stopAtMatchingEtags,
191                let directoryMetadata = dbManager.directoryMetadata(account: ncKitAccount, serverUrl: serverUrl) {
192
193                 let directoryEtag = directoryMetadata.etag
194
195                 guard directoryEtag == "" || directoryEtag != receivedItem.etag else {
196                     Logger.enumeration.debug("Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error.")
197
198                     let description = "Fetched directory etag is same as that stored locally. Not fetching child items."
199                     let nkError = NKError(errorCode: NKError.noChangesErrorCode, errorDescription: description)
200
201                     let metadatas = dbManager.itemMetadatas(account: account, serverUrl: serverUrl)
202
203                     completionHandler(metadatas, nil, nil, nil, nkError.error)
204                     return
205                 }
206             }
207
208             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")
209
210             ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: "1", showHiddenFiles: true) { account, files, _, error in
211                 guard error == .success else {
212                     Logger.enumeration.error("1 depth readFileOrFolder of url: \(serverUrl, privacy: OSLogPrivacy.auto(mask: .hash)) did not complete successfully, received error: \(error.errorDescription, privacy: .public)")
213                     completionHandler(nil, nil, nil, nil, error.error)
214                     return
215                 }
216
217                 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))")
218                 DispatchQueue.main.async {
219                     dbManager.convertNKFilesFromDirectoryReadToItemMetadatas(files, account: ncKitAccount) { directoryMetadata, childDirectoriesMetadata, metadatas in
220
221                         // STORE DATA FOR CURRENTLY SCANNED DIRECTORY
222                         // We have now scanned this directory's contents, so update with etag in order to not check again if not needed
223                         // unless it's the root container
224                         if serverUrl != ncAccount.davFilesUrl {
225                             dbManager.addItemMetadata(directoryMetadata)
226                         }
227
228                         // Don't update the etags for folders as we haven't checked their contents.
229                         // When we do a recursive check, if we update the etags now, we will think
230                         // that our local copies are up to date -- instead, leave them as the old.
231                         // They will get updated when they are the subject of a readServerUrl call.
232                         // (See above)
233                         let changedMetadatas = dbManager.updateItemMetadatas(account: ncKitAccount, serverUrl: serverUrl, updatedMetadatas: metadatas, updateDirectoryEtags: false)
234
235                         completionHandler(metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas, changedMetadatas.deletedMetadatas, nil)
236                     }
237                 }
238             }
239         }
240     }
241 }