Fixes importing games + controller skins from Files
Exposing Documents directory in Files app requires us to support opening files in place (despite LSSupportsOpeningDocumentsInPlace set to NO in Info.plist), so we now coordinate access to any external file URL
This commit is contained in:
parent
a739926e17
commit
5c9332e61e
@ -23,13 +23,24 @@ extension DatabaseManager
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
enum ImportError: Error, Hashable, Equatable
|
||||
enum ImportError: LocalizedError, Hashable, Equatable
|
||||
{
|
||||
case doesNotExist(URL)
|
||||
case invalid(URL)
|
||||
case unsupported(URL)
|
||||
case unknown(URL, NSError)
|
||||
case saveFailed(Set<URL>, NSError)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .doesNotExist: return NSLocalizedString("The file does not exist.", comment: "")
|
||||
case .invalid: return NSLocalizedString("The file is invalid.", comment: "")
|
||||
case .unsupported: return NSLocalizedString("This file is not supported.", comment: "")
|
||||
case .unknown(_, let error): return error.localizedDescription
|
||||
case .saveFailed(_, let error): return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +54,8 @@ final class DatabaseManager: RSTPersistentContainer
|
||||
|
||||
private var validationManagedObjectContext: NSManagedObjectContext?
|
||||
|
||||
private let importController = ImportController(documentTypes: [])
|
||||
|
||||
private init()
|
||||
{
|
||||
guard
|
||||
@ -218,7 +231,20 @@ extension DatabaseManager
|
||||
{
|
||||
func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?)
|
||||
{
|
||||
var errors = Set<ImportError>()
|
||||
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
|
||||
guard externalFileURLs.isEmpty else {
|
||||
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
|
||||
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
|
||||
availableFileURLs.formUnion(importedURLs)
|
||||
|
||||
self.importGames(at: Set(availableFileURLs)) { (importedGames, importErrors) in
|
||||
let allErrors = importErrors.union(externalImportErrors)
|
||||
completion?(importedGames, allErrors)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
|
||||
if zipFileURLs.count > 0
|
||||
@ -236,6 +262,7 @@ extension DatabaseManager
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
var errors = Set<ImportError>()
|
||||
var identifiers = Set<String>()
|
||||
|
||||
for url in urls
|
||||
@ -331,10 +358,24 @@ extension DatabaseManager
|
||||
|
||||
func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?)
|
||||
{
|
||||
var errors = Set<ImportError>()
|
||||
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
|
||||
guard externalFileURLs.isEmpty else {
|
||||
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
|
||||
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
|
||||
availableFileURLs.formUnion(importedURLs)
|
||||
|
||||
self.importControllerSkins(at: Set(availableFileURLs)) { (importedSkins, importErrors) in
|
||||
let allErrors = importErrors.union(externalImportErrors)
|
||||
completion?(importedSkins, allErrors)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
var errors = Set<ImportError>()
|
||||
var identifiers = Set<String>()
|
||||
|
||||
for url in urls
|
||||
@ -473,6 +514,32 @@ extension DatabaseManager
|
||||
completion(outputURLs, errors)
|
||||
}
|
||||
}
|
||||
|
||||
private func importExternalFiles(at urls: Set<URL>, completion: @escaping ((Set<URL>, Set<ImportError>) -> Void))
|
||||
{
|
||||
var outputURLs = Set<URL>()
|
||||
var errors = Set<ImportError>()
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for url in urls
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.importController.importExternalFile(at: url) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): errors.insert(.unknown(url, error as NSError))
|
||||
case .success(let fileURL): outputURLs.insert(fileURL)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
completion(outputURLs, errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - File URLs -
|
||||
|
||||
@ -20,16 +20,6 @@ extension UIAlertController
|
||||
|
||||
class func alertController(for importType: ImportType, with errors: Set<DatabaseManager.ImportError>) -> UIAlertController
|
||||
{
|
||||
let title: String
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
|
||||
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
|
||||
var urls = Set<URL>()
|
||||
|
||||
for error in errors
|
||||
@ -44,38 +34,54 @@ extension UIAlertController
|
||||
}
|
||||
}
|
||||
|
||||
let filenames = urls.map{ $0.lastPathComponent }.sorted()
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
if filenames.count > 0
|
||||
if let fileURL = urls.first, let error = errors.first, errors.count == 1
|
||||
{
|
||||
var message: String
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: message = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n"
|
||||
case .controllerSkins: message = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n"
|
||||
}
|
||||
|
||||
for filename in filenames
|
||||
{
|
||||
message += "\n" + filename
|
||||
}
|
||||
|
||||
alertController.message = message
|
||||
title = String(format: NSLocalizedString("Could not import “%@”.", comment: ""), fileURL.lastPathComponent)
|
||||
message = error.localizedDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
// This branch can be executed when there are no input URLs when importing, but there is an error saving the database anyway.
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: alertController.message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "")
|
||||
case .controllerSkins: alertController.message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "")
|
||||
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
|
||||
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
|
||||
}
|
||||
|
||||
if urls.count > 0
|
||||
{
|
||||
var tempMessage: String
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: tempMessage = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n"
|
||||
case .controllerSkins: tempMessage = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n"
|
||||
}
|
||||
|
||||
let filenames = urls.map { $0.lastPathComponent }.sorted()
|
||||
for filename in filenames
|
||||
{
|
||||
tempMessage += "\n" + filename
|
||||
}
|
||||
|
||||
message = tempMessage
|
||||
}
|
||||
else
|
||||
{
|
||||
// This branch can be executed when there are no input URLs when importing, but there is an error saving the database anyway.
|
||||
|
||||
switch importType
|
||||
{
|
||||
case .games: message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "")
|
||||
case .controllerSkins: message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
|
||||
|
||||
return alertController
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +131,39 @@ class ImportController: NSObject
|
||||
}
|
||||
}
|
||||
|
||||
extension ImportController
|
||||
{
|
||||
func importExternalFile(at fileURL: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.readingIntent(with: fileURL)
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
else
|
||||
{
|
||||
// User intent.url, not url, as the system may have updated it when requesting access.
|
||||
guard intent.url.startAccessingSecurityScopedResource() else { throw CocoaError.error(.fileReadNoPermission) }
|
||||
defer { intent.url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
// Use url, not intent.url, to ensure the file name matches what was in the document browser.
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: intent.url, to: temporaryURL, shouldReplace: true)
|
||||
|
||||
completionHandler(.success(temporaryURL))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImportController: UIDocumentBrowserViewControllerDelegate
|
||||
{
|
||||
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL])
|
||||
@ -144,31 +177,11 @@ extension ImportController: UIDocumentBrowserViewControllerDelegate
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url)
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in
|
||||
|
||||
do
|
||||
self.importExternalFile(at: url) { (result) in
|
||||
switch result
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
else
|
||||
{
|
||||
// User intent.url, not url, as the system may have updated it when requesting access.
|
||||
guard intent.url.startAccessingSecurityScopedResource() else { throw CocoaError.error(.fileReadNoPermission) }
|
||||
defer { intent.url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
// Use url, not intent.url, to ensure the file name matches what was in the document browser.
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: intent.url, to: temporaryURL, shouldReplace: true)
|
||||
|
||||
coordinatedURLs.insert(temporaryURL)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
errors.append(error)
|
||||
case .failure(let error): errors.append(error)
|
||||
case .success(let fileURL): coordinatedURLs.insert(fileURL)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user