NSURLConnection can be used to calculate the md5 on-the-fly:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)theData
// theData is a small piece
NSURLSessionDownloadTask is an "upgrade" of NSURLConnection. But how can we check the md5 without read through the whole file again after it is downloaded? Its interface is like:
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request
completionHandler:
^(NSURL *location, NSURLResponse *response, NSError *error) {
// the whole file is downloaded and saved at location.
}];
The key requirement here is low memory footprint and the file has to be downloaded completely.
If you want the data to arrive in small NSData pieces that you can examine and append to a larger NSMutableData bit by bit, as you did with connection:didReceiveData:, ask for a data task instead of a download task.
You call dataTaskWithRequest:, supply a delegate, and start the data task (with resume) - and the delegate receives URLSession:dataTask:didReceiveData:, exactly like in the old NSURLConnection days.
Here's a complete working example (except that I don't tell you what to do with the bits of data as they arrive):
- (NSURLSession*) configureSession {
NSURLSessionConfiguration* config =
[NSURLSessionConfiguration ephemeralSessionConfiguration];
config.allowsCellularAccess = NO;
NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
return session;
}
- (IBAction) doHTTP: (id) sender {
if (!self.session)
self.session = [self configureSession];
NSString* s = // some URL string
NSURL* url = [NSURL URLWithString:s];
NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
NSURLSessionDataTask* task = [[self session] dataTaskWithRequest:req];
self.task = task;
[task resume];
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(#"received %lu bytes of data", (unsigned long)data.length);
// do something with the data here!
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(#"completed; error: %#", error);
}
As Matt said, you can use a data task, which lets you easily see the data as it is downloaded.
However, if you want to observe a download task, you can accomplish a similar thing if you are willing to take a few small risks.
I'm sure I'll get a million down votes for the following, but just remember... when you need a screwdriver, and all you have is a hammer, you flip the hammer over, and use it as a screwdriver... or bang on the screw so much that it turns into a nail...
First, I think the API is broken. The delegates should provide at least one of these two things. If you agree, file a radar. The delegate should provide the temporary file (much less preferred - I think it should remain opaque) or it should provide the NSData that is being written in URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: -- this is the right answer.
Anyway, if you are willing to use an undocumented, unofficial approach...
The temporary files are stored in Library/Caches/com.apple.nsnetworkd/ so you can easily look in there and determine which files are being used as the temporary destination.
Or, you can, again unofficially, determine the temporary file by canceling the download with cancelByProducingResumeData: and then unarchiving the resume data blob -- the resume data blob is currently an archived dictionary -- and get the file path from the dictionary. Then, you can resume the download, knowing which temporary file is being used for the download.
Anyway, once you have the file, inside your URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: you can then just read the most recently written chunk from the file.
Now, having said that, you may want to just use a data task, because it will officially provide you the piece of data it just downloaded... but you can resort to this hack to get at the data that was downloaded to the file, if you must do a background download -- which must be down with a download task.
One problem you may have is that the file IO may be buffered, so what's been actually flushed (and available form a separate file descriptor) may be different from what has been reported in the delegate method. You may just need to keep track of the last byte you read, and inside that delegate, just read from there to the current end of the file...
Your mileage will most certainly vary, but it will give you access to the data as it is being written to the file.
You will have to do the same thing for URLSession:downloadTask:didFinishDownloadingToURL: to get the final piece of data.
Related
how can I download a Cocoa .bundle file from a server and then load it into a app? I've tried using a zip but the shouldDecodeSourceDataOfMIMEType function doesn't get called.
- (IBAction)testDownload:(id)sender {
// Create the request.
NSURLRequest *theRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://localhost/SampleBundle.zip"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create the connection with the request and start loading the data.
NSURLDownload *theDownload = [[NSURLDownload alloc] initWithRequest:theRequest
delegate:self];
if (theDownload) {
// Set the destination file.
[theDownload setDestination:#"/Users/developer/Desktop/Test-Downloads/SampleBundle" allowOverwrite:YES];
} else {
// inform the user that the download failed.
}
}
- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
{
download = nil;
// Inform the user.
NSLog(#"Download failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)downloadDidFinish:(NSURLDownload *)download
{
download = nil;
// Do something with the data.
NSLog(#"%#",#"downloadDidFinish");
}
- (BOOL)download:(NSURLDownload *)download shouldDecodeSourceDataOfMIMEType:(NSString *)encodingType;
{
BOOL shouldDecode = YES;
NSLog(#"EncodingType: %#",encodingType);
return shouldDecode;
}
So how can I download a .bundle from a server uncompress it and load it into the application?
Thanks
According to the documentation of download:shouldDecodeSourceDataOfMIMEType:
This method is not called if the downloaded file is not encoded.
So, I'm guessing that might have something to do with it. You're probably better off implementing download:didReceiveResponse: and examining the NSURLResponse object, especially the status code -- if that's not 200 than something is going wrong and you need to look at HTTP codes to see what exactly the problem is.
Also, I'm not sure of this, do you need elevated permissions to install a bundle, it being an executable?
The shouldDecodeSourceDataOfMIMEType delegate works great but only on gzip (.gz) archives. I've tested extensively with both .zip & .gz.
It should also be noted that it doesn't call tar, so if you applied compression and tar at the same time as in:
tar czvf ArchiveName.tar.gz ./ArchiveName/
the shouldDecodeSourceDataOfMIMEType delegate will leave you with:
ArchiveName.tar
So, the archive will not be immediately useable.
For .zip archives, as others have pointed out, your best bet is MiniZip (C API) or a Objective-C framework based on it like ZipArchive (2005) or the more recent SSZipArchive (2013).
My user enters a recipients address (Street address not email). I need to verify it with the USPS so I know that it is actually an address.
I am digging through their API right now and I think I understand it but I'm not exactly sure how to go about it with objective-c.
So pretty much it works like so:
I have to create an XML request that contains the recipient name, address, and zip code.
I have to post that to their server
They respond with an XML response
Here is an example of what one of their constructed XML request looks like:
http://SERVERNAME/ShippingAPITest.dll?API=Verify&XML=<AddressValidateRequest% 20USERID="xxxxxxx"><Address ID="0"><Address1></Address1>
<Address2>6406 Ivy Lane</Address2><City>Greenbelt</City><State>MD</State> <Zip5></Zip5><Zip4></Zip4></Address></AddressValidateRequest>
A bit garbled but broken down:
http://SERVERNAME/ShippingAPITest.dll?API=Verify&XML=
<AddressValidateRequest% 20USERID="xxxxxxx">
<Address ID="0">
<Address1></Address1>
<Address2>6406 Ivy Lane</Address2>
<City>Greenbelt</City>
<State>MD</State>
<Zip5></Zip5>
<Zip4></Zip4>
</Address>
</AddressValidateRequest>
My first idea seems obvious but there maybe a better way to go about it. Since the XML feed short, should I go about construction by simple doing something along the lines of:
NSString *request = [NSString stringWithFormat:#"......"]
Where it is filled in and formatted along the lines posted above.
The second question is how to go about correctly sending this to the server?
I simply create a NSURL request and with the URL as the constructed XML string?
Here what I have but I keep getting that the URL was constructed wrong:
- (void)verifyAddress:(Recipient*)_recipient {
NSURL *_url = [NSURL URLWithString:#"http://testing.shippingapis.com/ShippingAPITest.dll?API=Verify&XML=<AddressValidateRequest%20USERID=\"********\"><Address ID=\"0\"><Address1></Address1><Address2>6406 Ivy Lane</Address2><City>Greenbelt</City><State>MD</State><Zip5></Zip5><Zip4></Zip4></Address></AddressValidateRequest>"];
// Create the request.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:_url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData to hold the received data.
// receivedData is an instance variable declared elsewhere.
receivedData = [NSMutableData data];
NSString* newStr = [[NSString alloc] initWithData:receivedData
encoding:NSUTF8StringEncoding];
NSLog(#"the response '%#'", newStr);
} else {
// Inform the user that the connection failed.
NSLog(#"error");
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// This method is called when the server has determined that it
// has enough information to create the NSURLResponse.
// It can be called multiple times, for example in the case of a
// redirect, so each time we reset the data.
// receivedData is an instance variable declared elsewhere.
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Append the new data to receivedData.
// receivedData is an instance variable declared elsewhere.
[receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
// inform the user
NSLog(#"Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString* newStr = [[NSString alloc] initWithData:receivedData
encoding:NSUTF8StringEncoding];
NSLog(#"the response '%#'", newStr);
// do something with the data
// receivedData is declared as a method instance elsewhere
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
}
I get the following error:
Connection failed! Error - bad URL (null)
My only question now is, am I doing everything ok as far as NSURLConnection goes? I can play around with the URL, I just want to make sure my implementation is ok so Im not running around in circles. :P
You have % 20 in your URL. It should be %20 (no space).
There may be other problems, but that was one was easy to spot. If you are getting an error message, you need to edit your question and paste in the exact error message.
Also, you might consider using Apple's NSURLRequest and NSURLConnection classes, because more people are likely to be familiar with them so it may be easier for you to find help.
Cory, I work in the address validation industry (for SmartyStreets, where what you're trying to do is our specialty in fact) and have seen a lot of similar issues to yours.
We actually used to support an XML-endpoint for our address verification API (LiveAddress). Last year we deprecated it and deployed a new JSON format because the XML was clunky to use and had a lot of problems when it's actually just a simple task (for you, the developer).
So a few things to keep in mind... and while Rob's answer is programmatically comprehensive, these are important to consider also:
The USPS is the official source of addresses for the USA, but its core domain is not providing API service. Especially with recent financial troubles, I suspect that support and maintenance of the API will wane over time.
The License Agreement for the API you're using is quite restrictive. For example:
User agrees to use the USPS Web site, APIs and USPS data to facilitate USPS shipping transactions only. [27 Jan 2012]
Meaning, if you're shipping mail or packages via the USPS by using their API, it's fine, but for any other purpose it's not allowed and it violates the TOS.
I see you're developing for iOS. There's a great JSON library for that called TouchJSON that, in my opinion, is easier to use than XML formats in Objective-C.
While the USPS service does work, they CASS-certify private entities to provide their data at a better value (more specialty, experience, features, etc).
These and other maladies can be remedied by service from a third-party vendor. More details and reasons are documented here. Which provider you choose is up to you, but I'll be happy to personally answer any other address-validation-related questions.
I'm trying to solve the general situation of receiving a big chunk of data from web services via:
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
For small data, I just use:
[webData appendData:data];
and at the end, in:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
I write it to my file using an output stream.
What about when the data is huge, a couple of MBs of data? In this case, it would be better to dump data in connection:didReceiveData: several times to the same file. It sounds easy, using NSFileHandle, but syncing for read and write while considering async/non-blocking UI is not straightforward (at least for me).
Any idea what would be a good approach for this case?
I was thinking to use NSThread, queueing the chunk size each time. Is there any better/easier approach for this?
You know, I actually made a class for this a while back (non-ARC, but it still should work) available here
Example usage:
// in init
RJRStreamWriter myWriter = [[RJRStreamWriter alloc] initWithLocalFile:#"path/to/my/file" andAppend:NO];
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
static dispatch_queue_t asyncQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
asyncQueue = dispatch_queue_create("asyncQueue", NULL);
});
dispatch_async(asyncQueue, ^{
// this method is optimized, and aggregates NSOutputStream, NSMutableData, and NSFileHandle
[myWriter writeData:data];
});
}
For a start, a couple of MBs is not huge. Doing it the same way for a couple of MBs would be absolutely fine. But if you do want to write to the file as you receive the data then you could use an NSFileHandle. Create one and then write data to it and then call synchronizeFile on it when you want to flush the data to disk. You could call synchronizeFile after say every 10MB have been written or something (and obviously at the end as well).
So e.g.:
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filename];
...
[fileHandle writeData:dataComeInFromURLConnection];
...
[fileHandle synchronizeFile];
I have a Cocoa Mac application set up to download files to a specific folder using NSURLDownload. This works great with a single download at a time. However, if I attempt to start multiple downloads, all but the last will fail immediately.
Is there any way to use NSURLDownload for multiple simultaneous downloads? Or what would be a good way to queue up multiple URLs to be downloaded in order? Or is there a more appropriate way to accomplish this (NSURLConnection seemed possible but I was unsure if I could set the download location and filename as I can with NSURLDownload)?
Each NSURLDownload represents a single downloading instance. You're probably trying to reuse the same one multiple times. It's an inherently asynchronous system that already used background threads. Here's an example based on Apple's sample code:
- (void)startDownloadingURL:sender
{
// Create a couple requests.
NSURLRequest *requestOne = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.apple.com"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSURLRequest *requestTwo = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://stackoverflow.com"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create two download instances
NSURLDownload *downloadOne = [[NSURLDownload alloc] initWithRequest:requestOne delegate:self];
NSURLDownload *downloadTwo = [[NSURLDownload alloc] initWithRequest:requestTwo delegate:self];
if (downloadOne) {
// Set the destination file.
[downloadOne setDestination:#"/tmp" allowOverwrite:YES];
} else {
// inform the user that the download failed.
}
if (downloadTwo) {
// Set the destination file.
[downloadTwo setDestination:#"/tmp" allowOverwrite:YES];
} else {
// inform the user that the download failed.
}
}
- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
{
// Release the connection.
[download release];
// Inform the user.
NSLog(#"Download failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSErrorFailingURLStringErrorKey]);
}
- (void)downloadDidFinish:(NSURLDownload *)download
{
NSLog(#"The download %# has finished.", download)
// Release the download connection.
[download release];
}
If you attempt to use the same NSURLDownload for both NSURLRequests, then it will kill the previous connection.
I'd second using NSOperation if your on 10.5+ or greater. You could just throw 1 operation onto a queue for each download. Or you could even just use sendSynchronous request and use it with NSOperationQUeue's addOperationWithBlock (10.6+) method and then in your block you are throwing onto the queue you can just use [[NSOperationQueue mainQueue] addOperationWithBlock:^{ when you are doe with the code you need to execute or just periodically need to refresh the UI on the main thread, like so...
[myQueue addOperationWithBlock:^{
//download stuff here...
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//update main thread UI
}];
}];
you would just need to do this for each download.
If you're targeting 10.5+, you should look at NSOperation. It should allow you to build a generic solution for a single download, and then use the built-in queue facilities to manage dependencies if you require certain operations finish downloading before others begin.
Keep in mind that these APIs often expect delegate methods involved to be run on the main thread, so you'll need ensure that occurs if you're working with asynchronous APIs that function via delegate methods. (You can do this pretty simply by using performSelectorOnMainThread: and friends)
Basically what's happening is that I need to download a whole bunch of files in my app and I've set up a queue of sorts that downloads each file with an NSURLConnection and stores the server response incrementally in an NSMutableData until the download is finished and then writes the whole thing to disk.
Here's the relevant parts:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)_response {
response = [_response retain];
if([response expectedContentLength] < 1) {
data = [[NSMutableData alloc] init];
}
else {
data = [[NSMutableData dataWithCapacity:[response expectedContentLength]] retain];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)_data {
[data appendData:_data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"saved: %#", self.savePath);
[data writeToFile:self.savePath atomically:YES];
}
Any insights as to why this would be awfully slow? It's pretty bad with the Simulator and gets even worse on an actual device. My maximum download size is around 2 megabytes, so I figured storing the whole thing in memory until it finishes wouldn't be that bad of an idea. This gets up to about 20KB/s at best (with a direct ad-hoc wifi connection).
Edit: in all my test cases I do get a Content-Length header, so it's not a matter of growing the NSMutableData with each bit of response received.
Edit 2: this is all Shark gives me.
Edit 3: So this is how I set up the connection
NSMutableURLRequest *request = [[NSMutableURLRequest requestWithURL:[NSURL URLWithString:[#"http://xxx.xxx.xxx.xxx/index.php?service=" stringByAppendingString:service]]] retain];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:[[options JSONRepresentation] dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
Of course I don't actually have a hardcoded url and both request and conn are instance variables of the downloader class. Not that it should matter, but for JSON I'm using http://code.google.com/p/json-framework/. Options and service are method parameters (NSString and NSDictionary), not that those should matter either.
Boy this is embarrassing. Turns out my Content-Length header was inaccurate, which resulted in NSURLConnection needing to wait for some sort of timeout before it would finish, even though it had all the data. Makes sense really. Maybe this will help someone else out.
I would profile to find out where the slow down is occurring and in what pattern. Put a log statement in connection:didReceiveData to see record how often it is called. You're looking for:
The relative elapsed time between calls to the method.
Whether the time between calls increases as the app runs.
If the elapsed time between calls is where the app spends most of its time then the bottleneck is in the request itself. Either the request is misconfigured of the server is not sending quickly.
If the time between calls increases the longer the app runs, then it is probably a memory issue. As the data grows larger and the memory more constrained, the app has to swap more stuff in and out of memory which slows everything down. To test, log the various didReciveMemoryWarning methods in any active objects.
Update:
According to Shark, the problem is in your URL request and not the code you posted. You need to look at how you set up the request.