12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171 |
- /* 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 "GTMSessionFetcher/GTMSessionUploadFetcher.h"
- #if TARGET_OS_OSX && GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
- // To reconnect background sessions on Mac outside +load requires importing and linking
- // AppKit to access the NSApplicationDidFinishLaunching symbol.
- #import <AppKit/AppKit.h>
- #endif
- 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";
- static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
- static const NSTimeInterval kDefaultMinUploadRetryInterval = 1.0;
- // 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";
- NSString *const kGTMSessionFetcherUploadInitialBackoffStartedNotification =
- @"kGTMSessionFetcherUploadInitialBackoffStartedNotification";
- #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, 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;
- double _uploadRetryFactor;
- NSTimeInterval _nextUploadRetryInterval;
- NSTimeInterval _maxUploadRetryInterval;
- NSTimeInterval _minUploadRetryInterval;
- BOOL _isPaused;
- BOOL _isRestartedUpload;
- BOOL _shouldInitiateOffsetQuery;
- NSTimer *_uploadRetryTimer;
- // 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;
- }
- - (NSTimeInterval)nextUploadRetryIntervalUnsynchronized {
- GTMSessionCheckSynchronized(self);
- // The next wait interval is the factor (2.0) times the last interval,
- // but never less than the minimum interval.
- NSTimeInterval secs = _nextUploadRetryInterval * _uploadRetryFactor;
- if (_maxUploadRetryInterval > 0) {
- secs = MIN(secs, _maxUploadRetryInterval);
- }
- secs = MAX(secs, _minUploadRetryInterval);
- return secs;
- }
- + (void)load {
- #if GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_IPHONE
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc addObserver:self
- selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
- name:UIApplicationDidFinishLaunchingNotification
- object:nil];
- #elif GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_OSX
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc addObserver:self
- selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
- name:NSApplicationDidFinishLaunchingNotification
- object:nil];
- #else
- [self uploadFetchersForBackgroundSessions];
- #endif
- }
- + (void)reconnectFetchersForBackgroundSessionsOnAppLaunch:(NSNotification *)notification {
- // Give all other app-did-launch handlers a chance to complete before
- // reconnecting the fetchers. Not doing this may lead to reconnecting
- // before the app delegate has a chance to run.
- dispatch_async(dispatch_get_main_queue(), ^{
- [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:(nullable NSURL *)uploadLocationURL
- uploadMIMEType:(NSString *)uploadMIMEType
- chunkSize:(int64_t)chunkSize
- fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil {
- return [self uploadFetcherWithLocation:uploadLocationURL
- uploadMIMEType:uploadMIMEType
- chunkSize:chunkSize
- allowsCellularAccess:YES
- fetcherService:fetcherServiceOrNil];
- }
- + (instancetype)uploadFetcherWithLocation:(nullable NSURL *)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;
- __unused int numberOfSources = hasData + hasFileHandle + hasFileURL + hasUploadDataProvider;
- 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:(nullable NSURL *)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:(nullable dispatch_queue_t)queue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _delegateCallbackQueue = queue;
- }
- }
- - (nullable dispatch_queue_t)delegateCallbackQueue {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _delegateCallbackQueue;
- }
- }
- - (BOOL)isRestartedUpload {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _isRestartedUpload;
- }
- }
- - (nullable GTMSessionFetcher *)chunkFetcher {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _chunkFetcher;
- }
- }
- - (void)setChunkFetcher:(nullable GTMSessionFetcher *)fetcher {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _chunkFetcher = fetcher;
- }
- }
- - (void)setFetcherInFlight:(nullable GTMSessionFetcher *)fetcher {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _fetcherInFlight = fetcher;
- }
- }
- - (nullable GTMSessionFetcher *)fetcherInFlight {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _fetcherInFlight;
- }
- }
- - (void)setCancellationHandler:
- (nullable GTMSessionUploadFetcherCancellationHandler)cancellationHandler {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _cancellationHandler = cancellationHandler;
- }
- }
- - (nullable GTMSessionUploadFetcherCancellationHandler)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)destroyUploadRetryTimer {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- [_uploadRetryTimer invalidate];
- _uploadRetryTimer = nil;
- }
- }
- - (void)beginFetchWithCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
- GTMSessionCheckNotSynchronized(self);
- [self setInitialBodyLength:[self bodyLength]];
- if (_minUploadRetryInterval <= 0.0) {
- _minUploadRetryInterval = kDefaultMinUploadRetryInterval;
- }
- if (_maxUploadRetryInterval <= 0.0) {
- _maxUploadRetryInterval = kDefaultMaxUploadRetryInterval;
- }
- if (_uploadRetryFactor <= 0.0) {
- _uploadRetryFactor = 2.0;
- }
- // 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;
- __unused NSString *str = [[NSString alloc] initWithData:downloadedData
- encoding:NSUTF8StringEncoding];
- 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 {
- dispatch_queue_t queue;
- GTMSessionFetcherCompletionHandler handler;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (shouldInvalidateLocation) {
- _uploadLocationURL = nil;
- }
- // Wait to dispatch the completion handler until after releasing callbacks. Because
- // action on the upload fetcher often takes place on a background queue, there can
- // be issues with CI tests failing due to load making the dispatched callback to
- // execute and the tests resume and assert callbacks have been released prior to that
- // actually occurring.
- //
- // However under normal operation this should also be a perfectly fine change.
- queue = _delegateCallbackQueue;
- handler = _delegateCompletionHandler;
- } // @synchronized(self)
- [self releaseUploadAndBaseCallbacks:!self.userStoppedFetching];
- if (queue && handler) {
- [self invokeOnCallbackQueue:queue
- afterUserStopped:NO
- block:^{
- handler(data, error);
- }];
- }
- }
- - (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);
- [self destroyUploadRetryTimer];
- // 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)beginUploadRetryTimer {
- if (![NSThread isMainThread]) {
- // Defer creating and starting the timer until we're on the main thread to ensure it has
- // a run loop.
- dispatch_async(dispatch_get_main_queue(), ^{
- [self beginUploadRetryTimer];
- });
- return;
- }
- [self destroyUploadRetryTimer];
- if (_nextUploadRetryInterval == 0.0) {
- [self.chunkFetcher beginFetchWithDelegate:self
- didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
- return;
- }
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- NSTimeInterval nextInterval = _nextUploadRetryInterval;
- NSTimeInterval maxInterval = _maxUploadRetryInterval;
- NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX));
- NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0;
- _nextUploadRetryInterval = newInterval;
- _uploadRetryTimer = [NSTimer timerWithTimeInterval:newInterval
- target:self
- selector:@selector(uploadRetryTimerFired:)
- userInfo:nil
- repeats:NO];
- _uploadRetryTimer.tolerance = newIntervalTolerance;
- [[NSRunLoop mainRunLoop] addTimer:_uploadRetryTimer forMode:NSDefaultRunLoopMode];
- } // @synchronized(self)
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc postNotificationName:kGTMSessionFetcherUploadInitialBackoffStartedNotification object:self];
- }
- - (void)uploadRetryTimerFired:(NSTimer *)timer {
- [self destroyUploadRetryTimer];
- NSOperationQueue *queue = self.sessionDelegateQueue;
- [queue addOperationWithBlock:^{
- [self.chunkFetcher beginFetchWithDelegate:self
- didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
- }];
- }
- - (NSTimer *)uploadRetryTimer {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _uploadRetryTimer;
- } // @synchronized(self)
- }
- - (NSTimeInterval)maxUploadRetryInterval {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _maxUploadRetryInterval;
- } // @synchronized(self)
- }
- - (void)setMaxUploadRetryInterval:(NSTimeInterval)secs {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (secs > 0) {
- _maxUploadRetryInterval = secs;
- } else {
- _maxUploadRetryInterval = kDefaultMaxUploadRetryInterval;
- }
- } // @synchronized(self)
- }
- - (void)setMinUploadRetryInterval:(NSTimeInterval)secs {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (secs > 0) {
- _minUploadRetryInterval = secs;
- } else {
- _minUploadRetryInterval = kDefaultMinUploadRetryInterval;
- }
- } // @synchronized(self)
- }
- - (NSTimeInterval)minUploadRetryInterval {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- return _minUploadRetryInterval;
- } // @synchronized(self)
- }
- - (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;
- if (_nextUploadRetryInterval < _maxUploadRetryInterval) {
- [self beginUploadRetryTimer];
- } else {
- NSError *responseError =
- [self uploadChunkUnavailableErrorWithDescription:@"Retry Limit Reached"];
- [self invokeFinalCallbackWithData:nil error:responseError shouldInvalidateLocation:NO];
- }
- }
- - (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.stopFetchingTriggersCompletionHandler = self.stopFetchingTriggersCompletionHandler;
- 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;
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- _nextUploadRetryInterval = self.nextUploadRetryIntervalUnsynchronized;
- }
- [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 {
- int64_t newOffset;
- // The chunk has uploaded successfully.
- NSString *uploadSizeReceived =
- [chunkFetcher.responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];
- if (uploadSizeReceived) {
- newOffset = [uploadSizeReceived longLongValue];
- } else {
- 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.
- __unused BOOL hasUploadAllData = (newOffset == [self fullUploadLength]);
- __unused BOOL isFinalStatus = (uploadStatus == kStatusFinal);
- 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);
- // The clang included with Xcode 13.3 betas added a -Wunused-but-set-variable warning,
- // which doesn't (yet) skip variables annotated with objc_precie_lifetime. Since that
- // warning is not available in all Xcodes, turn off the -Wunused warning group entirely.
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wunused"
- // Ensure the chunk fetcher survives the callback in case the user pauses the upload process.
- __block GTMSessionFetcher *holdFetcher = self.chunkFetcher;
- #pragma clang diagnostic pop
- [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 < fullUploadLength) {
- 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.
- // clang-format off
- @synthesize currentOffset = _currentOffset,
- allowsCellularAccess = _allowsCellularAccess,
- delegateCompletionHandler = _delegateCompletionHandler,
- chunkFetcher = _chunkFetcher,
- lastChunkRequest = _lastChunkRequest,
- subdataGenerating = _subdataGenerating,
- shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery,
- uploadGranularity = _uploadGranularity,
- uploadRetryFactor = _uploadRetryFactor;
- // clang-format on
- // 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];
- }
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-implementations"
- - (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;
- }
- #pragma clang diagnostic pop
- @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
|