mirror of https://github.com/perkeep/perkeep.git
338 lines
12 KiB
Objective-C
338 lines
12 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;
|
|
}
|
|
|
|
#pragma mark - convenience
|
|
|
|
- (NSString*)name
|
|
{
|
|
return _file.blobRef;
|
|
}
|
|
|
|
#pragma mark - operation flow
|
|
|
|
// 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 {
|
|
if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) {
|
|
LALog(@"connection lost or unavailable");
|
|
[LACamliUtil statusText:@[
|
|
@"internet connection appears offline"
|
|
]];
|
|
} 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];
|
|
}
|
|
|
|
- (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) {
|
|
if ([error code] == NSURLErrorNotConnectedToInternet || [error code] == NSURLErrorNetworkConnectionLost) {
|
|
LALog(@"connection lost or unavailable");
|
|
[LACamliUtil statusText:@[
|
|
@"internet connection appears offline"
|
|
]];
|
|
} else {
|
|
LALog(@"upload error: %@", error);
|
|
[LACamliUtil errorText:@[
|
|
@"error uploading: ",
|
|
error
|
|
]];
|
|
}
|
|
_failedTransfer = YES;
|
|
[self finished];
|
|
} else {
|
|
[self vivifyChunks];
|
|
}
|
|
}];
|
|
|
|
[upload resume];
|
|
}
|
|
|
|
// 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 description]
|
|
]];
|
|
_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);
|
|
|
|
// There's an extra retain on this operation that I cannot find,
|
|
// this mitigates the issue so the leak is tiny
|
|
_file.allBlobs = nil;
|
|
|
|
[self willChangeValueForKey:@"isExecuting"];
|
|
[self willChangeValueForKey:@"isFinished"];
|
|
|
|
_isExecuting = NO;
|
|
_isFinished = YES;
|
|
|
|
[self didChangeValueForKey:@"isExecuting"];
|
|
[self didChangeValueForKey:@"isFinished"];
|
|
}
|
|
|
|
#pragma mark - nsurlsession delegate
|
|
|
|
- (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];
|
|
});
|
|
}
|
|
}
|
|
|
|
#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, 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 = [@{
|
|
@"camliVersion" : @1,
|
|
@"camliType" : @"file",
|
|
@"unixMTime" : [LACamliUtil rfc3339StringFromDate:_file.creation],
|
|
@"fileName" : _file.name
|
|
} mutableCopy];
|
|
|
|
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
|