205 Commits
Author SHA1 Message Date
Keith Duncan bfbcdafae4 Signal chain++ 2014-01-21 12:58:11 -08:00
Keith Duncan 8f2c9d08b4 Loop condition++ 2014-01-21 12:56:54 -08:00
Keith Duncan 409d6ef781 Dots-- 2014-01-21 12:56:00 -08:00
Keith Duncan 8fb6d0e64f Remove unused ignoreValues 2014-01-21 12:55:57 -08:00
Keith Duncan 67cc4c2e14 Remove unused ignoreValues 2014-01-21 12:55:28 -08:00
Keith Duncan feee9f7995 Add parameter assert 2014-01-21 12:55:22 -08:00
Keith Duncan 318b9a9081 Assert that request is non-nil 2014-01-21 12:49:26 -08:00
Keith Duncan 217ada5c47 Dots-- 2014-01-21 12:47:49 -08:00
Keith Duncan 819aa8f815 Remove redundant dispose 2014-01-21 12:45:08 -08:00
Keith Duncan f0f2523b5d Remove unused property 2014-01-21 12:39:34 -08:00
Keith Duncan 283f9ef559 Tidy up retrieve and download docs 2014-01-21 12:39:23 -08:00
Keith Duncan 0f32a80019 Remove error parameter 2014-01-21 12:32:16 -08:00
Keith Duncan c76b3158e7 Equality short circuit 2014-01-21 12:30:55 -08:00
Keith Duncan ca72079898 Strongly type the isEqual: argument 2014-01-21 12:29:55 -08:00
Keith Duncan 625edb1388 Pass a mutable dictionary to the write map block 2014-01-21 12:28:54 -08:00
Keith Duncan 48705686d0 Remove redundant key path maps 2014-01-21 12:27:52 -08:00
Keith Duncan 5cd1c0ecd8 Remove unused catch: 2014-01-21 12:19:24 -08:00
Keith Duncan 94c98f4e8e Move the SHA1 and base16 encoding into an NSData category 2014-01-21 12:09:04 -08:00
Keith Duncan f1c4012aa0 Refactor the property list read + error into one method 2014-01-21 12:00:48 -08:00
Keith Duncan bb370d2b42 Remove initialised variables 2014-01-21 11:54:16 -08:00
Keith Duncan eb8ef9cd48 Read the download index uncached 2014-01-21 11:52:48 -08:00
Keith Duncan dad7bdfa89 Revert "Make the download index read/write synchronous on subscription"
This reverts commit 79dab822fd.
2014-01-21 11:51:29 -08:00
Keith Duncan 92c37377ca Move documentation to the class extension 2014-01-21 11:34:52 -08:00
Keith Duncan c65597ee6f Use +defer: rather than return:] try:] 2014-01-21 11:33:23 -08:00
Keith Duncan 18c923f343 Check for NULL before releasing the queue 2014-01-21 11:29:49 -08:00
Keith Duncan 5c9702f1d4 Use background priority for reading/writing the download index 2014-01-21 11:29:08 -08:00
Keith Duncan 7850a0707b Update documentation 2014-01-21 11:28:58 -08:00
Keith Duncan 8a3bb3d896 Move the SQRLDownload initialiser into a class extension
Document the `SQRLDownload` class.
2014-01-21 11:23:58 -08:00
Keith Duncan 40cf893899 Remove extraneous tryMap 2014-01-21 11:19:06 -08:00
Keith Duncan 539c7983c8 Add a category for creating directories 2014-01-21 11:19:02 -08:00
Keith Duncan a13090740d Whitespace-- 2014-01-21 10:59:57 -08:00
Keith Duncan f82fbaf53d More signals for creating the app support URL 2014-01-21 10:58:38 -08:00
Keith Duncan ef2bc2345a Change the idempotent URL signal methods to properties
Rename the unique directory signal to include a verb.
2014-01-21 10:58:21 -08:00
Keith Duncan e74d412eb9 Use -try instead of identity mapping 2014-01-21 10:38:18 -08:00
Keith Duncan b8ece2fc80 Merge remote-tracking branch 'origin/rac-3.0' into download-updates
Conflicts:
	Squirrel/SQRLUpdater.m
2014-01-06 15:11:27 +00:00
Keith Duncan 17c7238489 Use -aggregate... rather than implement it ourselves 2013-12-05 21:17:03 +00:00
Keith Duncan 1109d19a9e Change a couple of waits
Make these asynchronous so that they timeout
2013-12-04 18:42:50 +00:00
Keith Duncan 2c567384db Support accumulating data in SQRLURLConnection 2013-12-04 18:41:18 +00:00
Keith Duncan fcc86ca470 Merge the file scheme testing 2013-12-04 17:33:14 +00:00
Keith Duncan 7af569196e Rename the downloader to SQRLURLConnection
Add retrieval support to the interface (unimplemented) and move clients to the new interface.
2013-12-04 17:26:48 +00:00
Keith Duncan 70e7436e11 Add test that the unpacked update is in a .noindex directory 2013-12-04 16:52:20 +00:00
Keith Duncan 086a23661d Broadcast the received updates
Uses the JSON serialisation of the downloaded update, this is so I can tell test application to update, then run assertions on the downloadedUpdate from the otest process.
2013-12-04 16:23:07 +00:00
Keith Duncan e1460343ff Fix signal name 2013-12-04 16:22:47 +00:00
Keith Duncan faccaf5f6f Use the global variables for the JSON key map 2013-12-04 16:22:29 +00:00
Keith Duncan 1f8d6e87cc Add JSON serialisation support to downloaded update 2013-12-04 16:22:12 +00:00
Keith Duncan a70db2793f Add Mantle and ReactiveCocoa to TestApplication’s copy phase 2013-12-04 16:21:35 +00:00
Keith Duncan cbb75b1495 Split unarchiving and downloading signals
Move the chaining of download, unpack and find into `downloadAndPrepareUpdate:`.
2013-12-04 13:40:29 +00:00
Keith Duncan df49ea7aca Give the download the suggestedFilename
This incorporates the file extension appropriate for the response `MIMEType`.
2013-12-04 13:29:47 +00:00
Keith Duncan 2d8ea97081 Change the type of the second download tuple value 2013-12-04 13:23:49 +00:00
Keith Duncan d07c2d4d9d Move response header look up into a category
Add test and change the resumable download to use it
2013-12-04 13:22:59 +00:00
Keith Duncan 89ee65156f Remove variable pre-declaration 2013-12-03 23:19:18 +00:00
Keith Duncan 58062408d7 Remove unused method 2013-12-03 23:18:09 +00:00
Keith Duncan 46d8fa7fa1 Fix docs 2013-12-03 23:18:02 +00:00
Keith Duncan 645d276fc3 Assertions++ 2013-12-03 19:04:58 +00:00
Keith Duncan 25cb739760 Move HTTP server schenanigans into a class 2013-12-03 19:03:26 +00:00
Keith Duncan 14ff8e2012 Remove :trollface: logs that I apparently missed 2013-12-03 19:02:52 +00:00
Keith Duncan 4826034831 Merge pull request #77 from Squirrel/fix-downloader-signals
Fix NSURLConnection signal ordering

Merging this to download-updates where further review can take place
2013-12-03 10:00:54 -08:00
Keith Duncan 35081231a3 Remove the delegate stubs
Cancelling the connection should prevent further delegate messages, and if necessary cancel should block for any in flight delegate messages to complete, per the documentation.

> After this method is called, the connection’s delegate no longer receives any messages for the connection.

This also changes the delegate signal method, at time of connection creation, the delegate MUST already respond to the delegate messages, implementing them subsequently by adding an implementation results in the connection not messaging us at all. This precludes the connection from being provided as an argument to the delegate signal method.
2013-12-03 17:52:14 +00:00
Keith Duncan aa1db49ca0 Remove mention of “callback scheduler” that was removed 2013-12-03 17:19:35 +00:00
Keith Duncan 173dd52f1c Don’t -concat: the signals that return the download
This was resulting in double writes, what I actually wanted was `-then:`
2013-12-03 17:18:31 +00:00
Keith Duncan b751e75e19 Use -[SQRLDownload resumeRequest] 2013-12-03 16:05:01 +00:00
Keith Duncan 789feef723 Update to RAC 3.0 API 2013-12-03 16:04:53 +00:00
Keith Duncan f4af70b715 Merge branch 'download-updates' into fix-downloader-signals
Conflicts:
	Squirrel/SQRLDownloader.m
2013-12-03 16:03:28 +00:00
Keith Duncan 22c1a8dc07 Make the resumable download a promise
We only need to fetch it once
2013-12-03 16:00:56 +00:00
Keith Duncan 79dab822fd Make the download index read/write synchronous on subscription 2013-12-03 16:00:32 +00:00
Keith Duncan 5792209feb Remove the replay and multicasts
Attempt to remove the deliverOn scheduler now that we’re synchronous
2013-12-03 15:45:22 +00:00
Keith Duncan 1075abadec Add length assertions
These will provide easier glance-able insight into the failure, than comparing streams of hexadecimal encoded binary.
2013-12-03 15:44:39 +00:00
Keith Duncan 9ac9e32509 Replace +createSignal: with +create: 2013-12-03 13:06:57 +00:00
Keith Duncan f1d818b321 Remove uses of RACSequence 2013-12-03 13:02:37 +00:00
Keith Duncan 7834599175 Fix missed open bracket in merge 2013-12-03 12:53:30 +00:00
Keith Duncan bf4355a522 Merge branch 'rac-3.0++' into fix-downloader-signals
Conflicts:
	Squirrel/SQRLUpdater.m
2013-12-03 12:52:13 +00:00
Keith Duncan 52f9eb8f4f Remove RunUnitTests 2013-11-21 17:35:40 +00:00
Keith Duncan cfc5bbf25f Experiment with splitting the download into two types 2013-11-21 16:04:24 +00:00
Keith Duncan aab4fc5d59 Remove unused method 2013-11-20 20:24:30 +00:00
Keith Duncan ae6db20fa7 Remove RunUnitTests 2013-11-20 20:22:51 +00:00
Justin Spahr-Summers 2010092d95 Clean up properties, document -signalForDelegate… 2013-11-08 11:11:46 -08:00
Justin Spahr-Summers 0166b2fef1 Document the logic behind the connection signals 2013-11-08 11:09:30 -08:00
Justin Spahr-Summers f80f67534c Refactor SQRLDownloader to group signal logic better 2013-11-08 11:03:07 -08:00
Justin Spahr-Summers 0b677cefc1 Stub delegate methods to "fix" exceptions 2013-11-08 10:29:37 -08:00
Justin Spahr-Summers d63ff8291e Serialize all NSURLConnection callbacks on a scheduler 2013-11-08 10:29:13 -08:00
Keith Duncan 742f46ff8f Add signal names 2013-11-07 19:06:27 +00:00
Keith Duncan 1e76fd3c15 Break the giant -download method down into smaller signal methods 2013-11-07 19:02:26 +00:00
Keith Duncan 856ac9f30f Work on inner signal as a function of response version 2013-11-07 16:44:39 +00:00
Keith Duncan e4ac04cb2b +merge: version of the response + data signal
I think this fixes ordering of the two signals, but is horrible
2013-11-07 12:15:45 +00:00
Keith Duncan 4640763f1b Merge branch 'always-create-applicationsupport' into download-updates
Conflicts:
	Squirrel/SQRLDirectoryManager.m
