github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.m (about)

     1  //
     2  //  LACamliUploadOperation.m
     3  //  photobackup
     4  //
     5  //  Created by Nick O'Neill on 11/29/13.
     6  //  Copyright (c) 2013 The Camlistore Authors. All rights reserved.
     7  //
     8  
     9  #import "LACamliUploadOperation.h"
    10  #import "LACamliFile.h"
    11  #import "LACamliClient.h"
    12  #import "LACamliUtil.h"
    13  
    14  static NSUInteger const camliVersion = 1;
    15  static NSString* const multipartBoundary = @"Qe43VdbVVaGtkkMd";
    16  
    17  @implementation LACamliUploadOperation
    18  
    19  - (id)initWithFile:(LACamliFile*)file andClient:(LACamliClient*)client
    20  {
    21      NSParameterAssert(file);
    22      NSParameterAssert(client);
    23  
    24      if (self = [super init]) {
    25          _file = file;
    26          _client = client;
    27          _isExecuting = NO;
    28          _isFinished = NO;
    29          _failedTransfer = NO;
    30          _session = [NSURLSession sessionWithConfiguration:_client.sessionConfig
    31                                                   delegate:self
    32                                              delegateQueue:nil];
    33      }
    34  
    35      return self;
    36  }
    37  
    38  - (BOOL)isConcurrent
    39  {
    40      return YES;
    41  }
    42  
    43  #pragma mark - convenience
    44  
    45  - (NSString*)name
    46  {
    47      return _file.blobRef;
    48  }
    49  
    50  #pragma mark - operation flow
    51  
    52  // request stats for each chunk, making sure the server doesn't already have the chunk
    53  - (void)start
    54  {
    55      [LACamliUtil statusText:@[
    56                                  @"performing stat..."
    57                              ]];
    58  
    59      _taskID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"uploadtask"
    60                                                             expirationHandler:^{
    61          LALog(@"upload task expired");
    62                                                             }];
    63  
    64      if (_client.backgroundID) {
    65          [[UIApplication sharedApplication] endBackgroundTask:_client.backgroundID];
    66      }
    67  
    68      [self willChangeValueForKey:@"isExecuting"];
    69      _isExecuting = YES;
    70      [self didChangeValueForKey:@"isExecuting"];
    71  
    72      NSMutableDictionary* params = [NSMutableDictionary dictionary];
    73      [params setObject:[NSNumber numberWithInt:camliVersion]
    74                 forKey:@"camliversion"];
    75  
    76      int i = 1;
    77      for (NSString* blobRef in _file.allBlobRefs) {
    78          [params setObject:blobRef
    79                     forKey:[NSString stringWithFormat:@"blob%d", i]];
    80          i++;
    81      }
    82  
    83      NSString* formValues = @"";
    84      for (NSString* key in params) {
    85          formValues = [formValues stringByAppendingString:[NSString stringWithFormat:@"%@=%@&", key, params[key]]];
    86      }
    87  
    88      LALog(@"uploading to %@", [_client statURL]);
    89      NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:[_client statURL]];
    90      [req setHTTPMethod:@"POST"];
    91      [req setHTTPBody:[formValues dataUsingEncoding:NSUTF8StringEncoding]];
    92  
    93      NSURLSessionDataTask *statTask = [_session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
    94      {
    95  
    96          if (!error) {
    97              //            LALog(@"data: %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    98  
    99              // we can remove any chunks that the server claims it already has
   100              NSError* err;
   101              NSMutableDictionary* resObj = [NSJSONSerialization JSONObjectWithData:data
   102                                                                            options:0
   103                                                                              error:&err];
   104              if (err) {
   105                  LALog(@"error getting json: %@", err);
   106              }
   107  
   108              if (resObj[@"stat"] != [NSNull null]) {
   109                  for (NSDictionary* stat in resObj[@"stat"]) {
   110                      for (NSString* blobRef in _file.allBlobRefs) {
   111                          if ([stat[@"blobRef"] isEqualToString:blobRef]) {
   112                              [_file.uploadMarks replaceObjectAtIndex:[_file.allBlobRefs indexOfObject:blobRef]
   113                                                           withObject:@NO];
   114                          }
   115                      }
   116                  }
   117              }
   118  
   119              BOOL allUploaded = YES;
   120              for (NSNumber* upload in _file.uploadMarks) {
   121                  if ([upload boolValue]) {
   122                      allUploaded = NO;
   123                  }
   124              }
   125  
   126              // TODO: there's a posibility all chunks have been uploaded but no permanode exists
   127              if (allUploaded) {
   128                  LALog(@"everything's been uploaded already for this file");
   129                  [LACamliUtil logText:@[
   130                                           @"everything already uploaded for ",
   131                                           _file.blobRef
   132                                       ]];
   133                  [self finished];
   134                  return;
   135              }
   136  
   137              [self uploadChunks];
   138          } else {
   139              if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) {
   140                  LALog(@"connection lost or unavailable");
   141                  [LACamliUtil statusText:@[
   142                                              @"internet connection appears offline"
   143                                          ]];
   144              } else {
   145                  LALog(@"failed stat: %@", error);
   146                  [LACamliUtil errorText:@[
   147                                             @"failed to stat: ",
   148                                             [error description]
   149                                         ]];
   150                  [LACamliUtil logText:@[
   151                                           [NSString stringWithFormat:@"failed to stat: %@", error]
   152                                       ]];
   153              }
   154  
   155              _failedTransfer = YES;
   156              [self finished];
   157          }
   158      }];
   159  
   160      [statTask resume];
   161  }
   162  
   163  - (void)uploadChunks
   164  {
   165      [LACamliUtil statusText:@[
   166                                  @"uploading..."
   167                              ]];
   168  
   169      NSMutableURLRequest* uploadReq = [NSMutableURLRequest requestWithURL:[_client uploadURL]];
   170      [uploadReq setHTTPMethod:@"POST"];
   171      [uploadReq setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary]
   172          forHTTPHeaderField:@"Content-Type"];
   173  
   174      NSMutableData* uploadData = [self multipartDataForChunks];
   175  
   176      NSURLSessionUploadTask *upload = [_session uploadTaskWithRequest:uploadReq fromData:uploadData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
   177      {
   178  
   179          //        LALog(@"upload response: %@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
   180  
   181          if (error) {
   182              if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) {
   183                  LALog(@"connection lost or unavailable");
   184                  [LACamliUtil statusText:@[
   185                                              @"internet connection appears offline"
   186                                          ]];
   187              } else {
   188                  LALog(@"upload error: %@", error);
   189                  [LACamliUtil errorText:@[
   190                                             @"error uploading: ",
   191                                             error
   192                                         ]];
   193              }
   194              _failedTransfer = YES;
   195              [self finished];
   196          } else {
   197              [self vivifyChunks];
   198          }
   199      }];
   200  
   201      [upload resume];
   202  }
   203  
   204  // ask the server to vivify the blobrefs into a file
   205  - (void)vivifyChunks
   206  {
   207      [LACamliUtil statusText:@[
   208                                  @"vivify"
   209                              ]];
   210  
   211      NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:[_client uploadURL]];
   212      [req setHTTPMethod:@"POST"];
   213      [req setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary]
   214          forHTTPHeaderField:@"Content-Type"];
   215      [req addValue:@"1"
   216          forHTTPHeaderField:@"X-Camlistore-Vivify"];
   217  
   218      NSMutableData* vivifyData = [self multipartVivifyDataForChunks];
   219  
   220      NSURLSessionUploadTask *vivify = [_session uploadTaskWithRequest:req fromData:vivifyData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
   221      {
   222          if (error) {
   223              LALog(@"error vivifying: %@", error);
   224              [LACamliUtil errorText:@[
   225                                         @"error vivify: ",
   226                                         [error description]
   227                                     ]];
   228              _failedTransfer = YES;
   229          }
   230  
   231          [self finished];
   232      }];
   233  
   234      [vivify resume];
   235  }
   236  
   237  - (void)finished
   238  {
   239      [LACamliUtil statusText:@[
   240                                  @"cleaning up..."
   241                              ]];
   242  
   243      _client.backgroundID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"queuesync"
   244                                                                          expirationHandler:^{
   245          LALog(@"queue sync task expired");
   246                                                                          }];
   247  
   248      [[UIApplication sharedApplication] endBackgroundTask:_taskID];
   249  
   250      LALog(@"finished op %@", _file.blobRef);
   251  
   252      // There's an extra retain on this operation that I cannot find,
   253      // this mitigates the issue so the leak is tiny
   254      _file.allBlobs = nil;
   255  
   256      [self willChangeValueForKey:@"isExecuting"];
   257      [self willChangeValueForKey:@"isFinished"];
   258  
   259      _isExecuting = NO;
   260      _isFinished = YES;
   261  
   262      [self didChangeValueForKey:@"isExecuting"];
   263      [self didChangeValueForKey:@"isFinished"];
   264  }
   265  
   266  #pragma mark - nsurlsession delegate
   267  
   268  - (void)URLSession:(NSURLSession*)session task:(NSURLSessionTask*)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
   269  {
   270      if ([_client.delegate respondsToSelector:@selector(uploadProgress:
   271                                                           forOperation:)]) {
   272          float progress = (float)totalBytesSent / (float)totalBytesExpectedToSend;
   273  
   274          dispatch_async(dispatch_get_main_queue(), ^{
   275              [_client.delegate uploadProgress:progress forOperation:self];
   276          });
   277      }
   278  }
   279  
   280  #pragma mark - multipart bits
   281  
   282  - (NSMutableData*)multipartDataForChunks
   283  {
   284      NSMutableData* data = [NSMutableData data];
   285  
   286      for (NSData* chunk in [_file blobsToUpload]) {
   287          [data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
   288          // server ignores this filename and mimetype, it doesn't matter what it is
   289          [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"image.jpg\"\r\n", [LACamliUtil blobRef:chunk]] dataUsingEncoding:NSUTF8StringEncoding]];
   290          [data appendData:[@"Content-Type: image/jpeg\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
   291          [data appendData:chunk];
   292          [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
   293      }
   294  
   295      [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
   296  
   297      return data;
   298  }
   299  
   300  - (NSMutableData*)multipartVivifyDataForChunks
   301  {
   302      NSMutableData* data = [NSMutableData data];
   303  
   304      NSMutableDictionary* schemaBlob = [@{
   305                                             @"camliVersion" : @1,
   306                                             @"camliType" : @"file",
   307                                             @"unixMTime" : [LACamliUtil rfc3339StringFromDate:_file.creation],
   308                                             @"fileName" : _file.name
   309                                         } mutableCopy];
   310  
   311      NSMutableArray* parts = [NSMutableArray array];
   312      int i = 0;
   313      for (NSString* blobRef in _file.allBlobRefs) {
   314          [parts addObject:@{
   315                               @"blobRef" : blobRef, @"size" : [NSNumber numberWithInteger:[[_file.allBlobs objectAtIndex:i] length]]
   316                           }];
   317          i++;
   318      }
   319      [schemaBlob setObject:parts
   320                     forKey:@"parts"];
   321  
   322      NSData* schemaData = [NSJSONSerialization dataWithJSONObject:schemaBlob
   323                                                           options:NSJSONWritingPrettyPrinted
   324                                                             error:nil];
   325  
   326      [data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
   327      [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"json\"\r\n", [LACamliUtil blobRef:schemaData]] dataUsingEncoding:NSUTF8StringEncoding]];
   328      [data appendData:[@"Content-Type: application/json\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
   329      [data appendData:schemaData];
   330      [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
   331  
   332      [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
   333  
   334      return data;
   335  }
   336  
   337  @end