blob: a7cea247aee58786dedeb31fd458c140f3ef5ed6 [file] [log] [blame]
/* Copyright (c) 2010 Google Inc.
*
* 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.
*/
//
// GTMHTTPUploadFetcher.m
//
#if (!GDATA_REQUIRE_SERVICE_INCLUDES) || GDATA_INCLUDE_DOCS_SERVICE || \
GDATA_INCLUDE_YOUTUBE_SERVICE || GDATA_INCLUDE_PHOTOS_SERVICE
#import "GTMHTTPUploadFetcher.h"
static NSUInteger const kQueryServerForOffset = NSUIntegerMax;
@interface GTMHTTPFetcher (ProtectedMethods)
@property (readwrite, retain) NSData *downloadedData;
- (void)releaseCallbacks;
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
@end
@interface GTMHTTPUploadFetcher ()
+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
fetcherService:(GTMHTTPFetcherService *)fetcherService;
- (void)setLocationURL:(NSURL *)location
uploadData:(NSData *)data
uploadFileHandle:(NSFileHandle *)fileHandle
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(NSUInteger)chunkSize;
- (void)uploadNextChunkWithOffset:(NSUInteger)offset;
- (void)uploadNextChunkWithOffset:(NSUInteger)offset
fetcherProperties:(NSMutableDictionary *)props;
- (void)destroyChunkFetcher;
- (void)handleResumeIncompleteStatusForChunkFetcher:(GTMHTTPFetcher *)chunkFetcher;
- (void)uploadFetcher:(GTMHTTPFetcher *)fetcher
didSendBytes:(NSInteger)bytesSent
totalBytesSent:(NSInteger)totalBytesSent
totalBytesExpectedToSend:(NSInteger)totalBytesExpected;
- (void)reportProgressManually;
- (NSUInteger)fullUploadLength;
-(BOOL)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher
willRetry:(BOOL)willRetry
forError:(NSError *)error;
- (void)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher
finishedWithData:(NSData *)data
error:(NSError *)error;
@end
@interface GTMHTTPUploadFetcher (PrivateMethods)
// private methods of the superclass
- (void)invokeSentDataCallback:(SEL)sel
target:(id)target
didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite;
- (void)invokeFetchCallback:(SEL)sel
target:(id)target
data:(NSData *)data
error:(NSError *)error;
- (BOOL)invokeRetryCallback:(SEL)sel
target:(id)target
willRetry:(BOOL)willRetry
error:(NSError *)error;
@end
@implementation GTMHTTPUploadFetcher
+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
uploadData:(NSData *)data
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(NSUInteger)chunkSize
fetcherService:(GTMHTTPFetcherService *)fetcherService {
GTMHTTPUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
fetcherService:fetcherService];
[fetcher setLocationURL:nil
uploadData:data
uploadFileHandle:nil
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize];
return fetcher;
}
+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
uploadFileHandle:(NSFileHandle *)fileHandle
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(NSUInteger)chunkSize
fetcherService:(GTMHTTPFetcherService *)fetcherService {
GTMHTTPUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
fetcherService:fetcherService];
[fetcher setLocationURL:nil
uploadData:nil
uploadFileHandle:fileHandle
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize];
return fetcher;
}
+ (GTMHTTPUploadFetcher *)uploadFetcherWithLocation:(NSURL *)locationURL
uploadFileHandle:(NSFileHandle *)fileHandle
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(NSUInteger)chunkSize
fetcherService:(GTMHTTPFetcherService *)fetcherService {
GTMHTTPUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil
fetcherService:fetcherService];
[fetcher setLocationURL:locationURL
uploadData:nil
uploadFileHandle:fileHandle
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize];
return fetcher;
}
+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
fetcherService:(GTMHTTPFetcherService *)fetcherService {
// Internal utility method for instantiating fetchers
GTMHTTPUploadFetcher *fetcher;
if (fetcherService) {
fetcher = [fetcherService fetcherWithRequest:request
fetcherClass:self];
} else {
fetcher = (GTMHTTPUploadFetcher *) [self fetcherWithRequest:request];
}
return fetcher;
}
- (void)setLocationURL:(NSURL *)location
uploadData:(NSData *)data
uploadFileHandle:(NSFileHandle *)fileHandle
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(NSUInteger)chunkSize {
#if DEBUG
NSAssert((data == nil) != (fileHandle == nil),
@"upload data and fileHandle are mutually exclusive");
NSAssert((self.mutableRequest == nil) != (location == nil),
@"request and location are mutually exclusive");
NSAssert(chunkSize > 0,@"chunk size is zero");
NSAssert(chunkSize != NSUIntegerMax, @"chunk size is sentinel value");
#endif
[self setLocationURL:location];
[self setUploadData:data];
[self setUploadFileHandle:fileHandle];
[self setUploadMIMEType:uploadMIMEType];
[self setChunkSize:chunkSize];
// indicate that we've not yet determined the file handle's length
uploadFileHandleLength_ = -1;
// indicate that we've not yet determined the upload fetcher status
statusCode_ = -1;
// if this is restarting an upload begun by another fetcher,
// the location is specified but the request is nil
isRestartedUpload_ = (location != nil);
// add our custom headers to the initial request indicating the data
// type and total size to be delivered later in the chunk requests
NSMutableURLRequest *mutableReq = [self mutableRequest];
NSNumber *lengthNum = [NSNumber numberWithUnsignedInteger:[self fullUploadLength]];
[mutableReq setValue:[lengthNum stringValue]
forHTTPHeaderField:@"X-Upload-Content-Length"];
[mutableReq setValue:uploadMIMEType
forHTTPHeaderField:@"X-Upload-Content-Type"];
}
- (void)dealloc {
[self releaseCallbacks];
[chunkFetcher_ release];
[locationURL_ release];
#if NS_BLOCKS_AVAILABLE
[locationChangeBlock_ release];
#endif
[uploadData_ release];
[uploadFileHandle_ release];
[uploadMIMEType_ release];
[responseHeaders_ release];
[super dealloc];
}
#pragma mark -
- (NSUInteger)fullUploadLength {
if (uploadData_) {
return [uploadData_ length];
} else {
if (uploadFileHandleLength_ == -1) {
// first time through, seek to end to determine file length
uploadFileHandleLength_ = (NSInteger) [uploadFileHandle_ seekToEndOfFile];
}
return (NSUInteger)uploadFileHandleLength_;
}
}
- (NSData *)uploadSubdataWithOffset:(NSUInteger)offset
length:(NSUInteger)length {
NSData *resultData = nil;
if (uploadData_) {
NSRange range = NSMakeRange(offset, length);
resultData = [uploadData_ subdataWithRange:range];
} else {
@try {
[uploadFileHandle_ seekToFileOffset:offset];
resultData = [uploadFileHandle_ readDataOfLength:length];
}
@catch (NSException *exception) {
NSLog(@"uploadFileHandle exception: %@", exception);
}
}
return resultData;
}
#pragma mark Method overrides affecting the initial fetch only
- (BOOL)beginFetchWithDelegate:(id)delegate
didFinishSelector:(SEL)finishedSEL {
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSEL,
@encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0);
// replace the finishedSEL with our own, since the initial finish callback
// is just the beginning of the upload experience
delegateFinishedSEL_ = finishedSEL;
// if the client is running early 10.5 or iPhone 2, we may need to manually
// send progress indication since NSURLConnection won't be calling back
// to us during uploads
needsManualProgress_ = ![GTMHTTPFetcher doesSupportSentDataCallback];
initialBodyLength_ = [[self postData] length];
if (isRestartedUpload_) {
if (![self isPaused]) {
if (delegate) {
[self setDelegate:delegate];
finishedSel_ = finishedSEL;
}
[self uploadNextChunkWithOffset:kQueryServerForOffset];
}
return YES;
}
// we don't need a finish selector since we're overriding
// -connectionDidFinishLoading
return [super beginFetchWithDelegate:delegate
didFinishSelector:NULL];
}
#if NS_BLOCKS_AVAILABLE
- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler {
// we don't want to call into the delegate'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
void (^holdBlock)(NSData *data, NSError *error) = [[handler copy] autorelease];
BOOL flag = [super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
// callback
if (!isRestartedUpload_) {
if (error == nil) {
// swap in the actual completion block now, as it will be called later
// when the upload chunks have completed
[completionBlock_ autorelease];
completionBlock_ = [holdBlock copy];
} else {
// pass the error on to the actual completion block
holdBlock(nil, error);
}
} 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.
holdBlock(data, error);
}
}];
return flag;
}
#endif
- (void)connection:(NSURLConnection *)connection
didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
// ignore this callback if we're doing manual progress, mainly so that
// we won't see duplicate progress callbacks when testing with
// doesSupportSentDataCallback turned off
if (needsManualProgress_) return;
[self uploadFetcher:self
didSendBytes:bytesWritten
totalBytesSent:totalBytesWritten
totalBytesExpectedToSend:totalBytesExpectedToWrite];
}
- (BOOL)shouldReleaseCallbacksUponCompletion {
// 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)invokeFinalCallbacksWithData:(NSData *)data
error:(NSError *)error
shouldInvalidateLocation:(BOOL)shouldInvalidateLocation {
// avoid issues due to being released indirectly by a callback
[[self retain] autorelease];
if (shouldInvalidateLocation) {
[self setLocationURL:nil];
}
if (delegate_ && delegateFinishedSEL_) {
[self invokeFetchCallback:delegateFinishedSEL_
target:delegate_
data:data
error:error];
}
#if NS_BLOCKS_AVAILABLE
if (completionBlock_) {
completionBlock_(data, error);
}
[self setLocationChangeBlock:nil];
#endif
[self releaseCallbacks];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// handle failure of the initial fetch as a simple fetcher failure, including
// calling the delegate, and allowing retry to happen if appropriate
SEL prevSel = finishedSel_; // should be null
finishedSel_ = delegateFinishedSEL_;
[super connection:connection didFailWithError:error];
// If retry later happens and succeeds, it shouldn't message the delegate
// since we'll continue to chunk uploads.
finishedSel_ = prevSel;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// we land here once the initial fetch sending the initial POST body
// has completed
// let the superclass end its connection
[super connectionDidFinishLoading:connection];
NSInteger statusCode = [super statusCode];
[self setStatusCode:statusCode];
NSData *downloadedData = [self downloadedData];
// we need to get the upload URL from the location header to continue
NSDictionary *responseHeaders = [self responseHeaders];
NSString *locationURLStr = [responseHeaders objectForKey:@"Location"];
NSError *error = nil;
if (statusCode >= 300) {
if (retryTimer_) return;
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
code:statusCode
userInfo:nil];
} else if ([downloadedData length] > 0) {
// The initial response of the resumable upload protocol should have an
// empty body
//
// This problem 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. It may also happen if an error JSON error body
// is returned.
//
// We'll consider this status 501 Not Implemented rather than try to parse
// the body to determine the actual error, but will provide the data
// as userInfo for clients to inspect.
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:downloadedData
forKey:kGTMHTTPFetcherStatusDataKey];
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
code:501
userInfo:userInfo];
} else {
#if DEBUG
NSAssert([locationURLStr length] > 0, @"need upload location hdr");
#endif
if ([locationURLStr length] == 0) {
// we cannot continue since we do not know the location to use
// as our upload destination
//
// we'll consider this status 501 Not Implemented
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
code:501
userInfo:nil];
}
}
if (error) {
[self invokeFinalCallbacksWithData:downloadedData
error:error
shouldInvalidateLocation:YES];
return;
}
[self setLocationURL:[NSURL URLWithString:locationURLStr]];
// we've now sent all of the initial post body data, so we need to include
// its size in future progress indicator callbacks
initialBodySent_ = initialBodyLength_;
if (needsManualProgress_) {
[self reportProgressManually];
}
// just in case the user paused us during the initial fetch...
if (![self isPaused]) {
[self uploadNextChunkWithOffset:0];
}
}
- (void)retryFetch {
// Override the fetcher's retryFetch to retry with the saved delegateFinishedSEL_.
[self stopFetchReleasingCallbacks:NO];
[self beginFetchWithDelegate:delegate_
didFinishSelector:delegateFinishedSEL_];
}
#pragma mark Chunk fetching methods
- (void)uploadNextChunkWithOffset:(NSUInteger)offset {
// use the properties in each chunk fetcher
NSMutableDictionary *props = [self properties];
[self uploadNextChunkWithOffset:offset
fetcherProperties:props];
}
- (void)uploadNextChunkWithOffset:(NSUInteger)offset
fetcherProperties:(NSMutableDictionary *)props {
// upload another chunk
NSUInteger chunkSize = [self chunkSize];
NSString *rangeStr, *lengthStr;
NSData *chunkData;
NSUInteger dataLen = [self fullUploadLength];
if (offset == kQueryServerForOffset) {
// resuming, so we'll initially send an empty data block and wait for the
// server to tell us where the current offset really is
chunkData = [NSData data];
rangeStr = [NSString stringWithFormat:@"bytes */%llu",
(unsigned long long)dataLen];
lengthStr = @"0";
offset = 0;
} else {
// uploading the next data chunk
if (dataLen == 0) {
#if DEBUG
NSAssert(offset == 0, @"offset %llu for empty data length", (unsigned long long)offset);
#endif
chunkData = [NSData data];
rangeStr = @"bytes */0";
lengthStr = @"0";
} else {
#if DEBUG
NSAssert(offset < dataLen , @"offset %llu exceeds data length %llu",
(unsigned long long)offset, (unsigned long long)dataLen);
#endif
NSUInteger thisChunkSize = chunkSize;
// 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
BOOL isChunkTooBig = (thisChunkSize + offset > dataLen);
BOOL isChunkAlmostBigEnough = (dataLen - offset < thisChunkSize + 2500);
if (isChunkTooBig || isChunkAlmostBigEnough) {
thisChunkSize = dataLen - offset;
}
chunkData = [self uploadSubdataWithOffset:offset
length:thisChunkSize];
rangeStr = [NSString stringWithFormat:@"bytes %llu-%llu/%llu",
(unsigned long long)offset,
(unsigned long long)(offset + thisChunkSize - 1),
(unsigned long long)dataLen];
lengthStr = [NSString stringWithFormat:@"%llu",
(unsigned long long)thisChunkSize];
}
}
// track the current offset for progress reporting
[self setCurrentOffset:offset];
//
// make the request for fetching
//
// the chunk upload URL requires no authentication header
NSURL *locURL = [self locationURL];
NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:locURL];
[chunkRequest setHTTPMethod:@"PUT"];
// copy the user-agent from the original connection
NSURLRequest *origRequest = [self mutableRequest];
NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"];
if ([userAgent length] > 0) {
[chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
[chunkRequest setValue:rangeStr forHTTPHeaderField:@"Content-Range"];
[chunkRequest setValue:lengthStr forHTTPHeaderField:@"Content-Length"];
NSString *uploadMIMEType = [self uploadMIMEType];
[chunkRequest setValue:uploadMIMEType forHTTPHeaderField:@"Content-Type"];
//
// make a new fetcher
//
GTMHTTPFetcher *chunkFetcher;
chunkFetcher = [GTMHTTPFetcher fetcherWithRequest:chunkRequest];
[chunkFetcher setDelegateQueue:[self delegateQueue]];
[chunkFetcher setRunLoopModes:[self runLoopModes]];
[chunkFetcher setAllowedInsecureSchemes:[self allowedInsecureSchemes]];
[chunkFetcher setAllowLocalhostRequest:[self allowLocalhostRequest]];
// if the upload fetcher has a comment, use the same comment for chunks
NSString *baseComment = [self comment];
if (baseComment) {
[chunkFetcher setCommentWithFormat:@"%@ (%@)", baseComment, rangeStr];
}
// give the chunk fetcher the same properties as the previous chunk fetcher
[chunkFetcher setProperties:props];
// post the appropriate subset of the full data
[chunkFetcher setPostData:chunkData];
// copy other fetcher settings to the new fetcher
[chunkFetcher setRetryEnabled:[self isRetryEnabled]];
[chunkFetcher setMaxRetryInterval:[self maxRetryInterval]];
[chunkFetcher setSentDataSelector:[self sentDataSelector]];
[chunkFetcher setCookieStorageMethod:[self cookieStorageMethod]];
if ([self isRetryEnabled]) {
// we interpose our own retry method both so the sender is the upload
// fetcher, and so we can change the request to ask the server to
// tell us where to resume the chunk
[chunkFetcher setRetrySelector:@selector(chunkFetcher:willRetry:forError:)];
}
[self setMutableRequest:chunkRequest];
// when fetching chunks, a 308 status means "upload more chunks", but
// success (200 or 201 status) and other failures are no different than
// for the regular object fetchers
BOOL didFetch = [chunkFetcher beginFetchWithDelegate:self
didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
if (!didFetch) {
// something went horribly wrong, like the chunk upload URL is invalid
NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
code:kGTMHTTPFetcherErrorChunkUploadFailed
userInfo:nil];
[self invokeFinalCallbacksWithData:nil
error:error
shouldInvalidateLocation:YES];
[self destroyChunkFetcher];
} else {
// hang on to the fetcher in case we need to cancel it
[self setChunkFetcher:chunkFetcher];
}
}
- (void)reportProgressManually {
// reportProgressManually should be called only when there's no
// NSURLConnection support for sent data callbacks
// the user wants upload progress, and there's no support in NSURLConnection
// for it, so we'll provide it here after each chunk
//
// the progress will be based on the uploadData and currentOffset,
// so we can pass zeros
[self uploadFetcher:self
didSendBytes:0
totalBytesSent:0
totalBytesExpectedToSend:0];
}
- (void)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher finishedWithData:(NSData *)data error:(NSError *)error {
[self setStatusCode:[chunkFetcher statusCode]];
[self setResponseHeaders:[chunkFetcher responseHeaders]];
if (error) {
int status = (int)[error code];
// status 308 is "resume incomplete", meaning we should get the offset
// from the Range header and upload the next chunk
//
// any other status really is an error
if (status == 308) {
[self handleResumeIncompleteStatusForChunkFetcher:chunkFetcher];
return;
} else {
// some unexpected status has occurred; handle it as we would a regular
// object fetcher failure
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
code:status
userInfo:nil];
[self invokeFinalCallbacksWithData:data
error:error
shouldInvalidateLocation:NO];
[self destroyChunkFetcher];
return;
}
} else {
// the final chunk has uploaded successfully
#if DEBUG && !defined(NS_BLOCK_ASSERTIONS)
NSInteger status = [chunkFetcher statusCode];
NSAssert1(status == 200 || status == 201,
@"unexpected chunks status %d", (int)status);
#endif // DEBUG && !defined(NS_BLOCK_ASSERTIONS)
// take the chunk fetcher's data as our own
self.downloadedData = data;
if (needsManualProgress_) {
// do a final upload progress report, indicating all of the chunk data
// has been sent
NSUInteger fullDataLength = [self fullUploadLength] + initialBodyLength_;
[self setCurrentOffset:fullDataLength];
[self reportProgressManually];
}
// we're done
[self invokeFinalCallbacksWithData:data
error:error
shouldInvalidateLocation:YES];
[self destroyChunkFetcher];
}
}
- (void)handleResumeIncompleteStatusForChunkFetcher:(GTMHTTPFetcher *)chunkFetcher {
NSDictionary *responseHeaders = [chunkFetcher responseHeaders];
// parse the Range header from the server, since that tells us where we really
// want the next chunk to begin.
//
// lack of a range header means the server has no bytes stored for this upload
NSString *rangeStr = [responseHeaders objectForKey:@"Range"];
NSUInteger newOffset = 0;
if (rangeStr != nil) {
// parse a content-range, like "bytes=0-999", to find where our new
// offset for uploading from the data really is (at the end of the
// range)
NSScanner *scanner = [NSScanner scannerWithString:rangeStr];
long long rangeStart = 0, rangeEnd = 0;
if ([scanner scanString:@"bytes=" intoString:nil]
&& [scanner scanLongLong:&rangeStart]
&& [scanner scanString:@"-" intoString:nil]
&& [scanner scanLongLong:&rangeEnd]) {
newOffset = (NSUInteger)rangeEnd + 1;
}
}
[self setCurrentOffset:newOffset];
if (needsManualProgress_) {
[self reportProgressManually];
}
// if the response specifies a location, use that for future chunks
NSString *locationURLStr = [responseHeaders objectForKey:@"Location"];
if ([locationURLStr length] > 0) {
[self setLocationURL:[NSURL URLWithString:locationURLStr]];
}
// we want to destroy this chunk fetcher before creating the next one, but
// we want to pass on its properties
NSMutableDictionary *props = [[[chunkFetcher properties] retain] autorelease];
// we no longer need to be able to cancel this chunkFetcher
[self destroyChunkFetcher];
// We may in the future handle Retry-After and ETag headers per
// http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal
// but they are not currently sent by the upload server
[self uploadNextChunkWithOffset:newOffset
fetcherProperties:props];
}
-(BOOL)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher willRetry:(BOOL)willRetry forError:(NSError *)error {
if ([error code] == 308
&& [[error domain] isEqual:kGTMHTTPFetcherStatusDomain]) {
// 308 is a normal chunk fethcher response, not an error
// that needs to be retried
return NO;
}
if (delegate_ && retrySel_) {
// call the client with the upload fetcher as the sender (not the chunk
// fetcher) to find out if it wants to retry
willRetry = [self invokeRetryCallback:retrySel_
target:delegate_
willRetry:willRetry
error:error];
}
#if NS_BLOCKS_AVAILABLE
if (retryBlock_) {
willRetry = retryBlock_(willRetry, error);
}
#endif
if (willRetry) {
// change the request being retried into a query to the server to
// tell us where to resume
NSMutableURLRequest *chunkRequest = [chunkFetcher mutableRequest];
NSUInteger dataLen = [self fullUploadLength];
NSString *rangeStr = [NSString stringWithFormat:@"bytes */%llu",
(unsigned long long)dataLen];
[chunkRequest setValue:rangeStr forHTTPHeaderField:@"Content-Range"];
[chunkRequest setValue:@"0" forHTTPHeaderField:@"Content-Length"];
[chunkFetcher setPostData:[NSData data]];
// we don't know what our actual offset is anymore, but the server
// will tell us
[self setCurrentOffset:0];
}
return willRetry;
}
- (void)destroyChunkFetcher {
[chunkFetcher_ stopFetching];
[chunkFetcher_ setProperties:nil];
[chunkFetcher_ autorelease];
chunkFetcher_ = nil;
}
// the chunk fetchers use this as their sentData method
- (void)uploadFetcher:(GTMHTTPFetcher *)chunkFetcher
didSendBytes:(NSInteger)bytesSent
totalBytesSent:(NSInteger)totalBytesSent
totalBytesExpectedToSend:(NSInteger)totalBytesExpected {
// the actual total bytes sent include the initial XML sent, plus the
// offset into the batched data prior to this fetcher
totalBytesSent += initialBodySent_ + currentOffset_;
// the total bytes expected include the initial XML and the full chunked
// data, independent of how big this fetcher's chunk is
totalBytesExpected = (NSInteger)(initialBodyLength_ + [self fullUploadLength]);
if (delegate_ && delegateSentDataSEL_) {
// ensure the chunk fetcher survives the callback in case the user pauses
// the upload process
[[chunkFetcher retain] autorelease];
[self invokeSentDataCallback:delegateSentDataSEL_
target:delegate_
didSendBodyData:bytesSent
totalBytesWritten:totalBytesSent
totalBytesExpectedToWrite:totalBytesExpected];
}
#if NS_BLOCKS_AVAILABLE
if (sentDataBlock_) {
sentDataBlock_(bytesSent, totalBytesSent, totalBytesExpected);
}
#endif
}
#pragma mark -
- (BOOL)isPaused {
return isPaused_;
}
- (void)pauseFetching {
isPaused_ = YES;
// pausing just means stopping the current chunk from uploading;
// when we resume, the magic offset value will force us to send
// a request to the server to figure out what bytes to start sending
//
// we won't try to cancel the initial data upload, but rather will look for
// the magic offset value in -connectionDidFinishLoading before
// creating first initial chunk fetcher, just in case the user
// paused during the initial data upload
[self destroyChunkFetcher];
}
- (void)resumeFetching {
if (isPaused_) {
isPaused_ = NO;
[self uploadNextChunkWithOffset:kQueryServerForOffset];
}
}
- (void)stopFetching {
// overrides the superclass
[self destroyChunkFetcher];
[super stopFetching];
}
#pragma mark -
@synthesize uploadData = uploadData_,
uploadFileHandle = uploadFileHandle_,
uploadMIMEType = uploadMIMEType_,
chunkSize = chunkSize_,
currentOffset = currentOffset_,
chunkFetcher = chunkFetcher_;
#if NS_BLOCKS_AVAILABLE
@synthesize locationChangeBlock = locationChangeBlock_;
#endif
@dynamic activeFetcher;
@dynamic responseHeaders;
@dynamic statusCode;
- (NSDictionary *)responseHeaders {
// overrides the superclass
// if asked for the fetcher's response, use the most recent fetcher
if (responseHeaders_) {
return responseHeaders_;
} else {
// no chunk fetcher yet completed, so return whatever we have from the
// initial fetch
return [super responseHeaders];
}
}
- (void)setResponseHeaders:(NSDictionary *)dict {
[responseHeaders_ autorelease];
responseHeaders_ = [dict retain];
}
- (NSInteger)statusCode {
if (statusCode_ != -1) {
// overrides the superclass to indicate status appropriate to the initial
// or latest chunk fetch
return statusCode_;
} else {
return [super statusCode];
}
}
- (void)setStatusCode:(NSInteger)val {
statusCode_ = val;
}
- (SEL)sentDataSelector {
// overrides the superclass
#if NS_BLOCKS_AVAILABLE
BOOL hasSentDataBlock = (sentDataBlock_ != NULL);
#else
BOOL hasSentDataBlock = NO;
#endif
if ((delegateSentDataSEL_ || hasSentDataBlock) && !needsManualProgress_) {
return @selector(uploadFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:);
} else {
return NULL;
}
}
- (void)setSentDataSelector:(SEL)theSelector {
// overrides the superclass
delegateSentDataSEL_ = theSelector;
}
- (GTMHTTPFetcher *)activeFetcher {
if (chunkFetcher_) {
return chunkFetcher_;
} else {
return self;
}
}
- (NSURL *)locationURL {
return locationURL_;
}
- (void)setLocationURL:(NSURL *)url {
if (url != locationURL_) {
[locationURL_ release];
locationURL_ = [url retain];
#if NS_BLOCKS_AVAILABLE
if (locationChangeBlock_) {
locationChangeBlock_(url);
}
#endif
}
}
@end
#endif // #if !GDATA_REQUIRE_SERVICE_INCLUDES