mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
Downloads app dependencies listed in AltStore.plist
Allows apps to download additional dependencies before installation, such as plug-ins.
This commit is contained in:
@@ -12,6 +12,23 @@ import Roxas
|
|||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
|
||||||
|
private extension DownloadAppOperation
|
||||||
|
{
|
||||||
|
struct DependencyError: ALTLocalizedError
|
||||||
|
{
|
||||||
|
let dependency: Dependency
|
||||||
|
let error: Error
|
||||||
|
|
||||||
|
var failure: String? {
|
||||||
|
return String(format: NSLocalizedString("Could not download “%@”.", comment: ""), self.dependency.preferredFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
var underlyingError: Error? {
|
||||||
|
return self.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc(DownloadAppOperation)
|
@objc(DownloadAppOperation)
|
||||||
class DownloadAppOperation: ResultOperation<ALTApplication>
|
class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
{
|
{
|
||||||
@@ -23,6 +40,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
private let destinationURL: URL
|
private let destinationURL: URL
|
||||||
|
|
||||||
private let session = URLSession(configuration: .default)
|
private let session = URLSession(configuration: .default)
|
||||||
|
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||||
|
|
||||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||||
{
|
{
|
||||||
@@ -35,7 +53,8 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.progress.totalUnitCount = 1
|
// App = 3, Dependencies = 1
|
||||||
|
self.progress.totalUnitCount = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
override func main()
|
override func main()
|
||||||
@@ -50,6 +69,66 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
|
|
||||||
print("Downloading App:", self.bundleIdentifier)
|
print("Downloading App:", self.bundleIdentifier)
|
||||||
|
|
||||||
|
self.downloadApp(from: self.sourceURL) { result in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let application = try result.get()
|
||||||
|
|
||||||
|
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
|
||||||
|
{
|
||||||
|
if var infoPlist = NSDictionary(contentsOf: application.bundle.infoPlistURL) as? [String: Any]
|
||||||
|
{
|
||||||
|
// Manually update the app's bundle identifier to match the one specified in the source.
|
||||||
|
// This allows people who previously installed the app to still update and refresh normally.
|
||||||
|
infoPlist[kCFBundleIdentifierKey as String] = StoreApp.dolphinAppID
|
||||||
|
(infoPlist as NSDictionary).write(to: application.bundle.infoPlistURL, atomically: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.downloadDependencies(for: application) { result in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_ = try result.get()
|
||||||
|
|
||||||
|
try FileManager.default.copyItem(at: application.fileURL, to: self.destinationURL, shouldReplace: true)
|
||||||
|
|
||||||
|
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
||||||
|
self.finish(.success(copiedApplication))
|
||||||
|
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func finish(_ result: Result<ALTApplication, Error>)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try FileManager.default.removeItem(at: self.temporaryDirectory)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.finish(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DownloadAppOperation
|
||||||
|
{
|
||||||
|
func downloadApp(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||||
|
{
|
||||||
func finishOperation(_ result: Result<URL, Error>)
|
func finishOperation(_ result: Result<URL, Error>)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -59,9 +138,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
var isDirectory: ObjCBool = false
|
var isDirectory: ObjCBool = false
|
||||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
|
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
|
||||||
|
|
||||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
|
|
||||||
|
|
||||||
let appBundleURL: URL
|
let appBundleURL: URL
|
||||||
|
|
||||||
@@ -70,62 +147,179 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
// Directory, so assuming this is .app bundle.
|
// Directory, so assuming this is .app bundle.
|
||||||
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
|
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
|
||||||
|
|
||||||
appBundleURL = fileURL
|
appBundleURL = self.temporaryDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||||
|
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// File, so assuming this is a .ipa file.
|
// File, so assuming this is a .ipa file.
|
||||||
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
|
appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: self.temporaryDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
guard let application = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
|
||||||
|
completionHandler(.success(application))
|
||||||
try FileManager.default.copyItem(at: appBundleURL, to: self.destinationURL, shouldReplace: true)
|
|
||||||
|
|
||||||
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
|
|
||||||
{
|
|
||||||
let infoPlistURL = self.destinationURL.appendingPathComponent("Info.plist")
|
|
||||||
|
|
||||||
if var infoPlist = NSDictionary(contentsOf: infoPlistURL) as? [String: Any]
|
|
||||||
{
|
|
||||||
// Manually update the app's bundle identifier to match the one specified in the source.
|
|
||||||
// This allows people who previously installed the app to still update and refresh normally.
|
|
||||||
infoPlist[kCFBundleIdentifierKey as String] = StoreApp.dolphinAppID
|
|
||||||
(infoPlist as NSDictionary).write(to: infoPlistURL, atomically: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
|
|
||||||
self.finish(.success(copiedApplication))
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
self.finish(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.sourceURL.isFileURL
|
if self.sourceURL.isFileURL
|
||||||
{
|
{
|
||||||
finishOperation(.success(self.sourceURL))
|
finishOperation(.success(sourceURL))
|
||||||
|
|
||||||
self.progress.completedUnitCount += 1
|
self.progress.completedUnitCount += 3
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
let downloadTask = self.session.downloadTask(with: self.sourceURL) { (fileURL, response, error) in
|
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
finishOperation(.success(fileURL))
|
finishOperation(.success(fileURL))
|
||||||
|
|
||||||
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
finishOperation(.failure(error))
|
finishOperation(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
|
||||||
|
|
||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension DownloadAppOperation
|
||||||
|
{
|
||||||
|
struct AltStorePlist: Decodable
|
||||||
|
{
|
||||||
|
private enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case dependencies = "ALTDependencies"
|
||||||
|
}
|
||||||
|
|
||||||
|
var dependencies: [Dependency]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Dependency: Decodable
|
||||||
|
{
|
||||||
|
var downloadURL: URL
|
||||||
|
var path: String?
|
||||||
|
|
||||||
|
var preferredFilename: String {
|
||||||
|
let preferredFilename = self.path.map { ($0 as NSString).lastPathComponent } ?? self.downloadURL.lastPathComponent
|
||||||
|
return preferredFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case downloadURL
|
||||||
|
case path
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
let urlString = try container.decode(String.self, forKey: .downloadURL)
|
||||||
|
let path = try container.decodeIfPresent(String.self, forKey: .path)
|
||||||
|
|
||||||
|
guard let downloadURL = URL(string: urlString) else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "downloadURL is not a valid URL.")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.downloadURL = downloadURL
|
||||||
|
self.path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadDependencies(for application: ALTApplication, completionHandler: @escaping (Result<Set<URL>, Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard FileManager.default.fileExists(atPath: application.bundle.altstorePlistURL.path) else {
|
||||||
|
return completionHandler(.success([]))
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let data = try Data(contentsOf: application.bundle.altstorePlistURL)
|
||||||
|
|
||||||
|
let altstorePlist = try PropertyListDecoder().decode(AltStorePlist.self, from: data)
|
||||||
|
|
||||||
|
var dependencyURLs = Set<URL>()
|
||||||
|
var dependencyError: DependencyError?
|
||||||
|
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
let progress = Progress(totalUnitCount: Int64(altstorePlist.dependencies.count), parent: self.progress, pendingUnitCount: 1)
|
||||||
|
|
||||||
|
for dependency in altstorePlist.dependencies
|
||||||
|
{
|
||||||
|
dispatchGroup.enter()
|
||||||
|
|
||||||
|
self.download(dependency, for: application, progress: progress) { result in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): dependencyError = error
|
||||||
|
case .success(let fileURL): dependencyURLs.insert(fileURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchGroup.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchGroup.notify(qos: .userInitiated, queue: .global()) {
|
||||||
|
if let dependencyError = dependencyError
|
||||||
|
{
|
||||||
|
completionHandler(.failure(dependencyError))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(.success(dependencyURLs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch let error as DecodingError
|
||||||
|
{
|
||||||
|
let nsError = (error as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not download dependencies for %@.", comment: ""), application.name))
|
||||||
|
completionHandler(.failure(nsError))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func download(_ dependency: Dependency, for application: ALTApplication, progress: Progress, completionHandler: @escaping (Result<URL, DependencyError>) -> Void)
|
||||||
|
{
|
||||||
|
let downloadTask = self.session.downloadTask(with: dependency.downloadURL) { (fileURL, response, error) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
|
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||||
|
|
||||||
|
let path = dependency.path ?? dependency.preferredFilename
|
||||||
|
let destinationURL = application.fileURL.appendingPathComponent(path)
|
||||||
|
|
||||||
|
let directoryURL = destinationURL.deletingLastPathComponent()
|
||||||
|
if !FileManager.default.fileExists(atPath: directoryURL.path)
|
||||||
|
{
|
||||||
|
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
||||||
|
|
||||||
|
completionHandler(.success(destinationURL))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(DependencyError(dependency: dependency, error: error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||||
|
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 74c08fc9dd...451588f56f
@@ -25,23 +25,31 @@ public extension Bundle
|
|||||||
|
|
||||||
public extension Bundle
|
public extension Bundle
|
||||||
{
|
{
|
||||||
static var baseAltStoreAppGroupID = "group.com.rileytestut.AltStore"
|
|
||||||
|
|
||||||
var infoPlistURL: URL {
|
var infoPlistURL: URL {
|
||||||
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
|
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
|
||||||
return infoPlistURL
|
return infoPlistURL
|
||||||
}
|
}
|
||||||
|
|
||||||
var provisioningProfileURL: URL {
|
var provisioningProfileURL: URL {
|
||||||
let infoPlistURL = self.bundleURL.appendingPathComponent("embedded.mobileprovision")
|
let provisioningProfileURL = self.bundleURL.appendingPathComponent("embedded.mobileprovision")
|
||||||
return infoPlistURL
|
return provisioningProfileURL
|
||||||
}
|
}
|
||||||
|
|
||||||
var certificateURL: URL {
|
var certificateURL: URL {
|
||||||
let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
|
let certificateURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
|
||||||
return infoPlistURL
|
return certificateURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var altstorePlistURL: URL {
|
||||||
|
let altstorePlistURL = self.bundleURL.appendingPathComponent("AltStore.plist")
|
||||||
|
return altstorePlistURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Bundle
|
||||||
|
{
|
||||||
|
static var baseAltStoreAppGroupID = "group.com.rileytestut.AltStore"
|
||||||
|
|
||||||
var appGroups: [String] {
|
var appGroups: [String] {
|
||||||
return self.infoDictionary?[Bundle.Info.appGroups] as? [String] ?? []
|
return self.infoDictionary?[Bundle.Info.appGroups] as? [String] ?? []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user