Pause/Resume downloads in Objective-C - iphone

Alright. Hopefully this will be my last post about the download manager I am writing in Objective-C. Everything seems to work well except the pause/resume functionality. My issue is that when a download tries to continue from where it left off, it appends the data it receives to the file, but it still seems that it's trying to download the entire file. This results in a file that is larger than the original file is supposed to be. Here is the code I am using for downloading files. Am I doing something wrong?
-(void)start:(unsigned int)fromByte {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:DEFAULT_TIMEOUT];
// Define the bytes we wish to download.
NSString *range = [NSString stringWithFormat:#"bytes=%i-", fromByte];
[request setValue:range forHTTPHeaderField:#"Range"];
// Data should immediately start downloading after the connection is created.
self.urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:TRUE];
if (!self.urlConnection) {
#warning Handle error.
}
}

I see you are specifying the Range header in the request. First thing to check is whether the server is actually honoring the Range request, by checking the headers in the response object (which should be an NSHTTPURLResponse) in connection:didReceiveResponse: for a proper Content-Range.

I finally figured this out. It turns out that the 'getFilesizeInBytes' method I had was get the NSFileSize object from the file's attributes, but I was directly casting this to an int. This caused the number to be about 20 times larger than it should have been. I was able to fix this by using [#"" intValue]. Once this was fixed, the servers were able to give me the rest of the file starting with the correct byte. It seems that before my issue was not that the server wasn't honoring my request, but that it couldn't honor my request due to me requesting data that was well beyond the final byte of the file.

There's no support for pause/resume in NSURLConnection. You can emulate it by stopping the request, then issuing a request for the rest of the content with a Range header on resume. Some support from the HTTP server is required for that, and is not guaranteed.
Looks like lack of support at the server is what you're facing.
Most download managers, however, implement their own HTTP stack on top of sockets.

Related

No request information sent with ASIHTTPRequest

