Skip to content

Instantly share code, notes, and snippets.

@horseshoe7
Last active April 27, 2021 08:33
Show Gist options
  • Save horseshoe7/e85bbb90278f626bea2f827ee5228703 to your computer and use it in GitHub Desktop.
Save horseshoe7/e85bbb90278f626bea2f827ee5228703 to your computer and use it in GitHub Desktop.
Incremental MHWMigrationManager extended for optional finalDestinationURL
#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