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