perkeep/clients/ios-objc/photobackup/LACamliClient/LACamliUploadOperation.m

268 lines
9.6 KiB
Objective-C

//
// LACamliUploadOperation.m
// photobackup
//
// Created by Nick O'Neill on 11/29/13.
// Copyright (c) 2013 The Camlistore Authors. All rights reserved.
//
#import "LACamliUploadOperation.h"
#import "LACamliFile.h"
#import "LACamliClient.h"
#import "LACamliUtil.h"
static NSUInteger const camliVersion = 1;
static NSString *const multipartBoundary = @"Qe43VdbVVaGtkkMd";
@implementation LACamliUploadOperation
- (id)initWithFile:(LACamliFile *)file andClient:(LACamliClient *)client
{
NSParameterAssert(file);
NSParameterAssert(client);
if (self = [super init]) {
_file = file;
_client = client;
_isExecuting = NO;
_isFinished = NO;
_failedTransfer = NO;
_session = [NSURLSession sessionWithConfiguration:_client.sessionConfig delegate:self delegateQueue:nil];
}
return self;
}
- (BOOL)isConcurrent
{
return YES;
}
- (NSString *)name
{
return _file.blobRef;
}
// request stats for each chunk, making sure the server doesn't already have the chunk
- (void)start
{
[LACamliUtil statusText:@[@"performing stat..."]];
_taskID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"uploadtask" expirationHandler:^{
LALog(@"upload task expired");
}];
if (_client.backgroundID) {
[[UIApplication sharedApplication] endBackgroundTask:_client.backgroundID];
}
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = YES;
[self didChangeValueForKey:@"isExecuting"];
NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params setObject:[NSNumber numberWithInt:camliVersion] forKey:@"camliversion"];
int i = 1;
for (NSString *blobRef in _file.allBlobRefs) {
[params setObject:blobRef forKey:[NSString stringWithFormat:@"blob%d",i]];
i++;
}
NSString *formValues = @"";
for (NSString *key in params) {
formValues = [formValues stringByAppendingString:[NSString stringWithFormat:@"%@=%@&",key,params[key]]];
}
LALog(@"uploading to %@",[_client statUrl]);
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[_client statUrl]];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:[formValues dataUsingEncoding:NSUTF8StringEncoding]];
NSURLSessionDataTask *statTask = [_session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
// LALog(@"data: %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
// we can remove any chunks that the server claims it already has
NSError *err;
NSMutableDictionary *resObj = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
if (err) {
LALog(@"error getting json: %@",err);
}
if (resObj[@"stat"] != [NSNull null]) {
for (NSDictionary *stat in resObj[@"stat"]) {
for (NSString *blobRef in _file.allBlobRefs) {
if ([stat[@"blobRef"] isEqualToString:blobRef]) {
[_file.uploadMarks replaceObjectAtIndex:[_file.allBlobRefs indexOfObject:blobRef] withObject:@NO];
}
}
}
}
BOOL allUploaded = YES;
for (NSNumber *upload in _file.uploadMarks) {
if ([upload boolValue]) {
allUploaded = NO;
}
}
// TODO: there's a posibility all chunks have been uploaded but no permanode exists
if (allUploaded) {
LALog(@"everything's been uploaded already for this file");
[LACamliUtil logText:@[@"everything already uploaded for ", _file.blobRef]];
[self finished];
return;
}
[self uploadChunks];
} else {
LALog(@"failed stat: %@",error);
[LACamliUtil errorText:@[@"failed to stat: ",[error description]]];
[LACamliUtil logText:@[[NSString stringWithFormat:@"failed to stat: %@",error]]];
_failedTransfer = YES;
[self finished];
}
}];
[statTask resume];
}
// post the chunks in a multipart request
//
- (void)uploadChunks
{
[LACamliUtil statusText:@[@"uploading..."]];
NSMutableURLRequest *uploadReq = [NSMutableURLRequest requestWithURL:[_client uploadUrl]];
[uploadReq setHTTPMethod:@"POST"];
[uploadReq setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary] forHTTPHeaderField:@"Content-Type"];
NSMutableData *uploadData = [self multipartDataForChunks];
NSURLSessionUploadTask *upload = [_session uploadTaskWithRequest:uploadReq fromData:uploadData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// LALog(@"upload response: %@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
if (error) {
LALog(@"upload error: %@",error);
[LACamliUtil errorText:@[@"error uploading: ",error]];
_failedTransfer = YES;
[self finished];
} else {
[self vivifyChunks];
}
}];
[upload resume];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
if ([_client.delegate respondsToSelector:@selector(uploadProgress:forOperation:)]) {
float progress = (float)totalBytesSent/(float)totalBytesExpectedToSend;
dispatch_async(dispatch_get_main_queue(), ^{
[_client.delegate uploadProgress:progress forOperation:self];
});
}
}
// ask the server to vivify the blobrefs into a file
- (void)vivifyChunks
{
[LACamliUtil statusText:@[@"vivify"]];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[_client uploadUrl]];
[req setHTTPMethod:@"POST"];
[req setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", multipartBoundary] forHTTPHeaderField:@"Content-Type"];
[req addValue:@"1" forHTTPHeaderField:@"X-Camlistore-Vivify"];
NSMutableData *vivifyData = [self multipartVivifyDataForChunks];
NSURLSessionUploadTask *vivify = [_session uploadTaskWithRequest:req fromData:vivifyData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
LALog(@"error vivifying: %@",error);
[LACamliUtil errorText:@[@"error vivify: ",error]];
_failedTransfer = YES;
}
[self finished];
}];
[vivify resume];
}
- (void)finished
{
[LACamliUtil statusText:@[@"cleaning up..."]];
_client.backgroundID = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"queuesync" expirationHandler:^{
LALog(@"queue sync task expired");
}];
[[UIApplication sharedApplication] endBackgroundTask:_taskID];
LALog(@"finished op %@",_file.blobRef);
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
#pragma mark - multipart bits
- (NSMutableData *)multipartDataForChunks
{
NSMutableData *data = [NSMutableData data];
for (NSData *chunk in [_file blobsToUpload]) {
[data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
// server ignores this filename and mimetype, so it doesn't matter what it is
[data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"image.jpg\"\r\n", [LACamliUtil blobRef:chunk]] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[@"Content-Type: image/jpeg\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:chunk];
[data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
}
[data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
return data;
}
- (NSMutableData *)multipartVivifyDataForChunks
{
NSMutableData *data = [NSMutableData data];
NSMutableDictionary *schemaBlob = [NSMutableDictionary dictionaryWithObjectsAndKeys:@1, @"camliVersion", @"file", @"camliType", [LACamliUtil rfc3339StringFromDate:_file.creation], @"unixMTime", nil];
NSMutableArray *parts = [NSMutableArray array];
int i = 0;
for (NSString *blobRef in _file.allBlobRefs) {
[parts addObject:@{@"blobRef":blobRef,@"size":[NSNumber numberWithInteger:[[_file.allBlobs objectAtIndex:i] length]]}];
i++;
}
[schemaBlob setObject:parts forKey:@"parts"];
NSData *schemaData = [NSJSONSerialization dataWithJSONObject:schemaBlob options:NSJSONWritingPrettyPrinted error:nil];
[data appendData:[[NSString stringWithFormat:@"--%@\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"json\"\r\n", [LACamliUtil blobRef:schemaData]] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[@"Content-Type: application/json\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:schemaData];
[data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[[NSString stringWithFormat:@"--%@--\r\n", multipartBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
return data;
}
@end