2013-11-07 12:11:58 +00:00
Keith Duncan 3b3408d809 Remove file not used 2013-11-05 21:43:36 +00:00
Keith Duncan d93f7d0ae8 Style change 2013-11-05 21:43:00 +00:00
Keith Duncan bdb4447511 Remove un-needed properties 2013-11-05 21:41:39 +00:00
Keith Duncan 4e36d604a0 Move the initial download lookup into a separate map 2013-11-05 21:27:33 +00:00
Keith Duncan 5bfdb4683c Remove block return types 2013-11-05 21:25:58 +00:00
Keith Duncan 860c010a06 Remove base16 alphabet 2013-11-05 21:18:52 +00:00
Keith Duncan 84652b5f5c Use NSDataReadingUncached 2013-11-05 21:17:04 +00:00
Keith Duncan a8e3bf6b30 Add NSURLErrorKey to pertinent errors 2013-11-05 21:15:29 +00:00
Keith Duncan 3f3856f38b Add disposables
Remove redundant block return types
2013-11-05 21:12:05 +00:00
Keith Duncan aa6ffea547 Don’t flattenMap and return a wrapped value 2013-11-05 21:11:45 +00:00
Keith Duncan 843f8adeb0 Use +[RACSignal merge:] 2013-11-05 21:04:55 +00:00
Keith Duncan 93df075824 Fix queue name 2013-11-05 21:00:46 +00:00
Keith Duncan 4d061c2138 Use the low priority queue for reading/writing the download index 2013-11-05 21:00:19 +00:00
Keith Duncan 854a4181ff Assert the directory manager is non nil 2013-11-05 21:00:10 +00:00
Keith Duncan 2f75b436c7 Remove static globals initialised to zero 2013-11-05 20:58:38 +00:00
Keith Duncan 6109b415f3 Style changes 2013-11-05 20:50:10 +00:00
Keith Duncan 30027e933e Functional ETag lookup 2013-11-05 20:47:52 +00:00
Keith Duncan 0a198ed1da Rename resumable request method 2013-11-05 20:44:12 +00:00
Keith Duncan ab93693ae0 Move private method documentation to the class extension 2013-11-05 20:42:52 +00:00
Keith Duncan 12ea8e209a Refactor SQRLDownloader to use -liftSelector: 2013-11-05 20:41:25 +00:00
Keith Duncan 931d19bc72 Rename the operation classes to drop operation 2013-11-05 15:51:01 +00:00
Keith Duncan fc41d39585 Use bracket imports 2013-11-05 15:21:47 +00:00
Keith Duncan a28abecb4d Remove redundant #import 2013-11-05 15:20:57 +00:00
Keith Duncan 8ba490e5d3 Revise documentation for the download operation initialiser 2013-11-05 15:20:57 +00:00
Keith Duncan be824ed1fb Revise download operation documentation 2013-11-05 15:20:57 +00:00
Keith Duncan cb06bd800e Use the plural “downloads” for the directory name 2013-11-05 15:20:57 +00:00
Keith Duncan 6c18360388 Fix indentation
Replace `-flattenMap:` with `-tryMap:`
2013-11-05 15:20:57 +00:00
Keith Duncan d6356b0255 Clarify documentation 2013-11-05 15:20:56 +00:00
Keith Duncan 5e4e7d6fbc Split the directory URL mapping from the creation 2013-11-05 15:20:56 +00:00
Keith Duncan bb56dc4c87 Rename the per update method, clarify the docs 2013-11-05 15:20:52 +00:00
Keith Duncan a91faeaa6a Always create the application support directory 2013-11-05 14:32:40 +00:00
Keith Duncan e5b241dfc1 Fix indentation
Replace `-flattenMap:` with `-tryMap:`
2013-11-05 14:15:57 +00:00
Keith Duncan 17975dd64e Clarify documentation 2013-11-05 14:14:33 +00:00
Keith Duncan 33ee8ca29d Split the directory URL mapping from the creation 2013-11-05 14:11:49 +00:00
Keith Duncan a9882a73d0 Rename the per update method, clarify the docs 2013-11-05 14:11:23 +00:00
Keith Duncan 0e9ed45112 Move the unique temporary directory into SQRLDirectoryManager
This revives the use of .noindex for the unpack directories, removes `mkdtemp` and replaces with `globallyUniqueIdentifier` based directory names.
2013-10-31 17:25:21 +00:00
Keith Duncan 1e59131f03 Add test that the downloads are cleaned up after successful install 2013-10-31 16:41:05 +00:00
Keith Duncan c86dfb14c3 Disable the deep codesign test
This is failing on 10.9 and is annoying
2013-10-31 16:08:31 +00:00
Keith Duncan fcb1810178 Ignore values when removing state, we only need errors 2013-10-31 16:01:08 +00:00
Keith Duncan 0126d18c00 More +tryMap: 2013-10-31 16:00:54 +00:00
Keith Duncan 6abd2a6c82 Move no such file error catching into SQRLResumableDownloadManager 2013-10-31 15:10:17 +00:00
Keith Duncan 7532c29e8e Remove resumable downloads after one completes
The assumption is that all other pending downloads are now redundant because we have an update and have moved it out of the resumable download cache.

This will be skipped if there’s an error, and we ignore errors removing the other downloads, the caller only wants to know about errors downloading the update it wanted.
2013-10-31 14:59:02 +00:00
Keith Duncan f3c96e219c Adopt +try: and +tryMap:
Also reduces the scope of the mutual exclusion in the index reader, we just need to read the data with writers excluded, the transform using `NSKeyedUnarchiver` can be done outside this scope.
2013-10-31 14:57:48 +00:00
Keith Duncan 6a85e07b93 Add default body data
If the connection doesn’t return an error we should provide non-nil body data
2013-10-31 14:27:11 +00:00
Keith Duncan 57f339b2e4 Merge branch 'master' into download-updates
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLUpdate.m
	Squirrel/SQRLUpdater.m
	SquirrelTests/SQRLTestCase.h
2013-10-31 14:23:56 +00:00
Keith Duncan 8c303fb371 Use Mantle for SQRLResumableDownload 2013-10-24 11:10:07 +01:00
Keith Duncan b3f397b81e Use RACTupleUnpack in the tests too
Update to the new type ordering
2013-10-23 14:47:43 +01:00
Keith Duncan 10e7af2f81 Plug the URLConnection and Download operations into the updater 2013-10-23 14:42:32 +01:00
Keith Duncan a71b6f5efe Change the order of values in the download tuple
This matches the URL connection tuple order of {response, value} where value is either the response data, or the NSURL of the data location.
2013-10-23 14:41:40 +01:00
Keith Duncan df45644b6f Remove block return types where applicable 2013-10-23 14:31:21 +01:00
Keith Duncan f4492c20a5 RAC SQRLURLConnectionOperation 2013-10-23 14:26:43 +01:00
Keith Duncan 2b5b334903 Use RACTuplePack and Unpack
Use a compound disposable
2013-10-23 14:26:16 +01:00
Keith Duncan 32cccdc77a Move the connection subject to before starting the connection 2013-10-23 14:03:54 +01:00
Keith Duncan 8247064cb6 RACify the download 2013-10-23 13:56:17 +01:00
Keith Duncan c20c540e8a Use sel_getName for the signal name
Avoids duplicating the selector written out twice
2013-10-23 12:10:02 +01:00
Keith Duncan 07cb7a07c9 More RAC for the download manager
Retains the previous multi reader single writer by using a dispatch queue and `+[RACSignal createSignal:]` to bring our own concurrency rather than using multiple schedulers.
2013-10-22 20:32:21 +01:00
Keith Duncan b35b602824 More RAC in SQRLResumableDownloadManager 2013-10-22 18:29:48 +01:00
Keith Duncan f400da11f3 Change the removeAllResumableDownloads signal
Now errors or completes, no values are sent
2013-10-22 17:23:54 +01:00
Keith Duncan 14a36b50a4 Add name to the downloadStoreIndexFileLocation signal 2013-10-22 17:23:41 +01:00
Keith Duncan 98cc37d907 Only check for nil hosts on non file:// scheme URLs
file scheme URLs have a nil host on 10.9
2013-10-22 17:22:05 +01:00
Keith Duncan e0cf250d7b Fix up tests with new SQRLResumableDownloadManager interface 2013-10-22 16:46:40 +01:00
Keith Duncan cf8b4bb853 Add missed specs from the RAC merge 2013-10-22 16:22:17 +01:00
Keith Duncan 8f870954a6 Merge remote-tracking branch 'origin/master' into download-updates 2013-10-22 16:07:21 +01:00
Keith Duncan 871379d9e0 Rename SQRLDownloadController -> SQRLResumableDownloadManager 2013-10-22 16:03:12 +01:00
Keith Duncan c13c8de24d Merge branch 'bring-the-rac' into download-updates
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLDirectoryManager.h
	Squirrel/SQRLDirectoryManager.m
	Squirrel/SQRLUpdate+Private.h
	Squirrel/SQRLUpdate.h
	Squirrel/SQRLUpdate.m
	Squirrel/SQRLUpdater.h
	Squirrel/SQRLUpdater.m
	Squirrel/SQRLZipArchiver.m
	Squirrel/SQRLZipOperation.h
	SquirrelTests/SQRLUpdateSpec.m
	SquirrelTests/SQRLUpdaterSpec.m
	SquirrelTests/SQRLZipOperationSpec.m

