Compare commits
205
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfbcdafae4 | ||
|
|
8f2c9d08b4 | ||
|
|
409d6ef781 | ||
|
|
8fb6d0e64f | ||
|
|
67cc4c2e14 | ||
|
|
feee9f7995 | ||
|
|
318b9a9081 | ||
|
|
217ada5c47 | ||
|
|
819aa8f815 | ||
|
|
f0f2523b5d | ||
|
|
283f9ef559 | ||
|
|
0f32a80019 | ||
|
|
c76b3158e7 | ||
|
|
ca72079898 | ||
|
|
625edb1388 | ||
|
|
48705686d0 | ||
|
|
5cd1c0ecd8 | ||
|
|
94c98f4e8e | ||
|
|
f1c4012aa0 | ||
|
|
bb370d2b42 | ||
|
|
eb8ef9cd48 | ||
|
|
dad7bdfa89 | ||
|
|
92c37377ca | ||
|
|
c65597ee6f | ||
|
|
18c923f343 | ||
|
|
5c9702f1d4 | ||
|
|
7850a0707b | ||
|
|
8a3bb3d896 | ||
|
|
40cf893899 | ||
|
|
539c7983c8 | ||
|
|
a13090740d | ||
|
|
f82fbaf53d | ||
|
|
ef2bc2345a | ||
|
|
e74d412eb9 | ||
|
|
b8ece2fc80 | ||
|
|
17c7238489 | ||
|
|
1109d19a9e | ||
|
|
2c567384db | ||
|
|
fcc86ca470 | ||
|
|
7af569196e | ||
|
|
70e7436e11 | ||
|
|
086a23661d | ||
|
|
e1460343ff | ||
|
|
faccaf5f6f | ||
|
|
1f8d6e87cc | ||
|
|
a70db2793f | ||
|
|
cbb75b1495 | ||
|
|
df49ea7aca | ||
|
|
2d8ea97081 | ||
|
|
d07c2d4d9d | ||
|
|
89ee65156f | ||
|
|
58062408d7 | ||
|
|
46d8fa7fa1 | ||
|
|
645d276fc3 | ||
|
|
25cb739760 | ||
|
|
14ff8e2012 | ||
|
|
4826034831 | ||
|
|
35081231a3 | ||
|
|
aa1db49ca0 | ||
|
|
173dd52f1c | ||
|
|
b751e75e19 | ||
|
|
789feef723 | ||
|
|
f4af70b715 | ||
|
|
22c1a8dc07 | ||
|
|
79dab822fd | ||
|
|
5792209feb | ||
|
|
1075abadec | ||
|
|
9ac9e32509 | ||
|
|
f1d818b321 | ||
|
|
7834599175 | ||
|
|
bf4355a522 | ||
|
|
52f9eb8f4f | ||
|
|
cfc5bbf25f | ||
|
|
aab4fc5d59 | ||
|
|
ae6db20fa7 | ||
|
|
2010092d95 | ||
|
|
0166b2fef1 | ||
|
|
f80f67534c | ||
|
|
0b677cefc1 | ||
|
|
d63ff8291e | ||
|
|
742f46ff8f | ||
|
|
1e76fd3c15 | ||
|
|
856ac9f30f | ||
|
|
e4ac04cb2b | ||
|
|
4640763f1b | ||
|
|
3b3408d809 | ||
|
|
d93f7d0ae8 | ||
|
|
bdb4447511 | ||
|
|
4e36d604a0 | ||
|
|
5bfdb4683c | ||
|
|
860c010a06 | ||
|
|
84652b5f5c | ||
|
|
a8e3bf6b30 | ||
|
|
3f3856f38b | ||
|
|
aa6ffea547 | ||
|
|
843f8adeb0 | ||
|
|
93df075824 | ||
|
|
4d061c2138 | ||
|
|
854a4181ff | ||
|
|
2f75b436c7 | ||
|
|
6109b415f3 | ||
|
|
30027e933e | ||
|
|
0a198ed1da | ||
|
|
ab93693ae0 | ||
|
|
12ea8e209a | ||
|
|
931d19bc72 | ||
|
|
fc41d39585 | ||
|
|
a28abecb4d | ||
|
|
8ba490e5d3 | ||
|
|
be824ed1fb | ||
|
|
cb06bd800e | ||
|
|
6c18360388 | ||
|
|
d6356b0255 | ||
|
|
5e4e7d6fbc | ||
|
|
bb56dc4c87 | ||
|
|
a91faeaa6a | ||
|
|
e5b241dfc1 | ||
|
|
17975dd64e | ||
|
|
33ee8ca29d | ||
|
|
a9882a73d0 | ||
|
|
0e9ed45112 | ||
|
|
1e59131f03 | ||
|
|
c86dfb14c3 | ||
|
|
fcb1810178 | ||
|
|
0126d18c00 | ||
|
|
6abd2a6c82 | ||
|
|
7532c29e8e | ||
|
|
f3c96e219c | ||
|
|
6a85e07b93 | ||
|
|
57f339b2e4 | ||
|
|
8c303fb371 | ||
|
|
b3f397b81e | ||
|
|
10e7af2f81 | ||
|
|
a71b6f5efe | ||
|
|
df45644b6f | ||
|
|
f4492c20a5 | ||
|
|
2b5b334903 | ||
|
|
32cccdc77a | ||
|
|
8247064cb6 | ||
|
|
c20c540e8a | ||
|
|
07cb7a07c9 | ||
|
|
b35b602824 | ||
|
|
f400da11f3 | ||
|
|
14a36b50a4 | ||
|
|
98cc37d907 | ||
|
|
e0cf250d7b | ||
|
|
cf8b4bb853 | ||
|
|
8f870954a6 | ||
|
|
871379d9e0 | ||
|
|
c13c8de24d | ||
|
|
01f4acfe5c | ||
|
|
0f9bd718a8 | ||
|
|
58b8376f54 | ||
|
|
d92516dee4 | ||
|
|
6f3ba37b19 | ||
|
|
69c05bf159 | ||
|
|
b35b11a634 | ||
|
|
07cd9c19a7 | ||
|
|
e5a9c2c740 | ||
|
|
1072ef3c16 | ||
|
|
7a23627711 | ||
|
|
2713404d29 | ||
|
|
d11f29346a | ||
|
|
f5e7c52ba8 | ||
|
|
704933eaa1 | ||
|
|
db416ac0f0 | ||
|
|
f2f3f9922a | ||
|
|
4f114dc343 | ||
|
|
bf64476b13 | ||
|
|
b31f881103 | ||
|
|
665b947a1e | ||
|
|
36b5feec15 | ||
|
|
85309ede82 | ||
|
|
d9e37ab259 | ||
|
|
fed28478e4 | ||
|
|
4b9c02563f | ||
|
|
94837037f6 | ||
|
|
d54b0c2bfa | ||
|
|
21bd5012d6 | ||
|
|
4ffe08f9c6 | ||
|
|
a314caa355 | ||
|
|
6819ce4215 | ||
|
|
efba16e355 | ||
|
|
af8694a8f6 | ||
|
|
4c562a7c2e | ||
|
|
f2c534ff30 | ||
|
|
937319e9f5 | ||
|
|
ef28defe9c | ||
|
|
1561118861 | ||
|
|
755d50263d | ||
|
|
97ecb1bf72 | ||
|
|
b09a0cb3c8 | ||
|
|
8f9bd34e2b | ||
|
|
3b2e3639d1 | ||
|
|
dc861aba42 | ||
|
|
a12d5d5cda | ||
|
|
c4144ca55d | ||
|
|
a2dfe45177 | ||
|
|
7a667b8fc4 | ||
|
|
c516873458 | ||
|
|
6761505219 | ||
|
|
f3b4ba98a9 | ||
|
|
c194810749 | ||
|
|
6702f9515d | ||
|
|
39cf57582f |
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user