Incomplete file downloads using ASIHTTPRequest and Rackspace Cloud Files - ios

I'm downloading mp3 files from Rackspace cloud files, and for large files i'm encountering an issue where the download is completed successfully but the file is not yet downloaded completely. For example, a 40 MB mp3 file (01:00:00 duration) is download as as 4.5 MB mp3 file (00:10:30 duration). This doesn't happen all the time.
Any pointers as to what's going on?
Why is this happening, and how can i fix this issue?
How can i build a simple checksum logic to check if the file was downloaded completely?
Here's how i create and send an async request:
ASIHTTPRequest *request;
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setShouldAttemptPersistentConnection:NO];
[request setAllowResumeForFileDownloads:YES];
[request setDownloadProgressDelegate:self];
[request setShouldContinueWhenAppEntersBackground:YES];
[request setUserInfo:userInfo];
[request setDownloadDestinationPath:downloadPath];
[request setTemporaryFileDownloadPath:[NSString stringWithFormat:#"%#.download", downloadPath]];
[self.networkQueue addOperation:request];
[self.networkQueue go];
Note i'm using a network queue with 4 concurrent downloads.
Thanks.
Edit (Mon March 5, 2012, 03:25 PM)
So, further investigation shows that ASINetworkQueue is calling requestDidFinishSelector delegate method instead of requestDidFailSelector. The status code returned by the ASIHTTPRequest object is 206, HTTP/1.1 206 Partial Content in requestDidFinishSelector method. The status code should be 200, HTTP/1.1 200 OK.
I still don't know why! and i still don't know how to fix this. It seems that i'll have to delete the partially downloaded file and start the download process again. At this point the temporary file i.e. %#.download is removed, and this partially downloaded file is put at the destination path.

So, this is what i ended up doing, and hopefully this'll be enough (to solve the problem).
Here's how i'm creating the network queue:
- (ASINetworkQueue *)networkQueue {
if (!_networkQueue) {
_networkQueue = [[ASINetworkQueue alloc] init];
[_networkQueue setShowAccurateProgress:YES];
[_networkQueue setRequestDidFinishSelector:#selector(contentRequestDidSucceed:)];
[_networkQueue setRequestDidFailSelector:#selector(contentRequestDidFail:)];
[_networkQueue setShouldCancelAllRequestsOnFailure:NO];
[_networkQueue setDelegate:self];
}
return _networkQueue;
}
And here's what my contentRequestDidSucceed: method does:
- (void)contentRequestDidSucceed:(ASIHTTPRequest *)request {
// ASIHTTPRequest doesn't use HTTP status codes (except for redirection),
// so it's up to us to look out for problems (ex: 404) in the requestDidFinishSelector selector.
// requestDidFailSelector will be called only if there is the server can not be reached
// (time out, no connection, connection interrupted, ...)
// In certain cases ASIHTTPRequest/ASINetworkQueue calls the delegate method requestDidFinishSelector,
// instead it should call requestDidFailSelector. I've encountered this specific case with status code 206 (HTTP/1.1 206 Partial Content). In this case the file was not completely downloaded, so we'll have to re-process the request.
if ([request responseStatusCode] != 200) {
NSLog(#" ");
NSLog(#"======= BEEP =======");
NSLog(#" ");
// We're double checking that the file was indeed not downloaded completely!
// During internal testing, we encountered a case where download was successful
// but we received 206 as the response code (maybe we received the cached value).
unsigned long long progress = [request totalBytesRead] + [request partialDownloadSize];
unsigned long long total = [request contentLength] + [request partialDownloadSize];
if (progress != total) {
NSString *downloadPath = [request downloadDestinationPath];
NSString *temporaryDownloadPath = [self temporaryPathForFile:downloadPath];
// Move the file at destination path to the temporary destination path (back again)
NSError *moveError = nil;
[[[[NSFileManager alloc] init] autorelease] moveItemAtPath:downloadPath
toPath:temporaryDownloadPath
error:&moveError];
if (moveError) {
NSLog(#"Failed to move file from '%#' to '%#'", downloadPath, temporaryDownloadPath);
NSError *removeError = nil;
[ASIHTTPRequest removeFileAtPath:downloadPath error:&removeError];
if (removeError) {
NSLog(#"Failed to remove file from '%#'", downloadPath);
}
}
// Call the requestDidFailSelector method
[self contentRequestDidFail:request];
// Don't continue
return;
}
}
// TODO: Process successful request!
// . . .
}
If there's a better way to handle this, please let me know.

Related

IIS webdav and Ensembles

I'm trying to use sync through an IIS 8 Webdav backend with Ensembles. The problem i encounter is that the first sync works fine, but when i try to sync a second time or on a second unit (iPad in this case) I get a server error 405 "method not allowed". Has anyone encountered this and got it working, to sync with IIS Webdav?
This is the allheaderfield property of the server response:
" UserInfo={NSLocalizedDescription=HTTP status code was {
Allow = "COPY, PROPFIND, DELETE, MOVE, PROPPATCH, LOCK, UNLOCK";
Connection = "Keep-Alive";
"Content-Length" = 1293;
"Content-Type" = "text/html";
Date = "Mon, 25 Jan 2016 12:02:07 GMT";
"Persistent-Auth" = true;
Server = "Microsoft-IIS/8.5";
"X-UA-Compatible" = "IE=8";
EDIT:
It might be possible that this isn't a configuration problem after all. I added a few logs and the createDirectoryAtPath method gives me HTTP error 405, this is the original code:
- (void)createDirectoryAtPath:(NSString *)path completion:(CDECompletionBlock)completion{
NSMutableURLRequest *request = [self mutableURLRequestForPath:path];
request.HTTPMethod = #"MKCOL";
[request setValue:#"application/xml" forHTTPHeaderField:#"Content-Type"];
[self sendURLRequest:request completion:^(NSError *error, NSInteger statusCode, NSData *responseData) {
if (completion) completion(error);
}];}
And this is the directoryExistsAtPath method:
- (void)directoryExistsAtPath:(NSString *)path completion:(CDEDirectoryExistenceCallback)completion{
[self sendPropertyFindRequestForPath:path depth:0 completion:^(NSError *error, NSInteger statusCode, NSData *responseData) {
if (error && statusCode != 404) {
if (completion) completion(NO, error);
}
else if (statusCode == 404) {
if (completion) completion(NO, nil);
}
else {
CDEWebDavResponseParser *parser = [[CDEWebDavResponseParser alloc] initWithData:responseData];
BOOL succeeded = [parser parse:&error];
if (!succeeded) {
if (completion) completion(NO, error);
return;
}
BOOL isDir = [parser.cloudItems.lastObject isKindOfClass:[CDECloudDirectory class]];
if (completion) completion(isDir, nil);
}
}];}
If i replace the first parameter (currently the isDir-variable) in the completion block at the end to YES, the 405 error does not appear.On logging the parser.clouditems.lastobject, I find that it is often (or always?) empty). So setting the parameter to YES, results in data being uploaded to my webdav, and the folders are in place. However, testing on a second unit (or reinstalling the app on the same unit), download never happens - the downloadFromPath never gets called, a "GET"-request is never sent.
Looking at the calling code in the underlying framework (CDECloudmanager mostly) hasn't led me anywhere so far.
As the directoryExistsAtPath is optional, i tried commenting it out, but i don't think it made a difference.
Another thing I noticed is that I get several baseline files in the baselines folder. According to the Ensembles documentation, there should only be one.
Any clues?
Ok seems I got this working at last. I had to make a change in the CDEWebDavCloudFileSystem class to avoid the 405 error. Doing that however, I encountered a 404 error. That one was solved by configuring the IIS webdav.
So step 1, I changed the request in the sendPropertyFindRequestForPath method:
new code:
static NSString *xml = #"<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propfind xmlns:D=\"DAV:\"><D:allprop/></D:propfind>";
original code:
static NSString *xml = #"<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:href/><D:resourcetype/><D:creationdate/><D:getlastmodified/><D:getcontentlength/><D:response/></D:prop></D:propfind>";
To remove the 404 error appearing after that I had to add mime type application/xml to the .cdeevent extension.
This link goes into detail of how to configure IIS for that:
http://anandthearchitect.com/2013/08/01/webdav-404file-or-directory-not-found/

Dispatch 100 HTTP Request in order

I am using objective-C to write an app which needs to dispatch 100 web request and the response will be handled in the call back. My question is, how can I execute web req0, wait for call back, then execute web req1 and so on?
Thanks for any tips and help.
NSURL *imageURL = [[contact photoLink] URL];
GDataServiceGoogleContact *service = [self contactService];
// requestForURL:ETag:httpMethod: sets the user agent header of the
// request and, when using ClientLogin, adds the authorization header
NSMutableURLRequest *request = [service requestForURL:imageURL
ETag: nil
httpMethod:nil];
[request setValue:#"image/*" forHTTPHeaderField:#"Accept"];
GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
fetcher.retryEnabled = YES;
fetcher.maxRetryInterval = 0.3;
fetcher.minRetryInterval = 0.3;
[fetcher setAuthorizer:[service authorizer]];
[fetcher beginFetchWithDelegate:self
didFinishSelector:#selector(imageFetcher:finishedWithData:error:)];
}
- (void)imageFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
if (error == nil) {
// got the data; display it in the image view. Because this is sample
// code, we won't be rigorous about verifying that the selected contact hasn't
// changed between when the fetch began and now.
// NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
// [mContactImageView setImage:image];
NSLog(#"successfully fetched the data");
} else {
NSLog(#"imageFetcher:%# failedWithError:%#", fetcher, error);
}
}
You can't simply call this code in a loop as GTMHTTPFetcher works asynchronously so the loop, as you see, will iterate and start all instances without any delay.
A simple option is to put all of the contacts into a mutable array, take the first contact from the array (remove it from the array) and start the first fetcher. Then, in the finishedWithData callback, check if the array contains anything, if it does remove the first item and start a fetch with it. In this way the fetches will run serially one after the other.
A better but more complex solution would be to create an asynchronous NSOperation (there are various guides on the web) which starts a fetch and waits for the callback before completing. The benefit of this approach is that you can create all of your operations and add them to an operation queue, then you can set the max concurrent count and run the queue - so you can run multiple fetch instances at the same time. You can also suspend the queue or cancel the operations if you need to.

NSURLConnection Returns No Data

I have a URL that I'm trying to get XML from.
Now in my iOS app I have this, to get the data.
- (void)viewDidLoad {
[super viewDidLoad];
[self loadDataUsingNSURLConnection];
}
- (void) loadDataUsingNSURLConnection {
NSString *url = #"http://64.182.231.116/~spencerf/university_of_albany/u_albany_alumni_menu_test.xml";
[self getMenuItems:url];
}
And then finally this,
- (void)getMenuItems:(NSString*)url{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(#"response == %#", response);
NSLog(#"data: %#", data);
/*
self.mealdata=[[MealData alloc]init:data];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.loadingView removeFromSuperview];
});
*/
}];
}
Now sometimes when I run my app it works great and data is returned, but then sometimes, I would say about 25% of the time, with me changing nothing in-between runs. It returns no data, and the NSLog returns
2015-03-10 18:28:05.472 APP[6289:97905] response == <NSHTTPURLResponse:
0x7fee7628e000> { URL: http://64.182.231.116/~spencerf/university_of_albany/u_albany_alumni_menu_test.xml } { status code: 200, headers {
"Accept-Ranges" = bytes;
Connection = "Keep-Alive";
"Content-Length" = 0;
"Content-Type" = "application/xml";
Date = "Tue, 10 Mar 2015 22:28:04 GMT";
Etag = "W/\"218ceff-0-510f6aa0317dd\"";
"Keep-Alive" = "timeout=5, max=90";
"Last-Modified" = "Tue, 10 Mar 2015 22:28:03 GMT";
Server = Apache;
} }
2015-03-10 18:28:05.472 App[6289:97905] data: <>
Not sure why this is happening, and I can't tell the difference between when it works and when it doesn't what is changing? So Im not sure how to fix this?
What I want is it to get the data every time?
Thanks for the help in adavence.
Try to increase timeout interval for your request:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[request setTimeoutInterval:60.0];//seconds
Your code is fine. It is the server that is giving you trouble.
I reloaded the page that you mentioned 20 times. It successfully loaded 13 times. 7 times it returned no error but also an empty response body.
What you can do is simply check for this condition in your app and run the request again to try again.
Or talk to the server owner to find out if they can improve the reliability of this service.
Running your code on device, when there is no data received, error message is either null or Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost.".
As to the latter error, you can see AFNetworking/issues/2314, below is a comment extracted from the post.
when iOS client received HTTP response with a Keep-Alive header, it
keeps this connection to re-use later (as it should), but it keeps it
for more than the timeout parameter of the Keep-Alive header and then
when a second request comes it tries to re-use a connection that has
been dropped by the server.
You could also refer to the solution here, if you can't change the server, I'd suggest you retry the request when error code is -1005 or received data length is 0.
Perhaps Long-Polling would help you. The web server intermittently returns data when I tried it.
Long-Polling will allow you to continue sending requests until you get the data you specify back.
This code shows you an example of how implement long-polling
- (void) longPoll {
//create an autorelease pool for the thread
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
//compose the request
NSError* error = nil;
NSURLResponse* response = nil;
NSURL* requestUrl = [NSURL URLWithString:#"http://www.example.com/pollUrl"];
NSURLRequest* request = [NSURLRequest requestWithURL:requestUrl];
//send the request (will block until a response comes back)
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
//pass the response on to the handler (can also check for errors here, if you want)
[self performSelectorOnMainThread:#selector(dataReceived:)
withObject:responseData waitUntilDone:YES];
//clear the pool
[pool drain];
//send the next poll request
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
-(void) startPoll {
//not covered in this example: stopping the poll or ensuring that only 1 poll is active at any given time
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
-(void) dataReceived: (NSData*) theData {
//process the response here
}
Your URL returns 404 Not Found, so you should fix server part

Multiple POST request using ASIFormDataRequest synchronous

Basically my app will retrieve an array of Data from database and upload it to the server(one at a time/ one after the other). I want to stop all the request when one of the data failed to upload (please check on the comments).
Code:
// 0 means need to upload to server
NSString *condition = [NSString stringWithFormat:#"isUploaded=\"0\""];
// array of ID on my database to be uploaded
NSArray *arrayOfID = [Registered distinctValuesWithAttribute:#"registeredID" predicate:[NSPredicate predicateWithFormat:condition]];
// loop every index and upload it to server
for (int i=0; i<arrayOfID.count && !isBreak; i++) {
// get the entity using ID
NSString *condition = [NSString stringWithFormat:#"registeredID=\"%#\"",[arrayOfID objectAtIndex:i]];
Registered *entity = [Registered getWithPredicate:[NSPredicate predicateWithFormat:condition]];
if (entity) {
__weak ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[self setRequest:request withEntity:entity]; // set delegate,POST,etc.
[request setCompletionBlock:^{
// returns dictionary (success/failed)
NSDictionary *dict = [[request responseString] JSON];
if ([[dict valueForKey:#"status"] isEqualToString:#"success"]) {
// set IsUploaded to 1 after successful upload to server
[entity setIsUploaded:[NSNumber numberWithInt:1]];
[Registered commit];
// Any necessary ideas that would make my code better
// and continue the POST request and proceed to the next entity???
}
else {
// I want to cancel all the request here and get out to this loop
}
}];
[request setFailedBlock:^{
// I want to cancel all the request here and get out to this loop
}];
// start startSynchronous
[request startSynchronous];
} else {
[GlobalMethods ShowAlertView:#"Database Error" message:#"Please try again later"];
isBreak=YES;
}
}
You could tweak your code slightly and use the provided ASINetworkQueue class. From the documentation, if one request in the queue fails, by default the rest are cancelled automatically.
You can use a break statement to immediately exit a loop. However, because the callbacks are asynchronous, putting one in the failure block will not have the effect you're looking for...assuming the failure block is fired in reaction to a server response and not something in the API itself, that loop will complete and fire off all your requests--and the enclosing method will return--long before any of your requests has the time to come back from the server and call its failure block (this is networking code, after all). So you're not going to be able to use a failure block to interrupt the creation of additional requests in the loop; that's not how asynchronous calls work.

Are there any iOS libraries that allow a POST message to be sent asynchronously and reliably?

I'm wanting to send some data to a web service which takes POST data.
However, if the internet connection drops then I need to queue the data. I.e. cache it and send it at the next available opportunity.
Are there any iOS libraries out there that might help with this?
At the moment I'm planning to do this using ASIFormDataRequest and, if that fails, store the data using NSUserDefaults. Then, I assume I'd need to complete the process in the background by:
looking out for a connection using an NSOperation that flags up a connection with an NSNotification
read the data from NSUserDefaults
send this data
remove the data from NSUserDefaults
Doesn't seem like a huge amount of work but am I re-inventing the wheel here or is this the best way to proceed?
Please have a look at the AFNetworking Framework at Github, this may be what you are looking for: https://github.com/AFNetworking/AFNetworking
I usually end up using ASIFormDataRequest and use NSThread to detach a new thread as well for the call. I then make it call my own delegate/protocol once it's done:
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:#"some_url"]];
[request setUseKeychainPersistence:YES];
[request addRequestHeader:#"Content-Type" value:#"application/json"];
for (NSString *key in dictionary) {
[request setPostValue:[dictionary objectForKey:key] forKey:key];
}
[request startSynchronous];
if([request responseStatusCode] == 200)
{
NSString *response = [NSString stringWithFormat:#"%#", [request responseString]];
if ([self _isValidDelegateForSelector:#selector(requestSucceeded)]) {
[_delegate requestSucceeded];
}
if ([self _isValidDelegateForSelector:#selector(itemAdded:)]) {
[_delegate itemAdded:[response JSONValue]];
}
}
else {
if ([self _isValidDelegateForSelector:#selector(requestFailedWithError:)])
NSLog(#"%#", [request debugDescription]);
[_delegate requestFailedWithError:[request error]];
}
This is all done in my own 'API' object that contains all the calls to the webs service, and hence why I called startSynchronous instead of asynchronous as I handle this in a different thread anyway.

Resources