Description:
I need to send a base 64 encoded string in a HTTP POST parameter that represents a file to be uploaded.
Current method: I'm using a ASIFormDataRequest and encoding my file into a base64 string like so:
NSData *data = [[NSFileManager defaultManager] contentsAtPath:#"my path here"];
NSString *base64Data = [data encodeBase64ForData];
However when uploading large files the App runs out of memory and dies a horrid death!
Proposed solution: Does anybody now how I would go about, say, reading the file from disk, converting it to base64 strings in chunks and attaching the converted base64 string to a parameter in the HTTP request?
Effectively any way that avoids reading the whole file into memory.
Any input would be greatly appreciated!
My http request code in full:
NSURL *url = [NSURL URLWithString:requestProgressURLString];
NSData *data = [[NSFileManager defaultManager] contentsAtPath:evidence.uri];
NSString *base64Data = [data encodeBase64ForData];
NSURL *fileURL = [[NSURL alloc] initWithString:evidence.uri];
request = [ASIFormDataRequest requestWithURL:url];
request.shouldAttemptPersistentConnection = false;
[request setShowAccurateProgress:YES];
[request setPostValue:accessToken forKey:#"AccessToken"];
[request setPostValue:[[fileURL path] lastPathComponent] forKey:#"Filename"];
[request setPostValue:evidence.name forKey:#"Title"];
[request setPostValue:base64Data forKey:#"FileData"]; //wants string value (base64 encoded)
[request addRequestHeader:#"Content-Type" value:#"application/x-www-form-urlencoded"];
[request setTimeOutSeconds:60];
[request setUploadProgressDelegate:self];
[request setDownloadProgressDelegate:self];
[request setDelegate:self];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
I would split this problem into two:
1. Encode large file to Base64 in chunks
2. Upload that file
1. Encode large file to Base64 in chunks:
Find some code that encodes NSData instances to Base64. Then use one NSInputStream to read data in chunks, encode it and use NSOutputStream to write to a temporary file. I found this question/answer: Is it possible to base64-encode a file in chunks? Make the chunk size a multiple of 3 and it should work. You may want to move this to background thread.
Important: This will create a temporary file, that is larger than the original. There must be enough space on device for that moment. After you begin uploading you can delete it.
2. Upload that file
It seems that you have this already done. You can use that ASIFormDataRequest exactly like in your example. And since it makes a private copy of the file (it builds the multipart POST file), you can delete your copy.
If you want to optimize it by parallelizing these steps, you will need to find another way to upload the multipart file.
Instead of reading the file in one hit you need to read it in chunks
You can use an input stream to do this
Apple have some info on here
Basically you need to create an NSInputStream pointed at your file as in the example
- (void)setUpStreamForFile:(NSString *)path {
// iStream is NSInputStream instance variable
iStream = [[NSInputStream alloc] initWithFileAtPath:path];
[iStream setDelegate:self];
[iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[iStream open];
}
You can then read the data in chunks using the NSStreamDelegate method
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
Again Apple give an example of this
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch(eventCode) {
case NSStreamEventHasBytesAvailable:
{
if(!_data) {
_data = [[NSMutableData data] retain];
}
uint8_t buf[1024];
unsigned int len = 0;
len = [(NSInputStream *)stream read:buf maxLength:1024];
if(len) {
[_data appendBytes:(const void *)buf length:len];
// bytesRead is an instance variable of type NSNumber.
[bytesRead setIntValue:[bytesRead intValue]+len];
// DO SOMETHING with DATA HERE
} else {
NSLog(#"no buffer!");
}
break;
}
The size of chunks you choose will need to be a balance of memory performance vs network performance as there is obviously some overhead with the HTTP headers
You can also further optimise by sending your NSData directly ( and therefore save the base64 overhead) check this post
File Upload to HTTP server in iphone programming
Related
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];
I'm trying to do a simple get in iOS (Objective C) using a simulator and not a real device.
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:theGetURL]
cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval:10];
[newRequest setHTTPMethod: #"GET"];
NSError *requestError2;
NSURLResponse *urlResponse2;
NSData *response2 = [NSURLConnection sendSynchronousRequest:newRequest returningResponse:&urlResponse2 error:&requestError2];
NSString* secondResponse = [[NSString alloc] initWithData:response2 encoding:NSUTF8StringEncoding];
NSLog(#"error = %#",requestError.localizedDescription);
NSLog(#"response=%#",secondResponse);
NSLog(#"url response = %#",urlResponse);
This code works perfectly when I'm passing a simple url. When I try the code with a longer (around 4000 characters) url, the code doesn't work (no error is printed).
I am aware that a post is better for this kind of thing, but my question is, is this expected from a get request?
Also, my url works perfectly in my mac and iOS browsers.
As you suspect, I think you need to consider moving to use POST rather then GET. The server side limit is 8K, however it seems this can be much less for the client side.
The following discussion sums everything up well. It also seems to imply the limit for Safari is 2K, which probably means it is the same or less for iOS, which would explain your problem with 4000 characters.
maximum length of HTTP GET request?
I think your URL query parameter might have any character that is not encoded. Try to ensure it.
For encoding you may try this code
- (NSString *)encodeQueryParameter:(NSString *)str
{
CFStringRef ref = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(__bridge CFStringRef)str,
NULL,
CFSTR(":/=,!$&'()*+;[]##?"),
kCFStringEncodingUTF8);
NSString *result = (__bridge_transfer NSString *)ref;
return result;
}
I am trying to upload multiple documents (say: images and videos) at the same time with hitting on single API. The API is developed in Ruby ON Rails. I am trying to upload documents in iPhone using ASIHTTP methods. Here is the screenshot of getpostman. On getpostman, It is working fine. Multiple documents are working fine but when I am using same API in xcode then its giving me error.
What I have done till now is, I have used two ways.
1.) Sending multiple documents's data using dictionaries. Here is the code snippet:-
[self setRequest:[ASIFormDataRequest requestWithURL:[NSURL URLWithString:#"http://onepgr.com/docs/create6_api"]]];
[request setPostValue:#"11" forKey:#"clientname"];
[request setPostValue:#"1" forKey:#"onepgr_apicall"];
[request setPostValue:pageID forKey:#"page_id"];
//[request setPostValue:#"true" forKey:#"multiple"];
//[request setPostValue:array forKey:#"doc"];
[request setPostValue:#"" forKey:#"passcode"];
[request setPostValue:#"33" forKey:#"clientappkey"];
[request setPostValue:#"22" forKey:#"clientappid"];
[request setTimeOutSeconds:20];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
[request setShouldContinueWhenAppEntersBackground:YES];
#endif
[request setUploadProgressDelegate:progressView];
[request setDelegate:self];
[request setDidFailSelector:#selector(uploadFailed:)];
[request setDidFinishSelector:#selector(uploadFinished:)];
//Uncomment For Multiple Upload
for (int i = 0; i<self.chosenImages.count; i++)
{
UIImage *img = [self.chosenImages objectAtIndex:i];
NSString *str = [NSString stringWithFormat:#"Image_File_%d.png",i+1];
strFileName = str;
strContentType = #"image/png";
fileData = UIImageJPEGRepresentation(img, 1.0);
if (i == 0)
[request setData:fileData withFileName:strFileName andContentType:strContentType forKey:#"doc"];
else
[request addData:fileData withFileName:strFileName andContentType:strContentType forKey:#"doc"];
}
2.) I have tried sending the data in for loop as well. But nothing worked for me.
Looking forward to have some answers that can help me out.
Thanks,
Nikhil
Use http://www.charlesproxy.com/ to check the differences between the 2 requests
My personal bet is that the problem is that you are setting the image data within the loop always to the same key.
I'm trying to fetch an XML-document from a server, but the response I'm getting in clear text is just ÿþ<.
I found out, by reading the HEX-data of the NSData that what follows after those first three bytes is the whole response I wanted, but with a nullbyte in between every byte. Now, I could just strip the data from those bytes, but if I get the same data with Netcat, there are no nullbytes there, so I figured there is something wrong with my code.
I've sent the exact same headers that the iPad-simulator has sent, but everything seems normal from there. This is the code I'm using to fetch the XML-data:
NSString *postData = #"SomePostData";
NSString *postLength = [NSString stringWithFormat:#"%d", [postData length]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
NSString *URLString = [NSString stringWithFormat:#"https://www.example.com/xmlexport?sessionid=%#", sessionID];
[request setURL:[NSURL URLWithString:URLString]];
[request setHTTPMethod:#"POST"];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setHTTPBody:[postData dataUsingEncoding:NSUTF8StringEncoding]];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
And these are the delegate methods I'm using.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
myData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[myData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSString *myString = [[NSString alloc] initWithData:myData encoding:NSWindowsCP1252StringEncoding];
NSLog(#"%#", myString);
}
This is the response I'm getting from the server:
{ URL: https://www5.whentowork.com/cgi-bin/w2wE.dll/mgrexportemplist?SID=2835115254228 } { status code: 200, headers {
"Access-Control-Allow-Origin" = "*";
"Content-Disposition" = "attachment;filename=empdata.xml";
"Content-Encoding" = gzip;
"Content-Type" = "application/x-msdownload";
Date = "Mon, 14 Apr 2014 19:37:54 GMT";
Server = "Microsoft-IIS/6.0";
"Transfer-Encoding" = Identity;
Vary = "Accept-Encoding";
} }
This is the first part of the XML:
<?xml version="1.0" encoding="windows-1252" standalone="yes"?>
<!-- Generated by Exmaple.com -->
<!DOCTYPE tutorial [
<!ENTITY auml "ä">
<!ENTITY ouml "ö">
<!ENTITY uuml "ü">
<!ENTITY Auml "Ä">
<!ENTITY Ouml "Ö">
<!ENTITY Uuml "Ü">
I'm running the latest Xcode and running the code in the iOS simulator (Version 7.1 (463.9.41)) for iPad.
I really don't get this. I spent so much time trying to figure out why I only got three bytes, then later figured out I got everything, but with nullbytes in between everywhere. I do not have control over the server, so I can not change it's configuration.
EDIT: An NSLog of the data return this: (just the first couple of lines)
fffe3c00 3f007800 6d006c00 20007600 65007200 73006900 6f006e00 3d002200 31002e00 30002200
20006500 6e006300 6f006400 69006e00 67003d00 22007700 69006e00 64006f00 77007300 2d003100
32003500 32002200 20007300 74006100 6e006400 61006c00 6f006e00 65003d00 22007900 65007300
22003f00 3e000d00 0a003c00 21002d00 2d002000 47006500 6e006500 72006100 74006500 64002000
62007900 20005700 68006500 6e005400 6f005700 6f007200 6b002d00 2d003e00 0d000a00 3c002100
44004f00 43005400 59005000 45002000 74007500 74006f00 72006900 61006c00 20005b00 0d000a00
20002000 20002000 3c002100 45004e00 54004900 54005900 20006100 75006d00 6c002000 22002600
61006d00 70003b00 61007500 6d006c00 3b002200 3e000d00 0a002000 20002000 20003c00 21004500
4e005400 49005400 59002000 6f007500 6d006c00 20002200 26006100 6d007000 3b006f00 75006d00
6c003b00 22003e00 0d000a00 20002000 20002000 3c002100 45004e00 54004900 54005900 20007500
75006d00 6c002000 22002600 61006d00 70003b00 75007500 6d006c00 3b002200 3e000d00 0a002000
20002000 20003c00 21004500 4e005400 49005400 59002000 41007500 6d006c00 20002200
The fffe at the start is a byte order mark that indicates that the response is is little endian UTF-16. (See this byte order mark table.) This is further confirmed if you look at the rest of your data, where 3c00 is the little endian UTF-16 representation of <, 3f00 is ?, 7800 is x, etc.
Thus:
fffe3c00 3f007800 6d006c00 20007600 65007200 73006900 6f006e00 3d002200
31002e00 30002200 20006500 6e006300 6f006400 69006e00 67003d00 22007700
69006e00 64006f00 77007300 2d003100 32003500 32002200 20007300 74006100
6e006400 61006c00 6f006e00 65003d00 22007900 65007300 22003f00 3e00
is
<?xml version="1.0" encoding="windows-1252" standalone="yes"?>
You should therefore convert it to a string using:
NSString *myString = [[NSString alloc] initWithData:myData encoding:NSUTF16LittleEndianStringEncoding];
This will automatically handle the byte order mark, as well (eliminating that curious ÿþ at the start).
By the way, if you're ever unclear about the representation, you can always try saving the NSData to a file and then using stringWithContentsOfFile:usedEncoding:error:. Personally, I'd always first look for a the byte order mark or for obvious UTF-16 or UTF-32 data (which is generally pretty easy to identify in western languages), as we did above, but this can be useful sometimes:
NSStringEncoding encoding;
NSError *error;
NSString *string = [NSString stringWithContentsOfFile:path usedEncoding:&encoding error:&error];
if (string) {
NSLog(#"string = %#", string);
NSLog(#"encoding = %d", encoding);
} else {
NSLog(#"stringWithContentsOfFile error: %#", error);
}
It doesn't always work, but sometimes it provides interesting clues.
I have a NSURLConnection that receives data output from a url pointing to a php script on my server.
Most of the time everything works fine and the data is retrieved in its complete form.
However, sometimes I receive NULL or broken (i.e. the bottom half) of data at:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
When this happens, if I reload the connection it will always return the same null or broken block
of data for the request.
EDIT*:
I've realized that when I receive what I thought was nil data, I actually received data
but the NSString created from this data is nil. I still don't understand why though. My php encoding output is always UTF-8 so I don't think it is an issue of encoding and besides it works most of the time with this.
I have checked the php script with that same request to verify that it is not a problem on the server side or with the php script and confirmed that it is NOT.
My code is Below:
-(void)setUpConnectionAndMakeRequest {
NSString *URLpath = #"http://www.example.com/myphp.php";
NSURL *myURL = [[NSURL alloc] initWithString:URLpath];
NSMutableURLRequest *myURLRequest = [NSMutableURLRequest requestWithURL:myURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];
[myURL release];
[myURLRequest setHTTPMethod:#"POST"];
//I added this because I thought it may be a problem relating to cache but it isn't
NSURLCache *cache = [NSURLCache sharedURLCache];
[cache removeAllCachedResponses];
NSString *httpBodystr = [NSString stringWithString:#"command=runscript"];
[myURLRequest setHTTPBody:[httpBodystr dataUsingEncoding:NSUTF8StringEncoding]];
[mailData setData:nil]; //mailData is a NSMutableData object which accumulates the data retrieved by the request
[NSURLConnection connectionWithRequest:myURLRequest delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSString *untrimmedDataSTR = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; //Created so I can see the data (always text) in NSLog
NSLog(#"Live Data: %#", untrimmedDataSTR); //This is where I see that the data is broken or null when it shouldn't be
[mailData appendData:data]; //Append accumulated data to NSMutableData object used later in my app.
[untrimmedDataSTR release];
}
Any help would be much appreciated.
According to the NSString reference, -initWithData:encoding: returns nil if "the initialization fails for some reason (for example if data does not represent valid data for encoding)."
That almost certainly means that the response from the server is not, in fact, UTF-8 encoded data.
The way to check would be to NSLog the data before trying to convert to an NSString:
NSLog(#"Raw Data: %#", data);
(The -description method on NSData will return a hexadecimal representation of the contents; that's what will get logged).