2

What's the best approach to make sure that an upload works?

I need to upload images, to a server and make sure, that the upload has worked. If, for whatever reason, the upload did not work, I'll have to retry later.

Currently, I'm using NSUrlSession to upload the image:

- (void)didCreateSignature:(UIImage *)image {

    BLog();

    NSArray   *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString  *documentsDirectory = [paths objectAtIndex:0];
    NSString  *imageFile = [NSString stringWithFormat:@"%@/%@", documentsDirectory,@"test.png"];


    NSData *imageData = UIImagePNGRepresentation(image);
    [imageData writeToFile:imageFile atomically:YES];

    while (![[NSFileManager defaultManager] fileExistsAtPath:imageFile]) {
        [NSThread sleepForTimeInterval:.5];
    }


    NSURLSession *session = [self backgroundSession];

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:self.uploadUrl]];
    [request setHTTPMethod:@"POST"];
    [request addValue:@"image/png" forHTTPHeaderField:@"Content-Type"];

    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:signatureFile]];

    [uploadTask resume];

}

Now let's assume that the user does not have internet connection, then the delegate will be fired:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    BLog();

    if (error == nil)
    {
        NSLog(@"Task: %@ completed successfully", task);
    }
    else
    {
        NSLog(@"Task: %@ completed with error: %@", [task originalRequest], [error localizedDescription]);


        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil];
            self.internetReachability = [Reachability reachabilityForInternetConnection];
            [self.internetReachability startNotifier];
        });


    }

}

According to the Apple Documentation you should retry later if the Reachability status has changed. So i was thinking of adding these tasks to an Array and start each task again once the Internet connection has become available.

Is this the right approach?

And what happens, if the user closes the app (via TaskManager). How can I make sure that these tasks will be resumed ?

dominik
  • 1,319
  • 13
  • 23

1 Answers1

2

In the meantime, I solved it by using a custom UploadManager Class and store downloads in Core Data until the upload was successful: (Below Code is not complete, and only shows the basic concept. If anybody is interested in the full implementation, please let me know)

@implementation UploadManager

- (void) uploadImage:(UIImage *)image toUrl:(NSString *)uploadUrl insertIntoDB:(BOOL)insertIntoDB

{
    if(insertIntoDB) {
        [self insertUploadTaskIntoDBWithImage:image andUploadUrl:uploadUrl];
    }


    // Background upload only works with files
    NSString *fileName = [NSString stringWithFormat:@"%@%@", [uploadUrl sha256], @".png"];
    NSString *signatureFile = [NSString stringWithFormat:@"%@/%@",  NSTemporaryDirectory(), fileName];

    NSData *imageData = UIImagePNGRepresentation(image);
    [imageData writeToFile:signatureFile atomically:YES];

    while (![[NSFileManager defaultManager] fileExistsAtPath:signatureFile]) {
        [NSThread sleepForTimeInterval:.5];
    }

    NSURLSession *session = [self backgroundSession];

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:uploadUrl]];
    [request setHTTPMethod:@"POST"];
    [request addValue:@"image/png" forHTTPHeaderField:@"Content-Type"];

    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:signatureFile]];

    [self showNetworkActivity:YES];

    self.taskCount++;
    [uploadTask resume];
}


-(void) uploadTasksInDatabase
{
    NSArray* uploadTasks = [self getAllUploadTasksFromDatabase];

    for(UploadTask *task in uploadTasks) {
        UIImage *image = [UIImage imageWithData:task.signature];
        [self uploadImage:image toUrl:task.uploadUrl insertIntoDB:NO];
    }
}


/*
 If an application has received an 
 application:handleEventsForBackgroundURLSession:completionHandler: message, the session
 delegate will receive this message to indicate that all messages previously enqueued for this
 session have been delivered. At this time it is safe to invoke the previously stored completion
 handler, or to begin any internal updates that will result in invoking the completion handler.
 */
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    if (appDelegate.backgroundSessionCompletionHandler) {
        void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
        appDelegate.backgroundSessionCompletionHandler = nil;
        completionHandler();
    }

    self.taskCount = 0;
    NSDebug(@"All tasks are finished");

    [self clearDatabase];

}



- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    BLog();


    self.taskCount--;
    [[NSNotificationCenter defaultCenter] postNotificationName:[task.originalRequest.URL description] object:self];

    if(self.taskCount == 0) {
        [self showNetworkActivity:NO];
    }


    if (error == nil)
    {
        NSLog(@"Task: %@ completed successfully", [task.originalRequest.URL description]);
        [self deleteUploadTaskWithUploadUrl:[task.originalRequest.URL description]];
    }
    else
    {
        NSLog(@"Task: %@ completed with error: %@", [task.originalRequest.URL description], [error localizedDescription]);

    }

}

In the App delegate insert the following:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Try to reupload all tasks in DB
    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(backgroundQueue, ^{
            [self.uploadManager uploadTasksInDatabase];
    });


    return YES;
}

This way it will try to reupload all unfinished tasks whenever the application is started again.

dominik
  • 1,319
  • 13
  • 23
  • I used this code because uploadTaskWithRequest:fromData: simply doesn't work for me (http://stackoverflow.com/questions/25127058/why-does-nsurlsession-uploadtaskwithrequestfromdata-fails-to-upload-to-php-ser) but this API fromFile: ends up creating a huge file for some reason, and php error log file says 13M file exceeds 8M limit. I know I could up the server limit, but why would it make the file so big? – marciokoko Aug 04 '14 at 21:06
  • Where does the image data come from? Have you tried compressing the image: http://stackoverflow.com/questions/8112166/compress-uiimage-but-keep-size – dominik Aug 05 '14 at 19:14
  • Got it, I was using PNGRepresentation. I switched over to JPEGRepresentation and added a compression parameter of 0.6 and it worked. But Im still unable to upload any files using either fromData or fromFile. – marciokoko Aug 05 '14 at 21:31
  • Thats the problem :) Im going crazy because I don't get any error. I get back HTTP Code 200 from the server but nothing gets uploaded. – marciokoko Aug 05 '14 at 22:22
  • What does the Server report for: var_dump($_FILES); ? – dominik Aug 06 '14 at 13:45
  • It prints out file count = 0 and array (0) – marciokoko Aug 06 '14 at 19:28
  • I just updated my post here with an image of the app where I display the server response: http://stackoverflow.com/questions/25127058/why-does-nsurlsession-uploadtaskwithrequestfromdata-fail-to-upload-to-php-serv – marciokoko Aug 06 '14 at 21:10