// // DatabaseManager.swift // Delta // // Created by Riley Testut on 10/4/15. // Copyright © 2015 Riley Testut. All rights reserved. // import CoreData // Workspace import Roxas import DeltaCore // Pods import FileMD5Hash class DatabaseManager { static let sharedManager = DatabaseManager() let managedObjectContext: NSManagedObjectContext private let privateManagedObjectContext: NSManagedObjectContext private let validationManagedObjectContext: NSManagedObjectContext // MARK: - Initialization - /// Initialization private init() { let modelURL = Bundle.main.urlForResource("Model", withExtension: "momd") let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL!) let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!) self.privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) self.privateManagedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator self.privateManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy self.managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) self.managedObjectContext.parent = self.privateManagedObjectContext self.managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy self.validationManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) self.validationManagedObjectContext.parent = self.managedObjectContext self.validationManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy NotificationCenter.default.addObserver(self, selector: #selector(DatabaseManager.managedObjectContextWillSave(_:)), name: NSNotification.Name.NSManagedObjectContextWillSave, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(DatabaseManager.managedObjectContextDidSave(_:)), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil) } func startWithCompletion(_ completionBlock: ((performingMigration: Bool) -> Void)?) { DispatchQueue.global(attributes: DispatchQueue.GlobalAttributes.qosUserInitiated).async { let storeURL = try! DatabaseManager.databaseDirectoryURL.appendingPathComponent("Delta.sqlite") let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true] var performingMigration = false if let sourceMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL, options: options), let managedObjectModel = self.privateManagedObjectContext.persistentStoreCoordinator?.managedObjectModel { performingMigration = !managedObjectModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata) } do { try self.privateManagedObjectContext.persistentStoreCoordinator?.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) } catch let error as NSError { if error.code == NSMigrationMissingSourceModelError { print("Migration failed. Try deleting \(storeURL)") } else { print(error) } abort() } if let completionBlock = completionBlock { completionBlock(performingMigration: performingMigration) } } } // MARK: - Importing - /// Importing func importGamesAtURLs(_ URLs: [URL], withCompletion completion: (([String]) -> Void)?) { let managedObjectContext = self.backgroundManagedObjectContext() managedObjectContext.perform() { var identifiers: [String] = [] for URL in URLs { let identifier = FileHash.sha1HashOfFile(atPath: URL.path) as String var filename = identifier if let pathExtension = URL.pathExtension { filename += "." + pathExtension } let game = Game.insertIntoManagedObjectContext(managedObjectContext) game.name = try! URL.deletingPathExtension().lastPathComponent ?? NSLocalizedString("Game", comment: "") game.identifier = identifier game.filename = filename if let pathExtension = URL.pathExtension { let gameCollection = GameCollection.gameSystemCollectionForPathExtension(pathExtension, inManagedObjectContext: managedObjectContext) game.type = GameType(rawValue: gameCollection.identifier) game.gameCollections.insert(gameCollection) } else { game.type = GameType.delta } do { let destinationURL = try! DatabaseManager.gamesDirectoryURL.appendingPathComponent(game.identifier + "." + game.preferredFileExtension) if let path = destinationURL.path { if FileManager.default.fileExists(atPath: path) { try FileManager.default.removeItem(at: URL) } else { try FileManager.default.moveItem(at: URL, to: destinationURL) } } identifiers.append(game.identifier) } catch { game.managedObjectContext?.delete(game) } } do { try managedObjectContext.save() } catch let error as NSError { print("Failed to save import context:", error) identifiers.removeAll() } if let completion = completion { completion(identifiers) } } } // MARK: - Background Contexts - /// Background Contexts func backgroundManagedObjectContext() -> NSManagedObjectContext { let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) managedObjectContext.parent = self.validationManagedObjectContext managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return managedObjectContext } } extension DatabaseManager { class var databaseDirectoryURL: URL { let documentsDirectoryURL: URL if UIDevice.current.userInterfaceIdiom == .tv { documentsDirectoryURL = FileManager.default.urlsForDirectory(FileManager.SearchPathDirectory.cachesDirectory, inDomains: FileManager.SearchPathDomainMask.userDomainMask).first! } else { documentsDirectoryURL = FileManager.default.urlsForDirectory(FileManager.SearchPathDirectory.documentDirectory, inDomains: FileManager.SearchPathDomainMask.userDomainMask).first! } let databaseDirectoryURL = try! documentsDirectoryURL.appendingPathComponent("Database") self.createDirectoryAtURLIfNeeded(databaseDirectoryURL) return databaseDirectoryURL } class var gamesDirectoryURL: URL { let gamesDirectoryURL = try! DatabaseManager.databaseDirectoryURL.appendingPathComponent("Games") self.createDirectoryAtURLIfNeeded(gamesDirectoryURL) return gamesDirectoryURL } class var saveStatesDirectoryURL: URL { let saveStatesDirectoryURL = try! DatabaseManager.databaseDirectoryURL.appendingPathComponent("Save States") self.createDirectoryAtURLIfNeeded(saveStatesDirectoryURL) return saveStatesDirectoryURL } class func saveStatesDirectoryURLForGame(_ game: Game) -> URL { let gameDirectoryURL = try! DatabaseManager.saveStatesDirectoryURL.appendingPathComponent(game.identifier) self.createDirectoryAtURLIfNeeded(gameDirectoryURL) return gameDirectoryURL } } private extension DatabaseManager { // MARK: - Saving - func save() { let backgroundTaskIdentifier = RSTBeginBackgroundTask("Save Database Task") self.validationManagedObjectContext.performAndWait { do { try self.validationManagedObjectContext.save() } catch let error as NSError { print("Failed to save validation context:", error) } // Update main managed object context self.managedObjectContext.performAndWait() { do { try self.managedObjectContext.save() } catch let error as NSError { print("Failed to save main context:", error) } // Save to disk self.privateManagedObjectContext.perform() { do { try self.privateManagedObjectContext.save() } catch let error as NSError { print("Failed to save private context to disk:", error) } RSTEndBackgroundTask(backgroundTaskIdentifier) } } } } // MARK: - Validation - func validateManagedObjectContextSave(_ managedObjectContext: NSManagedObjectContext) { // Remove deleted files from disk for object in managedObjectContext.deletedObjects { var fileURLs = Set() let temporaryObject = self.validationManagedObjectContext.object(with: object.objectID) switch temporaryObject { case let game as Game: fileURLs.insert(game.fileURL as URL) case let saveState as SaveState: fileURLs.insert(saveState.fileURL as URL) fileURLs.insert(saveState.imageFileURL as URL) default: break } for URL in fileURLs { do { try FileManager.default.removeItem(at: URL) } catch let error as NSError { print(error) } } } // Remove empty collections let collections = GameCollection.instancesWithPredicate(Predicate(format: "%K.@count == 0", GameCollection.Attributes.games.rawValue), inManagedObjectContext: self.validationManagedObjectContext, type: GameCollection.self) for collection in collections { self.validationManagedObjectContext.delete(collection) } } // MARK: - Notifications - @objc func managedObjectContextWillSave(_ notification: Notification) { guard let managedObjectContext = notification.object as? NSManagedObjectContext, managedObjectContext.parent == self.validationManagedObjectContext else { return } self.validationManagedObjectContext.performAndWait { self.validateManagedObjectContextSave(managedObjectContext) } } @objc func managedObjectContextDidSave(_ notification: Notification) { guard let managedObjectContext = notification.object as? NSManagedObjectContext, managedObjectContext.parent == self.validationManagedObjectContext else { return } self.save() } // MARK: - File Management - class func createDirectoryAtURLIfNeeded(_ URL: Foundation.URL) { do { try FileManager.default.createDirectory(at: URL, withIntermediateDirectories: true, attributes: nil) } catch { print(error) } } }