123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001 |
- /* 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"
- #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";
- // 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, 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 {
- #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;
- 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:(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)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.
- // clang-format off
- @synthesize currentOffset = _currentOffset,
- allowsCellularAccess = _allowsCellularAccess,
- delegateCompletionHandler = _delegateCompletionHandler,
- chunkFetcher = _chunkFetcher,
- lastChunkRequest = _lastChunkRequest,
- subdataGenerating = _subdataGenerating,
- shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery,
- uploadGranularity = _uploadGranularity;
- // 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
|