Skip to content

Commit 9fac0f8

Browse files
Added PFFileDataStream for streaming file Downloads.
This is an NSInputStream proxy that is used for file downloads while concurrently reading and writing to a file. This allows us to truly stream from parse using `-[PFFile getDataDownloadStreamInBackground]`, whereas before it had the potential to break if you consumed it too fast.
1 parent 247cc9a commit 9fac0f8

File tree

5 files changed

+283
-3
lines changed

5 files changed

+283
-3
lines changed

Parse.xcodeproj/project.pbxproj

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,8 @@
11381138
F55C740D1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F55C740B1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h */; };
11391139
F5732DE11B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */; };
11401140
F5732DE21B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */; };
1141+
F57E29B21BA388DD00A2C59D /* FileDataStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F57E29B11BA388DD00A2C59D /* FileDataStreamTests.m */; };
1142+
F57E29B31BA388DD00A2C59D /* FileDataStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F57E29B11BA388DD00A2C59D /* FileDataStreamTests.m */; };
11411143
F586B3511B1E3BD70082E3BD /* PFBaseState.m in Sources */ = {isa = PBXBuildFile; fileRef = F586B34F1B1E3BD70082E3BD /* PFBaseState.m */; };
11421144
F586B3521B1E3BE90082E3BD /* PFBaseState.m in Sources */ = {isa = PBXBuildFile; fileRef = F586B34F1B1E3BD70082E3BD /* PFBaseState.m */; };
11431145
F589894B1B7427FF008A566B /* AlertViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CE1B66D44500EFD14F /* AlertViewTests.m */; };
@@ -1200,6 +1202,10 @@
12001202
F5B0B34A1B44A33200F3EBC4 /* PFPushChannelsController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8831B27588800758E00 /* PFPushChannelsController.h */; };
12011203
F5B0B34B1B44A33200F3EBC4 /* PFPushUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = F50C66311B33A708001941A6 /* PFPushUtilities.h */; };
12021204
F5B0B34C1B44A33200F3EBC4 /* PFRelationState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE231B2912BC00EEA594 /* PFRelationState_Private.h */; };
1205+
F5B0C4F41BA248F7000AB0D5 /* PFFileDataStream.h in Headers */ = {isa = PBXBuildFile; fileRef = F5B0C4F21BA248F7000AB0D5 /* PFFileDataStream.h */; };
1206+
F5B0C4F51BA248F7000AB0D5 /* PFFileDataStream.h in Headers */ = {isa = PBXBuildFile; fileRef = F5B0C4F21BA248F7000AB0D5 /* PFFileDataStream.h */; };
1207+
F5B0C4F61BA248F7000AB0D5 /* PFFileDataStream.m in Sources */ = {isa = PBXBuildFile; fileRef = F5B0C4F31BA248F7000AB0D5 /* PFFileDataStream.m */; };
1208+
F5B0C4F71BA248F7000AB0D5 /* PFFileDataStream.m in Sources */ = {isa = PBXBuildFile; fileRef = F5B0C4F31BA248F7000AB0D5 /* PFFileDataStream.m */; };
12031209
F5C42CC71B34C22100C720D8 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 095ACE9913C69BF700566243 /* AudioToolbox.framework */; };
12041210
F5C42CD41B34F68C00C720D8 /* PFObjectSubclassingController.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */; };
12051211
F5C42CD51B34F68C00C720D8 /* PFObjectSubclassingController.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */; };
@@ -1839,6 +1845,7 @@
18391845
F55ABB811B4F3B3200A0ECD5 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.4.sdk/System/Library/Frameworks/CoreLocation.framework; sourceTree = DEVELOPER_DIR; };
18401846
F55C740B1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionCommandRunner_Private.h; sourceTree = "<group>"; };
18411847
F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLSessionDataTaskDelegateTests.m; sourceTree = "<group>"; };
1848+
F57E29B11BA388DD00A2C59D /* FileDataStreamTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileDataStreamTests.m; sourceTree = "<group>"; };
18421849
F586B34E1B1E3BD70082E3BD /* PFBaseState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFBaseState.h; sourceTree = "<group>"; };
18431850
F586B34F1B1E3BD70082E3BD /* PFBaseState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFBaseState.m; sourceTree = "<group>"; };
18441851
F5ADB9C51B6C503E002A819E /* TestFileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestFileManager.h; sourceTree = "<group>"; };
@@ -1847,6 +1854,8 @@
18471854
F5ADB9CA1B6C5047002A819E /* TestCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestCache.m; sourceTree = "<group>"; };
18481855
F5B0B3121B44A05100F3EBC4 /* PFPaymentTransactionObserver_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPaymentTransactionObserver_Private.h; sourceTree = "<group>"; };
18491856
F5B0B3141B44A21100F3EBC4 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.4.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; };
1857+
F5B0C4F21BA248F7000AB0D5 /* PFFileDataStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFileDataStream.h; sourceTree = "<group>"; };
1858+
F5B0C4F31BA248F7000AB0D5 /* PFFileDataStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFileDataStream.m; sourceTree = "<group>"; };
18501859
F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectSubclassingController.h; sourceTree = "<group>"; };
18511860
F5C42CD31B34F68C00C720D8 /* PFObjectSubclassingController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectSubclassingController.m; sourceTree = "<group>"; };
18521861
F5C42CD81B38761B00C720D8 /* PFObjectSubclassInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectSubclassInfo.h; sourceTree = "<group>"; };
@@ -1992,11 +2001,11 @@
19922001
8166FC9F1B503886003841A2 /* LocalDataStore */,
19932002
81CB7F6A1B166FC700DC601D /* Object */,
19942003
81CD664F1B4DA5A70042FC0B /* Installation */,
1995-
812FC61C1B0FF9E90043C07F /* Purchase */,
19962004
F5E8DE0F1B290B3700EEA594 /* Relation */,
19972005
81A2458A1B1E99C6006A6953 /* FieldOperation */,
19982006
81CB7F891B17957F00DC601D /* Push */,
19992007
8166FC8B1B5037F4003841A2 /* Product */,
2008+
812FC61C1B0FF9E90043C07F /* Purchase */,
20002009
8124C89B1B27BEC900758E00 /* Session */,
20012010
81EEE1AC1B446D600087AC4D /* User */,
20022011
814881411B795C63008763BF /* KeyValueCache */,
@@ -2500,6 +2509,7 @@
25002509
814915F31B66D44500EFD14F /* KeyValueCacheTests.m */,
25012510
814915F41B66D44500EFD14F /* LocationManagerMobileTests.m */,
25022511
814915F51B66D44500EFD14F /* LocationManagerTests.m */,
2512+
F57E29B11BA388DD00A2C59D /* FileDataStreamTests.m */,
25032513
814915F61B66D44500EFD14F /* ObjectBatchCommandTests.m */,
25042514
814915F71B66D44500EFD14F /* ObjectBatchControllerTests.m */,
25052515
814915F81B66D44500EFD14F /* ObjectCommandTests.m */,
@@ -3024,6 +3034,7 @@
30243034
isa = PBXGroup;
30253035
children = (
30263036
8166FC7B1B503787003841A2 /* PFFile_Private.h */,
3037+
F5B0C4ED1BA24708000AB0D5 /* FileDataStream */,
30273038
81EB595B1AF46429001EA1FC /* Controller */,
30283039
81C7F4961AF42187007B5418 /* State */,
30293040
);
@@ -3387,6 +3398,15 @@
33873398
path = FileManager;
33883399
sourceTree = "<group>";
33893400
};
3401+
F5B0C4ED1BA24708000AB0D5 /* FileDataStream */ = {
3402+
isa = PBXGroup;
3403+
children = (
3404+
F5B0C4F21BA248F7000AB0D5 /* PFFileDataStream.h */,
3405+
F5B0C4F31BA248F7000AB0D5 /* PFFileDataStream.m */,
3406+
);
3407+
path = FileDataStream;
3408+
sourceTree = "<group>";
3409+
};
33903410
F5C42CCA1B34C76D00C720D8 /* Subclassing */ = {
33913411
isa = PBXGroup;
33923412
children = (
@@ -3611,6 +3631,7 @@
36113631
8124C8731B26B9E700758E00 /* PFPinningObjectStore.h in Headers */,
36123632
810B7D761A0291FF003C0909 /* PFMacros.h in Headers */,
36133633
81BBE1351A0062B800622646 /* PFRESTAnalyticsCommand.h in Headers */,
3634+
F5B0C4F41BA248F7000AB0D5 /* PFFileDataStream.h in Headers */,
36143635
81CB7FA01B1800E400DC601D /* PFPushController.h in Headers */,
36153636
815EE93C19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h in Headers */,
36163637
F51535591B57573700C49F56 /* PFDefaultACLController.h in Headers */,
@@ -3968,6 +3989,7 @@
39683989
81BCB4CB1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h in Headers */,
39693990
F51535051B57240900C49F56 /* PFACLPrivate.h in Headers */,
39703991
81F0E89119E6F83E00812A88 /* PFAnalytics.h in Headers */,
3992+
F5B0C4F51BA248F7000AB0D5 /* PFFileDataStream.h in Headers */,
39713993
81F0E89319E6F83E00812A88 /* PFCloud.h in Headers */,
39723994
8124C8A01B27BF0900758E00 /* PFSessionController.h in Headers */,
39733995
814B64121A769EF500213055 /* PFLogger.h in Headers */,
@@ -4467,6 +4489,7 @@
44674489
81E0335C1B573F3E00B25168 /* PFMockURLResponse.m in Sources */,
44684490
814916CD1B66D44600EFD14F /* RelationUnitTests.m in Sources */,
44694491
81E0335A1B573F3E00B25168 /* PFMockURLProtocol.m in Sources */,
4492+
F57E29B21BA388DD00A2C59D /* FileDataStreamTests.m in Sources */,
44704493
814916591B66D44600EFD14F /* DeviceTests.m in Sources */,
44714494
814916DF1B66D44600EFD14F /* UserFileCodingLogicTests.m in Sources */,
44724495
814916B31B66D44600EFD14F /* PushCommandTests.m in Sources */,
@@ -4577,6 +4600,7 @@
45774600
814916601B66D44600EFD14F /* FieldOperationDecoderTests.m in Sources */,
45784601
81E0337F1B57441F00B25168 /* CLLocationManager+TestAdditions.m in Sources */,
45794602
814916861B66D44600EFD14F /* ObjectFileCoderTests.m in Sources */,
4603+
F57E29B31BA388DD00A2C59D /* FileDataStreamTests.m in Sources */,
45804604
8149163A1B66D44500EFD14F /* AnonymousUtilsTests.m in Sources */,
45814605
81308B721B5781F500FFFF44 /* PFTestSwizzlingUtilities.m in Sources */,
45824606
814916761B66D44600EFD14F /* KeychainStoreTests.m in Sources */,
@@ -4713,6 +4737,7 @@
47134737
8124C8861B27588800758E00 /* PFPushChannelsController.m in Sources */,
47144738
814881621B795CD4008763BF /* PFMultiProcessFileLock.m in Sources */,
47154739
81C3826819CCAD790066284A /* PFAlertView.m in Sources */,
4740+
F5B0C4F61BA248F7000AB0D5 /* PFFileDataStream.m in Sources */,
47164741
811214751B3E1CF10052741B /* PFObjectBatchController.m in Sources */,
47174742
8166FCDF1B503914003841A2 /* PFAnonymousAuthenticationProvider.m in Sources */,
47184743
8166FCC21B503886003841A2 /* PFSQLiteDatabaseResult.m in Sources */,
@@ -4876,6 +4901,7 @@
48764901
F5C8F2C01B1F7E7800CD98E7 /* PFAsyncTaskQueue.m in Sources */,
48774902
81D0EE9C19B0A2060000AE75 /* PFKeychainStore.m in Sources */,
48784903
814881671B795CD4008763BF /* PFMultiProcessFileLockController.m in Sources */,
4904+
F5B0C4F71BA248F7000AB0D5 /* PFFileDataStream.m in Sources */,
48794905
81068EF41AE0845D00A34D13 /* PFEncoder.m in Sources */,
48804906
F51535071B57240900C49F56 /* PFACLState.m in Sources */,
48814907
970110821630B45800AB761E /* PFGeoPoint.m in Sources */,

Parse/Internal/File/Controller/PFFileController.m

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import <Bolts/BFTaskCompletionSource.h>
1414

1515
#import "BFTask+Private.h"
16+
#import "PFFileDataStream.h"
1617
#import "PFAssert.h"
1718
#import "PFCommandResult.h"
1819
#import "PFCommandRunning.h"
@@ -135,15 +136,18 @@ - (BFTask *)downloadFileStreamAsyncWithState:(PFFileState *)fileState
135136
return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{
136137
BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource];
137138
NSString *filePath = [self _temporaryFileDownloadPathForFileState:fileState];
138-
NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:filePath];
139-
[self downloadFileAsyncWithState:fileState
139+
PFFileDataStream *stream = [[PFFileDataStream alloc] initWithFileAtPath:filePath];
140+
[[self downloadFileAsyncWithState:fileState
140141
cancellationToken:cancellationToken
141142
progressBlock:^(int percentDone) {
142143
[taskCompletionSource trySetResult:stream];
143144

144145
if (progressBlock) {
145146
progressBlock(percentDone);
146147
}
148+
}] continueWithBlock:^id(BFTask *task) {
149+
[stream stopBlocking];
150+
return task;
147151
}];
148152
return taskCompletionSource.task;
149153
}];
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
#import <Foundation/Foundation.h>
11+
12+
NS_ASSUME_NONNULL_BEGIN
13+
14+
/*!
15+
PFFileDataStream is an NSStream proxy which won't read the last byte of a file until the downlaod has finished.
16+
17+
When downloading a file stream via `-[PFFile getDataDownloadStreamInBackground]`, we need to be able to read and write
18+
to the same file on disk concurrently.
19+
20+
NSInputStream closes itself as soon as it hits EOF, so this class wraps an underlying NSInputStream and stops the
21+
stream from closing until after writing has finished.
22+
*/
23+
@interface PFFileDataStream : NSProxy
24+
25+
- (instancetype)initWithFileAtPath:(NSString *)path;
26+
27+
- (void)stopBlocking;
28+
29+
@end
30+
31+
NS_ASSUME_NONNULL_END
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
#import "PFFileDataStream.h"
11+
12+
#import <objc/runtime.h>
13+
#import <objc/message.h>
14+
15+
@interface PFFileDataStream() <NSStreamDelegate> {
16+
NSString *_path;
17+
NSInputStream *_inputStream;
18+
19+
int _fd;
20+
BOOL _finished;
21+
22+
__weak id<NSStreamDelegate> _delegate;
23+
}
24+
25+
@end
26+
27+
@implementation PFFileDataStream
28+
29+
- (instancetype)initWithFileAtPath:(NSString *)path {
30+
_finished = NO;
31+
32+
_path = path;
33+
_inputStream = [NSInputStream inputStreamWithFileAtPath:path];
34+
_inputStream.delegate = self;
35+
36+
return self;
37+
}
38+
39+
- (void)stopBlocking {
40+
_finished = YES;
41+
42+
[self stream:_inputStream handleEvent:NSStreamEventHasBytesAvailable];
43+
}
44+
45+
///--------------------------------------
46+
#pragma mark - NSProxy methods
47+
///--------------------------------------
48+
49+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
50+
return [_inputStream methodSignatureForSelector:sel];
51+
}
52+
53+
- (void)forwardInvocation:(NSInvocation *)invocation {
54+
[invocation invokeWithTarget:_inputStream];
55+
}
56+
57+
- (BOOL)respondsToSelector:(SEL)aSelector {
58+
Method implementation = class_getInstanceMethod([self class], aSelector);
59+
return implementation ? YES : [_inputStream respondsToSelector:aSelector];
60+
}
61+
62+
///--------------------------------------
63+
#pragma mark - NSInputStream methods
64+
///--------------------------------------
65+
66+
- (void)setDelegate:(id<NSStreamDelegate>)delegate {
67+
_delegate = delegate;
68+
}
69+
70+
- (id<NSStreamDelegate>)delegate {
71+
return _delegate;
72+
}
73+
74+
- (void)open {
75+
_fd = open([_path UTF8String], O_RDONLY | O_NONBLOCK);
76+
[_inputStream open];
77+
}
78+
79+
- (void)close {
80+
[_inputStream close];
81+
close(_fd);
82+
}
83+
84+
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
85+
if (!_finished) {
86+
off_t currentOffset = [[_inputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
87+
off_t fileSize = lseek(_fd, 0, SEEK_END);
88+
89+
len = (NSUInteger)MIN(len, ((fileSize - currentOffset) - 1));
90+
}
91+
92+
// Reading 0 bytes from an NSInputStream causes this strange undocumented behavior: it marks the stream as 'at end',
93+
// regardless of whether more bytes are available or not. lolwut?
94+
if (len == 0) {
95+
return 0;
96+
}
97+
98+
return [_inputStream read:buffer maxLength:len];
99+
}
100+
101+
///--------------------------------------
102+
#pragma mark - NSStreamDelegate
103+
///--------------------------------------
104+
105+
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
106+
id delegate = _delegate;
107+
if ([delegate respondsToSelector:@selector(stream:handleEvent:)]) {
108+
[delegate stream:(NSInputStream *)self handleEvent:eventCode];
109+
}
110+
}
111+
112+
@end

0 commit comments

Comments
 (0)