2023-03-21 15:28:48 +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
2024-01-22 09:40:47 +03:00
import Foundation
2023-03-21 15:28:48 +03:00
import NCDesktopClientSocketKit
import NextcloudKit
2024-04-15 12:30:08 +03:00
import NextcloudFileProviderKit
2024-01-22 09:40:47 +03:00
import OSLog
2023-03-21 15:28:48 +03:00
2024-09-30 08:52:18 +03:00
let AuthenticationTimeouts : [ UInt64 ] = [ // H a v e p r o g r e s s i v e l y l o n g e r t i m e o u t s t o n o t h a m m e r s e r v e r
3_000_000_000 , 6_000_000_000 , 30_000_000_000 , 60_000_000_000 , 120_000_000_000 , 300_000_000_000
]
2024-07-19 10:17:59 +03:00
extension FileProviderExtension : NSFileProviderServicing , ChangeNotificationInterface {
2024-01-23 17:37:49 +03:00
/*
This FileProviderExtension extension contains everything needed to communicate with the client .
We have two systems for communicating between the extensions and the client .
Apple ' s XPC based File Provider APIs let us easily communicate client -> extension .
This is what ClientCommunicationService is for .
We also use sockets , because the File Provider XPC system does not let us easily talk from
extension -> client .
We need this because the extension needs to be able to request account details . We can ' t
reliably do this via XPC because the extensions get torn down by the system , out of the control
of the app , and we can receive nil / no services from NSFileProviderManager . Once this is done
then XPC works ok .
*/
2024-01-02 09:12:49 +03:00
func supportedServiceSources (
for itemIdentifier : NSFileProviderItemIdentifier ,
completionHandler : @ escaping ( [ NSFileProviderServiceSource ] ? , Error ? ) -> Void
) -> Progress {
Logger . desktopClientConnection . debug ( " Serving supported service sources " )
let clientCommService = ClientCommunicationService ( fpExtension : self )
2024-02-27 16:25:22 +03:00
let fpuiExtService = FPUIExtensionServiceSource ( fpExtension : self )
let services : [ NSFileProviderServiceSource ] = [ clientCommService , fpuiExtService ]
2024-01-02 09:12:49 +03:00
completionHandler ( services , nil )
let progress = Progress ( )
progress . cancellationHandler = {
let error = NSError ( domain : NSCocoaErrorDomain , code : NSUserCancelledError )
completionHandler ( nil , error )
}
return progress
}
2023-11-21 05:40:54 +03:00
@objc func sendFileProviderDomainIdentifier ( ) {
2023-03-21 15:28:48 +03:00
let command = " FILE_PROVIDER_DOMAIN_IDENTIFIER_REQUEST_REPLY "
let argument = domain . identifier . rawValue
let message = command + " : " + argument + " \n "
socketClient ? . sendMessage ( message )
}
private func signalEnumeratorAfterAccountSetup ( ) {
guard let fpManager = NSFileProviderManager ( for : domain ) else {
2024-01-22 09:40:47 +03:00
Logger . fileProviderExtension . error (
" Could not get file provider manager for domain \( self . domain . displayName , privacy : . public ) , cannot notify after account setup "
)
2023-03-21 15:28:48 +03:00
return
}
assert ( ncAccount != nil )
fpManager . signalErrorResolved ( NSFileProviderError ( . notAuthenticated ) ) { error in
if error != nil {
2024-01-22 09:40:47 +03:00
Logger . fileProviderExtension . error (
" Error resolving not authenticated, received error: \( error ! . localizedDescription ) "
)
2023-03-21 15:28:48 +03:00
}
}
2024-01-22 09:40:47 +03:00
Logger . fileProviderExtension . debug (
" Signalling enumerators for user \( self . ncAccount ! . username ) at server \( self . ncAccount ! . serverUrl , privacy : . public ) "
)
2023-03-21 15:28:48 +03:00
2024-07-19 10:17:59 +03:00
notifyChange ( )
}
func notifyChange ( ) {
guard let fpManager = NSFileProviderManager ( for : domain ) else {
Logger . fileProviderExtension . error (
" Could not get file provider manager for domain \( self . domain . displayName , privacy : . public ) , cannot notify changes "
)
return
}
2023-03-21 15:28:48 +03:00
fpManager . signalEnumerator ( for : . workingSet ) { error in
if error != nil {
2024-01-22 09:40:47 +03:00
Logger . fileProviderExtension . error (
" Error signalling enumerator for working set, received error: \( error ! . localizedDescription , privacy : . public ) "
)
2023-03-21 15:28:48 +03:00
}
}
}
2024-09-30 11:05:30 +03:00
@objc func setupDomainAccount (
user : String , userId : String , serverUrl : String , password : String
) {
2024-09-30 08:53:55 +03:00
let semaphore = DispatchSemaphore ( value : 0 )
var authAttemptState = AuthenticationAttemptResultState . connectionError // d e f a u l t
Task {
let authTestNcKit = NextcloudKit ( )
2024-09-30 11:05:30 +03:00
authTestNcKit . setup ( user : user , userId : userId , password : password , urlBase : serverUrl )
2024-09-30 08:53:55 +03:00
// R e t r y a f e w t i m e s i f w e h a v e a c o n n e c t i o n i s s u e
for authTimeout in AuthenticationTimeouts {
authAttemptState = await authTestNcKit . tryAuthenticationAttempt ( )
guard authAttemptState = = . connectionError else { break }
Logger . fileProviderExtension . info (
" \( user , privacy : . public ) authentication try timed out. Trying again soon. "
)
try ? await Task . sleep ( nanoseconds : authTimeout )
}
semaphore . signal ( )
}
semaphore . wait ( )
switch ( authAttemptState ) {
case . authenticationError :
Logger . fileProviderExtension . info (
" \( user , privacy : . public ) authentication failed due to bad creds, stopping "
)
return
case . connectionError :
// D e s p i t e m u l t i p l e c o n n e c t i o n a t t e m p t s w e a r e s t i l l g e t t i n g c o n n e c t i o n i s s u e s , s o q u i t .
Logger . fileProviderExtension . info (
" \( user , privacy : . public ) authentication try failed, no connection. "
)
return
case . success :
Logger . fileProviderExtension . info (
" " "
Authenticated ! Nextcloud account set up in File Provider extension .
User : \ ( user , privacy : . public ) at server : \ ( serverUrl , privacy : . public )
" " "
)
}
2024-09-30 11:05:30 +03:00
let newNcAccount = Account ( user : user , id : userId , serverUrl : serverUrl , password : password )
2024-01-30 08:38:55 +03:00
guard newNcAccount != ncAccount else { return }
ncAccount = newNcAccount
2024-01-22 09:40:47 +03:00
ncKit . setup (
2024-04-16 13:22:53 +03:00
account : newNcAccount . ncKitAccount ,
user : newNcAccount . username ,
2024-09-30 11:05:30 +03:00
userId : newNcAccount . id ,
2024-04-16 13:22:53 +03:00
password : newNcAccount . password ,
urlBase : newNcAccount . serverUrl ,
2024-01-22 09:40:47 +03:00
userAgent : " Nextcloud-macOS/FileProviderExt " ,
nextcloudVersion : 25 ,
2024-01-30 08:38:55 +03:00
delegate : nil ) // TODO: a d d d e l e g a t e m e t h o d s f o r s e l f
2023-03-21 15:28:48 +03:00
2024-07-19 10:17:59 +03:00
changeObserver = RemoteChangeObserver (
remoteInterface : ncKit , changeNotificationInterface : self , domain : domain
)
2024-04-16 21:47:21 +03:00
ncKit . setup ( delegate : changeObserver )
2024-09-30 08:53:55 +03:00
signalEnumeratorAfterAccountSetup ( )
2023-03-21 15:28:48 +03:00
}
2023-11-21 05:40:54 +03:00
@objc func removeAccountConfig ( ) {
2024-01-22 09:40:47 +03:00
Logger . fileProviderExtension . info (
" Received instruction to remove account data for user \( self . ncAccount ! . username , privacy : . public ) at server \( self . ncAccount ! . serverUrl , privacy : . public ) "
)
2023-03-21 15:28:48 +03:00
ncAccount = nil
}
2024-07-16 14:19:58 +03:00
func updatedSyncStateReporting ( oldActions : Set < UUID > ) {
2024-07-16 14:35:09 +03:00
actionsLock . lock ( )
guard oldActions . isEmpty != syncActions . isEmpty else {
actionsLock . unlock ( )
return
}
2024-07-16 14:19:58 +03:00
let command = " FILE_PROVIDER_DOMAIN_SYNC_STATE_CHANGE "
var argument : String ?
if oldActions . isEmpty , ! syncActions . isEmpty {
argument = " SYNC_STARTED "
} else if ! oldActions . isEmpty , syncActions . isEmpty {
argument = errorActions . isEmpty ? " SYNC_FINISHED " : " SYNC_FAILED "
errorActions = [ ]
}
2024-07-16 14:35:09 +03:00
actionsLock . unlock ( )
2024-07-16 14:19:58 +03:00
guard let argument else { return }
Logger . fileProviderExtension . debug ( " Reporting sync \( argument ) " )
let message = command + " : " + argument + " \n "
socketClient ? . sendMessage ( message )
}
2023-03-21 15:28:48 +03:00
}