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