2018-03-06 14:29:25 +01:00
//
// C o p y r i g h t ( c ) 2 0 1 8 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import SignalServiceKit
import CloudKit
2018-03-20 22:22:19 +01:00
// W e d o n ' t w o r r y a b o u t a t o m i c w r i t e s . E a c h b a c k u p e x p o r t
// w i l l d i f f a g a i n s t l a s t s u c c e s s f u l b a c k u p .
//
// N o t e t h a t a l l o f o u r C l o u d K i t r e c o r d s a r e i m m u t a b l e .
// " P e r s i s t e n t " r e c o r d s a r e o n l y u p l o a d e d o n c e .
// " E p h e m e r a l " r e c o r d s a r e a l w a y s u p l o a d e d t o a n e w r e c o r d n a m e .
2018-03-06 14:29:25 +01:00
@objc public class OWSBackupAPI : NSObject {
2018-03-08 14:14:35 +01:00
// I f w e c h a n g e t h e r e c o r d t y p e s , w e n e e d t o e n s u r e i n d i c e s
// a r e c o n f i g u r e d p r o p e r l y i n t h e C l o u d K i t d a s h b o a r d .
2018-03-20 17:26:17 +01:00
//
// TODO: C h a n g e t h e r e c o r d t y p e s w h e n w e s h i p t o p r o d u c t i o n .
2018-03-08 14:14:35 +01:00
static let signalBackupRecordType = " signalBackup "
2018-11-26 22:05:09 +01:00
static let manifestRecordNameSuffix = " manifest "
2018-03-08 14:14:35 +01:00
static let payloadKey = " payload "
2018-03-13 18:39:00 +01:00
static let maxRetries = 5
2018-03-08 14:14:35 +01:00
2018-03-13 18:05:51 +01:00
private class func recordIdForTest ( ) -> String {
2018-03-06 14:29:25 +01:00
return " test- \( NSUUID ( ) . uuidString ) "
}
2018-03-13 18:05:51 +01:00
private class func database ( ) -> CKDatabase {
let myContainer = CKContainer . default ( )
let privateDatabase = myContainer . privateCloudDatabase
return privateDatabase
}
2018-03-13 18:39:00 +01:00
private class func invalidServiceResponseError ( ) -> Error {
return OWSErrorWithCodeDescription ( . backupFailure ,
NSLocalizedString ( " BACKUP_EXPORT_ERROR_INVALID_CLOUDKIT_RESPONSE " ,
comment : " Error indicating that the app received an invalid response from CloudKit. " ) )
}
2018-03-13 18:05:51 +01:00
// MARK: - U p l o a d
2018-03-06 14:29:25 +01:00
@objc
2018-11-26 21:55:47 +01:00
public class func saveTestFileToCloud ( recipientId : String ,
fileUrl : URL ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-11-26 21:55:47 +01:00
let recordName = " \( recordNamePrefix ( forRecipientId : recipientId ) ) test- \( NSUUID ( ) . uuidString ) "
2018-03-06 14:29:25 +01:00
saveFileToCloud ( fileUrl : fileUrl ,
2018-11-26 21:55:47 +01:00
recordName : recordName ,
2018-03-08 14:14:35 +01:00
recordType : signalBackupRecordType ,
2018-03-06 18:07:18 +01:00
success : success ,
failure : failure )
}
2018-03-06 19:58:06 +01:00
// " E p h e m e r a l " f i l e s a r e s p e c i f i c t o t h i s b a c k u p e x p o r t a n d w i l l a l w a y s n e e d t o
// b e s a v e d . F o r e x a m p l e , a c o m p l e t e i m a g e o f t h e d a t a b a s e i s e x p o r t e d e a c h t i m e .
// W e w o u l d n ' t w a n t t o o v e r w r i t e p r e v i o u s i m a g e s u n t i l t h e e n t i r e b a c k u p e x p o r t i s
// c o m p l e t e .
2018-03-06 18:07:18 +01:00
@objc
2018-11-26 21:55:47 +01:00
public class func saveEphemeralDatabaseFileToCloud ( recipientId : String ,
fileUrl : URL ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-11-26 21:55:47 +01:00
let recordName = " \( recordNamePrefix ( forRecipientId : recipientId ) ) ephemeralFile- \( NSUUID ( ) . uuidString ) "
2018-03-06 18:07:18 +01:00
saveFileToCloud ( fileUrl : fileUrl ,
2018-11-26 21:55:47 +01:00
recordName : recordName ,
2018-03-13 18:05:51 +01:00
recordType : signalBackupRecordType ,
success : success ,
failure : failure )
2018-03-06 19:58:06 +01:00
}
2018-03-17 21:29:57 +01:00
// " P e r s i s t e n t " f i l e s m a y b e s h a r e d b e t w e e n b a c k u p e x p o r t ; t h e y s h o u l d o n l y b e s a v e d
// o n c e . F o r e x a m p l e , a t t a c h m e n t f i l e s s h o u l d o n l y b e u p l o a d e d o n c e . S u b s e q u e n t
// b a c k u p s c a n r e u s e t h e s a m e r e c o r d .
@objc
2018-11-26 21:55:47 +01:00
public class func recordNameForPersistentFile ( recipientId : String ,
fileId : String ) -> String {
return " \( recordNamePrefix ( forRecipientId : recipientId ) ) persistentFile- \( fileId ) "
2018-03-17 21:29:57 +01:00
}
2018-03-06 19:58:06 +01:00
// " P e r s i s t e n t " f i l e s m a y b e s h a r e d b e t w e e n b a c k u p e x p o r t ; t h e y s h o u l d o n l y b e s a v e d
// o n c e . F o r e x a m p l e , a t t a c h m e n t f i l e s s h o u l d o n l y b e u p l o a d e d o n c e . S u b s e q u e n t
2018-03-08 14:14:35 +01:00
// b a c k u p s c a n r e u s e t h e s a m e r e c o r d .
2018-03-06 19:58:06 +01:00
@objc
2018-11-26 21:55:47 +01:00
public class func recordNameForManifest ( recipientId : String ) -> String {
2018-11-26 22:05:09 +01:00
return " \( recordNamePrefix ( forRecipientId : recipientId ) ) \( manifestRecordNameSuffix ) "
}
private class func isManifest ( recordName : String ) -> Bool {
return recordName . hasSuffix ( manifestRecordNameSuffix )
2018-11-26 21:55:47 +01:00
}
private class func recordNamePrefix ( forRecipientId recipientId : String ) -> String {
2018-11-26 22:05:09 +01:00
return " \( recipientId ) - "
2018-11-26 21:55:47 +01:00
}
private class func recipientId ( forRecordName recordName : String ) -> String ? {
let recipientIds = self . recipientIds ( forRecordNames : [ recordName ] )
guard let recipientId = recipientIds . first else {
return nil
}
return recipientId
}
private class func recipientIds ( forRecordNames recordNames : [ String ] ) -> [ String ] {
let regex : NSRegularExpression
do {
regex = try NSRegularExpression ( pattern : " ( \\ +[0-9]+) \\ - " )
} catch {
Logger . error ( " couldn't compile regex: \( error ) " )
return [ ]
}
var recipientIds = [ String ] ( )
for recordName in recordNames {
guard let match = regex . firstMatch ( in : recordName , options : [ ] , range : NSRange ( location : 0 , length : recordName . count ) ) else {
continue
}
guard match . range . location = = 0 else {
// M a t c h m u s t b e a t s t a r t o f s t r i n g .
continue
}
let recipientId = ( recordName as NSString ) . substring ( with : match . range ) as String
recipientIds . append ( recipientId )
}
return recipientIds
}
// " P e r s i s t e n t " f i l e s m a y b e s h a r e d b e t w e e n b a c k u p e x p o r t ; t h e y s h o u l d o n l y b e s a v e d
// o n c e . F o r e x a m p l e , a t t a c h m e n t f i l e s s h o u l d o n l y b e u p l o a d e d o n c e . S u b s e q u e n t
// b a c k u p s c a n r e u s e t h e s a m e r e c o r d .
@objc
public class func savePersistentFileOnceToCloud ( recipientId : String ,
fileId : String ,
2018-05-25 21:21:43 +02:00
fileUrlBlock : @ escaping ( ) -> URL ? ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-11-26 21:55:47 +01:00
let recordName = recordNameForPersistentFile ( recipientId : recipientId , fileId : fileId )
saveFileOnceToCloud ( recordName : recordName ,
2018-03-08 14:14:35 +01:00
recordType : signalBackupRecordType ,
fileUrlBlock : fileUrlBlock ,
success : success ,
failure : failure )
2018-03-06 14:29:25 +01:00
}
2018-03-06 19:23:55 +01:00
@objc
2018-11-26 21:55:47 +01:00
public class func upsertManifestFileToCloud ( recipientId : String ,
fileUrl : URL ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-06 19:23:55 +01:00
// W e w a n t t o u s e a w e l l - k n o w n r e c o r d i d a n d t y p e f o r m a n i f e s t f i l e s .
2018-11-26 21:55:47 +01:00
let recordName = recordNameForManifest ( recipientId : recipientId )
2018-03-06 19:23:55 +01:00
upsertFileToCloud ( fileUrl : fileUrl ,
2018-11-26 21:55:47 +01:00
recordName : recordName ,
2018-03-08 14:14:35 +01:00
recordType : signalBackupRecordType ,
2018-03-06 19:58:06 +01:00
success : success ,
failure : failure )
2018-03-06 19:23:55 +01:00
}
2018-03-06 14:29:25 +01:00
@objc
2018-03-06 14:36:05 +01:00
public class func saveFileToCloud ( fileUrl : URL ,
2018-03-06 19:23:55 +01:00
recordName : String ,
2018-03-06 14:36:05 +01:00
recordType : String ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-06 19:23:55 +01:00
let recordID = CKRecordID ( recordName : recordName )
2018-03-06 14:29:25 +01:00
let record = CKRecord ( recordType : recordType , recordID : recordID )
2018-03-06 14:36:05 +01:00
let asset = CKAsset ( fileURL : fileUrl )
2018-03-06 19:23:55 +01:00
record [ payloadKey ] = asset
2018-03-06 14:36:05 +01:00
saveRecordToCloud ( record : record ,
2018-03-06 18:07:18 +01:00
success : success ,
failure : failure )
2018-03-06 14:36:05 +01:00
}
@objc
public class func saveRecordToCloud ( record : CKRecord ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-13 18:05:51 +01:00
saveRecordToCloud ( record : record ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-13 18:05:51 +01:00
success : success ,
failure : failure )
2018-03-06 14:29:25 +01:00
}
2018-03-13 18:05:51 +01:00
private class func saveRecordToCloud ( record : CKRecord ,
remainingRetries : Int ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 14:14:35 +01:00
2018-11-22 01:36:41 +01:00
let saveOperation = CKModifyRecordsOperation ( recordsToSave : [ record ] , recordIDsToDelete : nil )
saveOperation . modifyRecordsCompletionBlock = { ( records , recordIds , error ) in
2018-03-08 14:14:35 +01:00
2018-03-14 14:12:20 +01:00
let outcome = outcomeForCloudKitError ( error : error ,
2018-03-13 18:05:51 +01:00
remainingRetries : remainingRetries ,
label : " Save Record " )
2018-03-14 14:12:20 +01:00
switch outcome {
2018-03-13 18:05:51 +01:00
case . success :
let recordName = record . recordID . recordName
success ( recordName )
2018-03-14 14:12:20 +01:00
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
2018-03-13 18:05:51 +01:00
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
saveRecordToCloud ( record : record ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
saveRecordToCloud ( record : record ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
}
2018-03-13 18:39:00 +01:00
case . unknownItem :
2018-08-27 16:27:48 +02:00
owsFailDebug ( " unexpected CloudKit response. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
2018-03-08 14:14:35 +01:00
}
}
2018-11-26 15:18:06 +01:00
saveOperation . isAtomic = false
2018-11-22 01:36:41 +01:00
// T h e s e A P I s a r e o n l y a v a i l a b l e i n i O S 9 . 3 a n d l a t e r .
if #available ( iOS 9.3 , * ) {
saveOperation . isLongLived = true
saveOperation . qualityOfService = . background
}
database ( ) . add ( saveOperation )
2018-03-08 14:14:35 +01:00
}
2018-03-08 14:31:35 +01:00
// C o m p a r e :
// * A n " u p s e r t " c r e a t e s a n e w r e c o r d i f n o n e e x i s t s a n d
// o r u p d a t e s i f t h e r e i s a n e x i s t i n g r e c o r d .
// * A " s a v e o n c e " c r e a t e s a n e w r e c o r d i f n o n e e x i s t s a n d
// d o e s n o t h i n g i f t h e r e i s a n e x i s t i n g r e c o r d .
2018-03-06 19:23:55 +01:00
@objc
public class func upsertFileToCloud ( fileUrl : URL ,
recordName : String ,
recordType : String ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 14:31:35 +01:00
checkForFileInCloud ( recordName : recordName ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-08 14:31:35 +01:00
success : { ( record ) in
if let record = record {
// R e c o r d f o u n d , u p d a t i n g e x i s t i n g r e c o r d .
let asset = CKAsset ( fileURL : fileUrl )
record [ payloadKey ] = asset
saveRecordToCloud ( record : record ,
success : success ,
failure : failure )
} else {
// N o r e c o r d f o u n d , s a v i n g n e w r e c o r d .
saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType ,
success : success ,
failure : failure )
}
} ,
failure : failure )
}
// C o m p a r e :
// * A n " u p s e r t " c r e a t e s a n e w r e c o r d i f n o n e e x i s t s a n d
// o r u p d a t e s i f t h e r e i s a n e x i s t i n g r e c o r d .
// * A " s a v e o n c e " c r e a t e s a n e w r e c o r d i f n o n e e x i s t s a n d
// d o e s n o t h i n g i f t h e r e i s a n e x i s t i n g r e c o r d .
@objc
public class func saveFileOnceToCloud ( recordName : String ,
recordType : String ,
2018-05-25 21:21:43 +02:00
fileUrlBlock : @ escaping ( ) -> URL ? ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( String ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 14:31:35 +01:00
checkForFileInCloud ( recordName : recordName ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-08 14:31:35 +01:00
success : { ( record ) in
if record != nil {
// R e c o r d f o u n d , s k i p p i n g s a v e .
success ( recordName )
} else {
// N o r e c o r d f o u n d , s a v i n g n e w r e c o r d .
2018-05-25 21:21:43 +02:00
guard let fileUrl = fileUrlBlock ( ) else {
2018-08-23 16:37:34 +02:00
Logger . error ( " error preparing file for upload. " )
2018-03-08 14:31:35 +01:00
failure ( OWSErrorWithCodeDescription ( . exportBackupError ,
NSLocalizedString ( " BACKUP_EXPORT_ERROR_SAVE_FILE_TO_CLOUD_FAILED " ,
2018-03-26 15:37:43 +02:00
comment : " Error indicating the backup export failed to save a file to the cloud. " ) ) )
2018-03-08 14:31:35 +01:00
return
}
saveFileToCloud ( fileUrl : fileUrl ,
recordName : recordName ,
recordType : recordType ,
success : success ,
failure : failure )
}
} ,
failure : failure )
}
2018-03-13 18:05:51 +01:00
// MARK: - D e l e t e
@objc
2018-03-16 18:58:18 +01:00
public class func deleteRecordsFromCloud ( recordNames : [ String ] ,
2018-05-25 21:21:43 +02:00
success : @ escaping ( ) -> Void ,
2018-03-14 14:12:20 +01:00
failure : @ escaping ( Error ) -> Void ) {
2018-03-16 18:58:18 +01:00
deleteRecordsFromCloud ( recordNames : recordNames ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-13 18:05:51 +01:00
success : success ,
failure : failure )
}
2018-03-16 18:58:18 +01:00
private class func deleteRecordsFromCloud ( recordNames : [ String ] ,
2018-03-13 18:05:51 +01:00
remainingRetries : Int ,
2018-05-25 21:21:43 +02:00
success : @ escaping ( ) -> Void ,
2018-03-14 14:12:20 +01:00
failure : @ escaping ( Error ) -> Void ) {
2018-03-13 18:05:51 +01:00
2018-03-20 16:23:45 +01:00
let recordIDs = recordNames . map { CKRecordID ( recordName : $0 ) }
2018-03-16 18:58:18 +01:00
let deleteOperation = CKModifyRecordsOperation ( recordsToSave : nil , recordIDsToDelete : recordIDs )
deleteOperation . modifyRecordsCompletionBlock = { ( records , recordIds , error ) in
2018-03-13 18:05:51 +01:00
2018-03-14 14:12:20 +01:00
let outcome = outcomeForCloudKitError ( error : error ,
2018-03-16 18:58:18 +01:00
remainingRetries : remainingRetries ,
label : " Delete Records " )
2018-03-14 14:12:20 +01:00
switch outcome {
2018-03-13 18:05:51 +01:00
case . success :
2018-05-25 21:21:43 +02:00
success ( )
2018-03-14 14:12:20 +01:00
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
2018-03-13 18:05:51 +01:00
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
2018-03-16 18:58:18 +01:00
deleteRecordsFromCloud ( recordNames : recordNames ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
2018-03-13 18:05:51 +01:00
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
2018-03-16 18:58:18 +01:00
deleteRecordsFromCloud ( recordNames : recordNames ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
2018-03-13 18:05:51 +01:00
}
2018-03-13 18:39:00 +01:00
case . unknownItem :
2018-08-27 16:27:48 +02:00
owsFailDebug ( " unexpected CloudKit response. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
2018-03-13 18:05:51 +01:00
}
}
2018-03-16 18:58:18 +01:00
database ( ) . add ( deleteOperation )
2018-03-13 18:05:51 +01:00
}
// MARK: - E x i s t s ?
2018-03-08 14:31:35 +01:00
private class func checkForFileInCloud ( recordName : String ,
2018-03-13 18:39:00 +01:00
remainingRetries : Int ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( CKRecord ? ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-06 19:23:55 +01:00
let recordId = CKRecordID ( recordName : recordName )
let fetchOperation = CKFetchRecordsOperation ( recordIDs : [ recordId ] )
2018-03-06 20:36:59 +01:00
// D o n ' t d o w n l o a d t h e f i l e ; w e ' r e j u s t u s i n g t h e f e t c h t o c h e c k w h e t h e r o r
// n o t t h i s r e c o r d a l r e a d y e x i s t s .
fetchOperation . desiredKeys = [ ]
2018-03-06 19:23:55 +01:00
fetchOperation . perRecordCompletionBlock = { ( record , recordId , error ) in
2018-03-13 18:39:00 +01:00
2018-03-14 14:12:20 +01:00
let outcome = outcomeForCloudKitError ( error : error ,
2018-03-13 18:39:00 +01:00
remainingRetries : remainingRetries ,
label : " Check for Record " )
2018-03-14 14:12:20 +01:00
switch outcome {
2018-03-13 18:39:00 +01:00
case . success :
guard let record = record else {
2018-08-27 16:27:48 +02:00
owsFailDebug ( " missing fetching record. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
return
2018-03-06 19:23:55 +01:00
}
2018-03-13 18:39:00 +01:00
// R e c o r d f o u n d .
success ( record )
2018-03-14 14:12:20 +01:00
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
2018-03-13 18:39:00 +01:00
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
checkForFileInCloud ( recordName : recordName ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
checkForFileInCloud ( recordName : recordName ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
}
case . unknownItem :
// R e c o r d n o t f o u n d .
success ( nil )
2018-03-06 19:23:55 +01:00
}
2018-03-06 19:58:06 +01:00
}
2018-03-13 18:05:51 +01:00
database ( ) . add ( fetchOperation )
2018-03-06 19:58:06 +01:00
}
@objc
2018-11-26 21:55:47 +01:00
public class func checkForManifestInCloud ( recipientId : String ,
success : @ escaping ( Bool ) -> Void ,
2018-03-14 14:12:20 +01:00
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 14:31:35 +01:00
2018-11-26 21:55:47 +01:00
let recordName = recordNameForManifest ( recipientId : recipientId )
checkForFileInCloud ( recordName : recordName ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-08 14:31:35 +01:00
success : { ( record ) in
success ( record != nil )
} ,
failure : failure )
2018-03-06 19:23:55 +01:00
}
2018-03-08 14:14:35 +01:00
@objc
2018-11-26 22:05:09 +01:00
public class func allRecipientIdsWithManifestsInCloud ( success : @ escaping ( [ String ] ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
let processResults = { ( recordNames : [ String ] ) in
DispatchQueue . global ( ) . async {
let manifestRecordNames = recordNames . filter ( { ( recordName ) -> Bool in
self . isManifest ( recordName : recordName )
} )
let recipientIds = self . recipientIds ( forRecordNames : manifestRecordNames )
success ( recipientIds )
}
}
2018-03-08 14:14:35 +01:00
let query = CKQuery ( recordType : signalBackupRecordType , predicate : NSPredicate ( value : true ) )
// F e t c h t h e f i r s t p a g e o f r e s u l t s f o r t h i s q u e r y .
2018-11-26 22:05:09 +01:00
fetchAllRecordNamesStep ( recipientId : nil ,
2018-11-26 21:55:47 +01:00
query : query ,
2018-03-08 14:14:35 +01:00
previousRecordNames : [ String ] ( ) ,
cursor : nil ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-11-26 22:05:09 +01:00
success : processResults ,
2018-03-08 14:14:35 +01:00
failure : failure )
}
2018-11-26 21:55:47 +01:00
@objc
2018-11-26 22:05:09 +01:00
public class func fetchAllRecordNames ( recipientId : String ,
success : @ escaping ( [ String ] ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-11-26 21:55:47 +01:00
let query = CKQuery ( recordType : signalBackupRecordType , predicate : NSPredicate ( value : true ) )
// F e t c h t h e f i r s t p a g e o f r e s u l t s f o r t h i s q u e r y .
2018-11-26 22:05:09 +01:00
fetchAllRecordNamesStep ( recipientId : recipientId ,
2018-11-26 21:55:47 +01:00
query : query ,
previousRecordNames : [ String ] ( ) ,
cursor : nil ,
remainingRetries : maxRetries ,
2018-11-26 22:05:09 +01:00
success : success ,
2018-11-26 21:55:47 +01:00
failure : failure )
}
2018-11-26 22:05:09 +01:00
// @ o b j c
// p u b l i c c l a s s f u n c f e t c h A l l B a c k u p R e c i p i e n t I d s ( s u c c e s s : @ e s c a p i n g ( [ S t r i n g ] ) - > V o i d ,
// f a i l u r e : @ e s c a p i n g ( E r r o r ) - > V o i d ) {
//
// l e t p r o c e s s R e s u l t s = { ( r e c o r d N a m e s : [ S t r i n g ] ) i n
// D i s p a t c h Q u e u e . g l o b a l ( ) . a s y n c {
// l e t r e c i p i e n t I d s = s e l f . r e c i p i e n t I d s ( f o r R e c o r d N a m e s : r e c o r d N a m e s )
// s u c c e s s ( r e c i p i e n t I d s )
// }
// }
//
// l e t q u e r y = C K Q u e r y ( r e c o r d T y p e : s i g n a l B a c k u p R e c o r d T y p e , p r e d i c a t e : N S P r e d i c a t e ( v a l u e : t r u e ) )
// / / F e t c h t h e f i r s t p a g e o f r e s u l t s f o r t h i s q u e r y .
// f e t c h A l l R e c o r d N a m e s S t e p ( r e c i p i e n t I d : n i l ,
// q u e r y : q u e r y ,
// p r e v i o u s R e c o r d N a m e s : [ S t r i n g ] ( ) ,
// c u r s o r : n i l ,
// r e m a i n i n g R e t r i e s : m a x R e t r i e s ,
// s u c c e s s : p r o c e s s R e s u l t s ,
// f a i l u r e : f a i l u r e )
// }
2018-11-26 21:55:47 +01:00
private class func fetchAllRecordNamesStep ( recipientId : String ? ,
query : CKQuery ,
2018-03-08 14:14:35 +01:00
previousRecordNames : [ String ] ,
cursor : CKQueryCursor ? ,
2018-03-13 18:39:00 +01:00
remainingRetries : Int ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( [ String ] ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 14:14:35 +01:00
var allRecordNames = previousRecordNames
2018-03-13 18:05:51 +01:00
let queryOperation = CKQueryOperation ( query : query )
2018-03-08 14:14:35 +01:00
// I f t h i s i s n ' t t h e f i r s t p a g e o f r e s u l t s f o r t h i s q u e r y , r e s u m e
// w h e r e w e l e f t o f f .
queryOperation . cursor = cursor
// D o n ' t d o w n l o a d t h e f i l e ; w e ' r e j u s t u s i n g t h e q u e r y t o g e t a l i s t o f r e c o r d n a m e s .
queryOperation . desiredKeys = [ ]
queryOperation . recordFetchedBlock = { ( record ) in
assert ( record . recordID . recordName . count > 0 )
2018-11-26 21:55:47 +01:00
let recordName = record . recordID . recordName
if let recipientId = recipientId {
let prefix = recordNamePrefix ( forRecipientId : recipientId )
guard recordName . hasPrefix ( prefix ) else {
Logger . info ( " Ignoring record: \( recordName ) " )
return
}
}
allRecordNames . append ( recordName )
2018-03-08 14:14:35 +01:00
}
queryOperation . queryCompletionBlock = { ( cursor , error ) in
2018-03-13 18:39:00 +01:00
2018-03-14 14:12:20 +01:00
let outcome = outcomeForCloudKitError ( error : error ,
2018-03-13 18:39:00 +01:00
remainingRetries : remainingRetries ,
label : " Fetch All Records " )
2018-03-14 14:12:20 +01:00
switch outcome {
2018-03-13 18:39:00 +01:00
case . success :
if let cursor = cursor {
2018-08-23 16:37:34 +02:00
Logger . verbose ( " fetching more record names \( allRecordNames . count ) . " )
2018-03-13 18:39:00 +01:00
// T h e r e a r e m o r e p a g e s o f r e s u l t s , c o n t i n u e f e t c h i n g .
2018-11-26 21:55:47 +01:00
fetchAllRecordNamesStep ( recipientId : recipientId ,
query : query ,
2018-03-13 18:39:00 +01:00
previousRecordNames : allRecordNames ,
cursor : cursor ,
remainingRetries : maxRetries ,
success : success ,
failure : failure )
return
}
2018-08-23 16:37:34 +02:00
Logger . info ( " fetched \( allRecordNames . count ) record names. " )
2018-03-13 18:39:00 +01:00
success ( allRecordNames )
2018-03-14 14:12:20 +01:00
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
2018-03-13 18:39:00 +01:00
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
2018-11-26 21:55:47 +01:00
fetchAllRecordNamesStep ( recipientId : recipientId ,
query : query ,
2018-03-13 18:39:00 +01:00
previousRecordNames : allRecordNames ,
cursor : cursor ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
2018-11-26 21:55:47 +01:00
fetchAllRecordNamesStep ( recipientId : recipientId ,
query : query ,
2018-03-13 18:39:00 +01:00
previousRecordNames : allRecordNames ,
cursor : cursor ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
}
case . unknownItem :
2018-08-27 16:27:48 +02:00
owsFailDebug ( " unexpected CloudKit response. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
2018-03-08 14:14:35 +01:00
}
}
2018-03-13 18:05:51 +01:00
database ( ) . add ( queryOperation )
2018-03-08 14:14:35 +01:00
}
2018-03-13 18:05:51 +01:00
// MARK: - D o w n l o a d
2018-03-08 19:38:42 +01:00
@objc
2018-11-26 21:55:47 +01:00
public class func downloadManifestFromCloud ( recipientId : String ,
success : @ escaping ( Data ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
let recordName = recordNameForManifest ( recipientId : recipientId )
downloadDataFromCloud ( recordName : recordName ,
2018-03-13 18:05:51 +01:00
success : success ,
failure : failure )
2018-03-08 19:38:42 +01:00
}
@objc
public class func downloadDataFromCloud ( recordName : String ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( Data ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 19:38:42 +01:00
downloadFromCloud ( recordName : recordName ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-08 19:38:42 +01:00
success : { ( asset ) in
DispatchQueue . global ( ) . async {
do {
let data = try Data ( contentsOf : asset . fileURL )
success ( data )
} catch {
2018-08-23 16:37:34 +02:00
Logger . error ( " couldn't load asset file: \( error ) . " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
2018-03-08 19:38:42 +01:00
}
}
} ,
failure : failure )
}
@objc
public class func downloadFileFromCloud ( recordName : String ,
toFileUrl : URL ,
2018-05-25 21:21:43 +02:00
success : @ escaping ( ) -> Void ,
2018-03-14 14:12:20 +01:00
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 19:38:42 +01:00
downloadFromCloud ( recordName : recordName ,
2018-03-13 18:39:00 +01:00
remainingRetries : maxRetries ,
2018-03-08 19:38:42 +01:00
success : { ( asset ) in
DispatchQueue . global ( ) . async {
do {
2018-03-12 20:10:37 +01:00
try FileManager . default . copyItem ( at : asset . fileURL , to : toFileUrl )
2018-05-25 21:21:43 +02:00
success ( )
2018-03-08 19:38:42 +01:00
} catch {
2018-08-23 16:37:34 +02:00
Logger . error ( " couldn't copy asset file: \( error ) . " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
2018-03-08 19:38:42 +01:00
}
}
} ,
failure : failure )
}
2018-03-13 18:39:00 +01:00
// W e r e t u r n t h e C K A s s e t a n d n o t i t s f i l e U r l b e c a u s e
// C l o u d K i t o f f e r s n o g u a r a n t e e s a r o u n d h o w l o n g i t ' l l
// k e e p a r o u n d t h e u n d e r l y i n g f i l e . P r e s u m a b l y w e c a n
// d e f e r c l e a n u p b y m a i n t a i n i n g a s t r o n g r e f e r e n c e t o
// t h e a s s e t .
2018-03-08 19:38:42 +01:00
private class func downloadFromCloud ( recordName : String ,
2018-03-13 18:39:00 +01:00
remainingRetries : Int ,
2018-03-14 14:12:20 +01:00
success : @ escaping ( CKAsset ) -> Void ,
failure : @ escaping ( Error ) -> Void ) {
2018-03-08 19:38:42 +01:00
let recordId = CKRecordID ( recordName : recordName )
let fetchOperation = CKFetchRecordsOperation ( recordIDs : [ recordId ] )
// D o w n l o a d a l l k e y s f o r t h i s r e c o r d .
fetchOperation . perRecordCompletionBlock = { ( record , recordId , error ) in
2018-03-13 18:39:00 +01:00
2018-03-14 14:12:20 +01:00
let outcome = outcomeForCloudKitError ( error : error ,
2018-03-13 18:39:00 +01:00
remainingRetries : remainingRetries ,
label : " Download Record " )
2018-03-14 14:12:20 +01:00
switch outcome {
2018-03-13 18:39:00 +01:00
case . success :
guard let record = record else {
2018-08-23 16:37:34 +02:00
Logger . error ( " missing fetching record. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
return
}
guard let asset = record [ payloadKey ] as ? CKAsset else {
2018-08-23 16:37:34 +02:00
Logger . error ( " record missing payload. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
return
}
success ( asset )
2018-03-14 14:12:20 +01:00
case . failureDoNotRetry ( let outcomeError ) :
failure ( outcomeError )
2018-03-13 18:39:00 +01:00
case . failureRetryAfterDelay ( let retryDelay ) :
DispatchQueue . global ( ) . asyncAfter ( deadline : DispatchTime . now ( ) + retryDelay , execute : {
downloadFromCloud ( recordName : recordName ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
} )
case . failureRetryWithoutDelay :
DispatchQueue . global ( ) . async {
downloadFromCloud ( recordName : recordName ,
remainingRetries : remainingRetries - 1 ,
success : success ,
failure : failure )
}
case . unknownItem :
2018-08-23 16:37:34 +02:00
Logger . error ( " missing fetching record. " )
2018-03-13 18:39:00 +01:00
failure ( invalidServiceResponseError ( ) )
2018-03-08 19:38:42 +01:00
}
}
2018-03-13 18:05:51 +01:00
database ( ) . add ( fetchOperation )
2018-03-08 19:38:42 +01:00
}
2018-03-13 18:05:51 +01:00
// MARK: - A c c e s s
2018-03-06 14:29:25 +01:00
@objc
2018-03-14 14:12:20 +01:00
public class func checkCloudKitAccess ( completion : @ escaping ( Bool ) -> Void ) {
2018-03-06 14:29:25 +01:00
CKContainer . default ( ) . accountStatus ( completionHandler : { ( accountStatus , error ) in
DispatchQueue . main . async {
2018-03-06 14:36:05 +01:00
switch accountStatus {
case . couldNotDetermine :
2018-08-23 16:37:34 +02:00
Logger . error ( " could not determine CloudKit account status: \( String ( describing : error ) ) . " )
2018-03-12 21:49:57 +01:00
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " CLOUDKIT_STATUS_COULD_NOT_DETERMINE " , comment : " Error indicating that the app could not determine that user's CloudKit account status " ) )
2018-03-06 14:36:05 +01:00
completion ( false )
case . noAccount :
2018-08-23 16:37:34 +02:00
Logger . error ( " no CloudKit account. " )
2018-03-12 21:49:57 +01:00
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " CLOUDKIT_STATUS_NO_ACCOUNT " , comment : " Error indicating that user does not have an iCloud account. " ) )
2018-03-06 14:36:05 +01:00
completion ( false )
case . restricted :
2018-08-23 16:37:34 +02:00
Logger . error ( " restricted CloudKit account. " )
2018-03-12 21:49:57 +01:00
OWSAlerts . showErrorAlert ( message : NSLocalizedString ( " CLOUDKIT_STATUS_RESTRICTED " , comment : " Error indicating that the app was prevented from accessing the user's CloudKit account. " ) )
2018-03-06 14:36:05 +01:00
completion ( false )
case . available :
completion ( true )
}
2018-03-06 14:29:25 +01:00
}
} )
}
2018-03-13 18:05:51 +01:00
// MARK: - R e t r y
2018-03-17 12:50:09 +01:00
private enum APIOutcome {
2018-03-13 18:05:51 +01:00
case success
case failureDoNotRetry ( error : Error )
2018-03-17 12:50:09 +01:00
case failureRetryAfterDelay ( retryDelay : TimeInterval )
2018-03-13 18:05:51 +01:00
case failureRetryWithoutDelay
2018-03-13 18:39:00 +01:00
// T h i s o n l y a p p l i e s t o f e t c h e s .
case unknownItem
2018-03-13 18:05:51 +01:00
}
2018-03-14 14:12:20 +01:00
private class func outcomeForCloudKitError ( error : Error ? ,
2018-03-13 18:05:51 +01:00
remainingRetries : Int ,
2018-03-17 12:50:09 +01:00
label : String ) -> APIOutcome {
2018-03-13 18:05:51 +01:00
if let error = error as ? CKError {
2018-03-13 18:39:00 +01:00
if error . code = = CKError . unknownItem {
// T h i s i s n o t a l w a y s a n e r r o r f o r o u r p u r p o s e s .
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) unknown item. " )
2018-03-13 18:39:00 +01:00
return . unknownItem
}
2018-08-23 16:37:34 +02:00
Logger . error ( " \( label ) failed: \( error ) " )
2018-03-13 18:39:00 +01:00
2018-03-13 18:05:51 +01:00
if remainingRetries < 1 {
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) no more retries. " )
2018-03-13 18:05:51 +01:00
return . failureDoNotRetry ( error : error )
}
if #available ( iOS 11 , * ) {
if error . code = = CKError . serverResponseLost {
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) retry without delay. " )
2018-03-13 18:05:51 +01:00
return . failureRetryWithoutDelay
}
}
switch error {
case CKError . requestRateLimited , CKError . serviceUnavailable , CKError . zoneBusy :
let retryDelay = error . retryAfterSeconds ? ? 3.0
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) retry with delay: \( retryDelay ) . " )
2018-03-13 18:05:51 +01:00
return . failureRetryAfterDelay ( retryDelay : retryDelay )
case CKError . networkFailure :
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) retry without delay. " )
2018-03-13 18:05:51 +01:00
return . failureRetryWithoutDelay
default :
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) unknown CKError. " )
2018-03-13 18:05:51 +01:00
return . failureDoNotRetry ( error : error )
}
} else if let error = error {
2018-08-23 16:37:34 +02:00
Logger . error ( " \( label ) failed: \( error ) " )
2018-03-13 18:05:51 +01:00
if remainingRetries < 1 {
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) no more retries. " )
2018-03-13 18:05:51 +01:00
return . failureDoNotRetry ( error : error )
}
2018-08-23 16:37:34 +02:00
Logger . verbose ( " \( label ) unknown error. " )
2018-03-13 18:05:51 +01:00
return . failureDoNotRetry ( error : error )
} else {
2018-08-23 16:37:34 +02:00
Logger . info ( " \( label ) succeeded. " )
2018-03-13 18:05:51 +01:00
return . success
}
}
2018-11-22 01:25:52 +01:00
// MARK: -
@objc
public class func setup ( ) {
cancelAllLongLivedOperations ( )
}
private class func cancelAllLongLivedOperations ( ) {
// T h e s e A P I s a r e o n l y a v a i l a b l e i n i O S 9 . 3 a n d l a t e r .
guard #available ( iOS 9.3 , * ) else {
return
}
let container = CKContainer . default ( )
container . fetchAllLongLivedOperationIDs { ( operationIds , error ) in
if let error = error {
Logger . error ( " Could not get all long lived operations: \( error ) " )
return
}
guard let operationIds = operationIds else {
Logger . error ( " No operation ids. " )
return
}
for operationId in operationIds {
container . fetchLongLivedOperation ( withID : operationId , completionHandler : { ( operation , error ) in
if let error = error {
Logger . error ( " Could not get long lived operation [ \( operationId ) ]: \( error ) " )
return
}
guard let operation = operation else {
Logger . error ( " No operation. " )
return
}
operation . cancel ( )
} )
}
}
}
2018-03-06 14:29:25 +01:00
}