github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/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 delegate:self delegateQueue:nil]; 31 } 32 33 return self; 34 } 35 36 - (BOOL)isConcurrent 37 { 38 return YES; 39 } 40 41 - (NSString *)name 42 { 43 return _file.blobRef; 44 } 45 46 // request stats for each chunk, making sure the server doesn't already have the chunk 47 - (void)start 48 { 49 [LACamliUtil statusText:@[@"performing stat..."]]; 50 51 _taskID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"uploadtask" expirationHandler:^{ 52 LALog(@"upload task expired"); 53 }]; 54 55 if (_client.backgroundID) { 56 [[UIApplication sharedApplication] endBackgroundTask:_client.backgroundID]; 57 } 58 59 [self willChangeValueForKey:@"isExecuting"]; 60 _isExecuting = YES; 61 [self didChangeValueForKey:@"isExecuting"]; 62 63 NSMutableDictionary *params = [NSMutableDictionary dictionary]; 64 [params setObject:[NSNumber numberWithInt:camliVersion] forKey:@"camliversion"]; 65 66 int i = 1; 67 for (NSString *blobRef in _file.allBlobRefs) { 68 [params setObject:blobRef forKey:[NSString stringWithFormat:@"blob%d",i]]; 69 i++; 70 } 71 72 NSString *formValues = @""; 73 for (NSString *key in params) { 74 formValues = [formValues stringByAppendingString:[NSString stringWithFormat:@"%@=%@&",key,params[key]]]; 75 } 76 77 LALog(@"uploading to %@",[_client statUrl]); 78 NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[_client statUrl]]; 79 [req setHTTPMethod:@"POST"]; 80 [req setHTTPBody:[formValues dataUsingEncoding:NSUTF8StringEncoding]]; 81 82 NSURLSessionDataTask *statTask = [_session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 83 84 if (!error) { 85 // LALog(@"data: %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); 86 87 // we can remove any chunks that the server claims it already has 88 NSError *err; 89 NSMutableDictionary *resObj = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err]; 90 if (err) { 91 LALog(@"error getting json: %@",err); 92 } 93 94 if (resObj[@"stat"] != [NSNull null]) { 95 for (NSDictionary *stat in resObj[@"stat"]) { 96 for (NSString *blobRef in _file.allBlobRefs) { 97 if ([stat[@"blobRef"] isEqualToString:blobRef]) { 98 [_file.uploadMarks replaceObjectAtIndex:[_file.allBlobRefs indexOfObject:blobRef] withObject:@NO]; 99 } 100 } 101 } 102 } 103 104 BOOL allUploaded = YES; 105 for (NSNumber *upload in _file.uploadMarks) { 106 if ([upload boolValue]) { 107 allUploaded = NO; 108 } 109 } 110 111 // TODO: there's a posibility all chunks have been uploaded but no permanode exists 112 if (allUploaded) { 113 LALog(@"everything's been uploaded already for this file"); 114 [LACamliUtil logText:@[@"everything already uploaded for ", _file.blobRef]]; 115 [self finished]; 116 return; 117 } 118 119 [self uploadChunks]; 120 } else { 121 LALog(@"failed stat: %@",error); 122 [LACamliUtil errorText:@[@"failed to stat: ",[error description]]]; 123 [LACamliUtil logText:@[[NSString stringWithFormat:@"failed to stat: %@",error]]]; 124 125 _failedTransfer = YES; 126 [self finished]; 127 } 128 }]; 129 130 [statTask resume]; 131 } 132 133 // post the chunks in a multipart request 134 // 135 - (void)uploadChunks 136 { 137 [LACamliUtil statusText:@[@"uploading..."]]; 138 139 NSMutableURLRequest *uploadReq = [NSMutableURLRequest requestWithURL:[_client uploadUrl]]; 140 [uploadReq setHTTPMethod:@"POST"]; 141 [uploadReq setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary] forHTTPHeaderField:@"Content-Type"]; 142 143 NSMutableData *uploadData = [self multipartDataForChunks]; 144 145 NSURLSessionUploadTask *upload = [_session uploadTaskWithRequest:uploadReq fromData:uploadData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 146 147 // LALog(@"upload response: %@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]); 148 149 if (error) { 150 LALog(@"upload error: %@",error); 151 [LACamliUtil errorText:@[@"error uploading: ",error]]; 152 _failedTransfer = YES; 153 [self finished]; 154 } else { 155 [self vivifyChunks]; 156 } 157 }]; 158 159 [upload resume]; 160 } 161 162 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend 163 { 164 if ([_client.delegate respondsToSelector:@selector(uploadProgress:forOperation:)]) { 165 float progress = (float)totalBytesSent/(float)totalBytesExpectedToSend; 166 167 dispatch_async(dispatch_get_main_queue(), ^{ 168 [_client.delegate uploadProgress:progress forOperation:self]; 169 }); 170 } 171 } 172 173 // ask the server to vivify the blobrefs into a file 174 - (void)vivifyChunks 175 { 176 [LACamliUtil statusText:@[@"vivify"]]; 177 178 NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[_client uploadUrl]]; 179 [req setHTTPMethod:@"POST"]; 180 [req setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary] forHTTPHeaderField:@"Content-Type"]; 181 [req addValue:@"1" forHTTPHeaderField:@"X-Camlistore-Vivify"]; 182 183 NSMutableData *vivifyData = [self multipartVivifyDataForChunks]; 184 185 NSURLSessionUploadTask *vivify = [_session uploadTaskWithRequest:req fromData:vivifyData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 186 if (error) { 187 LALog(@"error vivifying: %@",error); 188 [LACamliUtil errorText:@[@"error vivify: ",error]]; 189 _failedTransfer = YES; 190 } 191 192 [self finished]; 193 }]; 194 195 [vivify resume]; 196 } 197 198 - (void)finished 199 { 200 [LACamliUtil statusText:@[@"cleaning up..."]]; 201 202 _client.backgroundID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"queuesync" expirationHandler:^{ 203 LALog(@"queue sync task expired"); 204 }]; 205 206 [[UIApplication sharedApplication] endBackgroundTask:_taskID]; 207 208 LALog(@"finished op %@",_file.blobRef); 209 210 [self willChangeValueForKey:@"isExecuting"]; 211 [self willChangeValueForKey:@"isFinished"]; 212 213 _isExecuting = NO; 214 _isFinished = YES; 215 216 [self didChangeValueForKey:@"isExecuting"]; 217 [self didChangeValueForKey:@"isFinished"]; 218 } 219 220 #pragma mark - multipart bits 221 222 - (NSMutableData *)multipartDataForChunks 223 { 224 NSMutableData *data = [NSMutableData data]; 225 226 for (NSData *chunk in [_file blobsToUpload]) { 227 [data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 228 // server ignores this filename and mimetype, so it doesn't matter what it is 229 [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"image.jpg\"\r\n", [LACamliUtil blobRef:chunk]] dataUsingEncoding:NSUTF8StringEncoding]]; 230 [data appendData:[@"Content-Type: image/jpeg\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; 231 [data appendData:chunk]; 232 [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; 233 } 234 235 [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 236 237 return data; 238 } 239 240 - (NSMutableData *)multipartVivifyDataForChunks 241 { 242 NSMutableData *data = [NSMutableData data]; 243 244 NSMutableDictionary *schemaBlob = [NSMutableDictionary dictionaryWithObjectsAndKeys:@1, @"camliVersion", @"file", @"camliType", [LACamliUtil rfc3339StringFromDate:_file.creation], @"unixMTime", nil]; 245 246 NSMutableArray *parts = [NSMutableArray array]; 247 int i = 0; 248 for (NSString *blobRef in _file.allBlobRefs) { 249 [parts addObject:@{@"blobRef":blobRef,@"size":[NSNumber numberWithInteger:[[_file.allBlobs objectAtIndex:i] length]]}]; 250 i++; 251 } 252 [schemaBlob setObject:parts forKey:@"parts"]; 253 254 NSData *schemaData = [NSJSONSerialization dataWithJSONObject:schemaBlob options:NSJSONWritingPrettyPrinted error:nil]; 255 256 [data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 257 [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"json\"\r\n", [LACamliUtil blobRef:schemaData]] dataUsingEncoding:NSUTF8StringEncoding]]; 258 [data appendData:[@"Content-Type: application/json\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; 259 [data appendData:schemaData]; 260 [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]]; 261 262 [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 263 264 return data; 265 } 266 267 @end