I am experiencing an issue on iOS 4.3+ with ASIHTTPRequest where a request is fired but no data (Request methed, url, headers, etc) reaches the server. The connection times out because it never hears back.
The server hears the empty request (after some delay), then hears a valid request which is of course never reported to higher level code because the connection has timed out. This is all kind of strange because the request was not configured to resend data.
Often this happens after the app has been pushed to the background for some time (15 min or more) and the phone has been allowed to sleep.
My configuration of the request is as follows:
NSMutableData *postData = nil;
NSString *urlString = [NSString stringWithFormat:#"%#%#",[self baseURL],requestPath];
OTSHTTPRequest *request = [OTSHTTPRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setCachePolicy:ASIFallbackToCacheIfLoadFailsCachePolicy];
[request setTimeOutSeconds:45];
//Set up body
NSString *queryString = [self _RPcreateQueryString:query];
if ([queryString length]>0) {
if(method == RPAsyncServerMethodPost || method == RPAsyncServerMethodPut){
postData = [[[queryString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES] mutableCopy] autorelease];
}else{
NSURL *url = [NSURL URLWithString:[urlString stringByAppendingFormat:#"?%#",queryString]];
[request setURL:url];
if (!url) return nil; //url String malformed.
}
}
// ... ///
// method setting stripped for brevity
[request addRequestHeader:#"Content-Type" value:#"application/x-www-form-urlencoded"];
if(headers){
for (NSString* head in headers) {
if ([[headers valueForKey:head] isKindOfClass:[NSString class]])
[request addRequestHeader:head value:[headers valueForKey:head]];
}
}
[request addRequestHeader:#"Content-Length" value:postLength];
[request setPostBody:postData];
OTSHTTPRequest is simply a subclass of ASIHTTPRequest that contains properties for a string tag, a pretty description, and other bling for use by consuming objects and does not override any ASI stuff.
Can anyone shed a light on why/how ASI could open a connection and then send absolutely nothing for minutes at a time?
Edit: Just to clarify. The connections DO make contact with the server, it just never sends any data through the connection from what my server logs can tell. This seems to always happen on app wake and effects all connections including NSURLConnections spawned by MapKit. the whole app just seems to loose its marbles.
I also see a bunch of background tasks ending badly right before this, but i can never catch them while in the debugger.
It doesn't look like you are starting your request based on the code that you have provided. Try call the -[startSynchronous] or -[startAsynchronous] methods of your OTSHTTPRequest object after you are done setting its various properties.
Are you setting the delegate, either I overlooked it or you stripped it out.
I didnt want to say anything till a few days passed with out the issue. The solution in this case was very obscure.
It appears the version of TestFlight i was using has a bug in it that may have contributed to this issue. Since its removal i have not experienced the issue.

NSMutableURLRequest adding extra information to the body, making Content-Length incorrect

I'm using an NSMutableURLRequest to perform a simple file upload from iOS5 to a custom server. When I send the request, I have it print out the length of bodyContents, which contains the body of the request. For the particular request I am working on, it will say that the length is 46784. I don't set the content-length, since I found out it was doing that automatically, but I pull the info anyway in case I need it later. The headers being received by the server say that the content-length is 46784. When I do a character count on the body at the server end, it tells me that the length of the body is 46788, 4 too many. I ran another request, again, the request had 4 more characters than what I sent.
At first, I thought it had to be on the server side, just because it didn't make any sense. So I sent a request from a test program that I know works to the server, and checked the length of the body it sent against the content-length header and they were identical.
What is the best way to deal with this? Should I manually set the content-length to 4+[bodyContents length]? I don't think that would be what I would want to do, just because I shouldn't have to add extra space for data I don't know that I want.
Here is the code that sends the request
-(void)send:(id)delegate
{
bodyContents = [bodyContents stringByAppendingFormat:#"--%#--",boundry];
NSInteger length = [bodyContents length];
NSLog(#"Length is %i",length);
[request setHTTPBody:[bodyContents dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:delegate];
if(connection)
{
NSLog(#"Connection good");
}
else
{
NSLog(#"Connection bad");
}
}
I set the content-type property of the request in the init method of the class.
Any ideas?
When you specify an encoding, the size of the contents can change... get the size after you set the HTTPBody and then see if that adjusts things up by four bytes automatically.

Invalid S3 signature error when using server-side encryption and ASIHTTPRequest from an iPhone app

I have an iPhone application I've been using for some time that uses ASIHTTPRequest to upload videos to a bucket on Amazon S3. It has been functioning well without any problems. Recently, we decided to make use of the new "server-side encryption" that Amazon has implemented. This allows you to tell Amazon's server to encrypt files that have been posted to a bucket automatically by including an additional HTTP request header.
I have added a single line of code to my application to implement this, but now my Amazon uploads are failing. The specific error message that is appearing is:
"The request signature we calculated does not match the signature you provided. Check your key and signing method."
The name of the bucket I am using conforms to Amazon's naming standards, so I am confident that is not the issue. I am also confident that the secret and public keys I am using are correct.
It would appear that adding this header is somehow breaking the signature calculation, I am assuming because it is being included in the calculation on one side of the transmission but not the other.
Am I doing this incorrectly? Or is this a bug in ASIHTTPRequest?
Here is my code for reference:
[ASIS3Request setSharedSecretAccessKey:#"mysecretkey"];
[ASIS3Request setSharedAccessKey:#"myaccesskey"];
NSString *bucketPath = [NSString stringWithFormat:#"mypath/filename"];
ASIS3ObjectRequest *request = [ASIS3ObjectRequest PUTRequestForFile:filepath withBucket:#"my-bucket" key:bucketPath];
// If the following line is commented, the upload completes successfully
[request addRequestHeader:#"x-amz-server-side-encryption" value:#"AES256"];
////
request.requestScheme = ASIS3RequestSchemeHTTPS;
[request setShouldContinueWhenAppEntersBackground:YES];
[request startSynchronous];
if ([request error])
{
// The error messag is being displayed here
NSLog(#"xmit error: [%#]",[[request error] localizedDescription]);
}
You're doing everything right, the issue is that constructing the Authorization header (i.e. "the request signature") involves signing a string which includes all of the x-amz- headers; you've added one such header (x-amz-server-side-encryption), but you've not caused it to be factored into the signature.
I just created a branch of ASIHTTPRequest with support for SSE. If you use that branch, you should just be able to say [request setUseServerSideEncryption:YES];. Alternately, if you're more interested in the technique, here are the details of making it work.
I'm having the same issue, however I'm not using encryption. But what I found so far is that upper case letters get the wrong signature.
In my case I'm setting the storage class with ASIS3StorageClassReducedRedundancy that has a value of #"REDUCED_REDUNDANCY" which is in uppercase. If I don't set this option the request goes successful.
So maybe your problem is with the value AES256 that has uppercase letters.

problems with iphone code to check server file date

I want to check the dates on files -- zip, jpg, or whatever -- on my server and, if they are newer than the copies on my iPhone, to download them.
I wrote the following method based on a post here that's about a year old. It has two problems:
+ (NSString *) f_GetServerFileDate:(NSString *)MyURL {
NSURL *oURL = [NSURL URLWithString:MyURL];
NSURLRequest *oRequest = [NSURLRequest requestWithURL:oURL];
NSHTTPURLResponse *oResponse;
[NSURLConnection sendSynchronousRequest:oRequest returningResponse:&oResponse error:nil];
if ( [oResponse respondsToSelector:#selector( allHeaderFields )] ) {
NSDictionary *metaData = [oResponse allHeaderFields];
return [metaData objectForKey:#"Last-Modified"];
} else {
return #"00000000";
}
}
Problem 1: It returns "00000000" when given "http://www.mysite.com/myzip.zip" as a URL.
Problem 2: For an Active Server Page (just a test; not that I'd really download one) it returns a date that has no bearing on the date the file was last uploaded or modified.
What's the right way?
This is probably less a problem of your iPhone code but rather of the web server. A web server is not required to include the Last-Modified header attribute. And for dynamic pages (such as ASP pages) it is correct to return the date when the page was dynamically generated (i.e. the current date) and not the date when the page was developed or deployed.
I suggest you use a browser extension such as Live HTTP headers for Fireofx to observe what HTTP header attributes the web server returns. If the date is missing, you'll be out of luck (unless you have access to the web server and can fix it there).
Furthermore, your code will always download the image no matter when it has been last modified. You can prevent this if you include the HTTP header If-Modified-Since in your request. That way the web server will send the image if it has been modified since the specified date or just send a 304 (Not modified) result code if the image is still up to date. But again: It depends on the web server if this option is supported and it only works for static content unless the author of the web application has specifically implemented it.

iPhone URL Request: Add value to HTTP header field

I'm trying to add a value to the header for a URL request.
Something like this works just fine:
[urlRequest addValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
But this doesn't even show up in the header:
NSString *authString = [[NSString alloc] initWithString:
[defaults objectForKey:#"auth"]];
[urlRequest addValue:authString forHTTPHeaderField:#"iphoneID"];
I'm completely stumped. The auth string is around 90 characters long. Is this a problem?
Edit:
Here's the code I'm trying:
NSString *authString = [[NSString alloc] initWithString:[defaults objectForKey:#"auth"]];
[urlRequest addValue:authString forHTTPHeaderField:#"iphoneid"];
[urlRequest addValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
I can see the Accept-Encoding header being sent through Wireshark, but iphoneid is nowhere to be found. It's just a string, 80-90 characters long.
Another Update:
So it seems that the problem isn't the field "iphoneid" but rather the authString I'm trying to pass into it. Other strings that I just create with the #"something" work fine, but the auth string that I pull from NSUserDefaults doesn't appear.
Suggestions on how to debug this?
The true problem.
The string I was pulling from NSUserDefaults already had a line ending. When set as a header, another \r\n is appended, which apparently isn't valid. Thus, the header wouldn't appear in any outgoing packets.
The fix:
Use this to trim off the characters before setting as a header value.
[yourString stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
Testing checklist:
Verify that you actually have a NSMutableURLRequest (and not a NSURLRequest) at this point. In particular, check your logs for an exception due to "unrecognized selector."
Verify that urlRequest is not nil.
Switch to setValue:forHTTPHeaderField: rather than addValue:forHTTPHeaderField:.
Swap the forHTTPHeaderField: value to #"Accept-Encoding" to see if the field is the problem
Swap #"gzip" for auth to see if the value is the problem.
You need Charles web proxy, to see what header values are really outbound and inbound. Then you can see if the problem is really in your code or some magic on the server discarding things.
There's a free trial, and after you install it if you hit record any traffic the simulator sends will go through the proxy. Very nice.
http://www.charlesproxy.com/