This is the minimum-viable-merge, I haven’t RAC'd the download code yet, for instance using `first` to bridge values from the signals into the pre-existing code. Going to RAC this next.
2013-10-22 15:55:33 +01:00
Keith Duncan 01f4acfe5c Improve SQRLResumableDownload docs 2013-10-16 18:13:12 +01:00
Keith Duncan 0f9bd718a8 Add test for the download not being resumed if the ETag had changed 2013-10-09 16:57:24 +01:00
Keith Duncan 58b8376f54 Smarter server which uses a block for response->request mapping
Add expects for the requests to not have / have If-Range and Range headers
2013-10-09 16:37:16 +01:00
Keith Duncan d92516dee4 Add in process HTTP server to respond to requests
This is to test the download operation, and is very dumb at the moment, going to make it smarter in terms of responding to Range requests
2013-10-09 16:17:30 +01:00
Keith Duncan 6f3ba37b19 Fix wrong message to add the Range header 2013-10-09 16:17:18 +01:00
Keith Duncan 69c05bf159 Make addCleanupBlock: public 2013-10-09 16:16:41 +01:00
Keith Duncan b35b11a634 Add test for resumable downloads 2013-10-08 17:27:52 +01:00
Keith Duncan 07cd9c19a7 Initialise operations with a download controller 2013-10-08 17:26:28 +01:00
Keith Duncan e5a9c2c740 Replace update operation completionProvider block 2013-10-04 18:00:32 +01:00
Keith Duncan 1072ef3c16 Replace zip operation completion provider block 2013-10-04 17:59:13 +01:00
Keith Duncan 7a23627711 Replace download operation completionProvider block 2013-10-04 17:56:54 +01:00
Keith Duncan 2713404d29 Make SQRLURLConnectionOperation response provider a method
This is still a block internally so that the completion is atomically a failure or the entire response body.
2013-10-04 17:35:09 +01:00
Keith Duncan d11f29346a Add return statements when isCancelled 2013-10-04 17:19:30 +01:00
Keith Duncan f5e7c52ba8 Add missing default completion provider 2013-10-04 17:07:59 +01:00
Keith Duncan 704933eaa1 Rename the SQRLDirectoryManager methods 2013-10-04 16:58:48 +01:00
Keith Duncan db416ac0f0 Collapse return lines 2013-10-04 16:52:56 +01:00
Keith Duncan f2f3f9922a Replace block enumeration with fast enumeration 2013-10-04 16:52:03 +01:00
Keith Duncan 4f114dc343 Loop when writing data to the output stream
Handle {POSIX,EINTR} errors otherwise abort with an error
2013-10-04 16:48:15 +01:00
Keith Duncan bf64476b13 Move [connection cancel] into finish 2013-10-04 15:44:38 +01:00
Keith Duncan b31f881103 Remove duplicate [super cancel] 2013-10-04 15:40:11 +01:00
Keith Duncan 665b947a1e Tweak queue name 2013-10-01 12:01:39 +01:00
Keith Duncan 36b5feec15 Secure decoding-- 2013-09-30 23:19:07 +01:00
Keith Duncan 85309ede82 More removing of the typedef 2013-09-30 23:14:26 +01:00
Keith Duncan d9e37ab259 Fix removal of typedef, building++ 2013-09-30 23:11:12 +01:00
Keith Duncan fed28478e4 Remove block typedef 2013-09-30 23:06:28 +01:00
Keith Duncan 4b9c02563f Docs-- 2013-09-30 23:06:22 +01:00
Keith Duncan 94837037f6 Add to the comment on why we use .noindex 2013-09-30 22:59:20 +01:00
Keith Duncan d54b0c2bfa Docs++ 2013-09-30 22:48:02 +01:00
Keith Duncan 21bd5012d6 Don’t implicitly create directories
This makes error reporting hard, instead document that the directories are not created and have callers create them where the error can be reported
2013-09-30 22:46:53 +01:00
Keith Duncan 4ffe08f9c6 Prefer NSBundle.mainBundle for the current host
Only use `NSRunningApplication` when it fails which it does in otest
2013-09-30 15:46:25 +01:00
Keith Duncan a314caa355 Assert appIdentifier isn’t nil 2013-09-30 15:39:56 +01:00
Keith Duncan 6819ce4215 Rename SQRLFileManager to SQRLDirectoryManager 2013-09-30 15:38:55 +01:00
Keith Duncan efba16e355 self.class++ 2013-09-30 15:14:47 +01:00
Keith Duncan af8694a8f6 Change the downloadController interface to take requests 2013-09-30 15:13:27 +01:00
Keith Duncan 4c562a7c2e Rename the queues to use Class.property naming 2013-09-30 15:06:58 +01:00
Keith Duncan f2c534ff30 Change the operation state properties to atomic 2013-09-30 14:50:59 +01:00
Keith Duncan 937319e9f5 Remove unused variable 2013-09-30 14:46:38 +01:00
Keith Duncan ef28defe9c Fix some missing error byref and one success path case 2013-09-30 14:35:07 +01:00
Keith Duncan 1561118861 Replace file coordination with a dispatch queue
Use a barrier block on a concurrent queue to preserve multireader-singlewriter from the file coordination API
2013-09-30 14:14:40 +01:00
Keith Duncan 755d50263d Move Index.plist into the per-bundle directory out of downloads 2013-09-30 13:53:06 +01:00
Keith Duncan 97ecb1bf72 Fix leading zero when base16ing data 2013-09-30 13:41:33 +01:00
Keith Duncan b09a0cb3c8 Make download remove method error byref
Rather than logging allow callers to handle the error themselves, the tests simply log the error
2013-09-30 13:25:48 +01:00
Keith Duncan 8f9bd34e2b Imports++ 2013-09-30 13:22:34 +01:00
Keith Duncan 3b2e3639d1 Docs++ 2013-09-30 13:22:18 +01:00
Keith Duncan dc861aba42 Add SQRLResumableDownload model class 2013-09-30 13:15:59 +01:00
Keith Duncan a12d5d5cda Store the full NSHTTPURLResponse in the download 2013-09-30 12:40:41 +01:00
Keith Duncan c4144ca55d Make SQRLDownloadController methods instance methods
Add default instance class method, doesn’t initialise with any parameters currently so it just returns a new instance each time
2013-09-30 12:25:08 +01:00
Keith Duncan a2dfe45177 More docs 2013-09-27 18:54:29 +01:00
Keith Duncan 7a667b8fc4 Docs++ 2013-09-27 18:48:48 +01:00
Keith Duncan c516873458 Add SQRLUpdateOperation which replaces the state in SQRLUpdater
This operation creates the others to partition the work
2013-09-27 18:38:16 +01:00
Keith Duncan 6761505219 New SQRLUpdate initialisation method, supports errors byref
Move the root JSON type check into the update initialiser too
Add downloaded update property for the unpacked update ready to install
2013-09-27 17:39:21 +01:00
Keith Duncan f3b4ba98a9 Add SQRLURLConnectionOperation
This operation provides `NSOperation` dependencies to `NSURLConnection` and accumulates the response in memory
2013-09-27 17:32:52 +01:00
Keith Duncan c194810749 Add SQRLDownloadController and SRQLDownloadOperation
These classes enable resumable downloads by tracking the ETag for a response file, and issuing the requests with an `If-Range` header

