Home > Enterprise >  File loading doesn't work with NSURLSession via FTP
File loading doesn't work with NSURLSession via FTP

Time:09-28

Using URL session FTP download is not working. I tried using below code.

Approach 1

NSURL *url_upload = [NSURL URLWithString:@"ftp://user:[email protected]:/usr/path/file.json"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url_upload];
[request setHTTPMethod:@"PUT"];

NSString *docsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSURL *docsDirURL = [NSURL fileURLWithPath:[docsDir stringByAppendingPathComponent:@"prova.zip"]];

NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.timeoutIntervalForResource = 60.0;
sessionConfig.allowsCellularAccess = YES;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
NSURLSession *upLoadSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionUploadTask *uploadTask = [upLoadSession uploadTaskWithRequest:request fromFile:docsDirURL];
[uploadTask resume];

Approach 2

NSURL *url = [NSURL URLWithString:@"ftp://121.122.0.200:/usr/path/file.json"];
NSString * utente = @"raseen";
NSString * codice = @"raseen";
NSURLProtectionSpace * protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:url.host port:[url.port integerValue] protocol:url.scheme realm:nil authenticationMethod:nil];

NSURLCredential *cred = [NSURLCredential
                         credentialWithUser:utente
                         password:codice
                         persistence:NSURLCredentialPersistenceForSession];


NSURLCredentialStorage * cred_storage ;
[cred_storage setCredential:cred forProtectionSpace:protectionSpace];

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.URLCredentialStorage = cred_storage;
sessionConfiguration.allowsCellularAccess = YES;

NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];

NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];

The error I get is as follows:

the requested url is not found on this server

But the same url is working in terminal with SCP command and file is downloading successfully

CodePudding user response:

First of all, you should consider switching from ftp to sftp or https protocol, since they are much more secure and address some other problems.

Having that said, ftp protocol is not strictly prohibited in iOS (unlike, say, http), and you still can use it freely. However NSURLSession is not designed to work with ftp-upload tasks out of the box. So you either have to implement a custom NSURLProtocol which adopts such a request or just use other means without NSURLSession.

Either way you will have to rely on the deprecated Core Foundation API for FTP streams. First create a CFWriteStream which points to the destination url on your ftp-server like this:

CFWriteStreamRef writeStream = CFWriteStreamCreateWithFTPURL(kCFAllocatorDefault, (__bridge CFURLRef)uploadURL);
NSOutputStream *_outputStream = (__bridge_transfer NSOutputStream *)writeStream;

And specify the user's login and password in the newly created object:

[_outputStream setProperty:login forKey:(__bridge NSString *)kCFStreamPropertyFTPUserName];
[_outputStream setProperty:password forKey:(__bridge NSString *)kCFStreamPropertyFTPPassword];

Next, create an NSInputStream with the URL to the source file you want to upload to (it's not neccesarily, to bound the input part to the streams API, but I find it consistent, since you anyway have to deal with streams):

NSInputStream *_inputStream = [NSInputStream inputStreamWithURL:fileURL];

Now the complicated part. When it comes to streams with remote destination, you have to work with them asynchronously, but this part of API is dead-old, so it never adopted any blocks and other convenient features of modern Foundation framework. Instead you have to schedule the stream in a NSRunLoop and wait until it reports desired status to the delegate object of the stream:

_outputStream.delegate = self;
NSRunLoop *loop = NSRunLoop.currentRunLoop;
[_outputStream scheduleInRunLoop:loop forMode:NSDefaultRunLoopMode];
[_outputStream open];

Now the delegate object will be notified about any updates in the status of the stream via the stream:handleEvent: method. You should track the following statuses:

  • NSStreamEventOpenCompleted - the output stream has just established connection with the destination point. Here you can open the input stream or do some other preparations which became relevant shortly before writing the data to the ftp server;
  • NSStreamEventHasSpaceAvailable - the output stream is ready to receive the data. Here is where you actually write the data to the destination;
  • NSStreamEventErrorOccurred - any kind of error what may occur during the data transition / connection. Here you should halt processing the data.

Be advised that you don't want to upload a whole file in one go, first because you may easily end up with memory overflow in a mobile device, and second because remote file may not consume every byte sent immediately. In my implementation i'm sending the data with chunks of 32 KB:

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {   
    switch (eventCode) {
        case NSStreamEventOpenCompleted:
            [_inputStream open];
            return;
        case NSStreamEventHasSpaceAvailable:
            if (_dataBufferOffset == _dataBufferLimit) {
                NSInteger bytesRead = [_inputStream read:_dataBuffer maxLength:kDataBufferSize];
                
                switch (bytesRead) {
                    case -1:
                        [self p_cancelWithError:_inputStream.streamError];
                        return;
                    case 0:
                        [aStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];
                        // The work is done
                        return;
                    default:
                        _dataBufferOffset = 0;
                        _dataBufferLimit = bytesRead;
                }
            }
            
            if (_dataBufferOffset != _dataBufferLimit) {
                NSInteger bytesWritten = [_outputStream write:&_dataBuffer[_dataBufferOffset]
                                                    maxLength:_dataBufferLimit - _dataBufferOffset];
                if (bytesWritten == -1) {
                    [self p_cancelWithError:_outputStream.streamError];
                    return;
                } else {
                    self.dataBufferOffset  = bytesWritten;
                }
            }
            return;
        case NSStreamEventErrorOccurred:
            [self p_cancelWithError:_outputStream.streamError];
            return;
        default:
            break;
    }
}

At the line with // The work is done comment, the file is considered uploaded completely.


Provided how complex this approach is, and that it's not really feasible to fit all parts of it in a single SO answer, I made a helper class available in the gist here. You can use it in the client code as simple as that:

NSURL *filePathURL = [NSBundle.mainBundle URLForResource:@"895971" withExtension:@"png"];
NSURL *uploadURL = [[NSURL URLWithString:@"ftp://ftp.dlptest.com"] URLByAppendingPathComponent:filePathURL.lastPathComponent];
TDWFTPUploader *uploader = [[TDWFTPUploader alloc] initWithFileURL:filePathURL
                                                         uploadURL:uploadURL
                                                         userLogin:@"dlpuser"
                                                      userPassword:@"rNrKYTX9g7z3RgJRmxWuGHbeu"];
[uploader resumeWithCallback:^(NSError *_Nullable error) {
    if (error) {
        NSLog(@"Error: %@", error);
    } else {
        NSLog(@"File uploaded successfully");
    }
}];

It doesn't even need to be retained, because the class spawns a thread, which retain the instance until the work is done. I didn't pay too much attention to any corner cases, thus feel free to let me know if it has some errors or doesn't meet the required behaviour.

EDIT

For GET requests the only difference from any other protocol is that you pass login and password as part of URL and cannot use any secure means to do the same. Apart from that, it works straightforward:

NSURLComponents *components = [NSURLComponents componentsWithString:@"ftp://121.122.0.200"];
components.path = @"/usr/path/file.json";
components.user = @"user";
components.password = @"pwd";
[[NSURLSession.sharedSession dataTaskWithURL:[components URL] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable
response, NSError * _Nullable error) {
    NSLog(@"%@", response);
}] resume];
  • Related