Adds support for importing zip archives containing games
This commit is contained in:
parent
062abf6dbe
commit
7b43b48b51
@ -1 +1 @@
|
|||||||
Subproject commit e2e166d01c4e866c9707944fd6df53556335056b
|
Subproject commit 55ea13366edc40bb7cd9978c6127bd4f49aa45d1
|
||||||
@ -91,6 +91,7 @@
|
|||||||
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
|
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
|
||||||
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; };
|
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; };
|
||||||
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
|
BFF93AA01E0FB036005EC865 /* InputStreamOutputWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */; };
|
||||||
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; };
|
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; };
|
||||||
BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; };
|
BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; };
|
||||||
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */; };
|
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */; };
|
||||||
@ -193,6 +194,7 @@
|
|||||||
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
|
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesStoryboardSegue.swift; path = Segues/SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
|
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesStoryboardSegue.swift; path = Segues/SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
|
||||||
BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InputStreamOutputWriter.swift; path = Components/Importing/InputStreamOutputWriter.swift; sourceTree = "<group>"; };
|
||||||
BFFA71D71AAC406100EE9DD1 /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
BFFA71D71AAC406100EE9DD1 /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
@ -374,6 +376,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BF59428D1E09BCFB0051894B /* ImportController.swift */,
|
BF59428D1E09BCFB0051894B /* ImportController.swift */,
|
||||||
|
BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */,
|
||||||
);
|
);
|
||||||
name = Importing;
|
name = Importing;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -758,6 +761,7 @@
|
|||||||
BF59425C1E09BB810051894B /* Action.swift in Sources */,
|
BF59425C1E09BB810051894B /* Action.swift in Sources */,
|
||||||
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */,
|
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */,
|
||||||
BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */,
|
BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */,
|
||||||
|
BFF93AA01E0FB036005EC865 /* InputStreamOutputWriter.swift in Sources */,
|
||||||
BF59427F1E09BC830051894B /* GameCollection.swift in Sources */,
|
BF59427F1E09BC830051894B /* GameCollection.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
@ -83,7 +83,7 @@ extension AppDelegate
|
|||||||
guard url.isFileURL else { return false }
|
guard url.isFileURL else { return false }
|
||||||
|
|
||||||
let gameType = GameType.gameType(forFileExtension: url.pathExtension)
|
let gameType = GameType.gameType(forFileExtension: url.pathExtension)
|
||||||
if gameType != .unknown
|
if gameType != .unknown || url.pathExtension.lowercased() == "zip"
|
||||||
{
|
{
|
||||||
return self.importGame(at: url)
|
return self.importGame(at: url)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import ObjectiveC
|
|||||||
|
|
||||||
import DeltaCore
|
import DeltaCore
|
||||||
|
|
||||||
|
import MobileCoreServices
|
||||||
|
|
||||||
protocol ImportControllerDelegate
|
protocol ImportControllerDelegate
|
||||||
{
|
{
|
||||||
func importController(_ importController: ImportController, didImport games: Set<Game>)
|
func importController(_ importController: ImportController, didImport games: Set<Game>)
|
||||||
@ -40,6 +42,7 @@ class ImportController: NSObject
|
|||||||
|
|
||||||
var documentTypes = GameType.supportedTypes.map { $0.rawValue }
|
var documentTypes = GameType.supportedTypes.map { $0.rawValue }
|
||||||
documentTypes.append(kUTTypeDeltaControllerSkin as String)
|
documentTypes.append(kUTTypeDeltaControllerSkin as String)
|
||||||
|
documentTypes.append(kUTTypeZipArchive as String)
|
||||||
|
|
||||||
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
|
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
|
||||||
documentTypes.append("com.rileytestut.gba")
|
documentTypes.append("com.rileytestut.gba")
|
||||||
@ -72,7 +75,7 @@ class ImportController: NSObject
|
|||||||
let controllerSkinURLs = contents.filter { $0.pathExtension.lowercased() == "deltaskin" }
|
let controllerSkinURLs = contents.filter { $0.pathExtension.lowercased() == "deltaskin" }
|
||||||
self.importControllerSkins(at: controllerSkinURLs)
|
self.importControllerSkins(at: controllerSkinURLs)
|
||||||
|
|
||||||
let gameURLs = contents.filter { GameType.gameType(forFileExtension: $0.pathExtension) != .unknown }
|
let gameURLs = contents.filter { GameType.gameType(forFileExtension: $0.pathExtension) != .unknown || $0.pathExtension.lowercased() == "zip" }
|
||||||
self.importGames(at: gameURLs)
|
self.importGames(at: gameURLs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
148
Delta/Components/Importing/InputStreamOutputWriter.swift
Normal file
148
Delta/Components/Importing/InputStreamOutputWriter.swift
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
//
|
||||||
|
// InputStreamOutputWriter.swift
|
||||||
|
// Delta
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 12/25/16.
|
||||||
|
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private let MaximumBufferLength = 4 * 1024 // 4 KB
|
||||||
|
|
||||||
|
class InputStreamOutputWriter: NSObject
|
||||||
|
{
|
||||||
|
let inputStream: InputStream
|
||||||
|
let outputStream: OutputStream
|
||||||
|
|
||||||
|
fileprivate var completion: ((Error?) -> Void)?
|
||||||
|
|
||||||
|
fileprivate var dataBuffer = Data(capacity: MaximumBufferLength * 2)
|
||||||
|
|
||||||
|
init(inputStream: InputStream, outputStream: OutputStream)
|
||||||
|
{
|
||||||
|
self.inputStream = inputStream
|
||||||
|
self.outputStream = outputStream
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.inputStream.delegate = self
|
||||||
|
self.outputStream.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(with completion: @escaping ((Error?) -> Void))
|
||||||
|
{
|
||||||
|
guard self.completion == nil else { return }
|
||||||
|
|
||||||
|
self.completion = completion
|
||||||
|
|
||||||
|
let writingQueue = DispatchQueue(label: "com.rileytestut.InputStreamOutputWriter.writingQueue", qos: .userInitiated)
|
||||||
|
writingQueue.async {
|
||||||
|
self.inputStream.schedule(in: .current, forMode: .defaultRunLoopMode)
|
||||||
|
self.outputStream.schedule(in: .current, forMode: .defaultRunLoopMode)
|
||||||
|
|
||||||
|
self.outputStream.open()
|
||||||
|
self.inputStream.open()
|
||||||
|
|
||||||
|
RunLoop.current.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension InputStreamOutputWriter
|
||||||
|
{
|
||||||
|
func writeDataBuffer()
|
||||||
|
{
|
||||||
|
while self.outputStream.hasSpaceAvailable && self.dataBuffer.count > 0
|
||||||
|
{
|
||||||
|
self.dataBuffer.withUnsafeMutableBytes { (buffer: UnsafeMutablePointer<UInt8>) -> Void in
|
||||||
|
let writtenBytesCount = self.outputStream.write(buffer, maxLength: self.dataBuffer.count)
|
||||||
|
if writtenBytesCount >= 0
|
||||||
|
{
|
||||||
|
self.dataBuffer.removeSubrange(0 ..< writtenBytesCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishWriting()
|
||||||
|
{
|
||||||
|
self.inputStream.close()
|
||||||
|
self.outputStream.close()
|
||||||
|
|
||||||
|
self.inputStream.remove(from: .current, forMode: .commonModes)
|
||||||
|
self.outputStream.remove(from: .current, forMode: .commonModes)
|
||||||
|
|
||||||
|
self.completion?(self.inputStream.streamError ?? self.outputStream.streamError)
|
||||||
|
|
||||||
|
CFRunLoopStop(CFRunLoopGetCurrent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InputStreamOutputWriter: StreamDelegate
|
||||||
|
{
|
||||||
|
func stream(_ aStream: Stream, handle eventCode: Stream.Event)
|
||||||
|
{
|
||||||
|
if let inputStream = aStream as? InputStream
|
||||||
|
{
|
||||||
|
self.inputStream(inputStream, handle: eventCode)
|
||||||
|
}
|
||||||
|
else if let outputStream = aStream as? OutputStream
|
||||||
|
{
|
||||||
|
self.outputStream(outputStream, handle: eventCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func inputStream(_ inputStream: InputStream, handle eventCode: Stream.Event)
|
||||||
|
{
|
||||||
|
switch eventCode
|
||||||
|
{
|
||||||
|
case Stream.Event.hasBytesAvailable:
|
||||||
|
|
||||||
|
guard inputStream.streamError == nil else { return }
|
||||||
|
|
||||||
|
while inputStream.hasBytesAvailable
|
||||||
|
{
|
||||||
|
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: MaximumBufferLength)
|
||||||
|
|
||||||
|
let readBytesCount = inputStream.read(buffer, maxLength: MaximumBufferLength)
|
||||||
|
|
||||||
|
guard readBytesCount >= 0 else { break }
|
||||||
|
|
||||||
|
self.dataBuffer.append(buffer, count: readBytesCount)
|
||||||
|
|
||||||
|
buffer.deallocate(capacity: MaximumBufferLength)
|
||||||
|
|
||||||
|
self.writeDataBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
case Stream.Event.endEncountered:
|
||||||
|
if self.dataBuffer.count == 0
|
||||||
|
{
|
||||||
|
self.finishWriting()
|
||||||
|
}
|
||||||
|
|
||||||
|
case Stream.Event.errorOccurred: self.finishWriting()
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func outputStream(_ outputStream: OutputStream, handle eventCode: Stream.Event)
|
||||||
|
{
|
||||||
|
switch eventCode
|
||||||
|
{
|
||||||
|
case Stream.Event.hasSpaceAvailable:
|
||||||
|
self.writeDataBuffer()
|
||||||
|
|
||||||
|
if self.inputStream.streamStatus == .atEnd
|
||||||
|
{
|
||||||
|
self.finishWriting()
|
||||||
|
}
|
||||||
|
|
||||||
|
case Stream.Event.errorOccurred: self.finishWriting()
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import CoreData
|
|||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
import DeltaCore
|
import DeltaCore
|
||||||
|
import ZipZap
|
||||||
|
|
||||||
// Pods
|
// Pods
|
||||||
import FileMD5Hash
|
import FileMD5Hash
|
||||||
@ -107,6 +108,80 @@ private extension DatabaseManager
|
|||||||
/// Importing
|
/// Importing
|
||||||
extension DatabaseManager
|
extension DatabaseManager
|
||||||
{
|
{
|
||||||
|
func importGames(at urls: [URL], completion: ((Set<String>) -> Void)?)
|
||||||
|
{
|
||||||
|
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
|
||||||
|
if zipFileURLs.count > 0
|
||||||
|
{
|
||||||
|
self.extractCompressedGames(at: zipFileURLs) { (extractedURLs) in
|
||||||
|
let gameURLs = urls.filter { $0.pathExtension.lowercased() != "zip" } + extractedURLs
|
||||||
|
self.importGames(at: gameURLs, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.performBackgroundTask { (context) in
|
||||||
|
|
||||||
|
var identifiers = Set<String>()
|
||||||
|
|
||||||
|
for url in urls
|
||||||
|
{
|
||||||
|
guard FileManager.default.fileExists(atPath: url.path) else { continue }
|
||||||
|
|
||||||
|
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String
|
||||||
|
|
||||||
|
let filename = identifier + "." + url.pathExtension
|
||||||
|
|
||||||
|
let game = Game.insertIntoManagedObjectContext(context)
|
||||||
|
game.name = url.deletingPathExtension().lastPathComponent
|
||||||
|
game.identifier = identifier
|
||||||
|
game.filename = filename
|
||||||
|
game.artworkURL = self.gamesDatabase?.artworkURL(for: game)
|
||||||
|
|
||||||
|
let gameCollection = GameCollection.gameSystemCollectionForPathExtension(url.pathExtension, inManagedObjectContext: context)
|
||||||
|
game.type = GameType(rawValue: gameCollection.identifier)
|
||||||
|
game.gameCollections.insert(gameCollection)
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let destinationURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(filename)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: destinationURL.path)
|
||||||
|
{
|
||||||
|
// Game already exists, so we choose not to override it and just delete the new game instead
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try FileManager.default.moveItem(at: url, to: destinationURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
identifiers.insert(game.identifier)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Import Games error:", error)
|
||||||
|
game.managedObjectContext?.delete(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to save import context:", error)
|
||||||
|
|
||||||
|
identifiers.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
completion?(identifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func importControllerSkins(at urls: [URL], completion: ((Set<String>) -> Void)?)
|
func importControllerSkins(at urls: [URL], completion: ((Set<String>) -> Void)?)
|
||||||
{
|
{
|
||||||
self.performBackgroundTask { (context) in
|
self.performBackgroundTask { (context) in
|
||||||
@ -159,66 +234,97 @@ extension DatabaseManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func importGames(at urls: [URL], completion: ((Set<String>) -> Void)?)
|
private func extractCompressedGames(at urls: [URL], completion: @escaping ((Set<URL>) -> Void))
|
||||||
{
|
{
|
||||||
self.performBackgroundTask { (context) in
|
DispatchQueue.global().async {
|
||||||
|
|
||||||
var identifiers = Set<String>()
|
var semaphores = Set<DispatchSemaphore>()
|
||||||
|
var outputURLs = Set<URL>()
|
||||||
|
|
||||||
for url in urls
|
for url in urls
|
||||||
{
|
{
|
||||||
guard FileManager.default.fileExists(atPath: url.path) else { continue }
|
|
||||||
|
|
||||||
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String
|
|
||||||
|
|
||||||
let filename = identifier + "." + url.pathExtension
|
|
||||||
|
|
||||||
let game = Game.insertIntoManagedObjectContext(context)
|
|
||||||
game.name = url.deletingPathExtension().lastPathComponent
|
|
||||||
game.identifier = identifier
|
|
||||||
game.filename = filename
|
|
||||||
game.artworkURL = self.gamesDatabase?.artworkURL(for: game)
|
|
||||||
|
|
||||||
let gameCollection = GameCollection.gameSystemCollectionForPathExtension(url.pathExtension, inManagedObjectContext: context)
|
|
||||||
game.type = GameType(rawValue: gameCollection.identifier)
|
|
||||||
game.gameCollections.insert(gameCollection)
|
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let destinationURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(filename)
|
let archive = try ZZArchive(url: url)
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: destinationURL.path)
|
for entry in archive.entries
|
||||||
{
|
{
|
||||||
// Game already exists, so we choose not to override it and just delete the new game instead
|
// Ensure entry is not in a subdirectory
|
||||||
try FileManager.default.removeItem(at: url)
|
guard !entry.fileName.contains("/") else { continue }
|
||||||
|
|
||||||
|
let fileExtension = (entry.fileName as NSString).pathExtension
|
||||||
|
let gameType = GameType.gameType(forFileExtension: fileExtension)
|
||||||
|
|
||||||
|
guard gameType != .unknown else { continue }
|
||||||
|
|
||||||
|
// ROMs may potentially be very large, so we extract using file streams and not raw Data
|
||||||
|
let inputStream = try entry.newStream()
|
||||||
|
|
||||||
|
let outputURL = url.deletingLastPathComponent().appendingPathComponent(entry.fileName)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: outputURL.path)
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: outputURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let outputStream = OutputStream(url: outputURL, append: false) else { continue }
|
||||||
|
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
semaphores.insert(semaphore)
|
||||||
|
|
||||||
|
let outputWriter = InputStreamOutputWriter(inputStream: inputStream, outputStream: outputStream)
|
||||||
|
outputWriter.start { (error) in
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
if FileManager.default.fileExists(atPath: outputURL.path)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: outputURL)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputURLs.insert(outputURL)
|
||||||
|
semaphore.signal()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
try FileManager.default.moveItem(at: url, to: destinationURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
identifiers.insert(game.identifier)
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
print("Import Games error:", error)
|
print(error)
|
||||||
game.managedObjectContext?.delete(game)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
for semaphore in semaphores
|
||||||
{
|
{
|
||||||
try context.save()
|
semaphore.wait()
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("Failed to save import context:", error)
|
|
||||||
|
|
||||||
identifiers.removeAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
completion?(identifiers)
|
for url in urls
|
||||||
|
{
|
||||||
|
if FileManager.default.fileExists(atPath: url.path)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(outputURLs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,20 @@
|
|||||||
<string>com.rileytestut.delta.skin</string>
|
<string>com.rileytestut.delta.skin</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeIconFiles</key>
|
||||||
|
<array/>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>ZIP File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.pkware.zip-archive</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user