Merge pull request #204 from Fokka-Engineering/nsurlsession-additions

Implement NSURLSessionDownloadTask and NSURLSessionDownloadDelegate
This commit is contained in:
rfm 2021-09-06 15:25:28 +01:00 committed by GitHub
commit ed8be18af9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 314 additions and 13 deletions

View file

@ -24,6 +24,7 @@
@class NSURLResponse;
@class NSURLSessionConfiguration;
@class NSURLSessionDataTask;
@class NSURLSessionDownloadTask;
/**
@ -110,6 +111,12 @@ GS_EXPORT_CLASS
/* Creates a data task to retrieve the contents of the given URL. */
- (NSURLSessionDataTask*) dataTaskWithURL: (NSURL*)url;
/* Creates a download task with the given request. */
- (NSURLSessionDownloadTask *) downloadTaskWithRequest: (NSURLRequest *)request;
/* Creates a download task to download the contents of the given URL. */
- (NSURLSessionDownloadTask *) downloadTaskWithURL: (NSURL *)url;
@end
typedef NS_ENUM(NSUInteger, NSURLSessionTaskState) {
@ -430,6 +437,37 @@ didReceiveChallenge: (NSURLAuthenticationChallenge*)challenge
@end
@protocol NSURLSessionDownloadDelegate <NSURLSessionTaskDelegate>
/* Sent when a download task that has completed a download. The delegate should
* copy or move the file at the given location to a new location as it will be
* removed when the delegate message returns. URLSession:task:didCompleteWithError: will
* still be called.
*/
- (void) URLSession: (NSURLSession *)session
downloadTask: (NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL: (NSURL *)location;
@optional
/* Sent periodically to notify the delegate of download progress. */
- (void) URLSession: (NSURLSession *)session
downloadTask: (NSURLSessionDownloadTask *)downloadTask
didWriteData: (int64_t)bytesWritten
totalBytesWritten: (int64_t)totalBytesWritten
totalBytesExpectedToWrite: (int64_t)totalBytesExpectedToWrite;
/* Sent when a download has been resumed. If a download failed with an
* error, the -userInfo dictionary of the error will contain an
* NSURLSessionDownloadTaskResumeData key, whose value is the resume
* data.
*/
- (void) URLSession: (NSURLSession *)session
downloadTask: (NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset: (int64_t)fileOffset
expectedTotalBytes: (int64_t)expectedTotalBytes;
@end
#endif
#endif
#endif

View file

@ -1,3 +1,4 @@
#import "GSURLPrivate.h"
#import "GSNativeProtocol.h"
#import "GSTransferState.h"
#import "GSURLSessionTaskBody.h"
@ -9,6 +10,7 @@
#import "Foundation/NSURL.h"
#import "Foundation/NSURLError.h"
#import "Foundation/NSURLSession.h"
#import "Foundation/NSFileHandle.h"
static BOOL isEasyHandlePaused(GSNativeProtocolInternalState state)
@ -97,6 +99,8 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
- (void) setError: (NSError*)error;
- (void) setCountOfBytesReceived: (int64_t)count;
@end
@implementation NSURLSessionTask (Internal)
@ -138,6 +142,11 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
ASSIGN(_error, error);
}
- (void) setCountOfBytesReceived: (int64_t)count
{
_countOfBytesReceived = count;
}
@end
@implementation GSCompletionAction
@ -493,24 +502,24 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
- (void) notifyDelegateAboutReceivedData: (NSData*)data
{
NSURLSessionTask *task;
id<NSURLSessionDelegate> delegate;
NSURLSession *session;
NSURLSessionTask *task;
id<NSURLSessionDelegate> delegate;
task = [self task];
NSAssert(nil != task, @"Cannot notify");
delegate = [[task session] delegate];
session = [task session];
NSAssert(nil != session, @"Missing session");
delegate = [session delegate];
if (nil != delegate
&& [task isKindOfClass: [NSURLSessionDataTask class]]
&& [delegate respondsToSelector: @selector(URLSession:dataTask:didReceiveData:)])
{
id<NSURLSessionDataDelegate> dataDelegate;
NSURLSessionDataTask *dataTask;
NSURLSession *session;
session = [task session];
NSAssert(nil != session, @"Missing session");
dataDelegate = (id<NSURLSessionDataDelegate>)delegate;
dataTask = (NSURLSessionDataTask*)task;
[[session delegateQueue] addOperationWithBlock:
@ -520,6 +529,40 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
didReceiveData: data];
}];
}
/* Don't check whether delegate respondsToSelector.
* This delegate is optional. */
if (nil != delegate
&& [task isKindOfClass: [NSURLSessionDownloadTask class]])
{
id<NSURLSessionDownloadDelegate> downloadDelegate;
NSURLSessionDownloadTask *downloadTask;
GSDataDrain *dataDrain;
NSFileHandle *fileHandle;
downloadDelegate = (id<NSURLSessionDownloadDelegate>)delegate;
downloadTask = (NSURLSessionDownloadTask*)task;
dataDrain = [_transferState bodyDataDrain];
/* Write to file. GSDataDrain opens the fileHandle. */
fileHandle = [dataDrain fileHandle];
[fileHandle seekToEndOfFile];
[fileHandle writeData: data];
if ([delegate respondsToSelector: @selector
(URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)])
{
/* Calculate received data length */
[task setCountOfBytesReceived: (int64_t)[data length] + [task countOfBytesReceived]];
[[session delegateQueue] addOperationWithBlock:
^{
[downloadDelegate URLSession: session
downloadTask: downloadTask
didWriteData: (int64_t)[data length]
totalBytesWritten: [task countOfBytesReceived]
totalBytesExpectedToWrite: [task countOfBytesExpectedToReceive]];
}];
}
}
}
- (void) notifyDelegateAboutUploadedDataCount: (int64_t)count
@ -673,9 +716,10 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
- (void) completeTask
{
NSURLSessionTask *task;
GSDataDrain *bodyDataDrain;
id<NSURLProtocolClient> client;
NSURLSessionTask *task;
GSDataDrain *bodyDataDrain;
id<NSURLProtocolClient> client;
id<NSURLSessionDelegate> delegate;
NSAssert(_internalState == GSNativeProtocolInternalStateTransferCompleted,
@"Trying to complete the task, but its transfer isn't complete.");
@ -683,6 +727,7 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
task = [self task];
[task setResponse: [_transferState response]];
client = [self client];
delegate = [[task session] delegate];
// We don't want a timeout to be triggered after this. The timeout timer
// needs to be cancelled.
@ -712,6 +757,16 @@ static BOOL isEasyHandleAddedToMultiHandle(GSNativeProtocolInternalState state)
[self setInternalState: GSNativeProtocolInternalStateTaskCompleted];
}
// Add temporary file URL to NSURLRequest properties
// and close the fileHandle
if (nil != delegate
&& [task isKindOfClass: [NSURLSessionDownloadTask class]])
{
[[bodyDataDrain fileHandle] closeFile];
[[self request] _setProperty: [bodyDataDrain fileURL]
forKey: @"tempFileURL"];
}
if ([client respondsToSelector: @selector(URLProtocolDidFinishLoading:)])
{
[client URLProtocolDidFinishLoading: self];

View file

@ -10,6 +10,9 @@
#import "Foundation/NSURLError.h"
#import "Foundation/NSURLResponse.h"
#import "Foundation/NSURLSession.h"
#import "Foundation/NSUUID.h"
#import "Foundation/NSFileManager.h"
#import "Foundation/NSPathUtilities.h"
#define GS_DELIMITERS_CR 0x0d
#define GS_DELIMITERS_LR 0x0a
@ -277,6 +280,21 @@
- (NSURL*) fileURL
{
/* Generate a random fileURL if fileURL is not initialized.
* It would be nice to have this implemented in an initializer. */
if (!_fileURL)
{
NSUUID *randomUUID;
NSString *fileName;
NSURL *tempURL;
randomUUID = [NSUUID UUID];
fileName = [[randomUUID UUIDString] stringByAppendingPathExtension: @"tmp"];
tempURL = [NSURL fileURLWithPath: NSTemporaryDirectory()];
_fileURL = [NSURL fileURLWithPath: fileName relativeToURL: tempURL];
}
return _fileURL;
}
@ -287,6 +305,17 @@
- (NSFileHandle*) fileHandle
{
/* Create temporary file and open a fileHandle for writing. */
if (!_fileHandle)
{
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager createFileAtPath: [[self fileURL] relativePath]
contents: nil
attributes: nil];
_fileHandle = [NSFileHandle fileHandleForWritingToURL: [self fileURL] error: NULL];
}
return _fileHandle;
}