This only tests downloading from file:// scheme URLs, I’d like to add an in process HTTP server which disconnects part of the way through the download first time, we download a second time and then test that it resumed.
2013-09-27 17:29:26 +01:00
Keith Duncan 6702f9515d Add SQRLFileManager
For generating URLs for the directories the downloads and pending updates will be stored in
2013-09-27 17:03:37 +01:00
Keith Duncan 39cf57582f Replace the zip archiver with an operation and a completionProvider
Rather than waiting for the completionBlock to be invoked, add a dependency of the operation and invoke the completionProvider to get the result
2013-09-27 17:01:05 +01:00
32 changed files with 1900 additions and 94 deletions
+101 -2
View File
@@ -7,18 +7,42 @@
objects = {
/* Begin PBXBuildFile section */
5316AF7A188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */; };
5316AF7B188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */; };
5316AF95188EFFA100C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */; };
5316AFA8188F002100C070DF /* SQRLDownload+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AFA6188F002100C070DF /* SQRLDownload+Private.h */; };
5316AFAC188F0A8E00C070DF /* NSData+SQRLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */; };
5316AFAD188F0A8E00C070DF /* NSData+SQRLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */; };
533076EB17EB688300BDCCE0 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D097B9BC17BB4777006C3FEB /* IOKit.framework */; };
53390936184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */; };
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */; };
5339094A184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */; };
534FF35B17D8E8B90020A51A /* unused-helper in Copy LoginItems */ = {isa = PBXBuildFile; fileRef = D0B1DDCB17B487D90059C355 /* unused-helper */; };
534FF36117D8E90A0020A51A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F60CA7EA179FC4F60069F69A /* Foundation.framework */; };
534FF36717D8E90A0020A51A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534FF36517D8E90A0020A51A /* InfoPlist.strings */; };
534FF36A17D8E90A0020A51A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 534FF36917D8E90A0020A51A /* main.m */; };
534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; };
53542FC0184F86EC003A6AFD /* Mantle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D0A56BA41804B1FD00A84EDC /* Mantle.framework */; };
53542FCE184F86F1003A6AFD /* ReactiveCocoa.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D0F3F05617E83A1D003AFA6B /* ReactiveCocoa.framework */; };
53542FCF184F870C003A6AFD /* Mantle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A56BA41804B1FD00A84EDC /* Mantle.framework */; };
53542FD2184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */; };
536CD4E2183E5D7F000C0FD1 /* SQRLDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = 536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */; };
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */; };
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; };
53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; };
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 53AACF4917E9CA0500B41027 /* SQRLUpdate.h */; settings = {ATTRIBUTES = (Public, ); }; };
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */; };
53B0360B184E5C860079FB77 /* SQRLHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */; };
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */; };
53C3186F1816C38A0074B481 /* SQRLDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C318691816C38A0074B481 /* SQRLDownloadManager.h */; };
53C318701816C38A0074B481 /* SQRLDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */; };
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C3186B1816C38A0074B481 /* SQRLURLConnection.h */; };
53C318721816C38A0074B481 /* SQRLURLConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186C1816C38A0074B481 /* SQRLURLConnection.m */; };
53C318731816C38A0074B481 /* SQRLResumableDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */; };
53C318741816C38A0074B481 /* SQRLResumableDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */; };
53C3187C1816CCD70074B481 /* SQRLDownloadManagerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */; };
53C3187D1816CCD70074B481 /* SQRLURLConnectionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */; };
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = D000218D17BACEFF0050109A /* SQRLZipArchiver.m */; };
D000219717BAD34D0050109A /* TestApplication.app.zip in Resources */ = {isa = PBXBuildFile; fileRef = D000219617BAD34D0050109A /* TestApplication.app.zip */; };
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */; };
@@ -387,6 +411,8 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
53542FCE184F86F1003A6AFD /* ReactiveCocoa.framework in Copy Frameworks */,
53542FC0184F86EC003A6AFD /* Mantle.framework in Copy Frameworks */,
D08D4E4917B451F20012B22D /* Squirrel.framework in Copy Frameworks */,
);
name = "Copy Frameworks";
@@ -406,11 +432,22 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RACSignal+SQRLFileManagerExtensions.h"; sourceTree = "<group>"; };
5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RACSignal+SQRLFileManagerExtensions.m"; sourceTree = "<group>"; };
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQRLDownload+Private.h"; sourceTree = "<group>"; };
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+SQRLExtensions.h"; sourceTree = "<group>"; };
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+SQRLExtensions.m"; sourceTree = "<group>"; };
53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSHTTPURLResponse+SQRLExtensions.h"; sourceTree = "<group>"; };
53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSHTTPURLResponse+SQRLExtensions.m"; sourceTree = "<group>"; };
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSHTTPURLResponseExtensionsSpec.m; sourceTree = "<group>"; };
534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = com.github.Squirrel.TestApplication.TestService.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
534FF36417D8E90A0020A51A /* TestService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TestService-Info.plist"; sourceTree = "<group>"; };
534FF36617D8E90A0020A51A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
534FF36817D8E90A0020A51A /* TestService-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestService-Prefix.pch"; sourceTree = "<group>"; };
534FF36917D8E90A0020A51A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadedUpdateSpec.m; sourceTree = "<group>"; };
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLDownload.h; sourceTree = "<group>"; };
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownload.m; sourceTree = "<group>"; };
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDeepCodesignSpec.m; sourceTree = "<group>"; };
53710D7D17D8F5C300A992DE /* bootstrap */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bootstrap; sourceTree = "<group>"; };
53710D7E17D8F5C300A992DE /* cibuild */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = cibuild; sourceTree = "<group>"; };
@@ -422,6 +459,16 @@
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
53AACF4917E9CA0500B41027 /* SQRLUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLUpdate.h; sourceTree = "<group>"; };
53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdate.m; sourceTree = "<group>"; };
53B03609184E5C860079FB77 /* SQRLHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLHTTPServer.h; sourceTree = "<group>"; };
53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLHTTPServer.m; sourceTree = "<group>"; };
53C318691816C38A0074B481 /* SQRLDownloadManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLDownloadManager.h; sourceTree = "<group>"; };
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadManager.m; sourceTree = "<group>"; };
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLURLConnection.h; sourceTree = "<group>"; };
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLURLConnection.m; sourceTree = "<group>"; };
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLResumableDownload.h; sourceTree = "<group>"; };
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLResumableDownload.m; sourceTree = "<group>"; };
53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadManagerSpec.m; sourceTree = "<group>"; };
53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLURLConnectionSpec.m; sourceTree = "<group>"; };
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OHHTTPStubs.xcodeproj; path = External/ReactiveCocoa/external/specta/../../../OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj; sourceTree = "<group>"; };
D000218C17BACEFF0050109A /* SQRLZipArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLZipArchiver.h; sourceTree = "<group>"; };
D000218D17BACEFF0050109A /* SQRLZipArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLZipArchiver.m; sourceTree = "<group>"; };
@@ -546,6 +593,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
53542FCF184F870C003A6AFD /* Mantle.framework in Frameworks */,
D0F3F06417E83B0B003AFA6B /* ReactiveCocoa.framework in Frameworks */,
D08D4E4717B451E50012B22D /* Squirrel.framework in Frameworks */,
D08D4E0D17B451500012B22D /* Cocoa.framework in Frameworks */,
@@ -632,11 +680,29 @@
name = Products;
sourceTree = "<group>";
};
53C3185A1816C3730074B481 /* Downloads */ = {
isa = PBXGroup;
children = (
53C318691816C38A0074B481 /* SQRLDownloadManager.h */,
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */,
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */,
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */,
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */,
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */,
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */,
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */,
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */,
);
name = Downloads;
sourceTree = "<group>";
};
D00F5B8217E82CFB009A4818 /* Extensions */ = {
isa = PBXGroup;
children = (
D00F5B8917E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h */,
D00F5B8A17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m */,
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */,
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */,
);
name = Extensions;
sourceTree = "<group>";
@@ -671,6 +737,10 @@
D0964B3B17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m */,
D0EDBE9A17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.h */,
D0EDBE9B17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m */,
53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */,
53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */,
5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */,
5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */,
);
name = Extensions;
sourceTree = "<group>";
@@ -693,7 +763,8 @@
534FF36217D8E90A0020A51A /* TestService */,
D08D4E0F17B451500012B22D /* Supporting Files */,
);
path = TestApplication;
name = TestApplication;
path = ../TestApplication;
sourceTree = "<group>";
};
D08D4E0F17B451500012B22D /* Supporting Files */ = {
@@ -780,7 +851,6 @@
D0EDBE9F17B0C6930058BC3C /* Shared */,
D0C22BE0179CC00E00158214 /* Squirrel */,
D0C22BF6179CC00E00158214 /* SquirrelTests */,
D08D4E0E17B451500012B22D /* TestApplication */,
D014AC0317B97885007D79D0 /* ShipIt */,
D0C22BD9179CC00E00158214 /* Frameworks */,
53710D7C17D8F5C300A992DE /* Scripts */,
@@ -848,6 +918,7 @@
D0C22BF6179CC00E00158214 /* SquirrelTests */ = {
isa = PBXGroup;
children = (
D08D4E0E17B451500012B22D /* TestApplication */,
D0C22C4E179CC18000158214 /* Specs */,
D0C22BF7179CC00E00158214 /* Supporting Files */,
);
@@ -859,6 +930,8 @@
children = (
D049059E180558B7004E683E /* SQRLTestUpdate.h */,
D049059F180558B7004E683E /* SQRLTestUpdate.m */,
53B03609184E5C860079FB77 /* SQRLHTTPServer.h */,
53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */,
D000219617BAD34D0050109A /* TestApplication.app.zip */,
D09D244917B59AB30001FAF8 /* TestApplication 2.1.app */,
D0C22BF8179CC00E00158214 /* SquirrelTests-Info.plist */,
@@ -936,6 +1009,7 @@
isa = PBXGroup;
children = (
D0964B2817F2DEE700D88BF7 /* Updates */,
53C3185A1816C3730074B481 /* Downloads */,
D0964B3417F2DEF500D88BF7 /* Archives */,
D0964B3517F2DF3100D88BF7 /* IPC */,
);
@@ -946,6 +1020,7 @@
isa = PBXGroup;
children = (
D00F5B8D17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m */,
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */,
D0EDBEE017B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m */,
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */,
D049059A180554BA004E683E /* SQRLDirectoryManagerSpec.m */,
@@ -957,6 +1032,9 @@
D0C22C4F179CC18C00158214 /* SQRLUpdaterSpec.m */,
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */,
D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */,
53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */,
53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */,
53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */,
);
name = Specs;
sourceTree = "<group>";
@@ -1016,11 +1094,19 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */,
D0964B3C17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.h in Headers */,
D0C22C4C179CC15100158214 /* SQRLUpdater.h in Headers */,
5316AFAC188F0A8E00C070DF /* NSData+SQRLExtensions.h in Headers */,
D0C22C52179CC23900158214 /* Squirrel.h in Headers */,
5316AFA8188F002100C070DF /* SQRLDownload+Private.h in Headers */,
53390936184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h in Headers */,
D0964B3817F2E01500D88BF7 /* SQRLDownloadedUpdate.h in Headers */,
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */,
5316AF7A188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h in Headers */,
53C318731816C38A0074B481 /* SQRLResumableDownload.h in Headers */,
536CD4E2183E5D7F000C0FD1 /* SQRLDownload.h in Headers */,
53C3186F1816C38A0074B481 /* SQRLDownloadManager.h in Headers */,
D00F5B8B17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1408,6 +1494,7 @@
D014AC1117B9789C007D79D0 /* SQRLInstaller.m in Sources */,
D043F77217FF40C90012F996 /* SQRLTerminationListener.m in Sources */,
D014AC0517B97885007D79D0 /* ShipIt-main.m in Sources */,
5316AF95188EFFA100C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */,
D014AC1917B979A8007D79D0 /* SQRLCodeSignature.m in Sources */,
D06B58B618032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
D014AC1A17B979AA007D79D0 /* NSError+SQRLVerbosityExtensions.m in Sources */,
@@ -1430,18 +1517,25 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5316AFAD188F0A8E00C070DF /* NSData+SQRLExtensions.m in Sources */,
D0964B3D17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m in Sources */,
F6F2ED8417A33C4900F4D220 /* SQRLCodeSignature.m in Sources */,
D0D2B6261804E903000EA901 /* SQRLDirectoryManager.m in Sources */,
53C318741816C38A0074B481 /* SQRLResumableDownload.m in Sources */,
5316AF7B188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */,
D0964B3917F2E01500D88BF7 /* SQRLDownloadedUpdate.m in Sources */,
D014ABB817B96CB3007D79D0 /* SQRLShipItLauncher.m in Sources */,
53C318721816C38A0074B481 /* SQRLURLConnection.m in Sources */,
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */,
D0C22C4D179CC15100158214 /* SQRLUpdater.m in Sources */,
D0EDBE9D17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m in Sources */,
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
53C318701816C38A0074B481 /* SQRLDownloadManager.m in Sources */,
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */,
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1451,15 +1545,20 @@
files = (
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */,
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */,
53542FD2184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m in Sources */,
D043F77E17FF43640012F996 /* SQRLTerminationListener.m in Sources */,
D09D244117B595470001FAF8 /* SQRLInstallerSpec.m in Sources */,
D0C22C50179CC18C00158214 /* SQRLUpdaterSpec.m in Sources */,
D0EDBEDD17B0DB3E0058BC3C /* SQRLTestCase.m in Sources */,
5339094A184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m in Sources */,
53C3187C1816CCD70074B481 /* SQRLDownloadManagerSpec.m in Sources */,
D06B58D118035B5A00656D97 /* SQRLTerminationListenerSpec.m in Sources */,
D0EDBEE117B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m in Sources */,
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */,
D00F5B8E17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m in Sources */,
53B0360B184E5C860079FB77 /* SQRLHTTPServer.m in Sources */,
D049059B180554BA004E683E /* SQRLDirectoryManagerSpec.m in Sources */,
53C3187D1816CCD70074B481 /* SQRLURLConnectionSpec.m in Sources */,
D061E40217FD580D00CC134A /* SQRLInstaller.m in Sources */,
D04905A0180558B7004E683E /* SQRLTestUpdate.m in Sources */,
D049059D18055671004E683E /* SQRLShipItStateSpec.m in Sources */,
+19
View File
@@ -0,0 +1,19 @@
//
// NSData+SQRLExtensions.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSData (SQRLExtensions)
// Returns an NSData for the SHA1 hash of the receiver.
- (NSData *)sqrl_SHA1Hash;
// Returns the base16 encoding of the receiver.
- (NSString *)sqrl_base16String;
@end
+31
View File
@@ -0,0 +1,31 @@
//
// NSData+SQRLExtensions.m
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "NSData+SQRLExtensions.h"
#import <CommonCrypto/CommonCrypto.h>
@implementation NSData (SQRLExtensions)
- (NSData *)sqrl_SHA1Hash {
unsigned char hash[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(self.bytes, (CC_LONG)self.length, hash);
return [NSData dataWithBytes:hash length:sizeof(hash) / sizeof(*hash)];
}
- (NSString *)sqrl_base16String {
NSMutableString *base16 = [NSMutableString stringWithCapacity:self.length * 2];
uint8_t *bytes = (uint8_t *)self.bytes;
for (NSUInteger idx = 0; idx < self.length; idx++) {
uint8_t byte = *(bytes + idx);
[base16 appendFormat:@"%02hhx", byte];
}
return base16;
}
@end
@@ -0,0 +1,18 @@
//
// NSHTTPURLResponse+SQRLExtensions.h
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSHTTPURLResponse (SQRLExtensions)
// Case insensitively look up a header field value.
//
// Returns the value of the header if present or nil otherwise.
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field;
@end
@@ -0,0 +1,26 @@
//
// NSHTTPURLResponse+SQRLExtensions.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "NSHTTPURLResponse+SQRLExtensions.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@implementation NSHTTPURLResponse (SQRLExtensions)
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field {
return [[[self.allHeaderFields.rac_signal
filter:^ BOOL (RACTuple *keyValuePair) {
return [keyValuePair[0] caseInsensitiveCompare:field] == NSOrderedSame;
}]
reduceEach:^(NSString *key, NSString *value) {
return value;
}]
first];
}
@end
@@ -0,0 +1,15 @@
//
// RACSignal+SQRLFileManagerExtensions.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface RACSignal (SQRLFileManagerExtensions)
- (RACSignal *)sqrl_tryCreateDirectory;
@end
@@ -0,0 +1,21 @@
//
// RACSignal+SQRLFileManagerExtensions.m
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "RACSignal+SQRLFileManagerExtensions.h"
@implementation RACSignal (SQRLFileManagerExtensions)
- (RACSignal *)sqrl_tryCreateDirectory {
return [[self
try:^(NSURL *directoryURL, NSError **errorRef) {
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:nil error:errorRef];
}]
setNameWithFormat:@"%@ -sqrl_tryCreateDirectory", self];
}
@end
+16 -3
View File
@@ -26,14 +26,27 @@
// on-disk locations for. This must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
// Finds or creates an Application Support folder.
// Finds or creates an Application Support subdirectory for the receiver’s
// application identifier.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)applicationSupportURL;
@property (readonly, nonatomic) RACSignal *applicationSupportURL;
// Finds or creates a downloads directory to store update downloads in prior to
// installation. This is an applicationSupportURL subdirectory.
//
// Returns a signal which sends a URL then completes, or errors.
@property (readonly, nonatomic) RACSignal *downloadDirectoryURL;
// Creates a new directory to unpack a downloaded update into prior to
// installation. This is an applicationSupportURL subdirectory.
//
// Returns a signal which sends a URL then completes, or errors.
- (RACSignal *)createUniqueUpdateDirectoryURL;
// Determines where archived `SQRLShipItState` should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStateURL;
@property (readonly, nonatomic) RACSignal *shipItStateURL;
@end
+51 -9
View File
@@ -8,6 +8,8 @@
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import "RACSignal+SQRLFileManagerExtensions.h"
@interface SQRLDirectoryManager ()
@@ -53,25 +55,65 @@
#pragma mark Folder URLs
+ (NSString *)fileSystemNameForIdentifier:(NSString *)identifier {
// Periods are problematic in the filesystem because they denote file type.
// A directory can become a package which can be undesirable.
return [identifier stringByReplacingOccurrencesOfString:@"." withString:@"~"];
}
- (RACSignal *)applicationSupportURL {
return [[RACSignal
return [[[[RACSignal
defer:^{
NSError *error = nil;
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (folderURL == nil) {
NSURL *directoryURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (directoryURL == nil) {
return [RACSignal error:error];
}
folderURL = [folderURL URLByAppendingPathComponent:self.applicationIdentifier];
if (![NSFileManager.defaultManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:&error]) {
return [RACSignal error:error];
}
return [RACSignal return:folderURL];
return [RACSignal return:directoryURL];
}]
map:^ (NSURL *directoryURL) {
RACSignal *identifierComponents = [@[ self.applicationIdentifier, @"com.github.Squirrel" ].rac_signal
map:^(NSString *name) {
return [self.class fileSystemNameForIdentifier:name];
}];
RACSignal *pathComponents = [directoryURL.pathComponents.rac_signal concat:identifierComponents];
return [NSURL fileURLWithPathComponents:[pathComponents array]];
}]
sqrl_tryCreateDirectory]
setNameWithFormat:@"%@ -applicationSupportURL", self];
}
- (RACSignal *)downloadDirectoryURL {
return [[[[self
applicationSupportURL]
map:^(NSURL *directoryURL) {
return [directoryURL URLByAppendingPathComponent:@"downloads"];
}]
sqrl_tryCreateDirectory]
setNameWithFormat:@"%@ -downloadDirectoryURL", self];
}
- (RACSignal *)createUniqueUpdateDirectoryURL {
return [[[[self
applicationSupportURL]
map:^(NSURL *directoryURL) {
// noindex so that Spotlight doesn't pick up apps pending update and
// add them to the Launch Services database.
return [[directoryURL URLByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString] URLByAppendingPathExtension:@"noindex"];
}]
try:^(NSURL *directoryURL, NSError **errorRef) {
// Explicitly just provide the owner with permission, discarding the
// current umask. This matches the `mkdtemp` behaviour.
NSDictionary *directoryAttributes = @{
NSFilePosixPermissions: @(S_IRWXU),
};
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:directoryAttributes error:errorRef];
}]
setNameWithFormat:@"%@ -createUniqueUpdateDirectoryURL", self];
}
- (RACSignal *)shipItStateURL {
return [[[self
applicationSupportURL]
+21
View File
@@ -0,0 +1,21 @@
//
// SQRLDownload+Private.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLDownload.h"
@interface SQRLDownload ()
// Designated initialiser.
//
// request - Must not be nil. The request this download is for.
// fileURL - Must not be nil. The file location to save received data to.
//
// Returns an initialised download suitable for saving response data.
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL;
@end
+31
View File
@@ -0,0 +1,31 @@
//
// SQRLDownload.h
// Squirrel
//
// Created by Keith Duncan on 21/11/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class RACSignal;
// A download is a file which holds the response body for a request and can be
// resumed by a subsequent request if the initial transfer is interrupted.
//
// Don't create a download directly, instead retrieve one from
// `SQRLDownloadManager`.
@interface SQRLDownload : MTLModel
// The `request` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSURLRequest *request;
// The `fileURL` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSURL *fileURL;
// Calculate a request suitable for resuming the download.
//
// Returns a signal which sends an `NSURLRequest` then completes.
@property (readonly, nonatomic) RACSignal *resumableRequest;
@end
+29
View File
@@ -0,0 +1,29 @@
//
// SQRLDownload.m
// Squirrel
//
// Created by Keith Duncan on 21/11/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownload.h"
#import "SQRLDownload+Private.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@implementation SQRLDownload
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL {
return [self initWithDictionary:@{
@keypath(self.request): request,
@keypath(self.fileURL): fileURL,
} error:NULL];
}
- (RACSignal *)resumableRequest {
return [[RACSignal
return:self.request]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+61
View File
@@ -0,0 +1,61 @@
//
// SQRLDownloadController.h
// Squirrel
//
// Created by Keith Duncan on 26/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class SQRLDownload;
@class SQRLResumableDownload;
@class RACSignal;
@class SQRLDirectoryManager;
// Stores metadata for download resumption, and manages the disk locations for
// where they're stored.
@interface SQRLDownloadManager : NSObject
// Default download manager, initializes a download manager with the current
// application directory manager. Stores downloads in
// `directoryManager.downloadDirectoryURL`.
+ (instancetype)defaultDownloadManager;
// Designated initialiser.
//
// directoryManager - Must not be nil.
//
// Returns a download manager which can resume previously started downloads for
// the initialised directory manager's application.
- (instancetype)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
// Clean the resumable download state, removes downloaded data and tracking
// state.
//
// Returns a signal which errors or completes when all file operations have been
// attempted.
- (RACSignal *)removeAllResumableDownloads;
// Retrieve a previously started download, or initialise a new download.
//
// Previously started downloads may have been removed and require
// reinitialising.
//
// request - The request you are performing and require a disk location to save
// the response body to. Must not be nil.
//
// Returns a signal which sends an `SQRLDownload` object then completes, or
// errors.
- (RACSignal *)downloadForRequest:(NSURLRequest *)request;
// Store metadata for a download so that it can be resumed later.
//
// download - The download being stored for future resumption. Must not be nil.
// request - The request for which the download can be resumed. Must not be
// nil.
//
// Returns a signal which completes or errors.
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request;
@end
+239
View File
@@ -0,0 +1,239 @@
//
// SQRLDownloadController.m
// Squirrel
//
// Created by Keith Duncan on 26/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownloadManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SQRLDirectoryManager.h"
#import "SQRLDownload.h"
#import "SQRLDownload+Private.h"
#import "SQRLResumableDownload.h"
#import "NSData+SQRLExtensions.h"
@interface SQRLDownloadManager ()
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
@property (nonatomic, assign, readonly) dispatch_queue_t queue;
// Reads the download index.
//
// Returns a signal which sends the download index at time of read then
// completes, or errors.
- (RACSignal *)readDownloadIndex;
// Write a new download index.
//
// block - Map from the old download index to the new download index. Writing
// is serialised, subsequent writers will receive the output of the
// previous map block. If the output of the map is equal to the input
// no write is attempted and the returned signal completes.
//
// Returns a signal which completes or errors.
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block;
@end
@implementation SQRLDownloadManager
+ (instancetype)defaultDownloadManager {
static SQRLDownloadManager *defaultDownloadManager;
static dispatch_once_t defaultDownloadManagerPredicate;
dispatch_once(&defaultDownloadManagerPredicate, ^{
defaultDownloadManager = [[self alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
});
return defaultDownloadManager;
}
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
NSParameterAssert(directoryManager != nil);
self = [super init];
if (self == nil) return nil;
_directoryManager = directoryManager;
_queue = dispatch_queue_create("com.github.Squirrel.SQRLDownloadManager.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
return self;
}
- (void)dealloc {
if (_queue != NULL) dispatch_release(_queue);
}
- (RACSignal *)removeAllResumableDownloads {
return [[[RACSignal
merge:@[
[self downloadStoreIndexFileLocation],
[self.directoryManager downloadDirectoryURL]
]]
flattenMap:^ (NSURL *location) {
return [[RACSignal
defer:^{
NSError *error;
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:location error:&error];
return (remove ? [RACSignal empty] : [RACSignal error:error]);
}]
catch:^(NSError *error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
return [RACSignal error:error];
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)downloadStoreIndexFileLocation {
return [[[self.directoryManager
applicationSupportURL]
map:^(NSURL *directory) {
return [directory URLByAppendingPathComponent:@"DownloadIndex.plist"];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)readDownloadIndex {
return [[[self
downloadStoreIndexFileLocation]
flattenMap:^(NSURL *location) {
return [RACSignal
create:^(id<RACSubscriber> subscriber) {
dispatch_async(self.queue, ^{
if (subscriber.disposable.disposed) return;
NSError *error;
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
if (propertyList == nil) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:propertyList];
[subscriber sendCompleted];
});
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block {
NSParameterAssert(block != nil);
return [[[self
downloadStoreIndexFileLocation]
flattenMap:^(NSURL *location) {
return [RACSignal create:^(id<RACSubscriber> subscriber) {
dispatch_barrier_async(self.queue, ^{
if (subscriber.disposable.disposed) return;
NSError *error;
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
if (propertyList == nil) {
[subscriber sendError:error];
return;
}
NSDictionary *newPropertyList = block([propertyList mutableCopy]);
if ([newPropertyList isEqual:propertyList]) {
[subscriber sendCompleted];
return;
}
NSData *newData = [NSKeyedArchiver archivedDataWithRootObject:newPropertyList];
if (newData == nil) {
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
[subscriber sendError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListWriteStreamError userInfo:errorInfo]];
return;
}
BOOL write = [newData writeToURL:location options:NSDataWritingAtomic error:&error];
if (!write) {
[subscriber sendError:error];
return;
}
[subscriber sendCompleted];
});
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (NSDictionary *)readPropertyListWithContentsOfURL:(NSURL *)location error:(NSError **)errorRef {
NSData *propertyListData = [NSData dataWithContentsOfURL:location options:NSDataReadingUncached error:NULL];
if (propertyListData == nil) {
return @{};
}
NSDictionary *propertyList = [NSKeyedUnarchiver unarchiveObjectWithData:propertyListData];
if (propertyList == nil) {
if (errorRef != NULL) {
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
*errorRef = [NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListReadCorruptError userInfo:errorInfo];
}
return nil;
}
return propertyList;
}
+ (NSString *)keyForURL:(NSURL *)URL {
return URL.absoluteString;
}
+ (NSString *)fileNameForURL:(NSURL *)URL {
NSString *key = [self keyForURL:URL];
return [[[key dataUsingEncoding:NSUTF8StringEncoding] sqrl_SHA1Hash] sqrl_base16String];
}
- (RACSignal *)downloadForRequest:(NSURLRequest *)request {
NSParameterAssert(request != nil);
return [[[[self
readDownloadIndex]
map:^(NSDictionary *index) {
NSString *key = [self.class keyForURL:request.URL];
return index[key];
}]
flattenMap:^(SQRLResumableDownload *download) {
if (download != nil) return [RACSignal return:download];
return [[[self.directoryManager
downloadDirectoryURL]
map:^(NSURL *downloadDirectory) {
return [downloadDirectory URLByAppendingPathComponent:[self.class fileNameForURL:request.URL]];
}]
map:^(NSURL *location) {
return [[SQRLDownload alloc] initWithRequest:request fileURL:location];
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request {
NSParameterAssert(download.response != nil);
NSParameterAssert(request != nil);
return [[self
writeDownloadIndexWithBlock:^(NSMutableDictionary *index) {
NSString *key = [self.class keyForURL:request.URL];
if (download != nil) {
index[key] = download;
} else {
[index removeObjectForKey:key];
}
return index;
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+1 -1
View File
@@ -11,7 +11,7 @@
@class SQRLUpdate;
// A SQRLUpdate that has been successfully downloaded to disk.
@interface SQRLDownloadedUpdate : MTLModel
@interface SQRLDownloadedUpdate : MTLModel <MTLJSONSerializing>
// The application bundle representing the downloaded and unarchived update.
@property (nonatomic, strong, readonly) NSBundle *bundle;
+10 -3
View File
@@ -7,6 +7,7 @@
//
#import "SQRLDownloadedUpdate.h"
#import "SQRLUpdate.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLDownloadedUpdate ()
@@ -40,9 +41,15 @@
#pragma mark MTLJSONSerializing
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
}];
return @{};
}
+ (NSValueTransformer *)updateJSONTransformer {
return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:SQRLUpdate.class];
}
+ (NSValueTransformer *)bundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
@end
+27
View File
@@ -0,0 +1,27 @@
//
// SQRLResumableDownload.h
// Squirrel
//
// Created by Keith Duncan on 30/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SQRLDownload.h"
// A download which has already been started and already has partial data.
@interface SQRLResumableDownload : SQRLDownload
// Designated initialiser.
//
// request - See `SQRLDownload`.
// response - HTTP response whose body is being downloaded to `fileURL`.
// Must not be nil.
// fileURL - See `SQRLDownload`.
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL;
// The `response` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSHTTPURLResponse *response;
@end
+66
View File
@@ -0,0 +1,66 @@
//
// SQRLResumableDownload.m
// Squirrel
//
// Created by Keith Duncan on 30/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLResumableDownload.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSHTTPURLResponse+SQRLExtensions.h"
@implementation SQRLResumableDownload
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL {
NSParameterAssert(request != nil);
NSParameterAssert(response != nil);
NSParameterAssert(fileURL != nil);
return [self initWithDictionary:@{
@keypath(self.request): request,
@keypath(self.response): response,
@keypath(self.fileURL): fileURL,
} error:NULL];
}
- (BOOL)isEqual:(SQRLResumableDownload *)object {
if (self == object) return YES;
if (![object isKindOfClass:self.class]) return NO;
if (!(self.response == nil && object.response == nil) && ![self responseEqual:object.response]) return NO;
if (![object.fileURL isEqual:self.fileURL]) return NO;
return YES;
}
- (BOOL)responseEqual:(NSHTTPURLResponse *)response {
if (![response.URL isEqual:self.response.URL]) return NO;
if (response.statusCode != self.response.statusCode) return NO;
if (![response.allHeaderFields isEqual:self.response.allHeaderFields]) return NO;
return YES;
}
- (RACSignal *)resumableRequest {
return [[[super
resumableRequest]
map:^ NSURLRequest * (NSURLRequest *request) {
NSHTTPURLResponse *response = self.response;
NSString *ETag = [response sqrl_valueForHTTPHeaderField:@"ETag"];
if (ETag == nil) return request;
NSNumber *alreadyDownloadedSize = nil;
BOOL getAlreadyDownloadedSize = [self.fileURL getResourceValue:&alreadyDownloadedSize forKey:NSURLFileSizeKey error:NULL];
if (!getAlreadyDownloadedSize) return request;
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:ETag forHTTPHeaderField:@"If-Range"];
[newRequest setValue:[NSString stringWithFormat:@"%llu-", alreadyDownloadedSize.unsignedLongLongValue] forHTTPHeaderField:@"Range"];
return newRequest;
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+49
View File
@@ -0,0 +1,49 @@
//
// SQRLURLConnection.h
// Squirrel
//
// Created by Keith Duncan on 25/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class SQRLDownloadManager;
@class RACSignal;
// Resumably download a remote resource.
//
// If the resource has previously been downloaded this operation may return the
// URL of an existing file, contingent on the server matching the ETag of the
// original response.
@interface SQRLURLConnection : NSObject
// This is the designated initialiser. The body of the response is streamed to
// disk. If the response includes an ETag the download can be resumed
// automatically by subsequent download operations initialised with a request of
// equal URL.
//
// request - Must not be nil, the request is issued with NSURLConnection.
- (instancetype)initWithRequest:(NSURLRequest *)request __attribute__((nonnull (1)));
// Starts accumulating the resource body in memory, cancels when the
// subscription is disposed.
//
// Returns a signal when sends a tuple of `NSURLResponse` and `NSData`, then
// completes or errors.
- (RACSignal *)retrieve;
// Starts downloading the resource body to disk, resuming from a previous
// download if possible, cancels when the subscription is disposed.
//
// downloadManager - Must be non nil, determines where the downloads will be
// stored and resumed from. For a previously started download
// to be resumed, an equivalent download manager should be
// provided.
//
// Returns a signal which sends a tuple of `NSURLResponse` and a file
// scheme `NSURL` where the resource has been stored on disk, then completes, or
// errors.
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager __attribute__((nonnull (1)));
@end
+257
View File
@@ -0,0 +1,257 @@
//
// SQRLURLConnection.m
// Squirrel
//
// Created by Keith Duncan on 25/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLURLConnection.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
@interface SQRLURLConnection ()
// The request the downloader was initialised with.
@property (nonatomic, copy, readonly) NSURLRequest *request;
// Listens for invocations of `selector` on the receiver.
//
// The first argument of `selector` must be an `NSURLConnection` object.
//
// Returns a signal which sends the _second_ argument of `selector`, or
// RACUnit if there is only one argument, then completes when the receiver
// deallocates.
- (RACSignal *)signalForDelegateSelector:(SEL)selector;
// Subscribes to the connection delegate selectors and starts a connection for
// `request`.
//
// request - The request to perform, must not be nil.
// mapData - For each response, map the data received for that response to a
// result.
//
// Returns a signal which sends a tuple of the last response, and the first
// value from the last `mapData`, then completes, or errors.
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData;
@end
@implementation SQRLURLConnection
- (instancetype)initWithRequest:(NSURLRequest *)request {
NSParameterAssert(request != nil);
self = [self init];
if (self == nil) return nil;
_request = [request copy];
return self;
}
- (RACSignal *)signalForDelegateSelector:(SEL)selector {
NSParameterAssert(selector != NULL);
return [[self
rac_signalForSelector:selector]
map:^(RACTuple *args) {
return args[1] ?: RACUnit.defaultUnit;
}];
}
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData {
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
// A signal that will error if the connection fails for any reason.
RACSignal *errors = [[self
signalForDelegateSelector:@selector(connection:didFailWithError:)]
flattenMap:^(NSError *error) {
return [RACSignal error:error];
}];
// Sends (or replays) RACUnit when the connection has finished
// loading successfully.
RACSignal *finished = [[[[self
signalForDelegateSelector:@selector(connectionDidFinishLoading:)]
take:1]
promiseOnScheduler:RACScheduler.immediateScheduler]
start];
// A signal of all `NSURLResponse`s received on the connection.
RACSignal *responses = [[self
signalForDelegateSelector:@selector(connection:didReceiveResponse:)]
takeUntil:finished];
// A signal of all `NSData` received on the connection.
RACSignal *data = [[self
signalForDelegateSelector:@selector(connection:didReceiveData:)]
takeUntil:finished];
[[[[[RACSignal
merge:@[ responses, errors ]]
takeUntil:finished]
map:^(NSURLResponse *response) {
return [RACSignal
zip:@[
[RACSignal return:response],
mapData(response, data),
]];
}]
switchToLatest]
subscribe:subscriber];
NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
delegateQueue.maxConcurrentOperationCount = 1;
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
connection.delegateQueue = delegateQueue;
[connection start];
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
[connection cancel];
}]];
}]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), request];
}
#pragma mark Download
- (RACSignal *)truncateDownload:(SQRLDownload *)download {
NSParameterAssert(download != nil);
return [[[[RACSignal
defer:^{
NSError *error = nil;
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:download.fileURL error:&error];
return (remove ? [RACSignal empty] : [RACSignal error:error]);
}]
catch:^(NSError *error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
return [RACSignal error:error];
}]
concat:[RACSignal return:download]]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
}
- (RACSignal *)recordDownload:(SQRLResumableDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager {
NSParameterAssert(download != nil);
NSParameterAssert(downloadManager != nil);
return [[[downloadManager
setDownload:download forRequest:self.request]
concat:[RACSignal return:download]]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
}
- (RACSignal *)prepareDownload:(SQRLDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager forResponse:(NSURLResponse *)response {
return [[RACSignal
defer:^{
if (![response isKindOfClass:NSHTTPURLResponse.class]) {
return [self truncateDownload:download];
}
NSHTTPURLResponse *httpResponse = (id)response;
RACSignal *downloadSignal;
if (httpResponse.statusCode != 206 /* Partial Data */) {
downloadSignal = [self truncateDownload:download];
} else {
downloadSignal = [RACSignal empty];
}
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:self.request response:httpResponse fileURL:download.fileURL];
return [[downloadSignal
ignoreValues]
concat:[self recordDownload:newDownload downloadManager:downloadManager]];
}]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), response];
}
- (BOOL)appendData:(NSData *)data toURL:(NSURL *)fileURL error:(NSError **)errorRef {
NSOutputStream *outputStream = [NSOutputStream outputStreamWithURL:fileURL append:YES];
[outputStream open];
@onExit {
[outputStream close];
};
uint8_t const *bytes = data.bytes;
size_t length = data.length;
while (length > 0) {
NSInteger written = [outputStream write:bytes maxLength:length];
if (written == -1) {
NSError *streamError = outputStream.streamError;
if ([streamError.domain isEqualToString:NSPOSIXErrorDomain] && streamError.code == EINTR) continue;
if (errorRef != NULL) *errorRef = streamError;
return NO;
}
bytes += written;
length -= written;
}
return YES;
}
#pragma mark Start
- (RACSignal *)retrieve {
return [[self
connectionSignalWithRequest:self.request mapData:^(id _, RACSignal *data) {
return [data
aggregateWithStart:[NSMutableData data] reduce:^(NSMutableData *running, NSData *data) {
[running appendData:data];
return running;
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager {
NSParameterAssert(downloadManager != nil);
RACSignal *download = [[[downloadManager
downloadForRequest:self.request]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred];
return [[[download
flattenMap:^(SQRLDownload *download) {
return [download resumableRequest];
}]
flattenMap:^(NSURLRequest *request) {
return [self connectionSignalWithRequest:request mapData:^(NSURLResponse *response, RACSignal *data) {
RACSignal *downloadURL = [[[[download
flattenMap:^(SQRLDownload *download) {
return [self prepareDownload:download downloadManager:downloadManager forResponse:response];
}]
map:^(SQRLDownload *download) {
return download.fileURL;
}]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred];
return [[[[data
map:^(NSData *bodyData) {
return [[downloadURL
try:^(NSURL *fileURL, NSError **errorRef) {
return [self appendData:bodyData toURL:fileURL error:errorRef];
}]
ignoreValues];
}]
concat]
ignoreValues]
concat:downloadURL];
}];
}]
setNameWithFormat:@"%@ download: %@", self, downloadManager];
}
@end
+10 -8
View File
@@ -14,6 +14,8 @@ NSString * const SQRLUpdateJSONReleaseNotesKey = @"notes";
NSString * const SQRLUpdateJSONNameKey = @"name";
NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
@implementation SQRLUpdate
#pragma mark Lifecycle
@@ -50,7 +52,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
@@ -61,10 +63,10 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@keypath(SQRLUpdate.new, releaseNotes): @"notes",
@keypath(SQRLUpdate.new, releaseName): @"name",
@keypath(SQRLUpdate.new, releaseDate): @"pub_date",
@keypath(SQRLUpdate.new, updateURL): @"url",
@keypath(SQRLUpdate.new, releaseNotes): SQRLUpdateJSONReleaseNotesKey,
@keypath(SQRLUpdate.new, releaseName): SQRLUpdateJSONNameKey,
@keypath(SQRLUpdate.new, releaseDate): SQRLUpdateJSONPublicationDateKey,
@keypath(SQRLUpdate.new, updateURL): SQRLUpdateJSONURLKey,
};
}
@@ -94,8 +96,8 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
}
// If neither match, try removing the ':' in the time zone
static NSRegularExpression *timeZoneSuffix = nil;
static dispatch_once_t timeZoneSuffixPredicate = 0;
static NSRegularExpression *timeZoneSuffix;
static dispatch_once_t timeZoneSuffixPredicate;
dispatch_once(&timeZoneSuffixPredicate, ^ {
timeZoneSuffix = [NSRegularExpression regularExpressionWithPattern:@"([-+])([0-9]{2}):([0-9]{2})$" options:0 error:NULL];
});
@@ -142,7 +144,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
+63 -65
View File
@@ -17,9 +17,13 @@
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLUpdate.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLZipArchiver.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SQRLURLConnection.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
@@ -34,6 +38,9 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
@interface SQRLUpdater ()
// The directory manager used for downloads and unpacks.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
@@ -62,22 +69,25 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// errors, on a background thread.
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update;
// Downloads the archived bundle associated with the given update.
// Downloads the update archive for the given update.
//
// update - Describes the update to install. This must not be nil.
// downloadDirectory - A directory in which to create a temporary directory for this
// download. This must not be nil.
// downloadDirectory - A directory in which to create a temporary directory for
// this download. This must not be nil.
//
// Returns a signal which sends an unarchived `NSBundle` then completes, or
// errors, on a background thread.
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
// Returns a signal which sends an NSURL of the downloaded update archive then
// completes, or errors, on a background thread.
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
// Creates a unique directory in which to save the update bundle, for later use
// by ShipIt.
// Unpacks an update archive.
//
// Returns a signal which sends an `NSURL` then completes, or errors, on an
// unspecified thread.
- (RACSignal *)uniqueTemporaryDirectoryForUpdate;
// archiveURL - The location of the update archive, currently only ZIP
// archives are supported. Must not be nil.
// directoryURL - A directory in which to place the unpacked contents of the
// archive. Must not be nil.
//
// Returns a signal which completes or errors.
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL;
// Recursively searches the given directory for an application bundle that has
// the same identifier as the running application.
@@ -149,6 +159,8 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
_updateRequest = [updateRequest copy];
_updateClass = SQRLUpdate.class;
_directoryManager = SQRLDirectoryManager.currentApplicationManager;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
@@ -157,10 +169,10 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
_checkForUpdatesAction = [[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
flattenMap:^(NSURLRequest *request) {
@strongify(self);
return [self checkForUpdates:request];
}]
action];
@@ -184,7 +196,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
setNameWithFormat:@"shipItLauncher"];
return self;
}
@@ -296,14 +308,21 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
setNameWithFormat:@"%@ -updateFromJSONData:", self];
}
#pragma Download
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update {
NSParameterAssert(update != nil);
return [[[self
uniqueTemporaryDirectoryForUpdate]
return [[[self.directoryManager
createUniqueUpdateDirectoryURL]
flattenMap:^(NSURL *downloadDirectory) {
return [[[self
downloadBundleForUpdate:update intoDirectory:downloadDirectory]
return [[[[[[self
downloadArchiveForUpdate:update intoDirectory:downloadDirectory]
flattenMap:^(NSURL *updateArchiveURL) {
return [self unpackArchiveAtURL:updateArchiveURL intoDirectory:downloadDirectory];
}]
ignoreValues]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
flattenMap:^(NSBundle *updateBundle) {
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
}]
@@ -317,86 +336,65 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
}
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
- (RACSignal *)downloadArchiveForUpdate:(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];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
return [[[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:zipDownloadRequest];
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:self.directoryManager];
return [[[[connection
download:downloadManager]
reduceEach:^(NSURLResponse *response, NSURL *bodyLocation) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [RACSignal return:bodyData];
return [[RACSignal
return:bodyLocation]
tryMap:^ NSURL * (NSURL *bodyLocation, NSError **errorRef) {
NSURL *updateLocation = [downloadDirectory URLByAppendingPathComponent:response.suggestedFilename];
if (![NSFileManager.defaultManager moveItemAtURL:bodyLocation toURL:updateLocation error:errorRef]) return nil;
return updateLocation;
}];
}]
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];
}
}];
concat:[[downloadManager
removeAllResumableDownloads]
catchTo:RACSignal.empty]];
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
}]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
setNameWithFormat:@"%@ -downloadArchiveForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
return [[[RACSignal
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL {
NSParameterAssert(archiveURL != nil);
NSParameterAssert(directoryURL != nil);
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
return [SQRLZipArchiver unzipArchiveAtURL:archiveURL intoDirectoryAtURL:directoryURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
};
if (mkdtemp(updateDirectoryCString) == NULL) {
int code = errno;
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not create temporary directory", nil),
NSURLErrorKey: updateDirectoryTemplate
};
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
}
NSString *updateDirectoryPath = [NSFileManager.defaultManager stringWithFileSystemRepresentation:updateDirectoryCString length:strlen(updateDirectoryCString)];
return [RACSignal return:[NSURL fileURLWithPath:updateDirectoryPath isDirectory:YES]];
}]
setNameWithFormat:@"%@ -uniqueTemporaryDirectoryForUpdate", self];
setNameWithFormat:@"%@ unpackArchiveAtURL: %@ intoDirectory: %@", self, archiveURL, directoryURL];
}
- (RACSignal *)updateBundleMatchingCurrentApplicationInDirectory:(NSURL *)directory {
@@ -0,0 +1,23 @@
//
// NSHTTPURLResponseExtensionsSpec.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "NSHTTPURLResponse+SQRLExtensions.h"
SpecBegin(NSHTTPURLResponseExtensions)
it(@"should perform case insensitive key look up", ^{
NSDictionary *headers = @{
@"ETag": @"foo",
};
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://localhost"] statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:headers];
expect([response sqrl_valueForHTTPHeaderField:@"etag"]).to.equal(@"foo");
expect([response sqrl_valueForHTTPHeaderField:@"ETAG"]).to.equal(@"foo");
});
SpecEnd
+87
View File
@@ -0,0 +1,87 @@
//
// SQRLDownloadControllerSpec.m
// Squirrel
//
// Created by Keith Duncan on 27/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
#import "NSError+SQRLVerbosityExtensions.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
SpecBegin(SQRLDownloadManager)
__block SQRLDownloadManager *downloadManager = nil;
beforeAll(^{
downloadManager = SQRLDownloadManager.defaultDownloadManager;
NSError *removeError = nil;
BOOL remove = [[downloadManager removeAllResumableDownloads] waitUntilCompleted:&removeError];
if (!remove) {
if ([removeError.domain isEqualToString:NSCocoaErrorDomain] && removeError.code == NSFileNoSuchFileError) return;
NSLog(@"Couldn’t remove resumable downloads %@", removeError.sqrl_verboseDescription);
}
});
NSURL * (^newTestURL)() = ^ () {
return [[NSURL alloc] initWithScheme:@"http" host:@"localhost" path:[@"/" stringByAppendingString:NSProcessInfo.processInfo.globallyUniqueString]];
};
NSURL * (^newDownloadURL)() = ^ () {
NSError *error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:newTestURL()]] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSURL *downloadURL = download.fileURL;
expect(downloadURL).notTo.beNil();
return downloadURL;
};
it(@"should return a file that doesn't exist yet for new URLs", ^{
NSURL *downloadURL = newDownloadURL();
BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:downloadURL.path];
expect(exists).to.beFalsy();
});
it(@"should return a path in a writable directory for new URLs", ^{
NSURL *downloadURL = newDownloadURL();
BOOL writable = [NSFileManager.defaultManager isWritableFileAtPath:downloadURL.URLByDeletingLastPathComponent.path];
expect(writable).to.beTruthy();
});
it(@"should return the same path for the same URL", ^{
NSURL *testURL = newTestURL();
SQRLDownload *download1 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
SQRLDownload *download2 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
expect(download1).notTo.beNil();
expect(download1).to.equal(download2);
});
it(@"should remember a response", ^{
NSURLRequest *request = [NSURLRequest requestWithURL:newTestURL()];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:@{ @"ETag": NSProcessInfo.processInfo.globallyUniqueString }];
SQRLDownload *initialDownload = [[downloadManager downloadForRequest:request] first];
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:request response:response fileURL:initialDownload.fileURL];
expect(newDownload).notTo.equal(initialDownload);
NSError *error = nil;
BOOL setNewDownload = [[downloadManager setDownload:newDownload forRequest:request] waitUntilCompleted:&error];
expect(setNewDownload).to.beTruthy();
expect(error).to.beNil();
SQRLResumableDownload *resumedDownload = [[downloadManager downloadForRequest:request] first];
expect(newDownload).to.equal(resumedDownload);
});
SpecEnd
+40
View File
@@ -0,0 +1,40 @@
//
// SQRLDownloadedUpdateSpec.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLDownloadedUpdate)
it(@"should serialise to JSON", ^{
NSError *error = nil;
SQRLUpdate *update = [SQRLUpdate modelWithDictionary:@{
@keypath(SQRLUpdate.new, updateURL) : [NSURL URLWithString:@"http://fake.domain/path"],
} error:&error];
expect(update).notTo.beNil();
NSURL *testApplicationURL = [self createTestApplicationUpdate];
NSBundle *testApplicationBundle = [NSBundle bundleWithURL:testApplicationURL];
expect(testApplicationBundle).notTo.beNil();
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:testApplicationBundle];
expect(downloadedUpdate).notTo.beNil();
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:downloadedUpdate];
expect(JSONDictionary).notTo.beNil();
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
expect(JSONData).notTo.beNil();
expect(error).to.beNil();
JSONDictionary = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:&error];
expect(JSONDictionary).notTo.beNil();
SQRLDownloadedUpdate *downloadedUpdate2 = [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:&error];
expect(downloadedUpdate2).to.equal(downloadedUpdate);
expect(error).to.beNil();
});
SpecEnd
+18
View File
@@ -0,0 +1,18 @@
//
// SQRLHTTPServer.h
// Squirrel
//
// Created by Keith Duncan on 03/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface SQRLHTTPServer : NSObject
- (NSURL *)start:(NSError **)errorRef;
- (void)invalidate;
@property (copy, nonatomic) CFHTTPMessageRef (^responseBlock)(CFHTTPMessageRef request);
@end
+165
View File
@@ -0,0 +1,165 @@
//
// SQRLHTTPServer.m
// Squirrel
//
// Created by Keith Duncan on 03/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLHTTPServer.h"
#import <sys/socket.h>
#import <netinet/in.h>
@interface SQRLHTTPServer ()
@property (assign, nonatomic) dispatch_source_t listenerSource;
@property (strong, nonatomic) NSMutableArray *cleanupBlocks;
@end
@implementation SQRLHTTPServer
- (id)init {
self = [super init];
if (self == nil) return nil;
_cleanupBlocks = [[NSMutableArray alloc] init];
return self;
}
- (void)dealloc {
[self invalidate];
}
- (NSURL *)start:(NSError **)errorRef {
NSParameterAssert(self.listenerSource == NULL);
int listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in listenAddress = {
.sin_len = sizeof(listenAddress),
.sin_family = AF_INET,
.sin_port = 0,
.sin_addr = {
.s_addr = htonl(INADDR_LOOPBACK),
},
};
int bindError = bind(listenSocket, (struct sockaddr const *)&listenAddress, listenAddress.sin_len);
if (bindError != 0) {
close(listenSocket);
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:bindError userInfo:nil];
return nil;
}
listen(listenSocket, 128);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listenSocket, 0, NULL);
dispatch_source_set_event_handler(source, ^{
int connectionSocket = accept(listenSocket, NULL, NULL);
// Wait to close the connection until after the test case is complete.
// We need NSURLConnection to timeout, closing the connection causes it
// to error immediately.
[self addCleanupBlock:^{
close(connectionSocket);
}];
CFHTTPMessageRef request = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true));
while (1) {
uint8_t buffer;
ssize_t readLength = read(connectionSocket, &buffer, 1);
if (readLength < 0) {
return;
}
CFHTTPMessageAppendBytes(request, (UInt8 const *)&buffer, readLength);
if (!CFHTTPMessageIsHeaderComplete(request)) {
continue;
}
// Doesn't support reading requests with a body for simplicity
NSString *contentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Content-Length")));
if (contentLength != nil) {
return;
}
break;
}
NSLog(@"Received Request:");
NSLog(@"%@", [[NSString alloc] initWithData:CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)) encoding:NSASCIIStringEncoding]);
CFHTTPMessageRef response = self.responseBlock(request);
NSData *responseData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(response));
NSLog(@"Sending Response:");
NSLog(@"%@", [[NSString alloc] initWithData:responseData encoding:NSASCIIStringEncoding]);
size_t bufferLength = responseData.length;
uint8_t const *buffer = responseData.bytes;
while (1) {
ssize_t writeLength = write(connectionSocket, buffer, bufferLength);
if (writeLength < 0) {
return;
}
buffer += writeLength;
bufferLength -= writeLength;
if (bufferLength == 0) {
break;
}
}
});
dispatch_source_set_cancel_handler(source, ^{
close(listenSocket);
});
dispatch_resume(source);
struct sockaddr_storage localAddress = {};
socklen_t localAddressLength = sizeof(localAddress);
int localAddressError = getsockname(listenSocket, (struct sockaddr *)&localAddress, &localAddressLength);
if (localAddressError != 0) {
dispatch_release(source);
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:localAddressError userInfo:nil];
return nil;
}
self.listenerSource = source;
// IPv4 and IPv6 address transport layer port fields are at the same offset
// and are the same size
in_port_t port = ntohs(((struct sockaddr_in *)&localAddress)->sin_port);
return [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%ld/", (unsigned long)port]];
}
- (void)invalidate {
dispatch_source_t listenerSource = self.listenerSource;
if (listenerSource == NULL) return;
dispatch_source_cancel(listenerSource);
dispatch_release(listenerSource);
self.listenerSource = NULL;
[self performCleanup];
}
- (void)addCleanupBlock:(void (^)(void))block {
[self.cleanupBlocks addObject:[block copy]];
}
- (void)performCleanup {
NSArray *blocks = [self.cleanupBlocks copy];
[self.cleanupBlocks removeAllObjects];
for (void (^currentBlock)(void) in blocks) {
currentBlock();
}
}
@end
+311
View File
@@ -0,0 +1,311 @@
//
// SQRLURLConnectionSpec.m
// Squirrel
//
// Created by Keith Duncan on 27/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
#import <ReactiveCocoa/EXTScope.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "SQRLHTTPServer.h"
SpecBegin(SQRLURLConnection);
__block SQRLDownloadManager *downloadManager;
beforeAll(^{
downloadManager = SQRLDownloadManager.defaultDownloadManager;
NSError *removeError = nil;
BOOL remove = [[downloadManager
removeAllResumableDownloads]
waitUntilCompleted:&removeError];
expect(remove).to.beTruthy();
expect(removeError).to.beNil();
});
beforeEach(^{
Expecta.asynchronousTestTimeout = 60.;
});
afterAll(^{
Expecta.asynchronousTestTimeout = 1.;
});
describe(@"file scheme URLs", ^{
NSURL *fileLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"test"];
NSData *testContents = [@"test" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
BOOL write = [testContents writeToURL:fileLocation options:0 error:&error];
expect(write).to.beTruthy();
expect(error).to.beNil();
NSURLRequest *request = [NSURLRequest requestWithURL:fileLocation];
__block SQRLURLConnection *connection;
beforeEach(^{
connection = [[SQRLURLConnection alloc] initWithRequest:request];
});
void (^testBodyEqualTestContents)(NSData *) = ^ (NSData *body) {
expect(body.length).to.equal(testContents.length);
expect(body).to.equal(testContents);
};
it(@"should retrieve file:// scheme URLs", ^{
NSError *error;
RACTuple *result = [[connection retrieve] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
testBodyEqualTestContents(result[1]);
});
it(@"should download file:// scheme URLs", ^{
NSError *error;
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
NSData *body = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(error).to.beNil();
testBodyEqualTestContents(body);
});
});
static NSData * (^stringTimes)(NSString *, NSUInteger) = ^ (NSString *string, NSUInteger times) {
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *data = [NSMutableData dataWithCapacity:(stringData.length + 2) * times];
for (NSUInteger idx = 0; idx < times; idx++) {
[data appendData:stringData];
[data appendData:[NSData dataWithBytes:"\r\n" length:2]];
}
return data;
};
static SQRLHTTPServer * (^newHttpServer)(SQRLTestCase *) = ^ (SQRLTestCase *self) {
SQRLHTTPServer *server = [[SQRLHTTPServer alloc] init];
[self addCleanupBlock:^{
[server invalidate];
}];
return server;
};
it(@"should resume a download", ^{
// Start server
SQRLHTTPServer *server = newHttpServer(self);
NSError *error = nil;
NSURL *baseURL = [server start:&error];
expect(baseURL).notTo.beNil();
expect(error).to.beNil();
error = nil;
// Prepare half response
NSData *firstHalf = stringTimes(@"the quick brown fox", 100);
NSData *secondHalf = stringTimes(@"jumped over the lazy doge", 100);
NSString *ETag = NSProcessInfo.processInfo.globallyUniqueString;
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
NSDictionary *responseHeaders = @{
@"Content-Length": [@(firstHalf.length + secondHalf.length) stringValue],
@"ETag": ETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstHalf);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.beNil();
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.beNil();
return serverResponse;
};
// Issue first request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
request.timeoutInterval = 1.;
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).to.beNil();
expect(error).notTo.beNil();
error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
expect(downloadedData.length).to.equal(firstHalf.length);
expect(downloadedData).to.equal(firstHalf);
expect(error).to.beNil();
// Prepare remainder response
serverResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 206, NULL, kCFHTTPVersion1_1);
responseHeaders = @{
@"Content-Length": [@(secondHalf.length) stringValue],
@"Range": [NSString stringWithFormat:@"%lu-%lu", firstHalf.length, secondHalf.length - 1],
@"ETag": ETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondHalf);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.equal(ETag);
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstHalf.length]));
return serverResponse;
};
// Issue second request
connection = [[SQRLURLConnection alloc] initWithRequest:request];
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
NSMutableData *fullBody = [firstHalf mutableCopy];
[fullBody appendData:secondHalf];
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(downloadedData.length).to.equal(fullBody.length);
expect(downloadedData).to.equal(fullBody);
expect(error).to.beNil();
});
it(@"should not resume downloads for a response with a different ETag", ^{
// Start server
SQRLHTTPServer *server = newHttpServer(self);
NSError *error = nil;
NSURL *baseURL = [server start:&error];
expect(baseURL).notTo.beNil();
expect(error).to.beNil();
error = nil;
// Prepare first response
NSData *firstBody = stringTimes(@"the quick brown fox", 100);
NSString *firstETag = NSProcessInfo.processInfo.globallyUniqueString;
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
NSDictionary *responseHeaders = @{
// Claim the response to be longer so that the connection will timeout
// waiting for the extra bytes
@"Content-Length": [@(firstBody.length + 100) stringValue],
@"ETag": firstETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstBody);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.beNil();
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.beNil();
return serverResponse;
};
// Issue first request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
request.timeoutInterval = 1.;
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).to.beNil();
expect(error).notTo.beNil();
error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
expect(downloadedData.length).to.equal(firstBody.length);
expect(downloadedData).to.equal(firstBody);
expect(error).to.beNil();
// Prepare second response
NSData *secondBody = stringTimes(@"jumps over the lazy doge", 100);
NSString *secondETag = NSProcessInfo.processInfo.globallyUniqueString;
serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
responseHeaders = @{
@"Content-Length": [@(secondBody.length) stringValue],
@"ETag": secondETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondBody);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.equal(firstETag);
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstBody.length]));
return serverResponse;
};
// Issue second request
connection = [[SQRLURLConnection alloc] initWithRequest:request];
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(downloadedData.length).to.equal(secondBody.length);
expect(downloadedData).to.equal(secondBody);
expect(error).to.beNil();
});
SpecEnd
+6 -2
View File
@@ -39,13 +39,17 @@ it(@"should validate release name and notes", ^{
});
it(@"should parse Central style dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:NULL];
NSError *error = nil;
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:&error];
expect(update.releaseDate).notTo.beNil();
expect(error).to.beNil();
});
it(@"should parse ISO 8601 dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:NULL];
NSError *error = nil;
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:&error];
expect(update.releaseDate).notTo.beNil();
expect(error).to.beNil();
});
SpecEnd
+82
View File
@@ -9,6 +9,9 @@
#import "SQRLTestUpdate.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "SQRLDirectoryManager.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
SpecBegin(SQRLUpdater)
@@ -179,4 +182,83 @@ describe(@"response handling", ^{
});
});
it(@"should clean up resumable downloads after a successful download", ^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication"];
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:directoryManager];
NSError *error = nil;
BOOL remove = [[downloadManager removeAllResumableDownloads] asynchronouslyWaitUntilCompleted:&error];
expect(remove).to.beTruthy();
expect(error).to.beNil();
NSURL *updateApplicationBundle = [self createTestApplicationUpdate];
NSURL *firstDownload = zipUpdate(updateApplicationBundle);
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:firstDownload]];
BOOL download = [[connection download:downloadManager] asynchronouslyWaitUntilCompleted:&error];
expect(download).to.beTruthy();
expect(error).to.beNil();
NSURL *downloadDirectory = [[directoryManager downloadDirectoryURL] firstOrDefault:nil success:NULL error:&error];
expect(downloadDirectory).notTo.beNil();
expect(error).to.beNil();
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
expect(contents.count).to.equal(1);
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
@"updateURL": zipUpdate(updateApplicationBundle),
@"final": @YES,
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
if (contents == nil) {
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSFileReadNoSuchFileError);
} else {
expect(contents.count).to.equal(0);
}
});
it(@"should unpack update archives into a .noindex directory", ^{
NSURL *zippedUpdateLocation = zipUpdate([self createTestApplicationUpdate]);
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
@keypath(update.updateURL): zippedUpdateLocation,
} error:NULL];
writeUpdate(update);
__block SQRLDownloadedUpdate *downloadedUpdate;
[[[[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil]
map:^(NSNotification *notification) {
return notification.userInfo[@"update"];
}]
tryMap:^(NSString *serialisedUpdate, NSError **errorRef) {
NSData *JSONData = [serialisedUpdate dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:errorRef];
}]
tryMap:^(NSDictionary *JSONDictionary, NSError **errorRef) {
return [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:errorRef];
}]
subscribeNext:^(SQRLDownloadedUpdate *update) {
downloadedUpdate = update;
}];
launchWithEnvironment(nil);
expect(downloadedUpdate).willNot.beNil();
NSURL *bundleURL = downloadedUpdate.bundle.bundleURL;
expect([bundleURL.path rangeOfString:@".noindex"].location).notTo.equal(NSNotFound);
});
SpecEnd
+1 -1
View File
@@ -9,7 +9,7 @@
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
SpecBegin(SQRLZipArchiver)
SpecBegin(SQRLZipOperation)
it(@"should extract a zip archive created by the Finder", ^{
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
+5
View File
@@ -10,6 +10,7 @@
#import "SQRLDirectoryManager.h"
#import "SQRLShipItLauncher.h"
#import "SQRLTestUpdate.h"
#import "SQRLDownloadedUpdate.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@@ -74,6 +75,10 @@
}]
doNext:^(SQRLDownloadedUpdate *update) {
NSLog(@"Got a candidate update: %@", update);
NSString *serialisedUpdate = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[MTLJSONAdapter JSONDictionaryFromModel:update] options:0 error:NULL] encoding:NSUTF8StringEncoding];
[NSDistributedNotificationCenter.defaultCenter postNotificationName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil userInfo:@{ @"update": serialisedUpdate }];
}]
// Retry until we get the expected release.
repeat]