Last active
April 27, 2021 08:33
-
-
Save horseshoe7/e85bbb90278f626bea2f827ee5228703 to your computer and use it in GitHub Desktop.
Incremental MHWMigrationManager extended for optional finalDestinationURL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
#import <CoreData/CoreData.h> | |
@class MHWMigrationManager; | |
@protocol MHWMigrationManagerDelegate <NSObject> | |
NS_ASSUME_NONNULL_BEGIN | |
@optional | |
- (void)migrationManager:(MHWMigrationManager *)migrationManager migrationProgress:(float)migrationProgress; | |
- (NSArray *)migrationManager:(MHWMigrationManager *)migrationManager mappingModelsForSourceModel:(NSManagedObjectModel *)sourceModel; | |
- (NSURL*_Nullable)destinationURLOfFullyMigratedStoreRequestedByManager:(MHWMigrationManager*)migrationManager; // if you are migrating a store that isn't a version of the same DataModel, you may want a final location | |
@end | |
@interface MHWMigrationManager : NSObject | |
- (BOOL)progressivelyMigrateURL:(NSURL *)sourceStoreURL | |
ofType:(NSString *)type | |
options:(nullable NSDictionary*)storeOptions | |
toModel:(NSManagedObjectModel *)finalModel | |
error:(NSError **)error; | |
@property (nonatomic, weak) id<MHWMigrationManagerDelegate> delegate; | |
NS_ASSUME_NONNULL_END | |
@end | |
/////////////////////// | |
// and the .m File | |
#import "MHWMigrationManager.h" | |
// use your own logger here... | |
#define LogInfo(...) [[SBLogger shared] info:[NSString stringWithFormat:__VA_ARGS__]] | |
#define LogError(...) [[SBLogger shared] error:[NSString stringWithFormat:__VA_ARGS__]] | |
@implementation MHWMigrationManager | |
#pragma mark - | |
#pragma mark - Migration | |
- (BOOL)progressivelyMigrateURL:(NSURL *)sourceStoreURL | |
ofType:(NSString *)type | |
options:(nullable NSDictionary*)storeOptions | |
toModel:(NSManagedObjectModel *)finalModel | |
error:(NSError **)error | |
{ | |
NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:type URL:sourceStoreURL options:storeOptions error:error]; | |
if (!sourceMetadata) { | |
return NO; | |
} | |
if ([finalModel isConfiguration:nil | |
compatibleWithStoreMetadata:sourceMetadata]) { | |
if (NULL != error) { | |
*error = nil; | |
} | |
// i.e. we finished migrating. Now check if all that work should end up in a different location | |
if([self.delegate respondsToSelector:@selector(destinationURLOfFullyMigratedStoreRequestedByManager:)]) { | |
NSURL *finalDestinationURL = [self.delegate destinationURLOfFullyMigratedStoreRequestedByManager:self]; | |
// you want to move sourceStoreURL to finalDestination | |
if(finalDestinationURL != nil) { | |
NSError *logError = nil; | |
if([[NSFileManager defaultManager] fileExistsAtPath:finalDestinationURL.path]) { | |
NSURL *backupURL = [NSURL fileURLWithPath:[finalDestinationURL.path stringByAppendingString:@"_old"]]; | |
if ([[NSFileManager defaultManager] fileExistsAtPath:backupURL.path]) { | |
[[NSFileManager defaultManager] removeItemAtURL:backupURL error:&logError]; | |
if (logError) { | |
LogError(@"Error occurred while trying to remove a previous 'emergency' backup of the sqlite file. %@", logError.localizedDescription); | |
logError = nil; | |
} | |
} | |
[[NSFileManager defaultManager] moveItemAtURL:finalDestinationURL | |
toURL:backupURL | |
error:&logError]; | |
if (logError) { | |
LogError(@"Error occurred while trying to move a previous backup of the sqlite file. %@", logError.localizedDescription); | |
logError = nil; | |
} | |
[[NSFileManager defaultManager] removeItemAtURL:finalDestinationURL error:&logError]; | |
if (logError) { | |
LogError(@"Error occurred while trying to clear the save location for a previous sqlite file. %@", logError.localizedDescription); | |
logError = nil; | |
} | |
} | |
return [[NSFileManager defaultManager] moveItemAtURL:sourceStoreURL | |
toURL:finalDestinationURL | |
error:error]; | |
} | |
} | |
return YES; | |
} | |
NSManagedObjectModel *sourceModel = [self sourceModelForSourceMetadata:sourceMetadata]; | |
NSManagedObjectModel *destinationModel = nil; | |
NSMappingModel *mappingModel = nil; | |
NSString *modelName = nil; | |
if (![self getDestinationModel:&destinationModel | |
mappingModel:&mappingModel | |
modelName:&modelName | |
forSourceModel:sourceModel | |
error:error]) { | |
return NO; | |
} | |
NSArray *mappingModels = @[mappingModel]; | |
if ([self.delegate respondsToSelector:@selector(migrationManager:mappingModelsForSourceModel:)]) { | |
NSArray *explicitMappingModels = [self.delegate migrationManager:self mappingModelsForSourceModel:sourceModel]; | |
if (0 < explicitMappingModels.count) { | |
mappingModels = explicitMappingModels; | |
} | |
} | |
NSURL *destinationStoreURL = [self destinationStoreURLWithSourceStoreURL:sourceStoreURL | |
modelName:modelName]; | |
NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel | |
destinationModel:destinationModel]; | |
[manager addObserver:self | |
forKeyPath:@"migrationProgress" | |
options:NSKeyValueObservingOptionNew | |
context:nil]; | |
BOOL didMigrate = NO; | |
for (NSMappingModel *mappingModel in mappingModels) { | |
// stack overflow helped! https://stackoverflow.com/a/9428260/421797 | |
NSArray *newEntityMappings = [NSArray arrayWithArray:mappingModel.entityMappings]; | |
for (NSEntityMapping *entityMapping in newEntityMappings) { | |
[entityMapping setSourceEntityVersionHash:[sourceModel.entityVersionHashesByName valueForKey:entityMapping.sourceEntityName]]; | |
[entityMapping setDestinationEntityVersionHash:[destinationModel.entityVersionHashesByName valueForKey:entityMapping.destinationEntityName]]; | |
} | |
mappingModel.entityMappings = newEntityMappings; | |
/// end stackoverflow helper! | |
didMigrate = [manager migrateStoreFromURL:sourceStoreURL | |
type:type | |
options:nil | |
withMappingModel:mappingModel | |
toDestinationURL:destinationStoreURL | |
destinationType:type | |
destinationOptions:nil | |
error:error]; | |
} | |
[manager removeObserver:self | |
forKeyPath:@"migrationProgress"]; | |
if (!didMigrate) { | |
return NO; | |
} | |
// Migration was successful, move the files around to preserve the source in case things go bad | |
if (![self backupSourceStoreAtURL:sourceStoreURL | |
movingDestinationStoreAtURL:destinationStoreURL | |
error:error]) { | |
return NO; | |
} | |
// We may not be at the "current" model yet, so recurse | |
return [self progressivelyMigrateURL:sourceStoreURL | |
ofType:type | |
options:storeOptions | |
toModel:finalModel | |
error:error]; | |
} | |
- (NSArray *)modelPaths | |
{ | |
//Find all of the mom and momd files in the Resources directory | |
NSMutableArray *modelPaths = [NSMutableArray array]; | |
NSArray *momdArray = [[NSBundle mainBundle] pathsForResourcesOfType:@"momd" | |
inDirectory:nil]; | |
for (NSString *momdPath in momdArray) { | |
NSString *resourceSubpath = [momdPath lastPathComponent]; | |
NSArray *array = [[NSBundle mainBundle] pathsForResourcesOfType:@"mom" | |
inDirectory:resourceSubpath]; | |
[modelPaths addObjectsFromArray:array]; | |
} | |
NSArray *otherModels = [[NSBundle mainBundle] pathsForResourcesOfType:@"mom" | |
inDirectory:nil]; | |
[modelPaths addObjectsFromArray:otherModels]; | |
return modelPaths; | |
} | |
- (NSManagedObjectModel *)sourceModelForSourceMetadata:(NSDictionary *)sourceMetadata | |
{ | |
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil | |
forStoreMetadata:sourceMetadata]; | |
return model; | |
} | |
- (BOOL)getDestinationModel:(NSManagedObjectModel **)destinationModel | |
mappingModel:(NSMappingModel **)mappingModel | |
modelName:(NSString **)modelName | |
forSourceModel:(NSManagedObjectModel *)sourceModel | |
error:(NSError **)error | |
{ | |
NSArray *modelPaths = [self modelPaths]; | |
if (!modelPaths.count) { | |
//Throw an error if there are no models | |
if (NULL != error) { | |
LogError(@"No data models found in bundle!"); | |
*error = [NSError errorWithDomain:@"Zarra" | |
code:8001 | |
userInfo:@{ NSLocalizedDescriptionKey : @"No models found!" }]; | |
} | |
return NO; | |
} | |
//See if we can find a matching destination model | |
NSManagedObjectModel *model = nil; | |
NSMappingModel *mapping = nil; | |
NSString *modelPath = nil; | |
for (modelPath in modelPaths) { | |
LogInfo(@"Looking for mapping model at path: %@", modelPath); | |
model = [[NSManagedObjectModel alloc] initWithContentsOfURL:[NSURL fileURLWithPath:modelPath]]; | |
mapping = [NSMappingModel mappingModelFromBundles:@[[NSBundle mainBundle]] | |
forSourceModel:sourceModel | |
destinationModel:model]; | |
//If we found a mapping model then proceed | |
if (mapping) { | |
LogInfo(@"Found mapping model at path: %@"); | |
break; | |
} | |
} | |
//We have tested every model, if nil here we failed | |
if (!mapping) { | |
if (NULL != error) { | |
LogError(@"No mapping model found in bundle"); | |
*error = [NSError errorWithDomain:@"Zarra" | |
code:8001 | |
userInfo:@{ NSLocalizedDescriptionKey : @"No mapping model found in bundle" }]; | |
} | |
return NO; | |
} else { | |
*destinationModel = model; | |
*mappingModel = mapping; | |
*modelName = modelPath.lastPathComponent.stringByDeletingPathExtension; | |
} | |
return YES; | |
} | |
- (NSURL *)destinationStoreURLWithSourceStoreURL:(NSURL *)sourceStoreURL | |
modelName:(NSString *)modelName | |
{ | |
// We have a mapping model, time to migrate | |
NSString *storeExtension = sourceStoreURL.path.pathExtension.length > 0 ? sourceStoreURL.path.pathExtension : @"sqlite"; | |
NSString *storePath = sourceStoreURL.path.stringByDeletingPathExtension; | |
// Build a path to write the new store | |
storePath = [NSString stringWithFormat:@"%@.%@.%@", storePath, modelName, storeExtension]; | |
return [NSURL fileURLWithPath:storePath]; | |
} | |
- (BOOL)backupSourceStoreAtURL:(NSURL *)sourceStoreURL | |
movingDestinationStoreAtURL:(NSURL *)destinationStoreURL | |
error:(NSError **)error | |
{ | |
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString]; | |
NSString *backupPath = [NSTemporaryDirectory() stringByAppendingPathComponent:guid]; | |
LogInfo(@"Using Backup Path for old data: %@", backupPath); | |
NSFileManager *fileManager = [NSFileManager defaultManager]; | |
if (![fileManager moveItemAtPath:sourceStoreURL.path | |
toPath:backupPath | |
error:error]) { | |
//Failed to copy the file | |
LogError(@"Failed to copy a source store at %@ to %@", sourceStoreURL.path, backupPath); | |
return NO; | |
} | |
//Move the destination to the source path | |
if (![fileManager moveItemAtPath:destinationStoreURL.path | |
toPath:sourceStoreURL.path | |
error:error]) { | |
//Try to back out the source move first, no point in checking it for errors | |
LogError(@"Failed to copy the destination store at %@ to main store path %@", destinationStoreURL.path, sourceStoreURL.path); | |
[fileManager moveItemAtPath:backupPath | |
toPath:sourceStoreURL.path | |
error:nil]; | |
return NO; | |
} | |
return YES; | |
} | |
- (void)observeValueForKeyPath:(NSString *)keyPath | |
ofObject:(id)object | |
change:(NSDictionary *)change | |
context:(void *)context | |
{ | |
if ([keyPath isEqualToString:@"migrationProgress"]) { | |
LogInfo(@"progress: %f", [object migrationProgress]); | |
if ([self.delegate respondsToSelector:@selector(migrationManager:migrationProgress:)]) { | |
[self.delegate migrationManager:self migrationProgress:[(NSMigrationManager *)object migrationProgress]]; | |
} | |
} else { | |
[super observeValueForKeyPath:keyPath | |
ofObject:object | |
change:change | |
context:context]; | |
} | |
} | |
@end | |
////// YOU WILL NEED THESE CATEGORIES TOO | |
#import <Foundation/Foundation.h> | |
@interface NSFileManager (MHWAdditions) | |
+ (NSURL *)urlToApplicationSupportDirectory; | |
@end | |
// .m | |
#import "NSFileManager+MHWAdditions.h" | |
@implementation NSFileManager (MHWAdditions) | |
+ (NSURL *)urlToApplicationSupportDirectory | |
{ | |
NSString *applicationSupportDirectory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, | |
NSUserDomainMask, | |
YES) objectAtIndex:0]; | |
BOOL isDir = NO; | |
NSError *error = nil; | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
if (![fileManager fileExistsAtPath:applicationSupportDirectory | |
isDirectory:&isDir] && isDir == NO) { | |
[fileManager createDirectoryAtPath:applicationSupportDirectory | |
withIntermediateDirectories:NO | |
attributes:nil | |
error:&error]; | |
} | |
return [NSURL fileURLWithPath:applicationSupportDirectory]; | |
} | |
@end | |
///// AND THIS: | |
#import <CoreData/CoreData.h> | |
@interface NSManagedObjectModel (MHWAdditions) | |
+ (NSArray *_Nonnull)mhw_allModelPaths; | |
- (NSString *_Nullable)mhw_modelName; | |
@end | |
/// .m | |
#import "NSManagedObjectModel+MHWAdditions.h" | |
@implementation NSManagedObjectModel (MHWAdditions) | |
+ (NSArray *)mhw_allModelPaths | |
{ | |
//Find all of the mom and momd files in the Resources directory | |
NSMutableArray *modelPaths = [NSMutableArray array]; | |
NSArray *momdArray = [[NSBundle mainBundle] pathsForResourcesOfType:@"momd" | |
inDirectory:nil]; | |
for (NSString *momdPath in momdArray) { | |
NSString *resourceSubpath = [momdPath lastPathComponent]; | |
NSArray *array = [[NSBundle mainBundle] pathsForResourcesOfType:@"mom" | |
inDirectory:resourceSubpath]; | |
[modelPaths addObjectsFromArray:array]; | |
} | |
NSArray *otherModels = [[NSBundle mainBundle] pathsForResourcesOfType:@"mom" | |
inDirectory:nil]; | |
[modelPaths addObjectsFromArray:otherModels]; | |
return modelPaths; | |
} | |
- (NSString *)mhw_modelName | |
{ | |
NSString *modelName = nil; | |
NSArray *modelPaths = [[self class] mhw_allModelPaths]; | |
for (NSString *modelPath in modelPaths) { | |
NSURL *modelURL = [NSURL fileURLWithPath:modelPath]; | |
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; | |
if ([model isEqual:self]) { | |
modelName = modelURL.lastPathComponent.stringByDeletingPathExtension; | |
break; | |
} | |
} | |
return modelName; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment