views:

283

answers:

1

Hi,

I would like to download files directly from an URL to the disk using objective-c on the iPhone os.

Currently I am using NSURLConnection to send a synchronousRequest, writing the returned NSData into a file.

How can I change the download handling (still having the request beeing synchronous, it is already in a background thread) to write the data directly to disk, not using memory variables to store the complete content (only small parts)?

A sample code would be appreciated.

Thank you all in advance for your responses!

+4  A: 

You can do this, but it's a bit complicated to set up. Here's how I'd do it:

warning: the following code was typed in a browser and compiled in my head. Also, there's not a lot of error handling. Caveat Implementor.

//NSURLConnection+DirectDownload.h
@interface NSURLConnection (DirectDownload)

+ (BOOL) downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError **)error;

@end

//NSURLConnection+DirectDownload.m
@implementation NSURLConnection (DirectDownload)

+ (BOOL) downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError **)error {
  NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:url];
  //configure the request, or leave it as-is

  DirectDownloadDelegate * delegate = [[DirectDownloadDelegate alloc] initWithFilePath:localPath];
  NSURLConnection * connection = [[NSURLConnection alloc] initWithRequest:request delegate:delegate];
  [delegate autorelease];
  [request release];

  while ([delegate isDone] == NO) {
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
  }

  [connection release];

  NSError * downloadError = [delegate error];
  if (downloadError != nil) {
    if (error != nil) { *error = [[downloadError retain] autorelease]; }
    return NO;
  }

  return YES;
}

//DirectDownloadDelegate.h
@interface DirectDownloadDelegate : NSObject {
  NSError *error;
  NSURLResponse * response;
  BOOL done;
  NSFileHandle * outputHandle;
}
@property (readonly, getter=isDone) BOOL done;
@property (readonly) NSError *error;
@property (readonly) NSURLResponse * response;

@end

//DirectDownloadDelegate.m
@implementation DirectDownloadDelegate
@synthesize error, request, done;

- (id) initWithFilePath:(NSString *)path {
  if (self = [super init]) {
    if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
      [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
    }
    [[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil];
    outputHandle = [[NSFileHandle fileHandleForWritingAtPath:path] retain];
  }
  return self;
}

- (void) dealloc {
  [error release];
  [response release];
  [outputHandle closeFile];
  [outputHandle release];
  [super dealloc];
}

- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)anError {
  error = [anError retain];
  [self connectionDidFinishLoading:connection];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)someData {
  [outputHandle writeData:someData];
}

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)aResponse {
  response = [aResponse retain];
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
  done = YES;
}

The basic idea is that you create a standard NSURLConnection, which is normally asynchronous, but just block the thread by spinning the runloop yourself until the connection is done. You also use a custom url connection delegate to just pipe any data the connection receives directly to a file.

You can now do:

NSError * downloadError = nil;
BOOL ok = [NSURLConnection downloadItemAtURL:someURL toFile:someFile error:&downloadError];
if (!ok) {
  NSLog(@"ack there was an error: %@", error);
} else {
  NSLog(@"file downloaded to: %@", someFile);
}
Dave DeLong
Very nice idea! An endless loop ( ugh! ;) ) to make it synchronous. It is a good solution, thank you very much! It's also admirable if you really written it completely in the browser :-)
favo
well, to be honest, I had to do something like this last week and was going off what I remembered... ;)
Dave DeLong
Provided you're careful that any callers are happy for the main run loop to run inside that function (in particular, if you're inside an event handler, you'll be handling other events before the processing for the current event completes. Eww.)
tc.
@tc agreed; I wrote this to be used on a secondary thread. Using this on the main thread is not a Good Idea™.
Dave DeLong