PageRenderTime 161ms CodeModel.GetById 15ms app.highlight 134ms RepoModel.GetById 1ms app.codeStats 1ms

/Source/externals/GData/Source/HTTPFetcher/GTMHTTPUploadFetcher.m

http://google-email-uploader-mac.googlecode.com/
Objective C | 945 lines | 608 code | 162 blank | 175 comment | 99 complexity | 268ca25f43bbe3fdc608ae4b0ab29b73 MD5 | raw file
  1/* Copyright (c) 2010 Google Inc.
  2 *
  3 * Licensed under the Apache License, Version 2.0 (the "License");
  4 * you may not use this file except in compliance with the License.
  5 * You may obtain a copy of the License at
  6 *
  7 *     http://www.apache.org/licenses/LICENSE-2.0
  8 *
  9 * Unless required by applicable law or agreed to in writing, software
 10 * distributed under the License is distributed on an "AS IS" BASIS,
 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 * See the License for the specific language governing permissions and
 13 * limitations under the License.
 14 */
 15
 16//
 17//  GTMHTTPUploadFetcher.m
 18//
 19
 20#if (!GDATA_REQUIRE_SERVICE_INCLUDES) || GDATA_INCLUDE_DOCS_SERVICE || \
 21  GDATA_INCLUDE_YOUTUBE_SERVICE || GDATA_INCLUDE_PHOTOS_SERVICE
 22
 23#import "GTMHTTPUploadFetcher.h"
 24
 25static NSUInteger const kQueryServerForOffset = NSUIntegerMax;
 26
 27@interface GTMHTTPFetcher (ProtectedMethods)
 28@property (readwrite, retain) NSData *downloadedData;
 29- (void)releaseCallbacks;
 30- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
 31- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
 32- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
 33@end
 34
 35@interface GTMHTTPUploadFetcher ()
 36+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
 37                                    fetcherService:(GTMHTTPFetcherService *)fetcherService;
 38- (void)setLocationURL:(NSURL *)location
 39            uploadData:(NSData *)data
 40      uploadFileHandle:(NSFileHandle *)fileHandle
 41        uploadMIMEType:(NSString *)uploadMIMEType
 42             chunkSize:(NSUInteger)chunkSize;
 43
 44- (void)uploadNextChunkWithOffset:(NSUInteger)offset;
 45- (void)uploadNextChunkWithOffset:(NSUInteger)offset
 46                fetcherProperties:(NSMutableDictionary *)props;
 47- (void)destroyChunkFetcher;
 48
 49- (void)handleResumeIncompleteStatusForChunkFetcher:(GTMHTTPFetcher *)chunkFetcher;
 50
 51- (void)uploadFetcher:(GTMHTTPFetcher *)fetcher
 52         didSendBytes:(NSInteger)bytesSent
 53       totalBytesSent:(NSInteger)totalBytesSent
 54totalBytesExpectedToSend:(NSInteger)totalBytesExpected;
 55
 56- (void)reportProgressManually;
 57
 58- (NSUInteger)fullUploadLength;
 59
 60-(BOOL)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher
 61          willRetry:(BOOL)willRetry
 62           forError:(NSError *)error;
 63
 64- (void)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher
 65    finishedWithData:(NSData *)data
 66               error:(NSError *)error;
 67@end
 68
 69@interface GTMHTTPUploadFetcher (PrivateMethods)
 70// private methods of the superclass
 71- (void)invokeSentDataCallback:(SEL)sel
 72                        target:(id)target
 73               didSendBodyData:(NSInteger)bytesWritten
 74             totalBytesWritten:(NSInteger)totalBytesWritten
 75     totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite;
 76
 77- (void)invokeFetchCallback:(SEL)sel
 78                     target:(id)target
 79                       data:(NSData *)data
 80                      error:(NSError *)error;
 81
 82- (BOOL)invokeRetryCallback:(SEL)sel
 83                     target:(id)target
 84                  willRetry:(BOOL)willRetry
 85                      error:(NSError *)error;
 86@end
 87
 88@implementation GTMHTTPUploadFetcher
 89
 90+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
 91                                        uploadData:(NSData *)data
 92                                    uploadMIMEType:(NSString *)uploadMIMEType
 93                                         chunkSize:(NSUInteger)chunkSize
 94                                    fetcherService:(GTMHTTPFetcherService *)fetcherService {
 95  GTMHTTPUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
 96                                                  fetcherService:fetcherService];
 97  [fetcher setLocationURL:nil
 98               uploadData:data
 99         uploadFileHandle:nil
100           uploadMIMEType:uploadMIMEType
101                chunkSize:chunkSize];
102  return fetcher;
103}
104
105+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
106                                  uploadFileHandle:(NSFileHandle *)fileHandle
107                                    uploadMIMEType:(NSString *)uploadMIMEType
108                                         chunkSize:(NSUInteger)chunkSize
109                                    fetcherService:(GTMHTTPFetcherService *)fetcherService {
110  GTMHTTPUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
111                                                  fetcherService:fetcherService];
112  [fetcher setLocationURL:nil
113               uploadData:nil
114         uploadFileHandle:fileHandle
115           uploadMIMEType:uploadMIMEType
116                chunkSize:chunkSize];
117  return fetcher;
118}
119
120+ (GTMHTTPUploadFetcher *)uploadFetcherWithLocation:(NSURL *)locationURL
121                                   uploadFileHandle:(NSFileHandle *)fileHandle
122                                     uploadMIMEType:(NSString *)uploadMIMEType
123                                          chunkSize:(NSUInteger)chunkSize
124                                     fetcherService:(GTMHTTPFetcherService *)fetcherService {
125  GTMHTTPUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil
126                                                  fetcherService:fetcherService];
127  [fetcher setLocationURL:locationURL
128               uploadData:nil
129         uploadFileHandle:fileHandle
130           uploadMIMEType:uploadMIMEType
131                chunkSize:chunkSize];
132  return fetcher;
133}
134
135+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
136                                    fetcherService:(GTMHTTPFetcherService *)fetcherService {
137  // Internal utility method for instantiating fetchers
138  GTMHTTPUploadFetcher *fetcher;
139  if (fetcherService) {
140    fetcher = [fetcherService fetcherWithRequest:request
141                                    fetcherClass:self];
142  } else {
143    fetcher = (GTMHTTPUploadFetcher *) [self fetcherWithRequest:request];
144  }
145  return fetcher;
146}
147
148- (void)setLocationURL:(NSURL *)location
149            uploadData:(NSData *)data
150      uploadFileHandle:(NSFileHandle *)fileHandle
151        uploadMIMEType:(NSString *)uploadMIMEType
152             chunkSize:(NSUInteger)chunkSize {
153#if DEBUG
154  NSAssert((data == nil) != (fileHandle == nil),
155           @"upload data and fileHandle are mutually exclusive");
156  NSAssert((self.mutableRequest == nil) != (location == nil),
157           @"request and location are mutually exclusive");
158  NSAssert(chunkSize > 0,@"chunk size is zero");
159  NSAssert(chunkSize != NSUIntegerMax, @"chunk size is sentinel value");
160#endif
161  [self setLocationURL:location];
162  [self setUploadData:data];
163  [self setUploadFileHandle:fileHandle];
164  [self setUploadMIMEType:uploadMIMEType];
165  [self setChunkSize:chunkSize];
166
167  // indicate that we've not yet determined the file handle's length
168  uploadFileHandleLength_ = -1;
169
170  // indicate that we've not yet determined the upload fetcher status
171  statusCode_ = -1;
172
173  // if this is restarting an upload begun by another fetcher,
174  // the location is specified but the request is nil
175  isRestartedUpload_ = (location != nil);
176
177  // add our custom headers to the initial request indicating the data
178  // type and total size to be delivered later in the chunk requests
179  NSMutableURLRequest *mutableReq = [self mutableRequest];
180
181  NSNumber *lengthNum = [NSNumber numberWithUnsignedInteger:[self fullUploadLength]];
182  [mutableReq setValue:[lengthNum stringValue]
183    forHTTPHeaderField:@"X-Upload-Content-Length"];
184
185  [mutableReq setValue:uploadMIMEType
186    forHTTPHeaderField:@"X-Upload-Content-Type"];
187}
188
189- (void)dealloc {
190  [self releaseCallbacks];
191
192  [chunkFetcher_ release];
193  [locationURL_ release];
194#if NS_BLOCKS_AVAILABLE
195  [locationChangeBlock_ release];
196#endif
197  [uploadData_ release];
198  [uploadFileHandle_ release];
199  [uploadMIMEType_ release];
200  [responseHeaders_ release];
201  [super dealloc];
202}
203
204#pragma mark -
205
206- (NSUInteger)fullUploadLength {
207  if (uploadData_) {
208    return [uploadData_ length];
209  } else {
210    if (uploadFileHandleLength_ == -1) {
211      // first time through, seek to end to determine file length
212      uploadFileHandleLength_ = (NSInteger) [uploadFileHandle_ seekToEndOfFile];
213    }
214    return (NSUInteger)uploadFileHandleLength_;
215  }
216}
217
218- (NSData *)uploadSubdataWithOffset:(NSUInteger)offset
219                             length:(NSUInteger)length {
220  NSData *resultData = nil;
221
222  if (uploadData_) {
223    NSRange range = NSMakeRange(offset, length);
224    resultData = [uploadData_ subdataWithRange:range];
225  } else {
226    @try {
227      [uploadFileHandle_ seekToFileOffset:offset];
228      resultData = [uploadFileHandle_ readDataOfLength:length];
229    }
230    @catch (NSException *exception) {
231      NSLog(@"uploadFileHandle exception: %@", exception);
232    }
233  }
234
235  return resultData;
236}
237
238#pragma mark Method overrides affecting the initial fetch only
239
240- (BOOL)beginFetchWithDelegate:(id)delegate
241             didFinishSelector:(SEL)finishedSEL {
242
243  GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSEL,
244        @encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0);
245
246  // replace the finishedSEL with our own, since the initial finish callback
247  // is just the beginning of the upload experience
248  delegateFinishedSEL_ = finishedSEL;
249
250  // if the client is running early 10.5 or iPhone 2, we may need to manually
251  // send progress indication since NSURLConnection won't be calling back
252  // to us during uploads
253  needsManualProgress_ = ![GTMHTTPFetcher doesSupportSentDataCallback];
254
255  initialBodyLength_ = [[self postData] length];
256
257  if (isRestartedUpload_) {
258    if (![self isPaused]) {
259      if (delegate) {
260        [self setDelegate:delegate];
261        finishedSel_ = finishedSEL;
262      }
263      [self uploadNextChunkWithOffset:kQueryServerForOffset];
264    }
265    return YES;
266  }
267
268  // we don't need a finish selector since we're overriding
269  // -connectionDidFinishLoading
270  return [super beginFetchWithDelegate:delegate
271                     didFinishSelector:NULL];
272}
273
274#if NS_BLOCKS_AVAILABLE
275- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler {
276  // we don't want to call into the delegate's completion block immediately
277  // after the finish of the initial connection (the delegate is called only
278  // when uploading finishes), so we substitute our own completion block to be
279  // called when the initial connection finishes
280  void (^holdBlock)(NSData *data, NSError *error) = [[handler copy] autorelease];
281
282  BOOL flag = [super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
283    // callback
284    if (!isRestartedUpload_) {
285      if (error == nil) {
286        // swap in the actual completion block now, as it will be called later
287        // when the upload chunks have completed
288        [completionBlock_ autorelease];
289        completionBlock_ = [holdBlock copy];
290      } else {
291        // pass the error on to the actual completion block
292        holdBlock(nil, error);
293      }
294    } else {
295      // If there was no initial request, then this fetch is resuming some
296      // other uploadFetcher's initial request, and the superclass's connection
297      // is never used, so at this point we call the user's actual completion
298      // block.
299      holdBlock(data, error);
300    }
301  }];
302  return flag;
303}
304#endif
305
306- (void)connection:(NSURLConnection *)connection
307   didSendBodyData:(NSInteger)bytesWritten
308 totalBytesWritten:(NSInteger)totalBytesWritten
309totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
310
311  // ignore this callback if we're doing manual progress, mainly so that
312  // we won't see duplicate progress callbacks when testing with
313  // doesSupportSentDataCallback turned off
314  if (needsManualProgress_) return;
315
316  [self uploadFetcher:self
317         didSendBytes:bytesWritten
318       totalBytesSent:totalBytesWritten
319totalBytesExpectedToSend:totalBytesExpectedToWrite];
320}
321
322- (BOOL)shouldReleaseCallbacksUponCompletion {
323  // we don't want the superclass to release the delegate and callback
324  // blocks once the initial fetch has finished
325  //
326  // this is invoked for only successful completion of the connection;
327  // an error always will invoke and release the callbacks
328  return NO;
329}
330
331- (void)invokeFinalCallbacksWithData:(NSData *)data
332                               error:(NSError *)error
333            shouldInvalidateLocation:(BOOL)shouldInvalidateLocation {
334  // avoid issues due to being released indirectly by a callback
335  [[self retain] autorelease];
336
337  if (shouldInvalidateLocation) {
338    [self setLocationURL:nil];
339  }
340
341  if (delegate_ && delegateFinishedSEL_) {
342    [self invokeFetchCallback:delegateFinishedSEL_
343                       target:delegate_
344                         data:data
345                        error:error];
346  }
347
348#if NS_BLOCKS_AVAILABLE
349  if (completionBlock_) {
350    completionBlock_(data, error);
351  }
352
353  [self setLocationChangeBlock:nil];
354#endif
355
356  [self releaseCallbacks];
357}
358
359- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
360  // handle failure of the initial fetch as a simple fetcher failure, including
361  // calling the delegate, and allowing retry to happen if appropriate
362  SEL prevSel = finishedSel_;  // should be null
363  finishedSel_ = delegateFinishedSEL_;
364  [super connection:connection didFailWithError:error];
365
366  // If retry later happens and succeeds, it shouldn't message the delegate
367  // since we'll continue to chunk uploads.
368  finishedSel_ = prevSel;
369}
370
371- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
372
373  // we land here once the initial fetch sending the initial POST body
374  // has completed
375
376  // let the superclass end its connection
377  [super connectionDidFinishLoading:connection];
378
379  NSInteger statusCode = [super statusCode];
380  [self setStatusCode:statusCode];
381
382  NSData *downloadedData = [self downloadedData];
383
384  // we need to get the upload URL from the location header to continue
385  NSDictionary *responseHeaders = [self responseHeaders];
386  NSString *locationURLStr = [responseHeaders objectForKey:@"Location"];
387
388  NSError *error = nil;
389
390  if (statusCode >= 300) {
391    if (retryTimer_) return;
392
393    error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
394                                code:statusCode
395                            userInfo:nil];
396  } else if ([downloadedData length] > 0) {
397    // The initial response of the resumable upload protocol should have an
398    // empty body
399    //
400    // This problem typically happens because the upload create/edit link URL was
401    // not supplied with the request, and the server is thus expecting a non-
402    // resumable request/response. It may also happen if an error JSON error body
403    // is returned.
404    //
405    // We'll consider this status 501 Not Implemented rather than try to parse
406    // the body to determine the actual error, but will provide the data
407    // as userInfo for clients to inspect.
408    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:downloadedData
409                                                         forKey:kGTMHTTPFetcherStatusDataKey];
410    error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
411                                code:501
412                            userInfo:userInfo];
413  } else {
414#if DEBUG
415    NSAssert([locationURLStr length] > 0, @"need upload location hdr");
416#endif
417
418    if ([locationURLStr length] == 0) {
419      // we cannot continue since we do not know the location to use
420      // as our upload destination
421      //
422      // we'll consider this status 501 Not Implemented
423      error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
424                                  code:501
425                              userInfo:nil];
426    }
427  }
428
429  if (error) {
430    [self invokeFinalCallbacksWithData:downloadedData
431                                 error:error
432              shouldInvalidateLocation:YES];
433    return;
434  }
435
436  [self setLocationURL:[NSURL URLWithString:locationURLStr]];
437
438  // we've now sent all of the initial post body data, so we need to include
439  // its size in future progress indicator callbacks
440  initialBodySent_ = initialBodyLength_;
441
442  if (needsManualProgress_) {
443    [self reportProgressManually];
444  }
445
446  // just in case the user paused us during the initial fetch...
447  if (![self isPaused]) {
448    [self uploadNextChunkWithOffset:0];
449  }
450}
451
452- (void)retryFetch {
453  // Override the fetcher's retryFetch to retry with the saved delegateFinishedSEL_.
454  [self stopFetchReleasingCallbacks:NO];
455
456  [self beginFetchWithDelegate:delegate_
457             didFinishSelector:delegateFinishedSEL_];
458}
459
460#pragma mark Chunk fetching methods
461
462- (void)uploadNextChunkWithOffset:(NSUInteger)offset {
463  // use the properties in each chunk fetcher
464  NSMutableDictionary *props = [self properties];
465
466  [self uploadNextChunkWithOffset:offset
467                fetcherProperties:props];
468}
469
470- (void)uploadNextChunkWithOffset:(NSUInteger)offset
471                fetcherProperties:(NSMutableDictionary *)props {
472  // upload another chunk
473  NSUInteger chunkSize = [self chunkSize];
474
475  NSString *rangeStr, *lengthStr;
476  NSData *chunkData;
477
478  NSUInteger dataLen = [self fullUploadLength];
479
480  if (offset == kQueryServerForOffset) {
481    // resuming, so we'll initially send an empty data block and wait for the
482    // server to tell us where the current offset really is
483    chunkData = [NSData data];
484    rangeStr = [NSString stringWithFormat:@"bytes */%llu",
485                (unsigned long long)dataLen];
486    lengthStr = @"0";
487    offset = 0;
488  } else {
489    // uploading the next data chunk
490    if (dataLen == 0) {
491#if DEBUG
492      NSAssert(offset == 0, @"offset %llu for empty data length", (unsigned long long)offset);
493#endif
494      chunkData = [NSData data];
495      rangeStr = @"bytes */0";
496      lengthStr = @"0";
497    } else {
498#if DEBUG
499      NSAssert(offset < dataLen , @"offset %llu exceeds data length %llu",
500               (unsigned long long)offset, (unsigned long long)dataLen);
501#endif
502      NSUInteger thisChunkSize = chunkSize;
503
504      // if the chunk size is bigger than the remaining data, or else
505      // it's close enough in size to the remaining data that we'd rather
506      // avoid having a whole extra http fetch for the leftover bit, then make
507      // this chunk size exactly match the remaining data size
508      BOOL isChunkTooBig = (thisChunkSize + offset > dataLen);
509      BOOL isChunkAlmostBigEnough = (dataLen - offset < thisChunkSize + 2500);
510
511      if (isChunkTooBig || isChunkAlmostBigEnough) {
512        thisChunkSize = dataLen - offset;
513      }
514
515      chunkData = [self uploadSubdataWithOffset:offset
516                                         length:thisChunkSize];
517
518      rangeStr = [NSString stringWithFormat:@"bytes %llu-%llu/%llu",
519                  (unsigned long long)offset,
520                  (unsigned long long)(offset + thisChunkSize - 1),
521                  (unsigned long long)dataLen];
522      lengthStr = [NSString stringWithFormat:@"%llu",
523                   (unsigned long long)thisChunkSize];
524    }
525  }
526
527  // track the current offset for progress reporting
528  [self setCurrentOffset:offset];
529
530  //
531  // make the request for fetching
532  //
533
534  // the chunk upload URL requires no authentication header
535  NSURL *locURL = [self locationURL];
536  NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:locURL];
537
538  [chunkRequest setHTTPMethod:@"PUT"];
539
540  // copy the user-agent from the original connection
541  NSURLRequest *origRequest = [self mutableRequest];
542  NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"];
543  if ([userAgent length] > 0) {
544    [chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
545  }
546
547  [chunkRequest setValue:rangeStr forHTTPHeaderField:@"Content-Range"];
548  [chunkRequest setValue:lengthStr forHTTPHeaderField:@"Content-Length"];
549
550  NSString *uploadMIMEType = [self uploadMIMEType];
551  [chunkRequest setValue:uploadMIMEType forHTTPHeaderField:@"Content-Type"];
552
553  //
554  // make a new fetcher
555  //
556  GTMHTTPFetcher *chunkFetcher;
557
558  chunkFetcher = [GTMHTTPFetcher fetcherWithRequest:chunkRequest];
559  [chunkFetcher setDelegateQueue:[self delegateQueue]];
560  [chunkFetcher setRunLoopModes:[self runLoopModes]];
561
562  // if the upload fetcher has a comment, use the same comment for chunks
563  NSString *baseComment = [self comment];
564  if (baseComment) {
565    [chunkFetcher setCommentWithFormat:@"%@ (%@)", baseComment, rangeStr];
566  }
567
568  // give the chunk fetcher the same properties as the previous chunk fetcher
569  [chunkFetcher setProperties:props];
570
571  // post the appropriate subset of the full data
572  [chunkFetcher setPostData:chunkData];
573
574  // copy other fetcher settings to the new fetcher
575  [chunkFetcher setRetryEnabled:[self isRetryEnabled]];
576  [chunkFetcher setMaxRetryInterval:[self maxRetryInterval]];
577  [chunkFetcher setSentDataSelector:[self sentDataSelector]];
578  [chunkFetcher setCookieStorageMethod:[self cookieStorageMethod]];
579
580  if ([self isRetryEnabled]) {
581    // we interpose our own retry method both so the sender is the upload
582    // fetcher, and so we can change the request to ask the server to
583    // tell us where to resume the chunk
584    [chunkFetcher setRetrySelector:@selector(chunkFetcher:willRetry:forError:)];
585  }
586
587  [self setMutableRequest:chunkRequest];
588
589  // when fetching chunks, a 308 status means "upload more chunks", but
590  // success (200 or 201 status) and other failures are no different than
591  // for the regular object fetchers
592  BOOL didFetch = [chunkFetcher beginFetchWithDelegate:self
593                                     didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
594  if (!didFetch) {
595    // something went horribly wrong, like the chunk upload URL is invalid
596    NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
597                                         code:kGTMHTTPFetcherErrorChunkUploadFailed
598                                     userInfo:nil];
599
600    [self invokeFinalCallbacksWithData:nil
601                                 error:error
602              shouldInvalidateLocation:YES];
603    [self destroyChunkFetcher];
604  } else {
605    // hang on to the fetcher in case we need to cancel it
606    [self setChunkFetcher:chunkFetcher];
607  }
608}
609
610- (void)reportProgressManually {
611  // reportProgressManually should be called only when there's no
612  // NSURLConnection support for sent data callbacks
613
614  // the user wants upload progress, and there's no support in NSURLConnection
615  // for it, so we'll provide it here after each chunk
616  //
617  // the progress will be based on the uploadData and currentOffset,
618  // so we can pass zeros
619  [self uploadFetcher:self
620         didSendBytes:0
621       totalBytesSent:0
622totalBytesExpectedToSend:0];
623}
624
625- (void)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher finishedWithData:(NSData *)data error:(NSError *)error {
626  [self setStatusCode:[chunkFetcher statusCode]];
627  [self setResponseHeaders:[chunkFetcher responseHeaders]];
628
629  if (error) {
630    int status = (int)[error code];
631
632    // status 308 is "resume incomplete", meaning we should get the offset
633    // from the Range header and upload the next chunk
634    //
635    // any other status really is an error
636    if (status == 308) {
637      [self handleResumeIncompleteStatusForChunkFetcher:chunkFetcher];
638      return;
639    } else {
640      // some unexpected status has occurred; handle it as we would a regular
641      // object fetcher failure
642      error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
643                                  code:status
644                              userInfo:nil];
645      [self invokeFinalCallbacksWithData:data
646                                   error:error
647                shouldInvalidateLocation:NO];
648      [self destroyChunkFetcher];
649      return;
650    }
651  } else {
652    // the final chunk has uploaded successfully
653  #if DEBUG
654    NSInteger status = [chunkFetcher statusCode];
655    NSAssert1(status == 200 || status == 201,
656              @"unexpected chunks status %d", (int)status);
657  #endif
658
659    // take the chunk fetcher's data as our own
660    self.downloadedData = data;
661
662    if (needsManualProgress_) {
663      // do a final upload progress report, indicating all of the chunk data
664      // has been sent
665      NSUInteger fullDataLength = [self fullUploadLength] + initialBodyLength_;
666      [self setCurrentOffset:fullDataLength];
667
668      [self reportProgressManually];
669    }
670
671    // we're done
672    [self invokeFinalCallbacksWithData:data
673                                 error:error
674              shouldInvalidateLocation:YES];
675
676    [self destroyChunkFetcher];
677  }
678}
679
680- (void)handleResumeIncompleteStatusForChunkFetcher:(GTMHTTPFetcher *)chunkFetcher {
681
682  NSDictionary *responseHeaders = [chunkFetcher responseHeaders];
683
684  // parse the Range header from the server, since that tells us where we really
685  // want the next chunk to begin.
686  //
687  // lack of a range header means the server has no bytes stored for this upload
688  NSString *rangeStr = [responseHeaders objectForKey:@"Range"];
689  NSUInteger newOffset = 0;
690  if (rangeStr != nil) {
691    // parse a content-range, like "bytes=0-999", to find where our new
692    // offset for uploading from the data really is (at the end of the
693    // range)
694    NSScanner *scanner = [NSScanner scannerWithString:rangeStr];
695    long long rangeStart = 0, rangeEnd = 0;
696    if ([scanner scanString:@"bytes=" intoString:nil]
697        && [scanner scanLongLong:&rangeStart]
698        && [scanner scanString:@"-" intoString:nil]
699        && [scanner scanLongLong:&rangeEnd]) {
700      newOffset = (NSUInteger)rangeEnd + 1;
701    }
702  }
703
704  [self setCurrentOffset:newOffset];
705
706  if (needsManualProgress_) {
707    [self reportProgressManually];
708  }
709
710  // if the response specifies a location, use that for future chunks
711  NSString *locationURLStr = [responseHeaders objectForKey:@"Location"];
712  if ([locationURLStr length] > 0) {
713    [self setLocationURL:[NSURL URLWithString:locationURLStr]];
714  }
715
716  // we want to destroy this chunk fetcher before creating the next one, but
717  // we want to pass on its properties
718  NSMutableDictionary *props = [[[chunkFetcher properties] retain] autorelease];
719
720  // we no longer need to be able to cancel this chunkFetcher
721  [self destroyChunkFetcher];
722
723  // We may in the future handle Retry-After and ETag headers per
724  // http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal
725  // but they are not currently sent by the upload server
726
727  [self uploadNextChunkWithOffset:newOffset
728                fetcherProperties:props];
729}
730
731
732-(BOOL)chunkFetcher:(GTMHTTPFetcher *)chunkFetcher willRetry:(BOOL)willRetry forError:(NSError *)error {
733  if ([error code] == 308
734      && [[error domain] isEqual:kGTMHTTPFetcherStatusDomain]) {
735    // 308 is a normal chunk fethcher response, not an error
736    // that needs to be retried
737    return NO;
738  }
739
740  if (delegate_ && retrySel_) {
741
742    // call the client with the upload fetcher as the sender (not the chunk
743    // fetcher) to find out if it wants to retry
744    willRetry = [self invokeRetryCallback:retrySel_
745                                   target:delegate_
746                                willRetry:willRetry
747                                    error:error];
748  }
749
750#if NS_BLOCKS_AVAILABLE
751  if (retryBlock_) {
752    willRetry = retryBlock_(willRetry, error);
753  }
754#endif
755
756  if (willRetry) {
757    // change the request being retried into a query to the server to
758    // tell us where to resume
759    NSMutableURLRequest *chunkRequest = [chunkFetcher mutableRequest];
760
761    NSUInteger dataLen = [self fullUploadLength];
762    NSString *rangeStr = [NSString stringWithFormat:@"bytes */%llu",
763                          (unsigned long long)dataLen];
764
765    [chunkRequest setValue:rangeStr forHTTPHeaderField:@"Content-Range"];
766    [chunkRequest setValue:@"0" forHTTPHeaderField:@"Content-Length"];
767    [chunkFetcher setPostData:[NSData data]];
768
769    // we don't know what our actual offset is anymore, but the server
770    // will tell us
771    [self setCurrentOffset:0];
772  }
773
774  return willRetry;
775}
776
777- (void)destroyChunkFetcher {
778  [chunkFetcher_ stopFetching];
779  [chunkFetcher_ setProperties:nil];
780  [chunkFetcher_ autorelease];
781  chunkFetcher_ = nil;
782}
783
784// the chunk fetchers use this as their sentData method
785- (void)uploadFetcher:(GTMHTTPFetcher *)chunkFetcher
786         didSendBytes:(NSInteger)bytesSent
787       totalBytesSent:(NSInteger)totalBytesSent
788totalBytesExpectedToSend:(NSInteger)totalBytesExpected {
789  // the actual total bytes sent include the initial XML sent, plus the
790  // offset into the batched data prior to this fetcher
791  totalBytesSent += initialBodySent_ + currentOffset_;
792
793  // the total bytes expected include the initial XML and the full chunked
794  // data, independent of how big this fetcher's chunk is
795  totalBytesExpected = (NSInteger)(initialBodyLength_ + [self fullUploadLength]);
796
797  if (delegate_ && delegateSentDataSEL_) {
798    // ensure the chunk fetcher survives the callback in case the user pauses
799    // the upload process
800    [[chunkFetcher retain] autorelease];
801
802    [self invokeSentDataCallback:delegateSentDataSEL_
803                          target:delegate_
804                 didSendBodyData:bytesSent
805               totalBytesWritten:totalBytesSent
806       totalBytesExpectedToWrite:totalBytesExpected];
807  }
808
809#if NS_BLOCKS_AVAILABLE
810  if (sentDataBlock_) {
811    sentDataBlock_(bytesSent, totalBytesSent, totalBytesExpected);
812  }
813#endif
814}
815
816#pragma mark -
817
818- (BOOL)isPaused {
819  return isPaused_;
820}
821
822- (void)pauseFetching {
823  isPaused_ = YES;
824
825  // pausing just means stopping the current chunk from uploading;
826  // when we resume, the magic offset value will force us to send
827  // a request to the server to figure out what bytes to start sending
828  //
829  // we won't try to cancel the initial data upload, but rather will look for
830  // the magic offset value in -connectionDidFinishLoading before
831  // creating first initial chunk fetcher, just in case the user
832  // paused during the initial data upload
833  [self destroyChunkFetcher];
834}
835
836- (void)resumeFetching {
837  if (isPaused_) {
838    isPaused_ = NO;
839
840    [self uploadNextChunkWithOffset:kQueryServerForOffset];
841  }
842}
843
844- (void)stopFetching {
845  // overrides the superclass
846  [self destroyChunkFetcher];
847
848  [super stopFetching];
849}
850
851#pragma mark -
852
853@synthesize uploadData = uploadData_,
854            uploadFileHandle = uploadFileHandle_,
855            uploadMIMEType = uploadMIMEType_,
856            chunkSize = chunkSize_,
857            currentOffset = currentOffset_,
858            chunkFetcher = chunkFetcher_;
859
860#if NS_BLOCKS_AVAILABLE
861@synthesize locationChangeBlock = locationChangeBlock_;
862#endif
863
864@dynamic activeFetcher;
865@dynamic responseHeaders;
866@dynamic statusCode;
867
868- (NSDictionary *)responseHeaders {
869  // overrides the superclass
870
871  // if asked for the fetcher's response, use the most recent fetcher
872  if (responseHeaders_) {
873    return responseHeaders_;
874  } else {
875    // no chunk fetcher yet completed, so return whatever we have from the
876    // initial fetch
877    return [super responseHeaders];
878  }
879}
880
881- (void)setResponseHeaders:(NSDictionary *)dict {
882  [responseHeaders_ autorelease];
883  responseHeaders_ = [dict retain];
884}
885
886- (NSInteger)statusCode {
887  if (statusCode_ != -1) {
888    // overrides the superclass to indicate status appropriate to the initial
889    // or latest chunk fetch
890    return statusCode_;
891  } else {
892    return [super statusCode];
893  }
894}
895
896- (void)setStatusCode:(NSInteger)val {
897  statusCode_ = val;
898}
899
900- (SEL)sentDataSelector {
901  // overrides the superclass
902#if NS_BLOCKS_AVAILABLE
903  BOOL hasSentDataBlock = (sentDataBlock_ != NULL);
904#else
905  BOOL hasSentDataBlock = NO;
906#endif
907  if ((delegateSentDataSEL_ || hasSentDataBlock) && !needsManualProgress_) {
908    return @selector(uploadFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:);
909  } else {
910    return NULL;
911  }
912}
913
914- (void)setSentDataSelector:(SEL)theSelector {
915  // overrides the superclass
916  delegateSentDataSEL_ = theSelector;
917}
918
919- (GTMHTTPFetcher *)activeFetcher {
920  if (chunkFetcher_) {
921    return chunkFetcher_;
922  } else {
923    return self;
924  }
925}
926
927- (NSURL *)locationURL {
928  return locationURL_;
929}
930
931- (void)setLocationURL:(NSURL *)url {
932  if (url != locationURL_) {
933    [locationURL_ release];
934    locationURL_ = [url retain];
935
936#if NS_BLOCKS_AVAILABLE
937    if (locationChangeBlock_) {
938      locationChangeBlock_(url);
939    }
940#endif
941  }
942}
943@end
944
945#endif // #if !GDATA_REQUIRE_SERVICE_INCLUDES