File loading doesn't work with NSURLSession via FTP - ios

Using URL session FTP download is not working. I tried using below code.
Approach 1
NSURL *url_upload = [NSURL URLWithString:#"ftp://user:pwd#121.122.0.200:/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 = #"xxxx";
NSString * codice = #"xxxx";
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

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

Read Data/Response/Body of redirected NSURLDataTask

I am currently trying to access a webpage where the user can login using their credentials, after entering their user and password - if correct it will redirect to a new url. This new url loads a webpage with a single string which I intend to use.
However, how am I able to check the contents of the redirected url? At the moment I am only able to check the Response/Data/Contents of the initial page loaded by the following method;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
casSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSString *urlAddress = #"https://originalurl.com";
NSURL *httpUrl = [NSURL URLWithString:urlAddress];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:httpUrl];
[loginPage loadRequest:requestObj];
NSURLSessionDataTask *redirect = [casSession dataTaskWithURL:httpUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *newURL = [NSString stringWithFormat: #"%#", response.URL];
if ([newURL containsString:#"ticket=ST"]) {
NSString * registrationID = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"REGISTRATION: %#", registrationID);
if (registrationID != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
loginPage.hidden = YES;
});
}
} else {
NSLog(#"No ticket recieved");
}
}];
[redirect resume];
I'm not sure which delegate to use, in order to actively check every time a redirection happens and then obtain the contents of the new url?
Thanks.
You’re looking at this the wrong way. You should query the user for the login info directly and insert that into a single NSURLDataTask. Then the data task should query the server with the login info, and return some data.
This all happens with APIs (in a broad manner of speaking) where you will not present HTML contents to the user, but instead some sort of encoded data that is returned.
So for example, once you have a task defined from a URL or URLRequest, and you begin the task, you then use the completion handler to verify the returned data and/or error. If here, you may decode the returned data as a NSString, and then convert the JSON to objects, such as a user’s profile’s data (name, age, email, ...)
I did not go into detail in this answer because it is a very very broad topic, with many use cases. Look up some tutorials on NSURLDataTasks or consuming APIs from Swift and/or Objective-C.

Issue with NSURLSession with multiple downloads in background with multiple sessions and multiple segments

