18 Commits
Author SHA1 Message Date
Josh Abernathy bde5ff2983 Merge pull request #190 from slyngbaek/master
Add auth headers to zip download requests not just feed requests.
2016-11-08 10:38:28 -05:00
Steffen Lyngbaek ae78589d3a Update block type name 2016-11-07 14:56:36 -08:00
Steffen Lyngbaek abec74ed3f Add block to initializer to provide custom download requests.
Add a second initializer with with a block argument to handle
customization of download url requests.

Add a generic download request to the original initializer without
changing the current api.
2016-11-07 12:19:02 -08:00
Steffen Lyngbaek a994876d98 Don't clobber headers explicitly set on the zip download request. 2016-11-04 10:32:54 -07:00
Steffen Lyngbaek 9d5464c693 Add auth headers to zip download requests not just feed requests. 2016-11-03 14:14:51 -07:00
Keith Duncan f00ad7e91b Merge pull request #186 from Squirrel/dont-check-for-updates-when-readonly
Error if there's an update available but the app's on a read-only volume
2016-10-18 11:03:53 +11:00
joshaber 4b0d5ffe18 Error check the statfs call 2016-10-17 14:04:20 -04:00
joshaber d93abe66ec Use a better description 2016-10-17 14:04:09 -04:00
joshaber 196abf93b8 Use a unique error code 2016-10-17 14:03:59 -04:00
joshaber a0079fdee0 Only error once we know if it matters 2016-10-14 10:47:24 -04:00
joshaber fdb4ffa000 Throw an error instead of silently failing 2016-10-14 10:43:56 -04:00
joshaber cc047bdc6f Get outta town if the volume's readonly 2016-10-14 10:43:09 -04:00
joshaber 7231919408 Added -isRunningOnReadOnlyVolume 2016-10-14 10:40:33 -04:00
Josh Abernathy e9e2188cda Merge pull request #176 from Squirrel/fix-updates-again
Don't error if our conditional GET fails.
2016-06-15 08:05:34 -07:00
joshaber 413b1d2bca Reorganize to separate out the unarchiving 2016-06-14 15:37:24 -07:00
joshaber d92d385431 Don't error if our conditional GET fails. 2016-06-13 18:09:24 -07:00
Josh Abernathy 9ebfd397fd Merge pull request #175 from Squirrel/fix-pruning
Don't prune if we've already downloaded updates.
2016-06-12 11:28:48 -04:00
joshaber 7dffc4b143 Don't prune if we've already downloaded updates.
Fixes #174
2016-06-08 14:30:37 -04:00
2 changed files with 116 additions and 36 deletions
+24
View File
@@ -23,6 +23,9 @@ typedef enum : NSUInteger {
SQRLUpdaterStateAwaitingRelaunch,
} SQRLUpdaterState;
// Block for providing download requests given a download url
typedef NSURLRequest * (^SQRLRequestForDownload)(NSURL *);
// The domain for errors originating within SQRLUpdater.
extern NSString * const SQRLUpdaterErrorDomain;
@@ -92,6 +95,15 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// This property must never be set to nil.
@property (atomic, copy) NSURLRequest *updateRequest;
// The block used for fetching a given download request
//
// The default value is the argument that was originally passed to
// -initWithUpdateRequest:requestForDownload:.
//
// If initialized with -initWithUpdateRequest: this block will
// return a generic NSURLRequest with the provided url.
@property (nonatomic, copy) SQRLRequestForDownload requestForDownload;
// The `SQRLUpdate` subclass to instantiate with the server's response.
//
// By default, this is `SQRLUpdate` itself, but it can be set to a custom
@@ -111,6 +123,18 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest;
// Initializes an updater that will send the given request to check for updates
// and passes a block to provide requests for the update downloads.
//
// updateRequest - Same as with initWithUpdateRequest
// requestForDownload - Once the update url is found for the update download, allow
// providing custom requests that can be costomized as desired.
// Useful for including `Authorization` headers just like the
// updateRequest param.
//
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload;
// Executes `checkForUpdatesCommand` (if enabled) every `interval` seconds.
//
// The first check will not occur until `interval` seconds have passed.
+92 -36
View File
@@ -20,6 +20,7 @@
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/mount.h>
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
@@ -32,6 +33,9 @@ const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
/// The application's being run on a read-only volume.
const NSInteger SQRLUpdaterErrorReadOnlyVolume = 8;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@@ -140,11 +144,19 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest {
return [self initWithUpdateRequest:updateRequest requestForDownload:^(NSURL *downloadURL) {
return [NSURLRequest requestWithURL:downloadURL];
}];
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload {
NSParameterAssert(updateRequest != nil);
NSParameterAssert(requestForDownload != nil);
self = [super init];
if (self == nil) return nil;
_requestForDownload = [requestForDownload copy];
_updateRequest = [updateRequest copy];
_updateClass = SQRLUpdate.class;
@@ -194,6 +206,16 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
BOOL readOnlyVolume = [self isRunningOnReadOnlyVolume];
if (readOnlyVolume) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update while running on a read-only volume", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The application is on a read-only volume. Please move the application and try again. If you're on macOS Sierra or later, you'll need to move the application out of the Downloads directory. See https://github.com/Squirrel/Squirrel.Mac/issues/182 for more information.", nil),
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorReadOnlyVolume userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [RACSignal return:bodyData];
@@ -304,42 +326,86 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [[[self
uniqueTemporaryDirectoryForUpdate]
flattenMap:^(NSURL *downloadDirectory) {
void (^cleanUp)(void) = ^{
NSError *error;
if (![NSFileManager.defaultManager removeItemAtURL:downloadDirectory error:&error]) {
NSLog(@"Error removing temporary download directory at %@: %@", downloadDirectory, error.sqrl_verboseDescription);
}
};
return [[[self
downloadBundleForUpdate:update intoDirectory:downloadDirectory]
flattenMap:^(NSBundle *updateBundle) {
// If the bundle is nil it means our conditional GET told us
// we already downloaded the update. So just clean up.
if (updateBundle == nil) {
cleanUp();
return [RACSignal empty];
}
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
}]
doError:^(id _) {
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:downloadDirectory error:&error]) {
NSLog(@"Error removing temporary download directory at %@: %@", downloadDirectory, error.sqrl_verboseDescription);
}
cleanUp();
}];
}]
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
}
- (RACSignal *)unarchiveAndPrepareData:(NSData *)data withName:(NSString *)name intoDirectory:(NSURL *)downloadDirectory {
return [[[[[RACSignal
defer:^{
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:name];
NSError *error = nil;
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal return:zipOutputURL];
} else {
return [RACSignal error:error];
}
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [[[[SQRLZipArchiver
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
ignoreValues]
concat:[RACSignal return:zipOutputURL]]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
}]
flattenMap:^(NSURL *zipOutputURL) {
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
}]
setNameWithFormat:@"%@ -unarchiveAndPrepareData:withName: %@ intoDirectory: %@", self, name, downloadDirectory];
}
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
NSParameterAssert(update != nil);
NSParameterAssert(downloadDirectory != nil);
return [[[[[RACSignal
return [[RACSignal
defer:^{
NSURL *zipDownloadURL = update.updateURL;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
NSMutableURLRequest *zipDownloadRequest = [self.requestForDownload(zipDownloadURL) mutableCopy];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
if (self.etag != nil) {
[zipDownloadRequest setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
return [[[[NSURLConnection
return [[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (httpResponse.statusCode == 304 /* Not Modified */) {
return [RACSignal empty];
return [RACSignal return:nil];
}
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
@@ -355,35 +421,9 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
self.etag = httpResponse.allHeaderFields[@"ETag"];
}
return [RACSignal return:bodyData];
return [self unarchiveAndPrepareData:bodyData withName:zipDownloadURL.lastPathComponent intoDirectory:downloadDirectory];
}]
flatten]
flattenMap:^(NSData *data) {
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
NSError *error = nil;
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal return:zipOutputURL];
} else {
return [RACSignal error:error];
}
}];
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [[SQRLZipArchiver
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
flatten];
}]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
@@ -475,6 +515,19 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
setNameWithFormat:@"%@ -shipItStateURL", self];
}
/// Is the host app running on a read-only volume?
- (BOOL)isRunningOnReadOnlyVolume {
struct statfs statfsInfo;
NSURL *bundleURL = NSRunningApplication.currentApplication.bundleURL;
int result = statfs(bundleURL.fileSystemRepresentation, &statfsInfo);
if (result == 0) {
return (statfsInfo.f_flags & MNT_RDONLY) != 0;
} else {
// If we can't even check if the volume is read-only, assume it is.
return true;
}
}
- (RACSignal *)performHousekeeping {
return [[RACSignal
merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
@@ -495,6 +548,9 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
- (RACSignal *)pruneUpdateDirectories {
return [[[RACSignal
defer:^{
// If we already have updates downloaded we don't wanna prune them.
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
}]