/* 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 kGTMSessionIdentifierUploadAllowsCellularAccess = @"_upAllowsCellularAccess"; 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 kGTMSessionXGoogUploadProtocolResumable = @"resumable"; 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; @property(assign, atomic) BOOL allowsCellularAccess; @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 allowsCellularAccess:request.allowsCellularAccess]; return fetcher; } + (instancetype)uploadFetcherWithLocation:(NSURL *GTM_NULLABLE_TYPE)uploadLocationURL uploadMIMEType:(NSString *)uploadMIMEType chunkSize:(int64_t)chunkSize fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil { return [self uploadFetcherWithLocation:uploadLocationURL uploadMIMEType:uploadMIMEType chunkSize:chunkSize allowsCellularAccess:YES fetcherService:fetcherServiceOrNil]; } + (instancetype)uploadFetcherWithLocation:(NSURL *GTM_NULLABLE_TYPE)uploadLocationURL uploadMIMEType:(NSString *)uploadMIMEType chunkSize:(int64_t)chunkSize allowsCellularAccess:(BOOL)allowsCellularAccess fetcherService:(GTMSessionFetcherService *)fetcherService { GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil fetcherService:fetcherService]; [fetcher setLocationURL:uploadLocationURL uploadMIMEType:uploadMIMEType chunkSize:chunkSize allowsCellularAccess:allowsCellularAccess]; 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]; BOOL allowsCellularAccess = YES; if (metadata[kGTMSessionIdentifierUploadAllowsCellularAccess]) { allowsCellularAccess = [metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] boolValue]; } 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 allowsCellularAccess:allowsCellularAccess 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 { NSMutableSet *restoredSessionIdentifiers = [[NSMutableSet alloc] init]; NSMutableArray *uploadFetchers = [[NSMutableArray alloc] init]; NSPointerArray *uploadFetcherPointerArray = [self uploadFetcherPointerArrayForBackgroundSessions]; // Collect the background session upload fetchers that are still in memory. @synchronized(uploadFetcherPointerArray) { [uploadFetcherPointerArray compact]; for (GTMSessionUploadFetcher *uploadFetcher in uploadFetcherPointerArray) { NSString *sessionIdentifier = uploadFetcher.chunkFetcher.sessionIdentifier; if (sessionIdentifier) { [restoredSessionIdentifiers addObject:sessionIdentifier]; [uploadFetchers addObject:uploadFetcher]; } } } // @synchronized(uploadFetcherPointerArray) // 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; } } - (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:kGTMSessionXGoogUploadProtocolResumable 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 allowsCellularAccess:(BOOL)allowsCellularAccess { @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSESSION_ASSERT_DEBUG(chunkSize > 0, @"chunk size is zero"); _allowsCellularAccess = allowsCellularAccess; // 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); metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] = @(self.request.allowsCellularAccess); 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 // n.b. that self.request is nil for upload fetchers created with an existing upload location // URL. NSURLRequest *origRequest = self.request; chunkRequest.allowsCellularAccess = origRequest.allowsCellularAccess; if (!origRequest) { chunkRequest.allowsCellularAccess = _allowsCellularAccess; } NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"]; if (userAgent.length > 0) { [chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"]; } [chunkRequest setValue:kGTMSessionXGoogUploadProtocolResumable forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol]; // 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). NSString *previousContentLengthValue = [chunkFetcher.request valueForHTTPHeaderField:@"Content-Length"]; // The Content-Length header may not be present if the chunk fetcher was recreated from // a background session. BOOL hasKnownChunkSize = (previousContentLengthValue != nil); int64_t previousContentLength = [previousContentLengthValue longLongValue]; 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 || (!_uploadData && _uploadFileLength == 0) || (_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, allowsCellularAccess = _allowsCellularAccess, 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]; @synchronized(uploadFetcherPointerArrayForBackgroundSessions) { if (_useBackgroundSessionOnChunkFetchers) { [uploadFetcherPointerArrayForBackgroundSessions addPointer:(__bridge void *)self]; } else { [[self class] removePointer:(__bridge void *)self fromPointerArray:uploadFetcherPointerArrayForBackgroundSessions]; } } // @synchronized(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