We have crated session with below configuration code. I call this method for each task I crate.
+(NSURLSession ) getNewSessionWithID:(NSString )sessionID delegateObject:(id)sender
{
NSURLSession *session;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
NSOperationQueue *queue=[[NSOperationQueue alloc]init];
queue.maxConcurrentOperationCount=10;
queue.name=sessionID;
session = [NSURLSession sessionWithConfiguration:configuration delegate:sender delegateQueue:queue];
NSLog(#"Session ID :%#",session);
NSLog(#"QUEUE : %#",queue);
return session;
}
Even multiple sessions are created for multiple tasks only one session is active and only one task is executing and for that task only three part are downloading for that one session.
This method is called to create and start download task.
-(void)DLRequestAllRenge:(NSMutableArray*)arrayrange andFileinfo:(FileInfo *)fileInfoObj
{
NSMutableArray *arrayAllParts=[[NSMutableArray alloc]init];
fileInfoObj.tempPath=[fileInfoObj UniqueFileName:[NSTemporaryDirectory() stringByAppendingString:[fileInfoObj.Name stringByDeletingPathExtension]]];
[self createTempDirectory:fileInfoObj.tempPath];
NSLog(#"REQ Session:%#",fileInfoObj.session);
for (int i=0; i<arrayrange.count; i++)
{
NSString *rangString = [arrayrange objectAtIndex:i];
NSMutableURLRequest *request=[[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:fileInfoObj.URL]];
[request setValue:rangString forHTTPHeaderField:#"Range"];
NSString *fileName=[NSString stringWithFormat:#"%#(%d).data",[fileInfoObj.Name stringByDeletingPathExtension],i];
NSString *filePath=[fileInfoObj.tempPath stringByAppendingPathComponent:fileName];
FileInfo *subFileInfo=[[FileInfo alloc]init];
subFileInfo.URL=fileInfoObj.URL;
subFileInfo.Name=fileName;
subFileInfo.Path=filePath;
subFileInfo.Folder=[fileInfoObj getCurrentFolderName:filePath];
subFileInfo.Range=rangString;
subFileInfo.isDownloaded=NO;
subFileInfo.NSUrlSessionID=fileInfoObj.NSUrlSessionID;
subFileInfo.Progress=#"0";
subFileInfo.Priority=[NSString stringWithFormat:#"%d",i];
subFileInfo.fileDetail=#"Connecting...";
subFileInfo.fileStatus=RequestStatusDownloading;
subFileInfo.request=request;
subFileInfo.startTime=[NSDate date];
NSURLSessionDownloadTask *downloadTask = [fileInfoObj.session downloadTaskWithRequest:request];
downloadTask.taskDescription=filePath;
[downloadTask resume];
subFileInfo.DownloadTask=downloadTask;
[arrayAllParts addObject:subFileInfo];
}
fileInfoObj.parts=arrayAllParts;
[downloadingArray addObject:fileInfoObj];
[bgDownloadTableView reloadData];
}
Issue 1
Why all sessions are not active?
Issue 2
Why only three part for one task and one session is downloading?
Is there any way we can activate more session for download more part concurrently?
Please help me with this. Any help is appreciated.
Update
We are able to download data using the above code but the issue is I am not able to get any downloaded data when any downloading task is stopped using -suspend or -cancel.
Is there any way I am able to retrieve raw data not the resumeData but the original downloaded data?

Downloading data and processing asynchronous with NSURLSession

I am using NSURLSession to download xml files and then I want to do different processing to this files, like parsing them:
-(void)parseFeed:(NSURL *)url
{
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionDataTask* task = [FeedSessionManager.sharedManager.session dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error)
{
Parser* parser = [[Parser alloc] initWithData:data];
[self.feeds addObjectsFromArray:[parser items]];
}];
[task resume];
}
Parser object will parse the xml file using NSXMLParser. The parseFeed:(NSURL*)url is called from the ViewController:
Downloader* downloader = [[Downloader alloc] init];
[downloader parseFeed:[NSURL URLWithString:#"http://www.engadget.com/tag/features/rss.xml"]];
NSArray* items = [downloader feeds];
And this is how I create the NSURLSession object:
-(id)init
{
if(self = [super init])
{
_session = [NSURLSession sessionWithConfiguration:FeedSessionConfiguration.defaultSessionConfiguration delegate:self delegateQueue:nil];
}
return self;
}
Of course this approach doesn't work for me. Inside parseFeed method I want to wait until all data is downloaded and processed. Only then I want to access the self.feeds array in the ViewController.
Can someone point me into the right direction into doing this ? Or maybe point me to a different approach ?
I have used ASIHTTPRequest but now no longer maintained but you can use AFHTTPClient's operation queue
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:nil];
// Important if only downloading one file at a time
[client.operationQueue setMaxConcurrentOperationCount: 1];
NSArray *videoURLs; // An array of strings you want to download
for (NSString * videoURL in videoURLs) {
// …setup your requests as before
[client enqueueHTTPRequestOperation:downloadRequest];
}

Error in downloading an audio file form server synchronously

I am using a NSBlockOperation in which i am trying to downlaod an audio file from server & storing it in documents directory.
NSBlockOperation *audioOperation = [NSBlockOperation blockOperationWithBlock:^{
//Perform doanload
NSString *itemStoredPath = [self downloadPOIAudioByUrl:itemUrl itemName:itemName folderName:itemFolder iteInfo:cacheAudioDetails];
// Update database
.....
}];
-(NSString *)downloadPOIAudioByUrl:(NSString *)itemUrl itemName:(NSString *)itemName folderName:(NSString *)folderName iteInfo:(CacheAudioDetails *)itemInfo {
// Get the url for video upload
NSURL *audioUrl = [NSURL URLWithString:[itemUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
// Set the response parameter
NSURLResponse *serverResponce = nil;
// Set the error parameter
NSError *error = nil;
// Create a request & set the time out interval for 1 min.
//NSURLRequest *videoRequest = [NSURLRequest requestWithURL:videoUrl cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0];
NSURLRequest *audioRequest = [NSURLRequest requestWithURL:audioUrl];
// Set the connection
NSData *audioData = [NSURLConnection sendSynchronousRequest:audioRequest returningResponse:&serverResponce error:&error];
if (error == nil && audioData != nil) {
// Data Found
// Store in directory & return the path to store in database
return audioPath;
}
return nil;
}
I have made a synchronous call to downlaod an audio file. But it is taking too much time & after long time it returns zero bytes of NSData.I thought it was due to my timed out request for 60 sec. Then i removed the time out request but still the problem remains as it is. My query is
Time out is related to server connection & not to fetching data from server
What should be the reason of Zero bytes responce from server.

Using AVURLAsset on a custom NSURLProtocol

I have written a custom NSURLProtocol (called "memory:") that allows me to fetch stored NSData items from a NSDictionary based on a name. For example, this code registers the NSURLProtocol class and adds some data:
[VPMemoryURLProtocol register];
[VPMemoryURLProtocol addData:data withName:#"video"];
This allows me to refer to the NSData via a url like "memory://video".
Below is my custom NSURLProtocol implementation:
NSMutableDictionary* gMemoryMap = nil;
#implementation VPMemoryURLProtocol
{
}
+ (void)register
{
static BOOL inited = NO;
if (!inited)
{
[NSURLProtocol registerClass:[VPMemoryURLProtocol class]];
inited = YES;
}
}
+ (void)addData:(NSData *)data withName:(NSString *)name
{
if (!gMemoryMap)
{
gMemoryMap = [NSMutableDictionary new];
}
gMemoryMap[name] = data;
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSLog(#"URL: %#, Scheme: %#",
[[request URL] absoluteString],
[[request URL] scheme]);
NSString* theScheme = [[request URL] scheme];
return [theScheme caseInsensitiveCompare:#"memory"] == NSOrderedSame;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (void)startLoading
{
NSString* name = [[self.request URL] path];
NSData* data = gMemoryMap[name];
NSURLResponse* response = [[NSURLResponse alloc] initWithURL:[self.request URL]
MIMEType:#"video/mp4"
expectedContentLength:-1
textEncodingName:nil];
id<NSURLProtocolClient> client = [self client];
[client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
}
- (void)stopLoading
{
}
I am not sure whether this code works or not but that is not what I have a problem with. Despite registering the custom protocol, canInitWithRequest: is never called when I try to use the URL in this code:
NSURL* url = [NSURL URLWithString:#"memory://video"];
AVURLAsset* asset = [[AVURLAsset alloc] initWithURL:url options:nil];
AVAssetImageGenerator* imageGen = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
CMTime time = CMTimeMakeWithSeconds(0, 600);
NSError* error;
CMTime actualTime;
CGImageRef image = [imageGen copyCGImageAtTime:time
actualTime:&actualTime
error:&error];
UIImage* uiImage = [UIImage imageWithCGImage:image];
CGImageRelease(image);
image is always nil if I use "memory://video" but works fine if I use "file:///...". What am I missing? Why isn't canInitWithRequest not being called? Does AVFoundation only support specific URL protocols and not custom ones?
Thanks
Certainly the underpinnings used to only support particular URL schemes— as an eBook developer I've seen this happen for any media type loaded through a URL such as epub:// or zip://. In those cases, on iOS 5.x and earlier, tracing through the relevant code would wind up in a QuickTime method which compared the URL scheme against a small number of supported ones: file, http, https, ftp and whatever it is that iTunes uses-- I forget what it's called.
In iOS 6+ there is a new API in AVFoundation, however, which is designed to help here. While I've not used it personally, this is how it should work:
NSURL* url = [NSURL URLWithString:#"memory://video"];
AVURLAsset* asset = [[AVURLAsset alloc] initWithURL:url options:nil];
////////////////////////////////////////////////////////////////
// NEW CODE START
AVAssetResourceLoader* loader = [asset resourceLoader];
id<AVAssetResourceLoaderDelegate> delegate = [SomeClass newInstanceWithNSURLProtocolClass: [VPMemoryURLProtocol class]];
[loader setDelegate: delegate queue: some_dispatch_queue];
// NEW CODE END
////////////////////////////////////////////////////////////////
AVAssetImageGenerator* imageGen = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
CMTime time = CMTimeMakeWithSeconds(0, 600);
With this in place, you need only implement the AVAssetResourceLoader protocol somewhere, which is very simple as it contains only one method. Since you already have an NSURLProtocol implementation, all your real work is done and you can simply hand off the real work to the Cocoa loading system or your protocol class directly.
Again, I'll point out that I've yet to actually make use of this, so the above is entirely theoretical.

Resources