//
// UCUpdateDownload.m
// UpdateCore - https://indie.miln.eu
//
// Copyright © Graham Miln. All rights reserved. https://miln.eu
//
// This package is subject to the terms of the Artistic License 2.0.
// If a copy of the Artistic-2.0 was not distributed with this file, you can
// obtain one at https://indie.miln.eu/licence
#import "UCUpdateDownload.h"
#import "UCIsolatedService.h"
#import "UCIsolatedDownloadProtocol.h"
#import "UCIsolatedVerifyProtocol.h"
#import "UpdateCorePrivate.h"
NSString* UCUpdateDownloadKeyLocalizedProgress = @"localizedProgress";
static NSString* UCUpdateDownloadKVOContextProgress = @"UCUpdateDownloadKVOContextProgress";
static NSString* UCUpdateDownloadLocalizedPreparing = @"download.preparing";
static NSString* UCUpdateDownloadLocalizedVerifying = @"download.verifying";
static NSString* UCUpdateDownloadLocalizedDone = @"download.done";
static NSString* UCUpdateDownloadPrivateCacheFoldername = @"UpdateCore";
@interface UCUpdateDownload ()
@property(assign,readwrite) BOOL isFinished;
@property(strong,readwrite) NSError* error;
@property(strong,readwrite) NSURL* fileURL;
@property(strong,readwrite) NSString* localizedProgress;
@property(strong,readwrite) NSString* commonName;
// ...
@property(strong) NSOperationQueue* queue;
@property(strong) NSProgress* progress;
- (NSURL*)fileWithContentsOfRequest:(NSURLRequest*)inRequest error:(NSError**)outError;
- (NSString*)verifiedOriginatorOfPackageAtURL:(NSURL*)inPackageURL error:(NSError**)outError;
- (void)finishWithFile:(NSURL*)inFileURL commonName:(NSString*)inCommonName error:(NSError*)inError completionHandler:(void(^)(NSURL*,NSString*,NSError*))inHandler;
/** Update progress string on main thread; eases KVO in user interface */
- (void)updateProgress:(NSString*)inProgress localise:(BOOL)needsLocalisation;
@end
@implementation UCUpdateDownload
+ (instancetype)downloadWithRequest:(NSURLRequest*)inRequest queue:(NSOperationQueue*)inQueue completionHandler:(void(^)(NSURL*,NSString*,NSError*))inHandler {
return [(UCUpdateDownload*)[[self class] alloc] initWithRequest:inRequest queue:inQueue completionHandler:inHandler];
}
- (instancetype)initWithRequest:(NSURLRequest*)inRequest queue:(NSOperationQueue* __nullable)inQueue completionHandler:(void(^ __nullable)(NSURL* __nullable,NSString* __nullable, NSError* __nullable))inHandler {
NSParameterAssert(inRequest != nil);
if ((self = [super init])) {
self.queue = inQueue;
if (inQueue == nil) {
self.queue = UpdateCore.sharedQueue;
}
self.progress = [NSProgress progressWithTotalUnitCount:100];
self.progress.kind = NSProgressKindFile;
[self.progress setUserInfoObject:NSProgressFileOperationKindReceiving forKey:NSProgressFileOperationKindKey];
[self.progress setUserInfoObject:inRequest.URL forKey:NSProgressFileURLKey];
[self.progress setUserInfoObject:@(0) forKey:NSProgressFileCompletedCountKey];
[self.progress setUserInfoObject:@(1) forKey:NSProgressFileTotalCountKey];
[self updateProgress:UCUpdateDownloadLocalizedPreparing localise:YES];
[self.queue addOperationWithBlock:^{
[self.progress becomeCurrentWithPendingUnitCount:90];
// Download the request to a file
NSError* downloadError = nil;
NSURL* downloadedFile = [self fileWithContentsOfRequest:inRequest error:&downloadError];
if (downloadError != nil) {
[self.progress resignCurrent];
[self finishWithFile:nil commonName:nil error:downloadError completionHandler:inHandler];
return;
}
[self.progress resignCurrent];
[self.progress setUserInfoObject:NSProgressFileOperationKindDecompressingAfterDownloading forKey:NSProgressFileOperationKindKey];
[self.progress becomeCurrentWithPendingUnitCount:10];
[self updateProgress:UCUpdateDownloadLocalizedVerifying localise:YES];
NSError* verifyError = nil;
NSString* cn = [self verifiedOriginatorOfPackageAtURL:downloadedFile error:&verifyError];
if (verifyError != nil) {
[self finishWithFile:nil commonName:nil error:verifyError completionHandler:inHandler];
// Remove invalid downloaded file
NSError* removeError = nil;
(void) [[NSFileManager new] removeItemAtURL:downloadedFile error:&removeError];
if (removeError != nil) {
NSLog(@"[updatecore] Error removing invalid update <%@>: %@", downloadedFile, removeError);
}
[self.progress resignCurrent];
return;
}
[self.progress resignCurrent];
[self finishWithFile:downloadedFile commonName:cn error:nil completionHandler:inHandler];
[self updateProgress:UCUpdateDownloadLocalizedDone localise:YES];
}];
}
return self;
}
- (void)cancel {
// Use NSProgress to propogate cancellation
[self.progress cancel];
}
+ (void)clearCache {
// Clear cache from previous sessions
NSFileManager* fm = [NSFileManager new];
NSURL* cacheDirectoryURL = [fm URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:UCUpdateDownloadPrivateCacheFoldername isDirectory:YES];
if (cacheDirectoryURL != nil) {
(void) [fm removeItemAtURL:cacheDirectoryURL error:nil];
}
}
- (NSURL*)fileWithContentsOfRequest:(NSURLRequest*)inRequest error:(NSError**)outError {
NSParameterAssert(inRequest != nil);
NSParameterAssert(outError != nil);
UCIsolatedService* downloadService = [UCIsolatedService service:UCIsolatedServiceKeyDownload withProtocol:@protocol(UCIsolatedDownloadProtocol)];
NSUUID* downloadIdentifier = [NSUUID new];
// Track download progress
NSProgress* downloadProgress = [downloadService progressForIdentifier:downloadIdentifier];
downloadProgress.kind = NSProgressKindFile;
[downloadProgress setUserInfoObject:NSProgressFileOperationKindDownloading forKey:NSProgressFileOperationKindKey];
[downloadProgress setUserInfoObject:inRequest.URL forKey:NSProgressFileURLKey];
[downloadProgress setUserInfoObject:@(0) forKey:NSProgressFileCompletedCountKey];
[downloadProgress setUserInfoObject:@(1) forKey:NSProgressFileTotalCountKey];
// ...observe byte level progress tracking and propogate to instance property (binding is not available in Foundation)
NSArray<NSString*>* observedProgressKeys = @[NSStringFromSelector(@selector(localizedAdditionalDescription))];
for(NSString* keyPath in observedProgressKeys) {
[downloadProgress addObserver:self forKeyPath:keyPath options:0 context:(__bridge void * _Nullable)(UCUpdateDownloadKVOContextProgress)];
}
// Support cancelling while downloading
__block NSError* downloadError = nil;
downloadProgress.pausable = NO;
downloadProgress.cancellable = YES;
downloadProgress.cancellationHandler = ^(){
[[downloadService syncWithErrorHandler:^(NSError* inServiceError){
downloadError = inServiceError;
}] cancelDownloadWithIdentifier:downloadIdentifier];
};
__block NSURL* fileURL = nil;
[[downloadService syncWithErrorHandler:^(NSError* inServiceError) {
downloadError = inServiceError;
}] fileWithContentsOfRequest:inRequest identifier:downloadIdentifier withReply:^(NSData* inBookmark, NSString* inSuggestedFilename, NSError* inDownloadError) {
downloadError = inDownloadError;
if (inBookmark != nil && inDownloadError == nil) {
NSError* bookmarkError = nil;
NSURL* tmpURL = [NSURL URLByResolvingBookmarkData:inBookmark options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:nil error:&bookmarkError];
// Downloaded file will be removed when this handler finishes; open or move the file
NSFileManager* fm = [NSFileManager new];
// Prepare a URL to move the downloaded file to
NSError* urlError = nil;
NSURL* cacheDirectoryURL = [fm URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:tmpURL create:YES error:&urlError];
if (cacheDirectoryURL == nil || urlError != nil) {
downloadError = urlError;
return;
}
// ...append unique folder to reduce chance of overwriting an existing file or folder
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:UCUpdateDownloadPrivateCacheFoldername isDirectory:YES];
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString] isDirectory:YES];
NSError* createError = nil;
BOOL validCreate = [fm createDirectoryAtURL:cacheDirectoryURL withIntermediateDirectories:YES attributes:nil error:&createError];
if (validCreate == NO || createError != nil) {
downloadError = createError;
return;
}
// ...move and rename the downloaded assumed package file
NSString* filename = inSuggestedFilename;
if (filename == nil) {
filename = [tmpURL.lastPathComponent stringByAppendingPathExtension:@"pkg"];
}
NSURL* destinationURL = [cacheDirectoryURL URLByAppendingPathComponent:filename isDirectory:NO];
NSError* moveError = nil;
BOOL validMove = [fm moveItemAtURL:tmpURL toURL:destinationURL error:&moveError];
if (validMove == NO || moveError != nil) {
downloadError = moveError;
return;
}
fileURL = destinationURL;
downloadError = bookmarkError;
}
}];
// Stop observing progress
for(NSString* keyPath in observedProgressKeys) {
[downloadProgress removeObserver:self forKeyPath:keyPath context:(__bridge void * _Nullable)(UCUpdateDownloadKVOContextProgress)];
}
if (outError != nil) {
*outError = downloadError;
}
return fileURL;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == (__bridge void * _Nullable)(UCUpdateDownloadKVOContextProgress)) {
if ([object isKindOfClass:NSProgress.class] == YES) {
NSProgress* downloadProgress = object;
[self updateProgress:downloadProgress.localizedAdditionalDescription localise:NO];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (NSString*)verifiedOriginatorOfPackageAtURL:(NSURL*)inPackageURL error:(NSError**)outError {
NSParameterAssert(inPackageURL != nil);
NSParameterAssert(outError != nil);
UCIsolatedService* verifyService = [UCIsolatedService service:UCIsolatedServiceKeyVerify withProtocol:@protocol(UCIsolatedVerifyProtocol)];
__block NSString* originator = nil;
__block NSError* verifyError = nil;
[[verifyService syncWithErrorHandler:^(NSError* inServiceError) {
verifyError = inServiceError;
}] verifyPackageAtURL:inPackageURL withReply:^(NSString* inOriginator, NSError* inVerifyError) {
originator = inOriginator;
verifyError = inVerifyError;
}];
if (outError != nil) {
*outError = verifyError;
}
return originator;
}
- (void)finishWithFile:(NSURL*)inFileURL commonName:(NSString*)inCommonName error:(NSError*)inError completionHandler:(void(^)(NSURL*,NSString*,NSError*))inHandler {
dispatch_async(dispatch_get_main_queue(), ^{
self.error = inError;
self.fileURL = inFileURL;
self.commonName = inCommonName;
self.isFinished = YES;
if (inHandler != nil) {
inHandler(inFileURL, inCommonName, inError);
}
});
}
- (void)updateProgress:(NSString*)inLocalisedProgress localise:(BOOL)needsLocalisation {
if (needsLocalisation == YES) {
NSBundle* thisBundle = [NSBundle bundleForClass:self.class];
NSString* thisClass = NSStringFromClass(self.class);
inLocalisedProgress = [thisBundle localizedStringForKey:inLocalisedProgress value:inLocalisedProgress table:thisClass];
}
dispatch_async(dispatch_get_main_queue(), ^{
self.localizedProgress = inLocalisedProgress;
});
}
@end
UCUpdateDownload.m
This file can be downloaded as part of milnupdate.tbz.