Skip to content

Commit

Permalink
Merge pull request #42 from argon/feature/install-multiple-apps-at-once
Browse files Browse the repository at this point in the history
Install/Upgrade multiple apps
  • Loading branch information
argon authored Sep 15, 2016
2 parents e7750c4 + 2253189 commit 997aa80
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 22 deletions.
4 changes: 4 additions & 0 deletions mas-cli.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
EDB6CE8C1BAEC3D400648B4D /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6CE8B1BAEC3D400648B4D /* Version.swift */; };
EDC90B651C70045E0019E396 /* SignIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC90B641C70045E0019E396 /* SignIn.swift */; };
EDCBF9531D89AC6F000039C6 /* Reset.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCBF9521D89AC6F000039C6 /* Reset.swift */; };
EDCBF9551D89CFC7000039C6 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCBF9541D89CFC7000039C6 /* Utilities.swift */; };
EDD3B3631C34709400B56B88 /* Upgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD3B3621C34709400B56B88 /* Upgrade.swift */; };
EDE296531C700F4300554778 /* SignOut.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE296521C700F4300554778 /* SignOut.swift */; };
EDEAA0C01B51CE6200F2FC3F /* StoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDEAA0BF1B51CE6200F2FC3F /* StoreFoundation.framework */; };
Expand Down Expand Up @@ -93,6 +94,7 @@
EDC90B621C6FF50B0019E396 /* ISAccountService-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ISAccountService-Protocol.h"; sourceTree = "<group>"; };
EDC90B641C70045E0019E396 /* SignIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignIn.swift; sourceTree = "<group>"; };
EDCBF9521D89AC6F000039C6 /* Reset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reset.swift; sourceTree = "<group>"; };
EDCBF9541D89CFC7000039C6 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
EDD3B3621C34709400B56B88 /* Upgrade.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Upgrade.swift; sourceTree = "<group>"; };
EDE2964F1C700B0300554778 /* ISAuthenticationContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ISAuthenticationContext.h; sourceTree = "<group>"; };
EDE296501C700B0300554778 /* ISServiceRemoteObject-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ISServiceRemoteObject-Protocol.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -197,6 +199,7 @@
EDEAA12C1B51CF8000F2FC3F /* mas-cli-Bridging-Header.h */,
EDB6CE8A1BAEB95100648B4D /* mas-cli-Info.plist */,
693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */,
EDCBF9541D89CFC7000039C6 /* Utilities.swift */,
);
path = "mas-cli";
sourceTree = "<group>";
Expand Down Expand Up @@ -372,6 +375,7 @@
30EA893640B02CCF679F9C57 /* Option.swift in Sources */,
ED0F23851B87536A00AE40CD /* Outdated.swift in Sources */,
ED0F23891B87543D00AE40CD /* PurchaseDownloadObserver.swift in Sources */,
EDCBF9551D89CFC7000039C6 /* Utilities.swift in Sources */,
EDCBF9531D89AC6F000039C6 /* Reset.swift in Sources */,
0EBF5CDD379D7462C3389536 /* Result.swift in Sources */,
319FDBA6ED6443A912B9A65F /* ResultType.swift in Sources */,
Expand Down
13 changes: 10 additions & 3 deletions mas-cli/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func download(adamId: UInt64) -> MASError? {
let purchase = SSPurchase(adamId: adamId, account: account)

var purchaseError: MASError?
var observerIdentifier: AnyObject? = nil

dispatch_group_enter(group)
purchase.perform { purchase, unused, error, response in
Expand All @@ -27,7 +28,7 @@ func download(adamId: UInt64) -> MASError? {

if let downloads = response.downloads where downloads.count > 0 {
let observer = PurchaseDownloadObserver(purchase: purchase)

observer.errorHandler = { error in
purchaseError = error
dispatch_group_leave(group)
Expand All @@ -36,8 +37,9 @@ func download(adamId: UInt64) -> MASError? {
observer.completionHandler = {
dispatch_group_leave(group)
}

CKDownloadQueue.sharedDownloadQueue().addObserver(observer)

let downloadQueue = CKDownloadQueue.sharedDownloadQueue()
observerIdentifier = downloadQueue.addObserver(observer)
}
else {
print("No downloads")
Expand All @@ -47,5 +49,10 @@ func download(adamId: UInt64) -> MASError? {
}

dispatch_group_wait(group, DISPATCH_TIME_FOREVER)

if let observerIdentifier = observerIdentifier {
CKDownloadQueue.sharedDownloadQueue().removeObserver(observerIdentifier)
}

return purchaseError
}
40 changes: 31 additions & 9 deletions mas-cli/Commands/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,45 @@ struct InstallCommand: CommandType {
let function = "Install from the Mac App Store"

func run(options: Options) -> Result<(), MASError> {
if let error = download(options.appId) {
return .Failure(error)
// Try to download applications with given identifiers and collect results
let downloadResults = options.appIds.flatMap { (appId) -> MASError? in
if let product = installedApp(appId) where !options.forceInstall {
warn("\(product.appName) is already installed")
return nil
}

return download(appId)
}

switch downloadResults.count {
case 0:
return .Success()
case 1:
return .Failure(downloadResults[0])
default:
return .Failure(MASError(code: .DownloadFailed))
}
}

private func installedApp(appId: UInt64) -> CKSoftwareProduct? {
let appId = NSNumber(unsignedLongLong: appId)

return .Success(())
let softwareMap = CKSoftwareMap.sharedSoftwareMap()
return softwareMap.allProducts()?.filter { $0.itemIdentifier == appId }.first
}
}

struct InstallOptions: OptionsType {
let appId: UInt64
let appIds: [UInt64]
let forceInstall: Bool

static func create(appId: Int) -> InstallOptions {
return InstallOptions(appId: UInt64(appId))
static func create(appIds: [Int], forceInstall: Bool) -> InstallOptions {
return InstallOptions(appIds: appIds.map{UInt64($0)}, forceInstall: forceInstall)
}

static func evaluate(m: CommandMode) -> Result<InstallOptions, CommandantError<MASError>> {
return create
<*> m <| Argument(usage: "the app ID to install")
return curry(InstallOptions.create)
<*> m <| Argument(usage: "app ID(s) to install")
<*> m <| Switch(flag: nil, key: "force", usage: "force reinstall")
}
}
}
59 changes: 49 additions & 10 deletions mas-cli/Commands/Upgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,64 @@
//

struct UpgradeCommand: CommandType {
typealias Options = NoOptions<MASError>
typealias Options = UpgradeOptions
let verb = "upgrade"
let function = "Performs all pending updates from the Mac App Store"
let function = "Upgrade outdated apps from the Mac App Store"

func run(options: Options) -> Result<(), MASError> {
let updateController = CKUpdateController.sharedUpdateController()

guard let updates = updateController.availableUpdates() where updates.count > 0 else {
print("Everything is up-to-date")
return .Success(())
guard let pendingUpdates = updateController.availableUpdates() else {
return .Failure(MASError(code: .NoUpdatesFound))
}

let updates: [CKUpdate]
let appIds = options.appIds
if appIds.count > 0 {
updates = pendingUpdates.filter {
appIds.contains($0.itemIdentifier.unsignedLongLongValue)
}

guard updates.count > 0 else {
warn("Nothing found to upgrade")
return .Success(())
}
} else {
// Upgrade everything
guard pendingUpdates.count > 0 else {
print("Everything is up-to-date")
return .Success(())
}
updates = pendingUpdates
}

print("Upgrading \(updates.count) outdated application\(updates.count > 1 ? "s" : ""):")
print(updates.map({ "\($0.title) (\($0.bundleVersion))" }).joinWithSeparator(", "))
for update in updates {
if let error = download(UInt64(update.itemIdentifier.intValue)) {
return .Failure(error)
}

let updateResults = updates.flatMap {
download($0.itemIdentifier.unsignedLongLongValue)
}

switch updateResults.count {
case 0:
return .Success()
case 1:
return .Failure(updateResults[0])
default:
return .Failure(MASError(code: .DownloadFailed))
}
return .Success(())
}
}

struct UpgradeOptions: OptionsType {
let appIds: [UInt64]

static func create(appIds: [Int]) -> UpgradeOptions {
return UpgradeOptions(appIds: appIds.map { UInt64($0) })
}

static func evaluate(m: CommandMode) -> Result<UpgradeOptions, CommandantError<MASError>> {
return create
<*> m <| Argument(defaultValue: [], usage: "app ID(s) to install")
}
}
1 change: 1 addition & 0 deletions mas-cli/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum MASErrorCode: Int {
case AlreadySignedIn
case SearchError
case NoSearchResultsFound
case NoUpdatesFound

var exitCode: Int32 {
return Int32(self.rawValue)
Expand Down
27 changes: 27 additions & 0 deletions mas-cli/Utilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Utilities.swift
// mas-cli
//
// Created by Andrew Naylor on 14/09/2016.
// Copyright © 2016 Andrew Naylor. All rights reserved.
//

func warn(message: String) {
guard isatty(fileno(stdout)) != 0 else {
print("Warning: \(message)")
return
}

// Yellow, underlined "Warning:" prefix
print("\(csi)4m\(csi)33mWarning:\(csi)0m \(message)")
}

func error(message: String) {
guard isatty(fileno(stdout)) != 0 else {
print("Warning: \(message)")
return
}

// Red, underlined "Error:" prefix
print("\(csi)4m\(csi)31mError:\(csi)0m \(message)")
}

0 comments on commit 997aa80

Please sign in to comment.