View file

@ -284,6 +284,34 @@ static int nextSessionIdentifier()
return [self dataTaskWithRequest: request];
}
- (NSURLSessionDownloadTask *) downloadTaskWithRequest: (NSURLRequest *)request
{
NSURLSessionDownloadTask *task;
if (_invalidated)
{
return nil;
}
task = [[NSURLSessionDownloadTask alloc] initWithSession: self
request: request
taskIdentifier: _nextTaskIdentifier++];
[self addTask: task];
return AUTORELEASE(task);
}
- (NSURLSessionDownloadTask *) downloadTaskWithURL: (NSURL *)url
{
NSMutableURLRequest *request;
request = [NSMutableURLRequest requestWithURL: url];
[request setHTTPMethod: @"GET"];
return [self downloadTaskWithRequest: request];
}
- (void) addTask: (NSURLSessionTask*)task
{
[_taskRegistry addTask: task];
@ -584,6 +612,27 @@ static int nextSessionIdentifier()
if (nil != delegate)
{
// Send delegate with temporary fileURL
if ([task isKindOfClass: [NSURLSessionDownloadTask class]]
&& [delegate respondsToSelector: @selector(URLSession:downloadTask:didFinishDownloadingToURL:)])
{
id<NSURLSessionDownloadDelegate> downloadDelegate;
NSURLSessionDownloadTask *downloadTask;
NSURL *fileURL;
downloadDelegate = (id<NSURLSessionDownloadDelegate>)delegate;
downloadTask = (NSURLSessionDownloadTask *)task;
fileURL = [NSURLProtocol propertyForKey: @"tempFileURL"
inRequest: [protocol request]];
[delegateQueue addOperationWithBlock:
^{
[downloadDelegate URLSession: session
downloadTask: downloadTask
didFinishDownloadingToURL: fileURL];
}];
}
[delegateQueue addOperationWithBlock:
^{
if (NSURLSessionTaskStateCompleted == [task state])
@ -632,7 +681,7 @@ static int nextSessionIdentifier()
@implementation NSURLSessionTask
{
NSURLSession *_session; /* not retained */
NSURLSession *_session;
NSLock *_protocolLock;
NSURLSessionTaskProtocolState _protocolState;
NSURLProtocol *_protocol;
@ -653,6 +702,12 @@ static int nextSessionIdentifier()
NSData *data;
NSInputStream *stream;
/*
* Only retain the session once the -resume method is called
* and release the session as the last thing done once the
* task has completed. This avoids a retain loop causing
* session and tasks to be leaked.
*/
_session = session;
ASSIGN(_originalRequest, request);
ASSIGN(_currentRequest, request);
@ -857,6 +912,12 @@ static int nextSessionIdentifier()
- (void) resume
{
/*
* Properly retain the session to keep a reference
* to the task. This ensures correct API behaviour.
*/
RETAIN(_session);
dispatch_sync(_workQueue,
^{
if (NSURLSessionTaskStateCanceling == _state
@ -1070,6 +1131,11 @@ static int nextSessionIdentifier()
[_protocolLock lock];
_protocolState = NSURLSessionTaskProtocolStateInvalidated;
DESTROY(_protocol);
/*
* Release session at the end of the task
* and not when -dealloc is called.
*/
DESTROY(_session);
[_protocolLock unlock];
}
@ -1083,6 +1149,10 @@ static int nextSessionIdentifier()
@end
@implementation NSURLSessionDownloadTask
@end
@implementation NSURLSessionConfiguration
static NSURLSessionConfiguration *def = nil;

View file

@ -10,6 +10,7 @@
NSData *_taskData;
NSString *_taskText;
NSError *_taskError;
NSURL *_taskLocation;
}
- (BOOL) finished;
- (void) reset;
@ -48,6 +49,7 @@
DESTROY(_taskData);
DESTROY(_taskError);
DESTROY(_taskText);
DESTROY(_taskLocation);
_finished = NO;
}
@ -68,6 +70,11 @@
return _taskText;
}
- (NSURL*) taskLocation
{
return _taskLocation;
}
- (void) URLSession: (NSURLSession*)session
dataTask: (NSURLSessionDataTask*)dataTask
didReceiveResponse: (NSURLResponse*)response
@ -95,6 +102,14 @@
RELEASE(text);
}
/* NSURLSessionDownloadDelegate */
- (void) URLSession: (NSURLSession *)session
downloadTask: (NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL: (NSURL *)location
{
ASSIGN(_taskLocation, location);
}
- (void) URLSession: (NSURLSession*)session
task: (NSURLSessionTask*)task
didCompleteWithError: (NSError*)error
@ -104,7 +119,7 @@ didCompleteWithError: (NSError*)error
if (error == nil)
{
NSLog(@"Download is Succesfull");
NSLog(@"Download is Successful");
}
else
{

View file

@ -0,0 +1,94 @@
#import <Foundation/Foundation.h>
#import "Testing.h"
#import "ObjectTesting.h"
#import "../NSURLConnection/Helpers/TestWebServer.h"
#if GS_HAVE_NSURLSESSION
#import "delegate.g"
#endif
int main()
{
START_SET("NSURLSession test03")
#if !GS_HAVE_NSURLSESSION
SKIP("library built without NSURLSession support")
#else
NSFileManager *fm;
NSBundle *bundle;
NSString *helperPath;
// load the test suite's classes
fm = [NSFileManager defaultManager];
helperPath = [[fm currentDirectoryPath] stringByAppendingPathComponent:
@"../NSURLConnection/Helpers/TestConnection.bundle"];
bundle = [NSBundle bundleWithPath: helperPath];
NSCAssert([bundle load], NSInternalInconsistencyException);
TestWebServer *server;
Class c;
BOOL debug = YES;
// create a shared TestWebServer instance for performance
c = [bundle principalClass];
server = [[c testWebServerClass] new];
NSCAssert(server != nil, NSInternalInconsistencyException);
[server setDebug: debug];
[server start: nil]; // localhost:1234 HTTP
NSURLSessionConfiguration *configuration;
NSURLSession *defaultSession;
NSURLSessionDownloadTask *downloadTask;
NSMutableURLRequest *urlRequest;
NSURL *url;
NSOperationQueue *mainQueue;
MyDelegate *object;
NSString *content;
configuration = [[NSURLSessionConfiguration alloc] init];
object = AUTORELEASE([MyDelegate new]);
mainQueue = [NSOperationQueue mainQueue];
defaultSession = [NSURLSession sessionWithConfiguration: configuration
delegate: object
delegateQueue: mainQueue];
RELEASE(configuration);
url = [NSURL URLWithString: @"http://localhost:1234/index"];
urlRequest = [NSMutableURLRequest requestWithURL: url];
if ([urlRequest respondsToSelector: @selector(setDebug:)])
{
[urlRequest setDebug: YES];
}
downloadTask = [defaultSession downloadTaskWithRequest: urlRequest];
[downloadTask resume];
NSDate *limit = [NSDate dateWithTimeIntervalSinceNow: 60.0];
while ([object finished] == NO
&& [limit timeIntervalSinceNow] > 0.0)
{
ENTER_POOL
NSDate *when = [NSDate dateWithTimeIntervalSinceNow: 0.1];
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
beforeDate: when];
LEAVE_POOL
}
PASS(YES == [object finished], "request completed")
PASS_EQUAL([object taskError], nil, "request did not error")
/* Get content from file */
content = [NSString stringWithContentsOfFile: [[object taskLocation] path]
encoding: NSUTF8StringEncoding
error:nil];
NSString *expect = @"Please give login and password";
PASS_EQUAL(content, expect, "request returned text")
#endif
END_SET("NSURLSession test03")
return 0;
}