2023-03-18 15:41:31 +03:00
/*
* Copyright ( C ) 2023 by Claudio Cambra < claudio . cambra @ nextcloud . com >
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful , but
* WITHOUT ANY WARRANTY ; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE . See the GNU General Public License
* for more details .
*/
import FileProvider
import NextcloudKit
import OSLog
2023-03-18 20:04:19 +03:00
class NextcloudSyncEngine : NSObject {
var isInvalidated = false
func invalidate ( ) {
self . isInvalidated = true
}
2023-03-18 18:44:30 +03:00
func fullRecursiveScan ( ncAccount : NextcloudAccount ,
ncKit : NextcloudKit ,
scanChangesOnly : Bool ,
completionHandler : @ escaping ( _ metadatas : [ NextcloudItemMetadataTable ] ,
_ newMetadatas : [ NextcloudItemMetadataTable ] ,
_ updatedMetadatas : [ NextcloudItemMetadataTable ] ,
_ deletedMetadatas : [ NextcloudItemMetadataTable ] ,
_ error : NKError ? ) -> Void ) {
2023-03-18 15:41:31 +03:00
let rootContainerDirectoryMetadata = NextcloudItemMetadataTable ( )
rootContainerDirectoryMetadata . directory = true
rootContainerDirectoryMetadata . ocId = NSFileProviderItemIdentifier . rootContainer . rawValue
// C r e a t e a s e r i a l d i s p a t c h q u e u e
2023-03-18 20:04:19 +03:00
let dispatchQueue = DispatchQueue ( label : " recursiveChangeEnumerationQueue " , qos : . userInitiated )
2023-03-18 15:41:31 +03:00
dispatchQueue . async {
2023-03-18 18:44:30 +03:00
let results = self . scanRecursively ( rootContainerDirectoryMetadata ,
ncAccount : ncAccount ,
ncKit : ncKit ,
2023-03-18 21:01:10 +03:00
scanChangesOnly : scanChangesOnly )
2023-03-18 15:41:31 +03:00
2023-03-18 16:10:24 +03:00
// R u n a c h e c k t o e n s u r e f i l e s d e l e t e d i n o n e l o c a t i o n a r e n o t u p d a t e d i n a n o t h e r ( e . g . w h e n m o v e d )
// T h e r e c u r s i v e s c a n p r o v i d e s u s w i t h u p d a t e d / d e l e t e d m e t a d a t a s o n l y o n a f o l d e r b y f o l d e r b a s i s ;
// s o w e n e e d t o c h e c k w e a r e n o t s i m u l t a n e o u s l y m a r k i n g a m o v e d f i l e a s d e l e t e d a n d u p d a t e d
var checkedDeletedMetadatas = results . deletedMetadatas
for updatedMetadata in results . updatedMetadatas {
guard let matchingDeletedMetadataIdx = checkedDeletedMetadatas . firstIndex ( where : { $0 . ocId = = updatedMetadata . ocId } ) else {
continue ;
}
checkedDeletedMetadatas . remove ( at : matchingDeletedMetadataIdx )
}
2023-03-18 15:41:31 +03:00
DispatchQueue . main . async {
2023-03-18 16:10:24 +03:00
completionHandler ( results . metadatas , results . newMetadatas , results . updatedMetadatas , checkedDeletedMetadatas , results . error )
2023-03-18 15:41:31 +03:00
}
}
}
2023-03-18 18:44:30 +03:00
private func scanRecursively ( _ directoryMetadata : NextcloudItemMetadataTable ,
ncAccount : NextcloudAccount ,
ncKit : NextcloudKit ,
2023-03-18 21:01:10 +03:00
scanChangesOnly : Bool ) -> ( metadatas : [ NextcloudItemMetadataTable ] ,
newMetadatas : [ NextcloudItemMetadataTable ] ,
updatedMetadatas : [ NextcloudItemMetadataTable ] ,
deletedMetadatas : [ NextcloudItemMetadataTable ] ,
error : NKError ? ) {
2023-03-18 18:05:34 +03:00
if self . isInvalidated {
return ( [ ] , [ ] , [ ] , [ ] , nil )
}
2023-03-18 15:41:31 +03:00
assert ( directoryMetadata . directory , " Can only recursively scan a directory. " )
2023-03-18 18:44:30 +03:00
// S c a n n e d i n t h i s d i r e c t o r y
var currentMetadatas : [ NextcloudItemMetadataTable ] = [ ]
// W i l l i n c l u d e r e s u l t s o f r e c u r s i v e c a l l s
2023-03-18 15:41:31 +03:00
var allMetadatas : [ NextcloudItemMetadataTable ] = [ ]
var allNewMetadatas : [ NextcloudItemMetadataTable ] = [ ]
var allUpdatedMetadatas : [ NextcloudItemMetadataTable ] = [ ]
var allDeletedMetadatas : [ NextcloudItemMetadataTable ] = [ ]
let dbManager = NextcloudFilesDatabaseManager . shared
let dispatchGroup = DispatchGroup ( ) // TODO: M a y b e o w n t h r e a d ?
dispatchGroup . enter ( )
2023-03-18 15:57:02 +03:00
var criticalError : NKError ?
2023-03-18 15:41:31 +03:00
let itemServerUrl = directoryMetadata . ocId = = NSFileProviderItemIdentifier . rootContainer . rawValue ?
ncAccount . davFilesUrl : directoryMetadata . serverUrl + " / " + directoryMetadata . fileName
Logger . enumeration . debug ( " About to read: \( itemServerUrl , privacy : OSLogPrivacy . auto ( mask : . hash ) ) " )
2023-03-18 20:04:19 +03:00
NextcloudSyncEngine . readServerUrl ( itemServerUrl , ncAccount : ncAccount , ncKit : ncKit , stopAtMatchingEtags : scanChangesOnly ) { metadatas , newMetadatas , updatedMetadatas , deletedMetadatas , readError in
2023-03-18 15:41:31 +03:00
if readError != nil {
let nkReadError = NKError ( error : readError ! )
// I s t h e e r r o r i s t h a t w e h a v e f o u n d m a t c h i n g e t a g s o n t h i s i t e m , t h e n i g n o r e i t
// i f w e a r e d o i n g a f u l l r e s c a n
guard nkReadError . isNoChangesError && scanChangesOnly else {
Logger . enumeration . error ( " Finishing enumeration of changes at \( itemServerUrl , privacy : OSLogPrivacy . auto ( mask : . hash ) ) with \( readError ! . localizedDescription , privacy : . public ) " )
if nkReadError . isNotFoundError {
Logger . enumeration . info ( " 404 error means item no longer exists. Deleting metadata and reporting as deletion without error " )
if let deletedMetadatas = dbManager . deleteDirectoryAndSubdirectoriesMetadata ( ocId : directoryMetadata . ocId ) {
allDeletedMetadatas += deletedMetadatas
} else {
Logger . enumeration . error ( " An error occurred while trying to delete directory and children not found in recursive scan " )
}
2023-03-18 15:57:02 +03:00
2023-03-18 15:41:31 +03:00
} else if nkReadError . isNoChangesError { // A l l i s w e l l , j u s t n o c h a n g e d e t a g s
Logger . enumeration . info ( " Error was to say no changed files -- not bad error. No need to check children. " )
2023-03-18 15:57:02 +03:00
} else if nkReadError . isUnauthenticatedError || nkReadError . isCouldntConnectError {
// I f i t i s a c r i t i c a l e r r o r t h e n s t o p , i f n o t t h e n c o n t i n u e
Logger . enumeration . error ( " Error will affect next enumerated items, so stopping enumeration. " )
criticalError = nkReadError
2023-03-18 15:41:31 +03:00
}
dispatchGroup . leave ( )
return
}
}
Logger . enumeration . info ( " Finished reading serverUrl: \( itemServerUrl , privacy : OSLogPrivacy . auto ( mask : . hash ) ) for user: \( ncAccount . ncKitAccount , privacy : OSLogPrivacy . auto ( mask : . hash ) ) " )
if let metadatas = metadatas {
2023-03-18 18:44:30 +03:00
currentMetadatas = metadatas
2023-03-18 15:41:31 +03:00
allMetadatas += metadatas
} else {
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 ) ) " )
}
if let newMetadatas = newMetadatas {
allNewMetadatas += newMetadatas
} else {
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 ) ) " )
}
if let updatedMetadatas = updatedMetadatas {
allUpdatedMetadatas += updatedMetadatas
} else {
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 ) ) " )
}
if let deletedMetadatas = deletedMetadatas {
allDeletedMetadatas += deletedMetadatas
} else {
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 ) ) " )
}
dispatchGroup . leave ( )
}
dispatchGroup . wait ( )
2023-03-18 18:44:30 +03:00
guard criticalError = = nil else {
2023-03-18 15:57:02 +03:00
return ( [ ] , [ ] , [ ] , [ ] , error : criticalError )
}
2023-03-18 17:49:20 +03:00
var childDirectoriesToScan : [ NextcloudItemMetadataTable ] = [ ]
var candidateMetadatas : [ NextcloudItemMetadataTable ]
if scanChangesOnly {
candidateMetadatas = allUpdatedMetadatas
} else {
candidateMetadatas = allMetadatas
}
for candidateMetadata in candidateMetadatas {
if candidateMetadata . directory {
childDirectoriesToScan . append ( candidateMetadata )
2023-03-18 15:41:31 +03:00
}
}
2023-03-18 17:49:20 +03:00
if childDirectoriesToScan . isEmpty {
2023-03-18 15:57:02 +03:00
return ( metadatas : allMetadatas , newMetadatas : allNewMetadatas , updatedMetadatas : allUpdatedMetadatas , deletedMetadatas : allDeletedMetadatas , nil )
2023-03-18 15:41:31 +03:00
}
2023-03-18 17:49:20 +03:00
for childDirectory in childDirectoriesToScan {
2023-03-18 18:44:30 +03:00
let childScanResult = scanRecursively ( childDirectory ,
ncAccount : ncAccount ,
ncKit : ncKit ,
2023-03-18 21:01:10 +03:00
scanChangesOnly : scanChangesOnly )
2023-03-18 15:41:31 +03:00
allMetadatas += childScanResult . metadatas
allNewMetadatas += childScanResult . newMetadatas
allUpdatedMetadatas += childScanResult . updatedMetadatas
allDeletedMetadatas += childScanResult . deletedMetadatas
}
2023-03-18 15:57:02 +03:00
return ( metadatas : allMetadatas , newMetadatas : allNewMetadatas , updatedMetadatas : allUpdatedMetadatas , deletedMetadatas : allDeletedMetadatas , nil )
2023-03-18 15:41:31 +03:00
}
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 ) {
let dbManager = NextcloudFilesDatabaseManager . shared
let ncKitAccount = ncAccount . ncKitAccount
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 ) " )
ncKit . readFileOrFolder ( serverUrlFileName : serverUrl , depth : " 0 " , showHiddenFiles : true ) { account , files , _ , error in
guard error = = . success else {
Logger . enumeration . error ( " 0 depth readFileOrFolder of url: \( serverUrl , privacy : OSLogPrivacy . auto ( mask : . hash ) ) did not complete successfully, received error: \( error . errorDescription , privacy : . public ) " )
completionHandler ( nil , nil , nil , nil , error . error )
return
}
guard let receivedItem = files . first else {
Logger . enumeration . error ( " Received no items from readFileOrFolder of \( serverUrl , privacy : OSLogPrivacy . auto ( mask : . hash ) ) , not much we can do... " )
completionHandler ( nil , nil , nil , nil , error . error )
return
}
guard receivedItem . directory else {
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 ) ) " )
let itemMetadata = dbManager . convertNKFileToItemMetadata ( receivedItem , account : ncKitAccount )
dbManager . addItemMetadata ( itemMetadata ) // TODO: R e t u r n s o m e v a l u e w h e n i t i s a n u p d a t e
completionHandler ( [ itemMetadata ] , nil , nil , nil , error . error )
return
}
if stopAtMatchingEtags ,
let directoryMetadata = dbManager . directoryMetadata ( account : ncKitAccount , serverUrl : serverUrl ) {
let directoryEtag = directoryMetadata . etag
guard directoryEtag = = " " || directoryEtag != receivedItem . etag else {
Logger . enumeration . debug ( " Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error. " )
let description = " Fetched directory etag is same as that stored locally. Not fetching child items. "
let nkError = NKError ( errorCode : NKError . noChangesErrorCode , errorDescription : description )
let metadatas = dbManager . itemMetadatas ( account : account , serverUrl : serverUrl )
completionHandler ( metadatas , nil , nil , nil , nkError . error )
return
}
}
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 " )
ncKit . readFileOrFolder ( serverUrlFileName : serverUrl , depth : " 1 " , showHiddenFiles : true ) { account , files , _ , error in
guard error = = . success else {
Logger . enumeration . error ( " 1 depth readFileOrFolder of url: \( serverUrl , privacy : OSLogPrivacy . auto ( mask : . hash ) ) did not complete successfully, received error: \( error . errorDescription , privacy : . public ) " )
completionHandler ( nil , nil , nil , nil , error . error )
return
}
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 ) ) " )
DispatchQueue . main . async {
dbManager . convertNKFilesFromDirectoryReadToItemMetadatas ( files , account : ncKitAccount ) { directoryMetadata , childDirectoriesMetadata , metadatas in
// S T O R E D A T A F O R C U R R E N T L Y S C A N N E D D I R E C T O R Y
// W e h a v e n o w s c a n n e d t h i s d i r e c t o r y ' s c o n t e n t s , s o u p d a t e w i t h e t a g i n o r d e r t o n o t c h e c k a g a i n i f n o t n e e d e d
// u n l e s s i t ' s t h e r o o t c o n t a i n e r
if serverUrl != ncAccount . davFilesUrl {
dbManager . addItemMetadata ( directoryMetadata )
}
// D o n ' t u p d a t e t h e e t a g s f o r f o l d e r s a s w e h a v e n ' t c h e c k e d t h e i r c o n t e n t s .
// W h e n w e d o a r e c u r s i v e c h e c k , i f w e u p d a t e t h e e t a g s n o w , w e w i l l t h i n k
// t h a t o u r l o c a l c o p i e s a r e u p t o d a t e - - i n s t e a d , l e a v e t h e m a s t h e o l d .
// T h e y w i l l g e t u p d a t e d w h e n t h e y a r e t h e s u b j e c t o f a r e a d S e r v e r U r l c a l l .
// ( S e e a b o v e )
let changedMetadatas = dbManager . updateItemMetadatas ( account : ncKitAccount , serverUrl : serverUrl , updatedMetadatas : metadatas , updateDirectoryEtags : false )
completionHandler ( metadatas , changedMetadatas . newMetadatas , changedMetadatas . updatedMetadatas , changedMetadatas . deletedMetadatas , nil )
}
}
}
}
}
}