2020-05-15 11:35:44 -07:00
//
// B a c k u p C o n t r o l l e r . s w i f t
// A l t B a c k u p
//
// C r e a t e d b y R i l e y T e s t u t o n 5 / 1 2 / 2 0 .
// C o p y r i g h t © 2 0 2 0 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
extension ErrorUserInfoKey
{
static let sourceFile : String = " alt_sourceFile "
static let sourceFileLine : String = " alt_sourceFileLine "
}
extension Error
{
var sourceDescription : String ? {
guard let sourceFile = ( self as NSError ) . userInfo [ ErrorUserInfoKey . sourceFile ] as ? String , let sourceFileLine = ( self as NSError ) . userInfo [ ErrorUserInfoKey . sourceFileLine ] else {
return nil
}
return " ( \( ( sourceFile as NSString ) . lastPathComponent ) , Line \( sourceFileLine ) ) "
}
}
struct BackupError : ALTLocalizedError
{
enum Code
{
case invalidBundleID
case appGroupNotFound ( String ? )
case randomError // U s e d f o r d e b u g g i n g .
}
let code : Code
let sourceFile : String
let sourceFileLine : Int
var errorFailure : String ?
var failureReason : String ? {
switch self . code
{
case . invalidBundleID : return NSLocalizedString ( " The bundle identifier is invalid. " , comment : " " )
case . appGroupNotFound ( let appGroup ) :
if let appGroup = appGroup
{
return String ( format : NSLocalizedString ( " The app group “%@” could not be found. " , comment : " " ) , appGroup )
}
else
{
return NSLocalizedString ( " The AltStore app group could not be found. " , comment : " " )
}
case . randomError : return NSLocalizedString ( " A random error occured. " , comment : " " )
}
}
var errorUserInfo : [ String : Any ] {
let userInfo : [ String : Any ? ] = [ NSLocalizedDescriptionKey : self . errorDescription ,
NSLocalizedFailureReasonErrorKey : self . failureReason ,
NSLocalizedFailureErrorKey : self . errorFailure ,
ErrorUserInfoKey . sourceFile : self . sourceFile ,
ErrorUserInfoKey . sourceFileLine : self . sourceFileLine ]
return userInfo . compactMapValues { $0 }
}
init ( _ code : Code , description : String ? = nil , file : String = #file , line : Int = #line )
{
self . code = code
self . errorFailure = description
self . sourceFile = file
self . sourceFileLine = line
}
}
class BackupController : NSObject
{
private let fileCoordinator = NSFileCoordinator ( filePresenter : nil )
private let operationQueue = OperationQueue ( )
override init ( )
{
self . operationQueue . name = " AltBackup-BackupQueue "
}
func performBackup ( completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
{
do
{
guard let bundleIdentifier = Bundle . main . bundleIdentifier else { throw BackupError ( . invalidBundleID , description : NSLocalizedString ( " Unable to create backup directory. " , comment : " " ) ) }
guard
2020-05-15 14:27:53 -07:00
let altstoreAppGroup = Bundle . main . altstoreAppGroup ,
2020-05-15 11:35:44 -07:00
let sharedDirectoryURL = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : altstoreAppGroup )
else { throw BackupError ( . appGroupNotFound ( nil ) , description : NSLocalizedString ( " Unable to create backup directory. " , comment : " " ) ) }
let backupsDirectory = sharedDirectoryURL . appendingPathComponent ( " Backups " )
// U s e t e m p o r a r y d i r e c t o r y t o p r e v e n t m e s s i n g u p s u c c e s s f u l b a c k u p w i t h i n c o m p l e t e o n e .
let temporaryAppBackupDirectory = backupsDirectory . appendingPathComponent ( " Temp " , isDirectory : true ) . appendingPathComponent ( UUID ( ) . uuidString )
let appBackupDirectory = backupsDirectory . appendingPathComponent ( bundleIdentifier )
let writingIntent = NSFileAccessIntent . writingIntent ( with : temporaryAppBackupDirectory , options : [ ] )
let replacementIntent = NSFileAccessIntent . writingIntent ( with : appBackupDirectory , options : [ . forReplacing ] )
self . fileCoordinator . coordinate ( with : [ writingIntent , replacementIntent ] , queue : self . operationQueue ) { ( error ) in
do
{
if let error = error
{
throw error
}
do
{
let mainGroupBackupDirectory = temporaryAppBackupDirectory . appendingPathComponent ( " App " )
try FileManager . default . createDirectory ( at : mainGroupBackupDirectory , withIntermediateDirectories : true , attributes : nil )
let documentsDirectory = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) [ 0 ]
let backupDocumentsDirectory = mainGroupBackupDirectory . appendingPathComponent ( documentsDirectory . lastPathComponent )
if FileManager . default . fileExists ( atPath : backupDocumentsDirectory . path )
{
try FileManager . default . removeItem ( at : backupDocumentsDirectory )
}
if FileManager . default . fileExists ( atPath : documentsDirectory . path )
{
try FileManager . default . copyItem ( at : documentsDirectory , to : backupDocumentsDirectory )
}
print ( " Copied Documents directory from \( documentsDirectory ) to \( backupDocumentsDirectory ) " )
let libraryDirectory = FileManager . default . urls ( for : . libraryDirectory , in : . userDomainMask ) [ 0 ]
let backupLibraryDirectory = mainGroupBackupDirectory . appendingPathComponent ( libraryDirectory . lastPathComponent )
if FileManager . default . fileExists ( atPath : backupLibraryDirectory . path )
{
try FileManager . default . removeItem ( at : backupLibraryDirectory )
}
if FileManager . default . fileExists ( atPath : libraryDirectory . path )
{
try FileManager . default . copyItem ( at : libraryDirectory , to : backupLibraryDirectory )
}
print ( " Copied Library directory from \( libraryDirectory ) to \( backupLibraryDirectory ) " )
}
for appGroup in Bundle . main . appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : appGroup ) else {
throw BackupError ( . appGroupNotFound ( appGroup ) , description : NSLocalizedString ( " Unable to create app group backup directory. " , comment : " " ) )
}
let backupAppGroupURL = temporaryAppBackupDirectory . appendingPathComponent ( appGroup )
// T h e r e a r e s e v e r a l s y s t e m h i d d e n f i l e s t h a t w e d o n ' t h a v e p e r m i s s i o n t o r e a d , s o w e j u s t s k i p a l l h i d d e n f i l e s i n a p p g r o u p d i r e c t o r i e s .
try self . copyDirectoryContents ( at : appGroupURL , to : backupAppGroupURL , options : [ . skipsHiddenFiles ] )
}
// R e p l a c e p r e v i o u s b a c k u p w i t h n e w b a c k u p .
_ = try FileManager . default . replaceItemAt ( appBackupDirectory , withItemAt : temporaryAppBackupDirectory )
print ( " Replaced previous backup with new backup: " , temporaryAppBackupDirectory )
completionHandler ( . success ( ( ) ) )
}
catch
{
do { try FileManager . default . removeItem ( at : temporaryAppBackupDirectory ) }
catch { print ( " Failed to remove temporary directory. " , error ) }
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
func restoreBackup ( completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
{
do
{
guard let bundleIdentifier = Bundle . main . bundleIdentifier else { throw BackupError ( . invalidBundleID , description : NSLocalizedString ( " Unable to access backup. " , comment : " " ) ) }
guard
2020-05-15 14:27:53 -07:00
let altstoreAppGroup = Bundle . main . altstoreAppGroup ,
2020-05-15 11:35:44 -07:00
let sharedDirectoryURL = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : altstoreAppGroup )
else { throw BackupError ( . appGroupNotFound ( nil ) , description : NSLocalizedString ( " Unable to access backup. " , comment : " " ) ) }
let backupsDirectory = sharedDirectoryURL . appendingPathComponent ( " Backups " )
let appBackupDirectory = backupsDirectory . appendingPathComponent ( bundleIdentifier )
let readingIntent = NSFileAccessIntent . readingIntent ( with : appBackupDirectory , options : [ ] )
self . fileCoordinator . coordinate ( with : [ readingIntent ] , queue : self . operationQueue ) { ( error ) in
do
{
if let error = error
{
throw error
}
let mainGroupBackupDirectory = appBackupDirectory . appendingPathComponent ( " App " )
let documentsDirectory = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) [ 0 ]
let backupDocumentsDirectory = mainGroupBackupDirectory . appendingPathComponent ( documentsDirectory . lastPathComponent )
let libraryDirectory = FileManager . default . urls ( for : . libraryDirectory , in : . userDomainMask ) [ 0 ]
let backupLibraryDirectory = mainGroupBackupDirectory . appendingPathComponent ( libraryDirectory . lastPathComponent )
try self . copyDirectoryContents ( at : backupDocumentsDirectory , to : documentsDirectory )
try self . copyDirectoryContents ( at : backupLibraryDirectory , to : libraryDirectory )
for appGroup in Bundle . main . appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : appGroup ) else {
throw BackupError ( . appGroupNotFound ( appGroup ) , description : NSLocalizedString ( " Unable to read app group backup. " , comment : " " ) )
}
let backupAppGroupURL = appBackupDirectory . appendingPathComponent ( appGroup )
try self . copyDirectoryContents ( at : backupAppGroupURL , to : appGroupURL )
}
completionHandler ( . success ( ( ) ) )
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
catch
{
completionHandler ( . failure ( error ) )
}
}
}
private extension BackupController
{
func copyDirectoryContents ( at sourceDirectoryURL : URL , to destinationDirectoryURL : URL , options : FileManager . DirectoryEnumerationOptions = [ ] ) throws
{
guard FileManager . default . fileExists ( atPath : sourceDirectoryURL . path ) else { return }
if ! FileManager . default . fileExists ( atPath : destinationDirectoryURL . path )
{
try FileManager . default . createDirectory ( at : destinationDirectoryURL , withIntermediateDirectories : true , attributes : nil )
}
for fileURL in try FileManager . default . contentsOfDirectory ( at : sourceDirectoryURL , includingPropertiesForKeys : [ . isDirectoryKey ] , options : options )
{
let isDirectory = try fileURL . resourceValues ( forKeys : [ . isDirectoryKey ] ) . isDirectory ? ? false
let destinationURL = destinationDirectoryURL . appendingPathComponent ( fileURL . lastPathComponent )
if FileManager . default . fileExists ( atPath : destinationURL . path )
{
do {
try FileManager . default . removeItem ( at : destinationURL )
}
catch CocoaError . fileWriteNoPermission where isDirectory {
try self . copyDirectoryContents ( at : fileURL , to : destinationURL , options : options )
continue
}
catch {
print ( error )
throw error
}
}
do {
try FileManager . default . copyItem ( at : fileURL , to : destinationURL )
print ( " Copied item from \( fileURL ) to \( destinationURL ) " )
}
catch let error where fileURL . lastPathComponent = = " Inbox " && fileURL . deletingLastPathComponent ( ) . lastPathComponent = = " Documents " {
// I g n o r e e r r o r s f o r / D o c u m e n t s / I n b o x
print ( " Failed to copy Inbox directory: " , error )
}
catch {
print ( error )
throw error
}
}
}
}