Is there any easy way to update a request's URL after it was initiated in AFNetworking?
Let's say my app is downloading a 2GB in chunks, we'll have 200 chunks 10MB each. For each I'm creating a AFHTTPRequestOperation and add it to the queue.
Problem is, that the server from which I'm downloading has a timeout on all URLs, which means I'll get 403 after that time and I have to generate a new one. I need to do it before the time runs out.
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
NSURL *fileURL = [[NSURL alloc] initWithString:[downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSLog(#"Normal range: %lli-%lli", [downloadChunk startingByte], [downloadChunk endingByte]);
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", newStartingByte, [downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", [NSString stringWithFormat:#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]]);
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
}];
[queue addOperation:operation];
}
}
No, it's not possible to modify a scheduled operation. Generally it's a good thing, meaning that you can schedule the operation and be absolutely sure that it's going to be executed based on original conditions.
I see two options in your case. First, you can schedule operations one by one, and verify if the link is valid, updating it if required.
Second, you can react to 403 Forbidden by updating the link and re-scheduling same operation.
Related
I have a problem, I want to get the file size of my link that i put in my textfield and show it in label before starting the download.
I've done few things and i with [operation.response expectedContentLength]
i just get 0. my code in .m file is this :
NSString *fileName = [self.linkTextBox.stringValue lastPathComponent];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[self.linkTextBox stringValue]]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
NSLog(#"size :%lld", [operation.response expectedContentLength]);
self.detailText.stringValue = [NSString stringWithFormat:#"%lld", [operation.response expectedContentLength]];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:fileName];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(#"bytesRead: %lu, totalBytesRead: %lld, totalBytesExpectedToRead: %lld", (unsigned long)bytesRead, totalBytesRead, totalBytesExpectedToRead);
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Successfully downloaded file to %#", path);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation start];
You can send a HEAD request instead of a GET one, this will ask the server to send you only the headers for your request. Most servers support this and its the preferable method when you want to obtain some metadata info about the entity you're querying, without having to download it.
The HEAD request can be achieved by creating a NSMutableURLRequest and setting the HTTPMethod property, something like this:
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:anURL];
request.HTTPMethod = #"HEAD";
// the rest of the code
This assuming the server developer did his job and he is expecting HEAD requests and is populating the appropriate http header fields.
You can't get the length before you download, but you can get it as soon as you start to get a response.
You want to use setDownloadProgressBlock:, which is a method on the super class of AFHTTPRequestOperation which is AFURLConnectionOperation. The callback has three parameters, the third of which is totalBytesExpectedToRead.
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
if (totalBytesExpectedToRead > 0) {
// Do stuff...
}
}];
As others have noted, that value can be 0 if the server doesn't set the content-length header in the HTTP response, so please handle that case.
I am trying to create a multipartFormRequestWithMethod using AFNetworking's AFHttpClient. It must authenticate with a ASP.NET REST service using Http Authorization Header. Therefore I pass username and password to the Http Authorization Header using setAuthorizationHeaderWithUsername:password:. In the body of the request I am passing a large file, several MBs. If the Header authentication fails, I want the request to get block and go in failure state before finishing the file send. There should be something that prevents the database send in case of authentication failure, but I cannot figure out what. In the current situation the AFHttpRequestOperation starts to send the file and notifies of the error only at the end of the file send.
This is the code:
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#"http://v-moxdevelop/MOX.UploadDBService/UploadDB/"]];
NSString *psw = #"psw";
NSString *userName = #"username";
[httpClient setAuthorizationHeaderWithUsername:selectedLoginItem.user password:#"psw"];
NSMutableURLRequest *request = [httpClient multipartFormRequestWithMethod:#"POST" path: [NSString stringWithFormat:#"Upload/%#/%#/%#", UDID, [selectedLoginItem.firm stringByReplacingOccurrencesOfString:#"." withString:#""], [selectedLoginItem.user stringByReplacingOccurrencesOfString:#"." withString:#""]] parameters:nil constructingBodyWithBlock: ^(id <AFMultipartFormData>formData) {
[formData appendPartWithFileData:fileData name:zipFilePath fileName:[zipFilePath lastPathComponent] mimeType:#"application/zip"];
}];
[request setTimeoutInterval:INT32_MAX];
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
__weak typeof(self) weakSelf = self;
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *locOperation, id responseObject) {
}failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
}];
[httpClient enqueueHTTPRequestOperation:operation];
I am new to objective-c and I have been searching for a way to send a post request to my server (based on Rest URL) but also include an image with it... I have found many methods to post data... and methods to post just an image, but nothing that combines the two...
I am searching for a wrapper, class or library because it seems to be a tedious task to write all this from scratch. I found the "ASIHTTPRequest" but this is no longer supported, although Ic an turn off ARC, I would prefer to find something still supported...
I also found AFNetworking, which seems to still be supported but I could be wrong, I just cannot find a solution to combine VERY simply data and a profile image...
Any help is appreciated?
Should I just use the ASIHTTPRequest library... ?? Or does anyone have any sample code for the AFNetworking library?
Here is the code I am using for AFnetworking library...
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
_emailAddressField.text, #"email",
_usernameField.text, #"username",
_passwordField.text, #"password",
nil];
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:%"http://url.com/api/whatever/"];
[client postPath:#"/" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSString *text = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(#"Response: %#", text);
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"%#", [error localizedDescription]);
}];
If you're using AFNetworking, you can use multipartFormRequestWithMethod to upload an image:
// Create the http client
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseUrl:url];
// Set parameters
NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys: #"param1", #"key1", nil];
// Create the request with the image data and file name, mime type, etc.
NSMutableURLRequest *request = [httpClient multipartFormRequestWithMethod:method path:#"url/to/" parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:data name:nameData fileName:fileName mimeType:mimeType];
}];
And then you can add the upload progress block to get feedback of the upload process:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
//Manage upload percentage
}];
Also, you can add setCompletionBlockWithSuccess to catch success and failure in your operation. More info can be found here. At last but not least important, add the request to the operation queue:
[httpClient enqueueHTTPRequestOperation:operation];
Can anybody answer to me, how come below code eats up whole core of a C2D 2.6GHz CPU?
It just downloads files in 10MB chunks, there can be like 600 of them, but the NSOperationQueue has a limit of 6 concurrent tasks.
How come the same app on Windows (written in C# eats only 2%, not 80%!), it is just a simple HTTP request!
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
NSString *downloadPath = [[NSString stringWithFormat:#"%#/%#", [download downloadFolder], [download escapedTitle]] stringByExpandingTildeInPath];
NSString *chunkPath = [downloadPath stringByAppendingFormat:#"/%#.%i", [downloadFile fileName], [downloadChunk chunkId]];
NSError *attributesError = nil;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:chunkPath error:&attributesError];
NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
uint64_t fileSize = [fileSizeNumber longLongValue];
NSLog(#"Chunk file size: %lli", fileSize);
uint64_t expectedSize = ([downloadChunk endingByte] - [downloadChunk startingByte]) + 1;
NSLog(#"Chunk expected size: %lli", expectedSize);
uint64_t newStartingByte = [downloadChunk startingByte] + fileSize;
if (fileSize == expectedSize)
{
NSLog(#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]);
}
else
{
NSURL *fileURL = [[NSURL alloc] initWithString:[downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSLog(#"Normal range: %lli-%lli", [downloadChunk startingByte], [downloadChunk endingByte]);
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", newStartingByte, [downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", [NSString stringWithFormat:#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]]);
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
}];
[queue addOperation:operation];
}
}
}
Here are some time profiler screenshots, seems everything is because RunLoops if I read it correctly.
Seems like downgrading most of the properties to nonatomic seems to lower the usage from 80% to 20-30%. Now only downloadedBytes are atomic. What can I do also to make improvements?
In assuming the last progress block completes the download, you're sort of shoving a round peg down a square hole. I'm not sure if that's causing your high CPU usage or not, but I'd certainly fix that. AFNetworking has a function just for this, [AFHTTPRequestOperation setCompletionBlockWithSuccess:failure:], documented here. The correct block will be called based on the HTTP status code returned.
In other words, instead of this:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
}];
[queue addOperation:operation];
chunkId++;
Use this:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[output setCompletionBlockWithSuccess: ^(AFHTTPRequestOperation *operation, id responseObject) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
}
failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
// you should probably do something here
}];
After that, I doubt you'd even need downloadedBytes. That removes your last atomic property, which should improve things.
I am making a JSON request with AFNetworking and then call [operation waitUntilFinished] to wait on the operation and the success or failure blocks. But, it seems to fall right though - in terms of the log messages, I get "0", "3", "1" instead of "0", "1", "3"
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://google.com"]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:#"query", #"q", nil];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[url path] parameters:params];
NSLog(#"0");
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *innerRequest, NSHTTPURLResponse *response, id JSON) {
NSLog(#"1");
gotResponse = YES;
} failure:^(NSURLRequest *innerRequest, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"2");
gotResponse = YES;
}];
NSLog(#"Starting request");
[operation start];
[operation waitUntilFinished];
NSLog(#"3");
This works by using AFNetworking to set up the requests, but making a synchronous call then handling the completion blocks manually. Very simple. AFNetworking doesn't seem to support this https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-FAQ, though the work around is simple enough.
#import "SimpleClient.h"
#import "AFHTTPClient.h"
#import "AFJSONRequestOperation.h"
#import "AFJSONUtilities.h"
#implementation SimpleClient
+ (void) makeRequestTo:(NSString *) urlStr
parameters:(NSDictionary *) params
successCallback:(void (^)(id jsonResponse)) successCallback
errorCallback:(void (^)(NSError * error, NSString *errorMsg)) errorCallback {
NSURLResponse *response = nil;
NSError *error = nil;
NSURL *url = [NSURL URLWithString:urlStr];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:[url path] parameters:params];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if(error) {
errorCallback(error, nil);
} else {
id JSON = AFJSONDecode(data, &error);
successCallback(JSON);
}
}
#end
That should (almost) work. Your call to
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[url path] parameters:params];
should probably not pass [url path] to the path: parameter. In AFNetworking land, that path is everything after the base url (for example the base url could be "http://google.com" and the path "/gmail" or whatever).
That being said, it's probably not a good idea to make the asynchronous operation into a thread-blocking synchronous operation with waitUntilFinished, but I'm sure you have your reasons... ;)
I just had the same problem and found a different solution. I had two operations that depend on each other, but can load in parallel. However, the completion block of the second operation can not be executed before the completion block of the first one has finished.
As Colin pointed out, it might be a bad choice to make a web request block. This was essential to me, so I did it asynchronously.
This is my solution:
// This is our lock
#interface SomeController () {
NSLock *_dataLock;
}
#end
#implementation
// This is just an example, you might as well trigger both operations in separate
// places if you get the locking right
// This might be called e.g. in awakeFromNib
- (void)someStartpoint {
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url1]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id data) {
// We're done, we unlock so the next operation can continue its
// completion block
[_dataLock unlock];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id data) {
// The request has failed, so we need to unlock for the next try
[_dataLock unlock];
}];
AFJSONRequestOperation *operation2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url2]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id data) {
// The completion block (or at least the blocking part must be run in a
// separate thread
[NSThread detachNewThreadSelector:#selector(completionBlockOfOperation2:) toTarget:self withObject:data];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id data) {
// This second operation may fail without affecting the lock
}];
// We need to lock before both operations are started
[_dataLock lock];
// Order does not really matter here
[operation2 start];
[operation1 start];
}
- (void)completionBlockOfOperation2:(id)data {
// We wait for the first operation to finish its completion block
[_dataLock lock];
// It's done, so we can continue
// We need to unlock afterwards, so a next call to one of the operations
// wouldn't deadlock
[_dataLock unlock];
}
#end
Use Delegate method call
Put the method inside block which will call itself when downloading/uploading completes.