You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1947 lines
72 KiB

/* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#import "GTMSessionUploadFetcher.h"
static NSString *const kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey = @"_upChunk";
static NSString *const kGTMSessionIdentifierUploadFileURLMetadataKey = @"_upFileURL";
static NSString *const kGTMSessionIdentifierUploadFileLengthMetadataKey = @"_upFileLen";
static NSString *const kGTMSessionIdentifierUploadLocationURLMetadataKey = @"_upLocURL";
static NSString *const kGTMSessionIdentifierUploadMIMETypeMetadataKey = @"_uploadMIME";
static NSString *const kGTMSessionIdentifierUploadChunkSizeMetadataKey = @"_upChSize";
static NSString *const kGTMSessionIdentifierUploadCurrentOffsetMetadataKey = @"_upOffset";
static NSString *const kGTMSessionHeaderXGoogUploadChunkGranularity = @"X-Goog-Upload-Chunk-Granularity";
static NSString *const kGTMSessionHeaderXGoogUploadCommand = @"X-Goog-Upload-Command";
static NSString *const kGTMSessionHeaderXGoogUploadContentLength = @"X-Goog-Upload-Content-Length";
static NSString *const kGTMSessionHeaderXGoogUploadContentType = @"X-Goog-Upload-Content-Type";
static NSString *const kGTMSessionHeaderXGoogUploadOffset = @"X-Goog-Upload-Offset";
static NSString *const kGTMSessionHeaderXGoogUploadProtocol = @"X-Goog-Upload-Protocol";
static NSString *const kGTMSessionHeaderXGoogUploadSizeReceived = @"X-Goog-Upload-Size-Received";
static NSString *const kGTMSessionHeaderXGoogUploadStatus = @"X-Goog-Upload-Status";
static NSString *const kGTMSessionHeaderXGoogUploadURL = @"X-Goog-Upload-URL";
// Property of chunk fetchers identifying the parent upload fetcher. Non-retained NSValue.
static NSString *const kGTMSessionUploadFetcherChunkParentKey = @"_uploadFetcherChunkParent";
int64_t const kGTMSessionUploadFetcherUnknownFileSize = -1;
int64_t const kGTMSessionUploadFetcherStandardChunkSize = (int64_t)LLONG_MAX;
#if TARGET_OS_IPHONE
int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize = 10 * 1024 * 1024; // 10 MB for iOS, watchOS, tvOS
#else
int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize = 100 * 1024 * 1024; // 100 MB for macOS
#endif
typedef NS_ENUM(NSUInteger, GTMSessionUploadFetcherStatus) {
kStatusUnknown,
kStatusActive,
kStatusFinal,
kStatusCancelled,
};
NSString *const kGTMSessionFetcherUploadLocationObtainedNotification =
@"kGTMSessionFetcherUploadLocationObtainedNotification";
#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (ProtectedMethods)
// Access to non-public method on the parent fetcher class.
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
- (void)createSessionIdentifierWithMetadata:(NSDictionary *)metadata;
- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(id)target
didFinishSelector:(SEL)finishedSelector;
- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
afterUserStopped:(BOOL)afterStopped
block:(void (^)(void))block;
- (NSTimer *)retryTimer;
- (void)beginFetchForRetry;
@property(readwrite, strong) NSData *downloadedData;
- (void)releaseCallbacks;
- (NSInteger)statusCodeUnsynchronized;
- (BOOL)userStoppedFetching;
@end
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionUploadFetcher ()
// Changing readonly to readwrite.
@property(atomic, strong, readwrite) NSURLRequest *lastChunkRequest;
@property(atomic, readwrite, assign) int64_t currentOffset;
// Internal properties.
@property(strong, atomic, GTM_NULLABLE) GTMSessionFetcher *fetcherInFlight; // Synchronized on self.
@property(assign, atomic, getter=isSubdataGenerating) BOOL subdataGenerating;
@property(assign, atomic) BOOL shouldInitiateOffsetQuery;
@property(assign, atomic) int64_t uploadGranularity;
@end
@implementation GTMSessionUploadFetcher {
GTMSessionFetcher *_chunkFetcher;
// We'll call through to the delegate's completion handler.
GTMSessionFetcherCompletionHandler _delegateCompletionHandler;
dispatch_queue_t _delegateCallbackQueue;
// The initial fetch's body length and bytes actually sent are
// needed for calculating progress during subsequent chunk uploads
int64_t _initialBodyLength;
int64_t _initialBodySent;
// The upload server address for the chunks of this upload session.
NSURL *_uploadLocationURL;
// _uploadData, _uploadDataProvider, or _uploadFileHandle may be set, but only one.
NSData *_uploadData;
NSFileHandle *_uploadFileHandle;
GTMSessionUploadFetcherDataProvider _uploadDataProvider;
NSURL *_uploadFileURL;
int64_t _uploadFileLength;
NSString *_uploadMIMEType;
int64_t _chunkSize;
int64_t _uploadGranularity;
BOOL _isPaused;
BOOL _isRestartedUpload;
BOOL _shouldInitiateOffsetQuery;
// Tied to useBackgroundSession property, since this property is applicable to chunk fetchers.
BOOL _useBackgroundSessionOnChunkFetchers;
// We keep the latest offset into the upload data just for progress reporting.
int64_t _currentOffset;
NSDictionary *_recentChunkReponseHeaders;
NSInteger _recentChunkStatusCode;
// For waiting, we need to know the fetcher in flight, if any, and if subdata generation
// is in progress.
GTMSessionFetcher *_fetcherInFlight;
BOOL _isSubdataGenerating;
BOOL _isCancelInFlight;
GTMSessionUploadFetcherCancellationHandler _cancellationHandler;
}
+ (void)load {
[self uploadFetchersForBackgroundSessions];
}
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(GTMSessionFetcherService *)fetcherService {
GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
fetcherService:fetcherService];
[fetcher setLocationURL:nil
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize];
return fetcher;
}
+ (instancetype)uploadFetcherWithLocation:(NSURL * GTM_NULLABLE_TYPE)uploadLocationURL
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(GTMSessionFetcherService *)fetcherService {
GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil
fetcherService:fetcherService];
[fetcher setLocationURL:uploadLocationURL
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize];
return fetcher;
}
+ (instancetype)uploadFetcherForSessionIdentifierMetadata:(NSDictionary *)metadata {
GTMSESSION_ASSERT_DEBUG(
[metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue],
@"Session identifier metadata is not for an upload fetcher: %@", metadata);
NSNumber *uploadFileLengthNum = metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey];
GTMSESSION_ASSERT_DEBUG(uploadFileLengthNum != nil,
@"Session metadata missing an UploadFileSize");
if (uploadFileLengthNum == nil) return nil;
int64_t uploadFileLength = [uploadFileLengthNum longLongValue];
GTMSESSION_ASSERT_DEBUG(uploadFileLength >= 0, @"Session metadata UploadFileSize is unknown");
NSString *uploadFileURLString = metadata[kGTMSessionIdentifierUploadFileURLMetadataKey];
GTMSESSION_ASSERT_DEBUG(uploadFileURLString, @"Session metadata missing an UploadFileURL");
if (uploadFileURLString == nil) return nil;
NSURL *uploadFileURL = [NSURL URLWithString:uploadFileURLString];
// There used to be a call here to NSURL checkResourceIsReachableAndReturnError: to check for the
// existence of the file (also tried NSFileManager fileExistsAtPath:). We've determined
// empirically that the check can fail at startup even when the upload file does in fact exist.
// For now, we'll go ahead and restore the background upload fetcher. If the file doesn't exist,
// it will fail later.
NSString *uploadLocationURLString = metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey];
NSURL *uploadLocationURL =
uploadLocationURLString ? [NSURL URLWithString:uploadLocationURLString] : nil;
NSString *uploadMIMEType =
metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey];
int64_t uploadChunkSize =
[metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] longLongValue];
if (uploadChunkSize <= 0) {
uploadChunkSize = kGTMSessionUploadFetcherStandardChunkSize;
}
int64_t currentOffset =
[metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] longLongValue];
GTMSESSION_ASSERT_DEBUG(currentOffset <= uploadFileLength,
@"CurrentOffset (%lld) exceeds UploadFileSize (%lld)",
currentOffset, uploadFileLength);
if (currentOffset > uploadFileLength) return nil;
GTMSessionUploadFetcher *uploadFetcher = [self uploadFetcherWithLocation:uploadLocationURL
uploadMIMEType:uploadMIMEType
chunkSize:uploadChunkSize
fetcherService:nil];
// Set the upload file length before setting the upload file URL tries to determine the length.
[uploadFetcher setUploadFileLength:uploadFileLength];
uploadFetcher.uploadFileURL = uploadFileURL;
uploadFetcher.sessionUserInfo = metadata;
uploadFetcher.useBackgroundSession = YES;
uploadFetcher.currentOffset = currentOffset;
uploadFetcher.delegateCallbackQueue = uploadFetcher.callbackQueue;
uploadFetcher.allowedInsecureSchemes = @[ @"http" ]; // Allowed on restored upload fetcher.
return uploadFetcher;
}
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
fetcherService:(GTMSessionFetcherService *)fetcherService {
// Internal utility method for instantiating fetchers
GTMSessionUploadFetcher *fetcher;
if ([fetcherService isKindOfClass:[GTMSessionFetcherService class]]) {
fetcher = [fetcherService fetcherWithRequest:request
fetcherClass:self];
} else {
fetcher = [self fetcherWithRequest:request];
}
fetcher.useBackgroundSession = YES;
return fetcher;
}
+ (NSPointerArray *)uploadFetcherPointerArrayForBackgroundSessions {
static NSPointerArray *gUploadFetcherPointerArrayForBackgroundSessions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gUploadFetcherPointerArrayForBackgroundSessions = [NSPointerArray weakObjectsPointerArray];
});
return gUploadFetcherPointerArrayForBackgroundSessions;
}
+ (instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier {
GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
NSArray *uploadFetchersForBackgroundSessions = [self uploadFetchersForBackgroundSessions];
for (GTMSessionUploadFetcher *uploadFetcher in uploadFetchersForBackgroundSessions) {
if ([uploadFetcher.chunkFetcher.sessionIdentifier isEqual:sessionIdentifier]) {
return uploadFetcher;
}
}
return nil;
}
+ (NSArray *)uploadFetchersForBackgroundSessions {
// Collect the background session upload fetchers that are still in memory.
NSPointerArray *uploadFetcherPointerArray = [self uploadFetcherPointerArrayForBackgroundSessions];
[uploadFetcherPointerArray compact];
NSMutableSet *restoredSessionIdentifiers = [[NSMutableSet alloc] init];
NSMutableArray *uploadFetchers = [[NSMutableArray alloc] init];
for (GTMSessionUploadFetcher *uploadFetcher in uploadFetcherPointerArray) {
NSString *sessionIdentifier = uploadFetcher.chunkFetcher.sessionIdentifier;
if (sessionIdentifier) {
[restoredSessionIdentifiers addObject:sessionIdentifier];
[uploadFetchers addObject:uploadFetcher];
}
}
// The system may have other ongoing background upload sessions. Restore upload fetchers for those
// too.
NSArray *fetchers = [GTMSessionFetcher fetchersForBackgroundSessions];
for (GTMSessionFetcher *fetcher in fetchers) {
NSString *sessionIdentifier = fetcher.sessionIdentifier;
if (!sessionIdentifier || [restoredSessionIdentifiers containsObject:sessionIdentifier]) {
continue;
}
NSDictionary *sessionIdentifierMetadata = [fetcher sessionIdentifierMetadata];
if (sessionIdentifierMetadata == nil) {
continue;
}
if (![sessionIdentifierMetadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue]) {
continue;
}
GTMSessionUploadFetcher *uploadFetcher =
[self uploadFetcherForSessionIdentifierMetadata:sessionIdentifierMetadata];
if (uploadFetcher == nil) {
// Something went wrong with this upload fetcher, so kill the restored chunk fetcher.
[fetcher stopFetching];
continue;
}
[uploadFetchers addObject:uploadFetcher];
uploadFetcher->_chunkFetcher = fetcher;
uploadFetcher->_fetcherInFlight = fetcher;
[uploadFetcher attachSendProgressBlockToChunkFetcher:fetcher];
fetcher.completionHandler =
[fetcher completionHandlerWithTarget:uploadFetcher
didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
GTMSESSION_LOG_DEBUG(@"%@ restoring upload fetcher %@ for chunk fetcher %@",
[self class], uploadFetcher, fetcher);
}
return uploadFetchers;
}
- (void)setUploadData:(NSData *)data {
BOOL changed = NO;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadData != data) {
_uploadData = data;
changed = YES;
}
}
if (changed) {
[self setupRequestHeaders];
}
}
- (NSData *)uploadData {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadData;
}
}
- (void)setUploadFileHandle:(NSFileHandle *)fh {
BOOL changed = NO;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadFileHandle != fh) {
_uploadFileHandle = fh;
changed = YES;
}
}
if (changed) {
[self setupRequestHeaders];
}
}
- (NSFileHandle *)uploadFileHandle {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadFileHandle;
}
}
- (void)setUploadFileURL:(NSURL *)uploadURL {
BOOL changed = NO;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadFileURL != uploadURL) {
_uploadFileURL = uploadURL;
changed = YES;
}
}
if (changed) {
[self setupRequestHeaders];
}
}
- (NSURL *)uploadFileURL {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadFileURL;
}
}
- (void)setUploadFileLength:(int64_t)fullLength {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize &&
fullLength != kGTMSessionUploadFetcherUnknownFileSize) {
_uploadFileLength = fullLength;
}
}
}
- (void)setUploadDataLength:(int64_t)fullLength
provider:(GTMSessionUploadFetcherDataProvider)block {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_uploadDataProvider = [block copy];
_uploadFileLength = fullLength;
}
[self setupRequestHeaders];
}
- (GTMSessionUploadFetcherDataProvider)uploadDataProvider {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadDataProvider;
}
}
- (void)setUploadMIMEType:(NSString *)uploadMIMEType {
GTMSESSION_ASSERT_DEBUG(0, @"TODO: disallow setUploadMIMEType by making declaration readonly");
// (and uploadMIMEType, chunksize, currentOffset)
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_uploadMIMEType = uploadMIMEType;
}
}
- (NSString *)uploadMIMEType {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadMIMEType;
}
}
- (void)setChunkSize:(int64_t)chunkSize {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_chunkSize = chunkSize;
}
}
- (int64_t)chunkSize {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _chunkSize;
}
}
- (void)setupRequestHeaders {
GTMSessionCheckNotSynchronized(self);
#if DEBUG
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
int hasData = (_uploadData != nil) ? 1 : 0;
int hasFileHandle = (_uploadFileHandle != nil) ? 1 : 0;
int hasFileURL = (_uploadFileURL != nil) ? 1 : 0;
int hasUploadDataProvider = (_uploadDataProvider != nil) ? 1 : 0;
int numberOfSources = hasData + hasFileHandle + hasFileURL + hasUploadDataProvider;
#pragma unused(numberOfSources)
GTMSESSION_ASSERT_DEBUG(numberOfSources == 1,
@"Need just one upload source (%d)", numberOfSources);
} // @synchronized(self)
#endif
// Add our custom headers to the initial request indicating the data
// type and total size to be delivered later in the chunk requests.
NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
GTMSESSION_ASSERT_DEBUG((mutableRequest == nil) != (_uploadLocationURL == nil),
@"Request and location are mutually exclusive");
if (!mutableRequest) return;
[mutableRequest setValue:@"resumable"
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];
[mutableRequest setValue:@"start"
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
[mutableRequest setValue:_uploadMIMEType
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentType];
[mutableRequest setValue:@([self fullUploadLength]).stringValue
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
NSString *method = mutableRequest.HTTPMethod;
if (method == nil || [method caseInsensitiveCompare:@"GET"] == NSOrderedSame) {
[mutableRequest setHTTPMethod:@"POST"];
}
// Ensure the user agent header identifies this to the upload server as a
// GTMSessionUploadFetcher client. The /1 can be incremented in the unlikely circumstance
// we need to make a bug fix in the client that the server can recognize.
NSString *const kUserAgentStub = @"(GTMSUF/1)";
NSString *userAgent = [mutableRequest valueForHTTPHeaderField:@"User-Agent"];
if (userAgent == nil
|| [userAgent rangeOfString:kUserAgentStub].location == NSNotFound) {
if (userAgent.length == 0) {
userAgent = GTMFetcherStandardUserAgentString(nil);
}
userAgent = [userAgent stringByAppendingFormat:@" %@", kUserAgentStub];
[mutableRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
[self setRequest:mutableRequest];
}
- (void)setLocationURL:(NSURL * GTM_NULLABLE_TYPE)location
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
GTMSESSION_ASSERT_DEBUG(chunkSize > 0, @"chunk size is zero");
// When resuming an upload, set the known upload target URL.
_uploadLocationURL = location;
_uploadMIMEType = uploadMIMEType;
_chunkSize = chunkSize;
// Indicate that we've not yet determined the file handle's length
_uploadFileLength = kGTMSessionUploadFetcherUnknownFileSize;
// Indicate that we've not yet determined the upload fetcher status
_recentChunkStatusCode = -1;
// If this is restarting an upload begun by another fetcher,
// the location is specified but the request is nil
_isRestartedUpload = (location != nil);
} // @synchronized(self)
}
- (int64_t)fullUploadLength {
int64_t result;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadData) {
result = (int64_t)_uploadData.length;
} else {
if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize) {
if (_uploadFileHandle) {
// First time through, seek to end to determine file length
_uploadFileLength = (int64_t)[_uploadFileHandle seekToEndOfFile];
} else if (_uploadDataProvider) {
// _uploadFileLength is set when the _uploadDataProvider is set.
GTMSESSION_ASSERT_DEBUG(_uploadFileLength >= 0, @"No uploadDataProvider length set");
} else {
NSNumber *filesizeNum;
NSError *valueError;
if ([_uploadFileURL getResourceValue:&filesizeNum
forKey:NSURLFileSizeKey
error:&valueError]) {
_uploadFileLength = filesizeNum.longLongValue;
} else {
GTMSESSION_ASSERT_DEBUG(NO, @"Cannot get file size: %@\n %@",
valueError, _uploadFileURL.path);
_uploadFileLength = 0;
}
}
}
result = _uploadFileLength;
}
} // @synchronized(self)
return result;
}
// Make a subdata of the upload data.
- (void)generateChunkSubdataWithOffset:(int64_t)offset
length:(int64_t)length
response:(GTMSessionUploadFetcherDataProviderResponse)response {
GTMSessionUploadFetcherDataProvider uploadDataProvider = self.uploadDataProvider;
if (uploadDataProvider) {
uploadDataProvider(offset, length, response);
return;
}
NSData *uploadData = self.uploadData;
if (uploadData) {
// NSData provided.
NSData *resultData;
if (offset == 0 && length == (int64_t)uploadData.length) {
resultData = uploadData;
} else {
int64_t dataLength = (int64_t)uploadData.length;
// Ensure our range is valid. b/18007814
if (offset + length > dataLength) {
NSString *errorMessage = [NSString stringWithFormat:
@"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld",
offset, length, dataLength];
GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
response(nil,
kGTMSessionUploadFetcherUnknownFileSize,
[self uploadChunkUnavailableErrorWithDescription:errorMessage]);
return;
}
NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
@try {
resultData = [uploadData subdataWithRange:range];
}
@catch (NSException *exception) {
NSString *errorMessage = exception.description;
GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
response(nil,
kGTMSessionUploadFetcherUnknownFileSize,
[self uploadChunkUnavailableErrorWithDescription:errorMessage]);
return;
}
}
response(resultData, kGTMSessionUploadFetcherUnknownFileSize, nil);
return;
}
NSURL *uploadFileURL = self.uploadFileURL;
if (uploadFileURL) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self generateChunkSubdataFromFileURL:uploadFileURL
offset:offset
length:length
response:response];
});
return;
}
GTMSESSION_ASSERT_DEBUG(_uploadFileHandle, @"Unexpectedly missing upload data package");
NSFileHandle *uploadFileHandle = self.uploadFileHandle;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self generateChunkSubdataFromFileHandle:uploadFileHandle
offset:offset
length:length
response:response];
});
}
- (void)generateChunkSubdataFromFileHandle:(NSFileHandle *)fileHandle
offset:(int64_t)offset
length:(int64_t)length
response:(GTMSessionUploadFetcherDataProviderResponse)response {
NSData *resultData;
NSError *error;
@try {
[fileHandle seekToFileOffset:(unsigned long long)offset];
resultData = [fileHandle readDataOfLength:(NSUInteger)length];
}
@catch (NSException *exception) {
GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileHandle failed to read, %@", exception);
error = [self uploadChunkUnavailableErrorWithDescription:exception.description];
}
// The response always re-dispatches to the main thread, so we skip doing that here.
response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
}
- (void)generateChunkSubdataFromFileURL:(NSURL *)fileURL
offset:(int64_t)offset
length:(int64_t)length
response:(GTMSessionUploadFetcherDataProviderResponse)response {
GTMSessionCheckNotSynchronized(self);
NSData *resultData;
NSError *error;
int64_t fullUploadLength = [self fullUploadLength];
NSData *mappedData =
[NSData dataWithContentsOfURL:fileURL
options:NSDataReadingMappedAlways + NSDataReadingUncached
error:&error];
if (!mappedData) {
// We could not create an NSData by memory-mapping the file.
#if TARGET_IPHONE_SIMULATOR
// NSTemporaryDirectory() can differ in the simulator between app restarts,
// yet the contents for the new path remains unchanged, so try the latest temp path.
if ([error.domain isEqual:NSCocoaErrorDomain] && (error.code == NSFileReadNoSuchFileError)) {
NSString *filename = [fileURL lastPathComponent];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
NSURL *newFileURL = [NSURL fileURLWithPath:filePath];
if (![newFileURL isEqual:fileURL]) {
[self generateChunkSubdataFromFileURL:newFileURL
offset:offset
length:length
response:response];
return;
}
}
#endif
// If the file is just too large to create an NSData for, or if for some other reason we can't
// map it, create an NSFileHandle instead to read a subset into an NSData.
#if DEBUG
NSNumber *fileSizeNum;
BOOL hasFileSize = [fileURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL];
GTMSESSION_LOG_DEBUG(@"Note: uploadFileURL is falling back to creating upload chunks by reading"
@" an NSFileHandle since uploadFileURL failed to map the upload file,"
@" file size %@, %@",
hasFileSize ? fileSizeNum : @"unknown", error);
#endif
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL
error:&error];
if (fileHandle != nil) {
[self generateChunkSubdataFromFileHandle:fileHandle
offset:offset
length:length
response:response];
return;
}
GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileURL failed to read, %@", error);
// Fall through with the error.
} else {
// Successfully created an NSData by memory-mapping the file.
if ((NSUInteger)(offset + length) > mappedData.length) {
NSString *errorMessage = [NSString stringWithFormat:
@"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld\texpected UploadLength: %lld",
offset, length, (long long)mappedData.length, fullUploadLength];
GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
response(nil,
kGTMSessionUploadFetcherUnknownFileSize,
[self uploadChunkUnavailableErrorWithDescription:errorMessage]);
return;
}
if (offset > 0 || length < fullUploadLength) {
NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
resultData = [mappedData subdataWithRange:range];
} else {
resultData = mappedData;
}
}
// The response always re-dispatches to the main thread, so we skip re-dispatching here.
response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
}
- (NSError *)uploadChunkUnavailableErrorWithDescription:(NSString *)description {
// The description in the userInfo is intended as a clue to programmers, not
// for client code to examine or rely on.
NSDictionary *userInfo = @{ @"description" : description };
return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
code:GTMSessionFetcherErrorUploadChunkUnavailable
userInfo:userInfo];
}
- (NSError *)prematureFailureErrorWithUserInfo:(NSDictionary *)userInfo {
// An error for if we get an unexpected status from the upload server or
// otherwise cannot continue. This is an issue beyond the upload protocol;
// there's no way the client can do anything useful except give up.
NSError *error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
code:501 // Not implemented
userInfo:userInfo];
return error;
}
+ (GTMSessionUploadFetcherStatus)uploadStatusFromResponseHeaders:(NSDictionary *)responseHeaders {
NSString *statusString = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus];
if ([statusString isEqual:@"active"]) {
return kStatusActive;
}
if ([statusString isEqual:@"final"]) {
return kStatusFinal;
}
if ([statusString isEqual:@"cancelled"]) {
return kStatusCancelled;
}
return kStatusUnknown;
}
#pragma mark Method overrides affecting the initial fetch only
- (void)setCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateCompletionHandler = handler;
}
}
- (void)setDelegateCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateCallbackQueue = queue;
}
}
- (dispatch_queue_t GTM_NULLABLE_TYPE)delegateCallbackQueue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateCallbackQueue;
}
}
- (BOOL)isRestartedUpload {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _isRestartedUpload;
}
}
- (GTMSessionFetcher * GTM_NULLABLE_TYPE)chunkFetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _chunkFetcher;
}
}
- (void)setChunkFetcher:(GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_chunkFetcher = fetcher;
}
}
- (void)setFetcherInFlight:(GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_fetcherInFlight = fetcher;
}
}
- (GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcherInFlight {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _fetcherInFlight;
}
}
- (void)setCancellationHandler:(GTMSessionUploadFetcherCancellationHandler GTM_NULLABLE_TYPE)
cancellationHandler {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_cancellationHandler = cancellationHandler;
}
}
- (GTMSessionUploadFetcherCancellationHandler GTM_NULLABLE_TYPE)cancellationHandler {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _cancellationHandler;
}
}
- (void)beginFetchForRetry {
GTMSessionCheckNotSynchronized(self);
// Override the superclass to reset the initial body length and fetcher-in-flight,
// then call the superclass implementation.
[self setInitialBodyLength:[self bodyLength]];
GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
self.fetcherInFlight);
self.fetcherInFlight = self;
[super beginFetchForRetry];
}
- (void)beginFetchWithCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
GTMSessionCheckNotSynchronized(self);
[self setInitialBodyLength:[self bodyLength]];
// We'll hold onto the superclass's callback queue so we can invoke the handler
// even after the superclass has released the queue and its callback handler, as
// happens during auth failure.
[self setDelegateCallbackQueue:self.callbackQueue];
self.completionHandler = handler;
if ([self isRestartedUpload]) {
// When restarting an upload, we know the destination location for chunk fetches,
// but we need to query to find the initial offset.
if (![self isPaused]) {
[self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
}
return;
}
// We don't want to call into the client's completion block immediately
// after the finish of the initial connection (the delegate is called only
// when uploading finishes), so we substitute our own completion block to be
// called when the initial connection finishes
GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
self.fetcherInFlight);
self.fetcherInFlight = self;
[super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
self.fetcherInFlight = nil;
// callback
BOOL hasTestBlock = (self.testBlock != nil);
if (![self isRestartedUpload] && !hasTestBlock) {
if (error == nil) {
[self beginChunkFetches];
} else {
if ([self retryTimer] == nil) {
[self invokeFinalCallbackWithData:nil
error:error
shouldInvalidateLocation:YES];
}
}
} else {
// If there was no initial request, then this fetch is resuming some
// other uploadFetcher's initial request, and the superclass's connection
// is never used, so at this point we call the user's actual completion
// block.
if (!hasTestBlock) {
[self invokeFinalCallbackWithData:data
error:error
shouldInvalidateLocation:YES];
} else {
// There was a test block, so we won't do chunk fetches, but we simulate obtaining
// the data to be uploaded from the upload data provider block or the file handle,
// and then call back.
[self generateChunkSubdataWithOffset:0
length:[self fullUploadLength]
response:^(NSData *generateData, int64_t fullUploadLength, NSError *generateError) {
[self invokeFinalCallbackWithData:data
error:error
shouldInvalidateLocation:YES];
}];
}
}
}];
}
- (void)beginChunkFetches {
GTMSessionCheckNotSynchronized(self);
#if DEBUG
// The initial response of the resumable upload protocol should have an
// empty body
//
// This assert typically happens because the upload create/edit link URL was
// not supplied with the request, and the server is thus expecting a non-
// resumable request/response.
if (self.downloadedData.length > 0) {
NSData *downloadedData = self.downloadedData;
NSString *str = [[NSString alloc] initWithData:downloadedData
encoding:NSUTF8StringEncoding];
#pragma unused(str)
GTMSESSION_ASSERT_DEBUG(NO, @"unexpected response data (uploading to the wrong URL?)\n%@", str);
}
#endif
// We need to get the upload URL from the location header to continue.
NSDictionary *responseHeaders = [self responseHeaders];
[self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
GTMSessionUploadFetcherStatus uploadStatus =
[[self class] uploadStatusFromResponseHeaders:responseHeaders];
GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown,
@"beginChunkFetches has unexpected upload status for headers %@", responseHeaders);
BOOL isPrematureStop = (uploadStatus == kStatusFinal) || (uploadStatus == kStatusCancelled);
NSString *uploadLocationURLStr = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadURL];
BOOL hasUploadLocation = (uploadLocationURLStr.length > 0);
if (isPrematureStop || !hasUploadLocation) {
GTMSESSION_ASSERT_DEBUG(NO, @"Premature failure: upload-status:\"%@\" location:%@",
[responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus], uploadLocationURLStr);
// We cannot continue since we do not know the location to use
// as our upload destination.
NSDictionary *userInfo = nil;
NSData *downloadedData = self.downloadedData;
if (downloadedData.length > 0) {
userInfo = @{ kGTMSessionFetcherStatusDataKey : downloadedData };
}
NSError *failureError = [self prematureFailureErrorWithUserInfo:userInfo];
[self invokeFinalCallbackWithData:nil
error:failureError
shouldInvalidateLocation:YES];
return;
}
self.uploadLocationURL = [NSURL URLWithString:uploadLocationURLStr];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMSessionFetcherUploadLocationObtainedNotification
object:self];
// we've now sent all of the initial post body data, so we need to include
// its size in future progress indicator callbacks
[self setInitialBodySent:[self initialBodyLength]];
// just in case the user paused us during the initial fetch...
if (![self isPaused]) {
[self uploadNextChunkWithOffset:0];
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
// Overrides the superclass.
[self invokeDelegateWithDidSendBytes:bytesSent
totalBytesSent:totalBytesSent
totalBytesExpectedToSend:totalBytesExpectedToSend + [self fullUploadLength]];
}
- (BOOL)shouldReleaseCallbacksUponCompletion {
// Overrides the superclass.
// We don't want the superclass to release the delegate and callback
// blocks once the initial fetch has finished
//
// This is invoked for only successful completion of the connection;
// an error always will invoke and release the callbacks
return NO;
}
- (void)invokeFinalCallbackWithData:(NSData *)data
error:(NSError *)error
shouldInvalidateLocation:(BOOL)shouldInvalidateLocation {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (shouldInvalidateLocation) {
_uploadLocationURL = nil;
}
dispatch_queue_t queue = _delegateCallbackQueue;
GTMSessionFetcherCompletionHandler handler = _delegateCompletionHandler;
if (queue && handler) {
[self invokeOnCallbackQueue:queue
afterUserStopped:NO
block:^{
handler(data, error);
}];
}
} // @synchronized(self)
[self releaseUploadAndBaseCallbacks:!self.userStoppedFetching];
}
- (void)releaseUploadAndBaseCallbacks:(BOOL)shouldReleaseCancellation {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateCallbackQueue = nil;
_delegateCompletionHandler = nil;
_uploadDataProvider = nil;
if (shouldReleaseCancellation) {
_cancellationHandler = nil;
}
}
// Release the base class's callbacks, too, if needed.
[self releaseCallbacks];
}
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
GTMSessionCheckNotSynchronized(self);
// Clear _fetcherInFlight when stopped. Moved from stopFetching, since that's a public method,
// where this method does the work. Fixes issue clearing value when retryBlock included.
GTMSessionFetcher *fetcherInFlight = self.fetcherInFlight;
if (fetcherInFlight == self) {
self.fetcherInFlight = nil;
}
[super stopFetchReleasingCallbacks:shouldReleaseCallbacks];
if (shouldReleaseCallbacks) {
[self releaseUploadAndBaseCallbacks:NO];
}
}
#pragma mark Chunk fetching methods
- (void)uploadNextChunkWithOffset:(int64_t)offset {
// use the properties in each chunk fetcher
NSDictionary *props = [self properties];
[self uploadNextChunkWithOffset:offset
fetcherProperties:props];
}
- (void)sendQueryForUploadOffsetWithFetcherProperties:(NSDictionary *)props {
GTMSessionFetcher *queryFetcher = [self uploadFetcherWithProperties:props
isQueryFetch:YES];
queryFetcher.bodyData = [NSData data];
NSString *originalComment = self.comment;
[queryFetcher setCommentWithFormat:@"%@ (query offset)",
originalComment ? originalComment : @"upload"];
[queryFetcher setRequestValue:@"query" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
self.fetcherInFlight = queryFetcher;
[queryFetcher beginFetchWithDelegate:self
didFinishSelector:@selector(queryFetcher:finishedWithData:error:)];
}
- (void)queryFetcher:(GTMSessionFetcher *)queryFetcher
finishedWithData:(NSData *)data
error:(NSError *)error {
self.fetcherInFlight = nil;
NSDictionary *responseHeaders = [queryFetcher responseHeaders];
NSString *sizeReceivedHeader;
GTMSessionUploadFetcherStatus uploadStatus =
[[self class] uploadStatusFromResponseHeaders:responseHeaders];
GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown || error != nil,
@"query fetcher completion has unexpected upload status for headers %@", responseHeaders);
if (error == nil) {
sizeReceivedHeader = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];
if (uploadStatus == kStatusCancelled ||
(uploadStatus == kStatusActive && sizeReceivedHeader == nil)) {
NSDictionary *userInfo = nil;
if (data.length > 0) {
userInfo = @{ kGTMSessionFetcherStatusDataKey : data };
}
error = [self prematureFailureErrorWithUserInfo:userInfo];
}
}
if (error == nil) {
int64_t offset = [sizeReceivedHeader longLongValue];
int64_t fullUploadLength = [self fullUploadLength];
if (uploadStatus == kStatusFinal ||
(offset >= fullUploadLength &&
fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize)) {
// Handle we're done
[self chunkFetcher:queryFetcher finishedWithData:data error:nil];
} else {
[self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
[self uploadNextChunkWithOffset:offset];
}
} else {
// Handle query error
[self chunkFetcher:queryFetcher finishedWithData:data error:error];
}
}
- (void)sendCancelUploadWithFetcherProperties:(NSDictionary *)props {
@synchronized(self) {
_isCancelInFlight = YES;
}
GTMSessionFetcher *cancelFetcher = [self uploadFetcherWithProperties:props
isQueryFetch:YES];
cancelFetcher.bodyData = [NSData data];
NSString *originalComment = self.comment;
[cancelFetcher setCommentWithFormat:@"%@ (cancel)",
originalComment ? originalComment : @"upload"];
[cancelFetcher setRequestValue:@"cancel" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
self.fetcherInFlight = cancelFetcher;
[cancelFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
self.fetcherInFlight = nil;
if (![self triggerCancellationHandlerForFetch:cancelFetcher data:data error:error]) {
if (error) {
GTMSESSION_LOG_DEBUG(@"cancelFetcher %@", error);
}
}
@synchronized(self) {
self->_isCancelInFlight = NO;
}
}];
}
- (void)uploadNextChunkWithOffset:(int64_t)offset
fetcherProperties:(NSDictionary *)props {
GTMSessionCheckNotSynchronized(self);
// Example chunk headers:
// X-Goog-Upload-Command: upload, finalize
// X-Goog-Upload-Offset: 0
// Content-Length: 2000000
// Content-Type: image/jpeg
//
// {bytes 0-1999999}
// The chunk upload URL requires no authentication header.
GTMSessionFetcher *chunkFetcher = [self uploadFetcherWithProperties:props
isQueryFetch:NO];
[self attachSendProgressBlockToChunkFetcher:chunkFetcher];
int64_t chunkSize = [self updateChunkFetcher:chunkFetcher
forChunkAtOffset:offset];
BOOL isUploadingFileURL = (self.uploadFileURL != nil);
int64_t fullUploadLength = [self fullUploadLength];
// The chunk size may have changed, so determine again if we're uploading the full file.
BOOL isUploadingFullFile = (offset == 0 &&
fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize &&
chunkSize >= fullUploadLength);
if (isUploadingFullFile && isUploadingFileURL) {
// The data is the full upload file URL.
chunkFetcher.bodyFileURL = self.uploadFileURL;
[self beginChunkFetcher:chunkFetcher
offset:offset];
} else {
// Make an NSData for the subset for this upload chunk.
self.subdataGenerating = YES;
[self generateChunkSubdataWithOffset:offset
length:chunkSize
response:^(NSData *chunkData, int64_t uploadFileLength, NSError *chunkError) {
// The subdata methods may leave us on a background thread.
dispatch_async(dispatch_get_main_queue(), ^{
self.subdataGenerating = NO;
// dont allow the updating of fileLength for uploads not using a data provider as they
// should know the file length before the upload starts.
if (self->_uploadDataProvider != nil && uploadFileLength > 0) {
[self setUploadFileLength:uploadFileLength];
// Update the command and content-length headers if this is the last chunk to be sent.
if (offset + chunkSize >= uploadFileLength) {
int64_t updatedChunkSize = [self updateChunkFetcher:chunkFetcher
forChunkAtOffset:offset];
if (updatedChunkSize == 0) {
// Calling beginChunkFetcher early when there is no more data to send allows us to
// properly handle nil chunkData below without having to account for the case where
// we are just finalizing the file.
chunkFetcher.bodyData = [[NSData alloc] init];
[self beginChunkFetcher:chunkFetcher
offset:offset];
return;
}
}
}
if (chunkData == nil) {
NSError *responseError = chunkError;
if (!responseError) {
responseError = [self uploadChunkUnavailableErrorWithDescription:@"chunkData is nil"];
}
[self invokeFinalCallbackWithData:nil
error:responseError
shouldInvalidateLocation:YES];
return;
}
BOOL didWriteFile = NO;
if (isUploadingFileURL) {
// Make a temporary file with the data subset.
NSString *tempName =
[NSString stringWithFormat:@"GTMUpload_temp_%@", [[NSUUID UUID] UUIDString]];
NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName];
NSError *writeError;
didWriteFile = [chunkData writeToFile:tempPath
options:NSDataWritingAtomic
error:&writeError];
if (didWriteFile) {
chunkFetcher.bodyFileURL = [NSURL fileURLWithPath:tempPath];
} else {
GTMSESSION_LOG_DEBUG(@"writeToFile failed: %@\n%@", writeError, tempPath);
}
}
if (!didWriteFile) {
chunkFetcher.bodyData = [chunkData copy];
}
[self beginChunkFetcher:chunkFetcher
offset:offset];
});
}];
}
}
- (void)beginChunkFetcher:(GTMSessionFetcher *)chunkFetcher
offset:(int64_t)offset {
// Track the current offset for progress reporting
self.currentOffset = offset;
// Hang on to the fetcher in case we need to cancel it. We set these before beginning the
// chunk fetch so the observers notified of chunk fetches can inspect the upload fetcher to
// match to the chunk.
self.chunkFetcher = chunkFetcher;
self.fetcherInFlight = chunkFetcher;
// Update the last chunk request, including any request headers.
self.lastChunkRequest = chunkFetcher.request;
[chunkFetcher beginFetchWithDelegate:self
didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
}
- (void)attachSendProgressBlockToChunkFetcher:(GTMSessionFetcher *)chunkFetcher {
chunkFetcher.sendProgressBlock = ^(int64_t bytesSent, int64_t totalBytesSent,
int64_t totalBytesExpectedToSend) {
// The total bytes expected include the initial body and the full chunked
// data, independent of how big this fetcher's chunk is.
int64_t initialBodySent = [self bodyLength]; // TODO(grobbins) use [self initialBodySent]
int64_t totalSent = initialBodySent + self.currentOffset + totalBytesSent;
int64_t totalExpected = initialBodySent + [self fullUploadLength];
[self invokeDelegateWithDidSendBytes:bytesSent
totalBytesSent:totalSent
totalBytesExpectedToSend:totalExpected];
};
}
- (NSDictionary *)uploadSessionIdentifierMetadata {
NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] = @YES;
GTMSESSION_ASSERT_DEBUG(self.uploadFileURL,
@"Invalid upload fetcher to create session identifier for metadata");
metadata[kGTMSessionIdentifierUploadFileURLMetadataKey] = [self.uploadFileURL absoluteString];
metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey] = @([self fullUploadLength]);
if (self.uploadLocationURL) {
metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey] =
[self.uploadLocationURL absoluteString];
}
if (self.uploadMIMEType) {
metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey] = self.uploadMIMEType;
}
metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] = @(self.chunkSize);
metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] = @(self.currentOffset);
return metadata;
}
- (GTMSessionFetcher *)uploadFetcherWithProperties:(NSDictionary *)properties
isQueryFetch:(BOOL)isQueryFetch {
GTMSessionCheckNotSynchronized(self);
// Common code to make a request for a query command or for a chunk upload.
NSURL *uploadLocationURL = self.uploadLocationURL;
NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:uploadLocationURL];
[chunkRequest setHTTPMethod:@"PUT"];
// copy the user-agent from the original connection
NSURLRequest *origRequest = self.request;
NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"];
if (userAgent.length > 0) {
[chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
// To avoid timeouts when debugging, copy the timeout of the initial fetcher.
NSTimeInterval origTimeout = [origRequest timeoutInterval];
[chunkRequest setTimeoutInterval:origTimeout];
//
// Make a new chunk fetcher.
//
GTMSessionFetcher *chunkFetcher = [GTMSessionFetcher fetcherWithRequest:chunkRequest];
chunkFetcher.callbackQueue = self.callbackQueue;
chunkFetcher.sessionUserInfo = self.sessionUserInfo;
chunkFetcher.configurationBlock = self.configurationBlock;
chunkFetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
chunkFetcher.allowLocalhostRequest = self.allowLocalhostRequest;
chunkFetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
chunkFetcher.useUploadTask = !isQueryFetch;
if (self.uploadFileURL && !isQueryFetch && self.useBackgroundSession) {
[chunkFetcher createSessionIdentifierWithMetadata:[self uploadSessionIdentifierMetadata]];
}
// Give the chunk fetcher the same properties as the previous chunk fetcher
chunkFetcher.properties = [properties mutableCopy];
[chunkFetcher setProperty:[NSValue valueWithNonretainedObject:self]
forKey:kGTMSessionUploadFetcherChunkParentKey];
// copy other fetcher settings to the new fetcher
chunkFetcher.retryEnabled = self.retryEnabled;
chunkFetcher.maxRetryInterval = self.maxRetryInterval;
if ([self isRetryEnabled]) {
// We interpose our own retry method both so we can change the request to ask the server to
// tell us where to resume the chunk.
chunkFetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *chunkError,
GTMSessionFetcherRetryResponse response) {
void (^finish)(BOOL) = ^(BOOL shouldRetry){
// We'll retry by sending an offset query.
if (shouldRetry) {
self.shouldInitiateOffsetQuery = !isQueryFetch;
// We don't know what our actual offset is anymore, but the server will tell us.
self.currentOffset = 0;
}
// We don't actually want to retry this specific fetcher.
response(NO);
};
GTMSessionFetcherRetryBlock retryBlock = self.retryBlock;
if (retryBlock) {
// Ask the client, then call the finish block above.
retryBlock(suggestedWillRetry, chunkError, finish);
} else {
finish(suggestedWillRetry);
}
};
}
return chunkFetcher;
}
- (void)chunkFetcher:(GTMSessionFetcher *)chunkFetcher
finishedWithData:(NSData *)data
error:(NSError *)error {
BOOL hasDestroyedOldChunkFetcher = NO;
self.fetcherInFlight = nil;
NSDictionary *responseHeaders = [chunkFetcher responseHeaders];
GTMSessionUploadFetcherStatus uploadStatus =
[[self class] uploadStatusFromResponseHeaders:responseHeaders];
GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown
|| error != nil
|| self.wasCreatedFromBackgroundSession,
@"chunk fetcher completion has kStatusUnknown upload status for headers %@ fetcher %@",
responseHeaders, self);
BOOL isUploadStatusStopped = (uploadStatus == kStatusFinal || uploadStatus == kStatusCancelled);
// Check if the fetcher was actually querying. If it failed, do not retry,
// as it would enter an infinite retry loop.
NSString *uploadCommand =
chunkFetcher.request.allHTTPHeaderFields[kGTMSessionHeaderXGoogUploadCommand];
BOOL isQueryFetch = [uploadCommand isEqual:@"query"];
// TODO
// Maybe here we can check to see if the request had x goog content length set. (the file length one).
int64_t previousContentLength =
[[chunkFetcher.request valueForHTTPHeaderField:@"Content-Length"] longLongValue];
// The Content-Length header may not be present if the chunk fetcher was recreated from
// a background session.
BOOL hasKnownChunkSize = (previousContentLength > 0);
BOOL needsQuery = (!hasKnownChunkSize && !isUploadStatusStopped);
if (error || (needsQuery && !isQueryFetch)) {
NSInteger status = error.code;
// Status 4xx indicates a bad offset in the Google upload protocol. However, do not retry status
// 404 per spec, nor if the upload size appears to have been zero (since the server will just
// keep asking us to retry.)
if (self.shouldInitiateOffsetQuery ||
(needsQuery && !isQueryFetch) ||
([error.domain isEqual:kGTMSessionFetcherStatusDomain] &&
status >= 400 && status <= 499 &&
status != 404 &&
uploadStatus == kStatusActive &&
previousContentLength > 0)) {
self.shouldInitiateOffsetQuery = NO;
[self destroyChunkFetcher];
hasDestroyedOldChunkFetcher = YES;
[self sendQueryForUploadOffsetWithFetcherProperties:chunkFetcher.properties];
} else {
// Some unexpected status has occurred; handle it as we would a regular
// object fetcher failure.
[self invokeFinalCallbackWithData:data
error:error
shouldInvalidateLocation:NO];
}
} else {
// The chunk has uploaded successfully.
int64_t newOffset = self.currentOffset + previousContentLength;
#if DEBUG
// Verify that if we think all of the uploading data has been sent, the server responded with
// the "final" upload status.
BOOL hasUploadAllData = (newOffset == [self fullUploadLength]);
BOOL isFinalStatus = (uploadStatus == kStatusFinal);
#pragma unused(hasUploadAllData,isFinalStatus)
GTMSESSION_ASSERT_DEBUG(hasUploadAllData == isFinalStatus || !hasKnownChunkSize,
@"uploadStatus:%@ newOffset:%lld (%lld + %lld) fullUploadLength:%lld"
@" chunkFetcher:%@ requestHeaders:%@ responseHeaders:%@",
[responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus],
newOffset, self.currentOffset, previousContentLength,
[self fullUploadLength],
chunkFetcher, chunkFetcher.request.allHTTPHeaderFields,
responseHeaders);
#endif
if (isUploadStatusStopped || (_currentOffset > _uploadFileLength && _uploadFileLength > 0)) {
// This was the last chunk.
if (error == nil && uploadStatus == kStatusCancelled) {
// Report cancelled status as an error.
NSDictionary *userInfo = nil;
if (data.length > 0) {
userInfo = @{ kGTMSessionFetcherStatusDataKey : data };
}
data = nil;
error = [self prematureFailureErrorWithUserInfo:userInfo];
} else {
// The upload is in final status.
//
// Take the chunk fetcher's data as the superclass data.
self.downloadedData = data;
self.statusCode = chunkFetcher.statusCode;
}
// we're done
[self invokeFinalCallbackWithData:data
error:error
shouldInvalidateLocation:YES];
} else {
// Start the next chunk.
self.currentOffset = newOffset;
// We want to destroy this chunk fetcher before creating the next one, but
// we want to pass on its properties
NSDictionary *props = [chunkFetcher properties];
// We no longer need to be able to cancel this chunkFetcher. Destroy it
// before we create a new chunk fetcher.
[self destroyChunkFetcher];
hasDestroyedOldChunkFetcher = YES;
[self uploadNextChunkWithOffset:newOffset
fetcherProperties:props];
}
}
if (!hasDestroyedOldChunkFetcher) {
[self destroyChunkFetcher];
}
}
- (void)destroyChunkFetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_fetcherInFlight == _chunkFetcher) {
_fetcherInFlight = nil;
}
[_chunkFetcher stopFetching];
NSURL *chunkFileURL = _chunkFetcher.bodyFileURL;
BOOL wasTemporaryUploadFile = ![chunkFileURL isEqual:_uploadFileURL];
if (wasTemporaryUploadFile) {
NSError *error;
[[NSFileManager defaultManager] removeItemAtURL:chunkFileURL
error:&error];
if (error) {
GTMSESSION_LOG_DEBUG(@"removingItemAtURL failed: %@\n%@", error, chunkFileURL);
}
}
_recentChunkReponseHeaders = _chunkFetcher.responseHeaders;
// To avoid retain cycles, remove all properties except the parent identifier.
_chunkFetcher.properties =
@{ kGTMSessionUploadFetcherChunkParentKey : [NSValue valueWithNonretainedObject:self] };
_chunkFetcher.retryBlock = nil;
_chunkFetcher.sendProgressBlock = nil;
_chunkFetcher = nil;
} // @synchronized(self)
}
// This method calculates the proper values to pass to the client's send progress block.
//
// The actual total bytes sent include the initial body sent, plus the
// offset into the batched data prior to the current chunk fetcher
- (void)invokeDelegateWithDidSendBytes:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpected {
GTMSessionCheckNotSynchronized(self);
// Ensure the chunk fetcher survives the callback in case the user pauses the upload process.
__block GTMSessionFetcher *holdFetcher = self.chunkFetcher;
[self invokeOnCallbackQueue:self.delegateCallbackQueue
afterUserStopped:NO
block:^{
GTMSessionFetcherSendProgressBlock sendProgressBlock = self.sendProgressBlock;
if (sendProgressBlock) {
sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpected);
}
holdFetcher = nil;
}];
}
- (void)retrieveUploadChunkGranularityFromResponseHeaders:(NSDictionary *)responseHeaders {
GTMSessionCheckNotSynchronized(self);
// Standard granularity for Google uploads is 256K.
NSString *chunkGranularityHeader =
[responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadChunkGranularity];
self.uploadGranularity = chunkGranularityHeader.longLongValue;
}
#pragma mark -
- (BOOL)isPaused {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _isPaused;
} // @synchronized(self)
}
- (void)pauseFetching {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_isPaused = YES;
} // @synchronized(self)
// Pausing just means stopping the current chunk from uploading;
// when we resume, we will send a query request to the server to
// figure out what bytes to resume sending.
//
// We won't try to cancel the initial data upload, but rather will check
// for being paused in beginChunkFetches.
[self destroyChunkFetcher];
}
- (void)resumeFetching {
BOOL wasPaused;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
wasPaused = _isPaused;
_isPaused = NO;
} // @synchronized(self)
if (wasPaused) {
[self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
}
}
- (void)stopFetching {
// Overrides the superclass
[self destroyChunkFetcher];
// If we think the server is waiting for more data, then tell it there won't be more.
if (self.uploadLocationURL) {
[self sendCancelUploadWithFetcherProperties:[self properties]];
self.uploadLocationURL = nil;
} else {
[self invokeOnCallbackQueue:self.callbackQueue
afterUserStopped:YES
block:^{
// Repeated calls to stopFetching may cause this path to be reached despite having sent a real
// cancel request, check here to ensure that the cancellation handler invocation which fires
// will definitely be for the real request sent previously.
@synchronized(self) {
if (self->_isCancelInFlight) {
return;
}
}
[self triggerCancellationHandlerForFetch:nil data:nil error:nil];
}];
}
[super stopFetching];
}
// Fires the cancellation handler, returning whether there was a handler to be fired.
- (BOOL)triggerCancellationHandlerForFetch:(GTMSessionFetcher *)fetcher
data:(NSData *)data
error:(NSError *)error {
GTMSessionUploadFetcherCancellationHandler handler = self.cancellationHandler;
if (handler) {
handler(fetcher, data, error);
self.cancellationHandler = nil;
return YES;
}
return NO;
}
#pragma mark -
- (int64_t)updateChunkFetcher:(GTMSessionFetcher *)chunkFetcher
forChunkAtOffset:(int64_t)offset {
BOOL isUploadingFileURL = (self.uploadFileURL != nil);
// Upload another chunk, meeting server-required granularity.
int64_t chunkSize = self.chunkSize;
int64_t fullUploadLength = [self fullUploadLength];
BOOL isFileLengthKnown = fullUploadLength >= 0;
BOOL isUploadingFullFile = (offset == 0 && isFileLengthKnown && chunkSize >= fullUploadLength);
if (!isUploadingFileURL || !isUploadingFullFile) {
// We're not uploading the entire file and given the file URL. Since we'll be
// allocating a subdata block for a chunk, we need to bound it to something that
// won't blow the process's memory.
if (chunkSize > kGTMSessionUploadFetcherMaximumDemandBufferSize) {
chunkSize = kGTMSessionUploadFetcherMaximumDemandBufferSize;
}
}
int64_t granularity = self.uploadGranularity;
if (granularity > 0) {
if (chunkSize < granularity) {
chunkSize = granularity;
} else {
chunkSize = chunkSize - (chunkSize % granularity);
}
}
GTMSESSION_ASSERT_DEBUG(offset < fullUploadLength || fullUploadLength == 0,
@"offset %lld exceeds data length %lld", offset, fullUploadLength);
if (granularity > 0) {
offset = offset - (offset % granularity);
}
// If the chunk size is bigger than the remaining data, or else
// it's close enough in size to the remaining data that we'd rather
// avoid having a whole extra http fetch for the leftover bit, then make
// this chunk size exactly match the remaining data size
NSString *command;
int64_t thisChunkSize = chunkSize;
BOOL isChunkTooBig = (thisChunkSize >= (fullUploadLength - offset));
BOOL isChunkAlmostBigEnough = (fullUploadLength - offset - 2500 < thisChunkSize);
BOOL isFinalChunk = (isChunkTooBig || isChunkAlmostBigEnough) && isFileLengthKnown;
if (isFinalChunk) {
thisChunkSize = fullUploadLength - offset;
if (thisChunkSize > 0) {
command = @"upload, finalize";
} else {
command = @"finalize";
}
} else {
command = @"upload";
}
NSString *lengthStr = @(thisChunkSize).stringValue;
NSString *offsetStr = @(offset).stringValue;
[chunkFetcher setRequestValue:command forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
[chunkFetcher setRequestValue:lengthStr forHTTPHeaderField:@"Content-Length"];
[chunkFetcher setRequestValue:offsetStr forHTTPHeaderField:kGTMSessionHeaderXGoogUploadOffset];
if (_uploadFileLength != kGTMSessionUploadFetcherUnknownFileSize) {
[chunkFetcher setRequestValue:@([self fullUploadLength]).stringValue
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
}
// Append the range of bytes in this chunk to the fetcher comment.
NSString *baseComment = self.comment;
[chunkFetcher setCommentWithFormat:@"%@ (%lld-%lld)",
baseComment ? baseComment : @"upload", offset, MAX(0, offset + thisChunkSize - 1)];
return thisChunkSize;
}
// Public properties.
@synthesize currentOffset = _currentOffset,
delegateCompletionHandler = _delegateCompletionHandler,
chunkFetcher = _chunkFetcher,
lastChunkRequest = _lastChunkRequest,
subdataGenerating = _subdataGenerating,
shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery,
uploadGranularity = _uploadGranularity;
// Internal properties.
@dynamic fetcherInFlight;
@dynamic activeFetcher;
@dynamic statusCode;
@dynamic delegateCallbackQueue;
+ (void)removePointer:(void *)pointer fromPointerArray:(NSPointerArray *)pointerArray {
for (NSUInteger index = 0, count = pointerArray.count; index < count; ++index) {
void *pointerAtIndex = [pointerArray pointerAtIndex:index];
if (pointerAtIndex == pointer) {
[pointerArray removePointerAtIndex:index];
return;
}
}
}
- (BOOL)useBackgroundSession {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _useBackgroundSessionOnChunkFetchers;
} // @synchronized(self
}
- (void)setUseBackgroundSession:(BOOL)useBackgroundSession {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_useBackgroundSessionOnChunkFetchers != useBackgroundSession) {
_useBackgroundSessionOnChunkFetchers = useBackgroundSession;
NSPointerArray *uploadFetcherPointerArrayForBackgroundSessions =
[[self class] uploadFetcherPointerArrayForBackgroundSessions];
if (_useBackgroundSessionOnChunkFetchers) {
[uploadFetcherPointerArrayForBackgroundSessions addPointer:(__bridge void *)self];
} else {
[[self class] removePointer:(__bridge void *)self
fromPointerArray:uploadFetcherPointerArrayForBackgroundSessions];
}
}
} // @synchronized(self
}
- (BOOL)canFetchWithBackgroundSession {
// The initial upload fetcher is always a foreground session; the
// useBackgroundSession property will apply only to chunk fetchers,
// not to queries.
return NO;
}
- (NSDictionary *)responseHeaders {
GTMSessionCheckNotSynchronized(self);
// Overrides the superclass
// If asked for the fetcher's response, use the most recent chunk fetcher's response,
// since the original request's response lacks useful information like the actual
// Content-Type.
NSDictionary *dict = self.chunkFetcher.responseHeaders;
if (dict) {
return dict;
}
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_recentChunkReponseHeaders) {
return _recentChunkReponseHeaders;
}
} // @synchronized(self
// No chunk fetcher yet completed, so return whatever we have from the initial fetch.
return [super responseHeaders];
}
- (NSInteger)statusCodeUnsynchronized {
GTMSessionCheckSynchronized(self);
if (_recentChunkStatusCode != -1) {
// Overrides the superclass to indicate status appropriate to the initial
// or latest chunk fetch
return _recentChunkStatusCode;
} else {
return [super statusCodeUnsynchronized];
}
}
- (void)setStatusCode:(NSInteger)val {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_recentChunkStatusCode = val;
}
}
- (int64_t)initialBodyLength {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _initialBodyLength;
}
}
- (void)setInitialBodyLength:(int64_t)length {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_initialBodyLength = length;
}
}
- (int64_t)initialBodySent {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _initialBodySent;
}
}
- (void)setInitialBodySent:(int64_t)length {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_initialBodySent = length;
}
}
- (NSURL *)uploadLocationURL {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadLocationURL;
}
}
- (void)setUploadLocationURL:(NSURL *)locationURL {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_uploadLocationURL = locationURL;
}
}
- (GTMSessionFetcher *)activeFetcher {
GTMSessionFetcher *result = self.fetcherInFlight;
if (result) return result;
return self;
}
- (BOOL)isFetching {
// If there is an active chunk fetcher, then the upload fetcher is considered
// to still be fetching.
if (self.fetcherInFlight != nil) return YES;
return [super isFetching];
}
- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
while (self.fetcherInFlight || self.subdataGenerating) {
if ([timeoutDate timeIntervalSinceNow] < 0) return NO;
if (self.subdataGenerating) {
// Allow time for subdata generation.
NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
[[NSRunLoop currentRunLoop] runUntilDate:stopDate];
} else {
// Wait for any chunk or query fetchers that still have pending callbacks or
// notifications.
BOOL timedOut;
if (self.fetcherInFlight == self) {
timedOut = ![super waitForCompletionWithTimeout:timeoutInSeconds];
} else {
timedOut = ![self.fetcherInFlight waitForCompletionWithTimeout:timeoutInSeconds];
}
if (timedOut) return NO;
}
}
return YES;
}
@end
@implementation GTMSessionFetcher (GTMSessionUploadFetcherMethods)
- (GTMSessionUploadFetcher *)parentUploadFetcher {
NSValue *property = [self propertyForKey:kGTMSessionUploadFetcherChunkParentKey];
if (!property) return nil;
GTMSessionUploadFetcher *uploadFetcher = property.nonretainedObjectValue;
GTMSESSION_ASSERT_DEBUG([uploadFetcher isKindOfClass:[GTMSessionUploadFetcher class]],
@"Unexpected parent upload fetcher class: %@", [uploadFetcher class]);
return uploadFetcher;
}
@end