mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-11 23:54:18 +00:00
Add new URL::downloadToFile method
This commit is contained in:
parent
fd86a73407
commit
caa65e8ff1
9 changed files with 452 additions and 5 deletions
|
|
@ -80,9 +80,17 @@ public:
|
|||
{}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
cancel();
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
if (stream != 0)
|
||||
{
|
||||
stream.callVoidMethod (HTTPStream.release);
|
||||
stream.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool connect (WebInputStream::Listener* listener)
|
||||
|
|
@ -154,7 +162,11 @@ public:
|
|||
|
||||
responseHeaders.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -219,3 +231,8 @@ private:
|
|||
GlobalRef stream;
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,11 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
cleanup();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool setOptions ()
|
||||
{
|
||||
|
|
@ -512,3 +517,8 @@ public:
|
|||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,14 @@ public:
|
|||
return (statusCode != 0);
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
statusCode = -1;
|
||||
finished = true;
|
||||
|
||||
closeSocket();
|
||||
}
|
||||
|
||||
//==============================================================================w
|
||||
bool isError() const { return socketHandle < 0; }
|
||||
bool isExhausted() { return finished; }
|
||||
|
|
@ -556,4 +564,9 @@ private:
|
|||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener);
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -150,6 +150,12 @@ public:
|
|||
[data release];
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
signalThreadShouldExit();
|
||||
stopThread (10000);
|
||||
}
|
||||
|
||||
bool start (WebInputStream& inputStream, WebInputStream::Listener* listener)
|
||||
{
|
||||
startThread();
|
||||
|
|
@ -369,6 +375,200 @@ private:
|
|||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (URLConnectionState)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_IOS
|
||||
struct BackgroundDownloadTask : public URL::DownloadTask
|
||||
{
|
||||
BackgroundDownloadTask (const URL& urlToUse,
|
||||
const File& targetLocationToUse,
|
||||
String extraHeadersToUse,
|
||||
URL::DownloadTask::Listener* listenerToUse)
|
||||
: targetLocation (targetLocationToUse), listener (listenerToUse),
|
||||
delegate (nullptr), session (nullptr), downloadTask (nullptr),
|
||||
connectFinished (false), calledComplete (0)
|
||||
{
|
||||
downloaded = -1;
|
||||
|
||||
static DelegateClass cls;
|
||||
delegate = [cls.createInstance() init];
|
||||
DelegateClass::setState (delegate, this);
|
||||
|
||||
String uniqueIdentifier = String (urlToUse.toString (true).hashCode64()) + String (Random().nextInt64());
|
||||
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:juceStringToNS (urlToUse.toString (true))]];
|
||||
|
||||
StringArray headerLines;
|
||||
headerLines.addLines (extraHeadersToUse);
|
||||
headerLines.removeEmptyStrings (true);
|
||||
|
||||
for (int i = 0; i < headerLines.size(); ++i)
|
||||
{
|
||||
String key = headerLines[i].upToFirstOccurrenceOf (":", false, false).trim();
|
||||
String value = headerLines[i].fromFirstOccurrenceOf (":", false, false).trim();
|
||||
|
||||
if (key.isNotEmpty() && value.isNotEmpty())
|
||||
[request addValue: juceStringToNS (value) forHTTPHeaderField: juceStringToNS (key)];
|
||||
}
|
||||
|
||||
session =
|
||||
[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:juceStringToNS (uniqueIdentifier)]
|
||||
delegate:delegate
|
||||
delegateQueue:nullptr];
|
||||
|
||||
if (session != nullptr)
|
||||
downloadTask = [session downloadTaskWithRequest:request];
|
||||
|
||||
[request release];
|
||||
}
|
||||
|
||||
~BackgroundDownloadTask()
|
||||
{
|
||||
[session release];
|
||||
[delegate release];
|
||||
}
|
||||
|
||||
bool initOK()
|
||||
{
|
||||
return (downloadTask != nullptr);
|
||||
}
|
||||
|
||||
bool connect()
|
||||
{
|
||||
[downloadTask resume];
|
||||
while (downloaded == -1 && finished == false)
|
||||
Thread::sleep (1);
|
||||
|
||||
connectFinished = true;
|
||||
return ! error;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
File targetLocation;
|
||||
URL::DownloadTask::Listener* listener;
|
||||
NSObject<NSURLSessionDelegate>* delegate;
|
||||
NSURLSession* session;
|
||||
NSURLSessionDownloadTask* downloadTask;
|
||||
bool connectFinished;
|
||||
Atomic<int> calledComplete;
|
||||
|
||||
void didWriteData (int64 totalBytesWritten, int64 totalBytesExpectedToWrite)
|
||||
{
|
||||
downloaded = totalBytesWritten;
|
||||
|
||||
if (contentLength == -1)
|
||||
contentLength = totalBytesExpectedToWrite;
|
||||
|
||||
if (connectFinished && error == false && finished == false && listener != nullptr)
|
||||
listener->progress (this, totalBytesWritten, contentLength);
|
||||
}
|
||||
|
||||
void didFinishDownloadingToURL (NSURL* location)
|
||||
{
|
||||
NSFileManager* fileManager = [[NSFileManager alloc] init];
|
||||
error = ([fileManager moveItemAtURL:location
|
||||
toURL:[NSURL fileURLWithPath:juceStringToNS (targetLocation.getFullPathName())]
|
||||
error:nullptr] == NO);
|
||||
httpCode = 200;
|
||||
finished = true;
|
||||
|
||||
if (listener != nullptr && calledComplete.exchange (1) == 0)
|
||||
{
|
||||
if (contentLength > 0 && downloaded < contentLength)
|
||||
{
|
||||
downloaded = contentLength;
|
||||
listener->progress (this, downloaded, contentLength);
|
||||
}
|
||||
|
||||
listener->didComplete (this, !error);
|
||||
}
|
||||
}
|
||||
|
||||
void didCompleteWithError (NSError* nsError)
|
||||
{
|
||||
if (calledComplete.exchange (1) == 0)
|
||||
{
|
||||
httpCode = -1;
|
||||
|
||||
if (nsError != nullptr)
|
||||
{
|
||||
// see https://developer.apple.com/reference/foundation/nsurlsessiondownloadtask?language=objc
|
||||
switch ([nsError code])
|
||||
{
|
||||
case NSURLErrorUserAuthenticationRequired:
|
||||
httpCode = 401;
|
||||
break;
|
||||
case NSURLErrorNoPermissionsToReadFile:
|
||||
httpCode = 403;
|
||||
break;
|
||||
case NSURLErrorFileDoesNotExist:
|
||||
httpCode = 404;
|
||||
break;
|
||||
default:
|
||||
httpCode = 500;
|
||||
}
|
||||
}
|
||||
|
||||
error = true;
|
||||
finished = true;
|
||||
|
||||
if (listener != nullptr)
|
||||
listener->didComplete (this, ! error);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct DelegateClass : public ObjCClass<NSObject<NSURLSessionDelegate> >
|
||||
{
|
||||
DelegateClass() : ObjCClass<NSObject<NSURLSessionDelegate> > ("JUCE_URLDelegate_")
|
||||
{
|
||||
addIvar<BackgroundDownloadTask*> ("state");
|
||||
|
||||
addMethod (@selector (URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:),
|
||||
didWriteData, "v@:@@qqq");
|
||||
addMethod (@selector (URLSession:downloadTask:didFinishDownloadingToURL:),
|
||||
didFinishDownloadingToURL, "v@:@@@");
|
||||
addMethod (@selector (URLSession:task:didCompleteWithError:),
|
||||
didCompleteWithError, "v@:@@@");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static void setState (id self, BackgroundDownloadTask* state) { object_setInstanceVariable (self, "state", state); }
|
||||
static BackgroundDownloadTask* getState (id self) { return getIvar<BackgroundDownloadTask*> (self, "state"); }
|
||||
|
||||
private:
|
||||
static void didWriteData (id self, SEL, NSURLSession*, NSURLSessionDownloadTask*, int64_t, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite)
|
||||
{
|
||||
if (auto state = getState (self)) state->didWriteData (totalBytesWritten, totalBytesExpectedToWrite);
|
||||
}
|
||||
|
||||
static void didFinishDownloadingToURL (id self, SEL, NSURLSession*, NSURLSessionDownloadTask*, NSURL* location)
|
||||
{
|
||||
if (auto state = getState (self)) state->didFinishDownloadingToURL (location);
|
||||
}
|
||||
|
||||
static void didCompleteWithError (id self, SEL, NSURLSession*, NSURLSessionTask*, NSError* nsError)
|
||||
{
|
||||
if (auto state = getState (self)) state->didCompleteWithError (nsError);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
ScopedPointer<BackgroundDownloadTask> downloadTask = new BackgroundDownloadTask (*this, targetLocation, extraHeaders, listener);
|
||||
|
||||
if (downloadTask->initOK() && downloadTask->connect())
|
||||
return downloadTask.release();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
#else
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener);
|
||||
}
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
#else
|
||||
|
||||
|
|
@ -440,6 +640,12 @@ public:
|
|||
stopThread (10000);
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
hasFinished = hasFailed = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
int read (char* dest, int numBytes)
|
||||
{
|
||||
int numDone = 0;
|
||||
|
|
@ -615,6 +821,11 @@ private:
|
|||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (URLConnectionState)
|
||||
};
|
||||
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener);
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
#endif
|
||||
|
|
@ -728,6 +939,15 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
if (finished || isError())
|
||||
return;
|
||||
|
||||
if (connection != nullptr)
|
||||
connection->cancel();
|
||||
}
|
||||
|
||||
int statusCode;
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -185,6 +185,11 @@ public:
|
|||
return (int) bytesRead;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool setPosition (int64 wantedPos)
|
||||
{
|
||||
if (isError())
|
||||
|
|
@ -231,11 +236,11 @@ private:
|
|||
|
||||
void close()
|
||||
{
|
||||
if (request != 0)
|
||||
{
|
||||
InternetCloseHandle (request);
|
||||
request = 0;
|
||||
}
|
||||
HINTERNET requestCopy = request;
|
||||
|
||||
request = 0;
|
||||
if (requestCopy != 0)
|
||||
InternetCloseHandle (requestCopy);
|
||||
|
||||
if (connection != 0)
|
||||
{
|
||||
|
|
@ -548,3 +553,8 @@ bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& targetEmailA
|
|||
|
||||
return mapiSendMail (0, 0, &message, MAPI_DIALOG | MAPI_LOGON_UI, 0) == SUCCESS_SUCCESS;
|
||||
}
|
||||
|
||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener)
|
||||
{
|
||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,104 @@
|
|||
==============================================================================
|
||||
*/
|
||||
|
||||
//==============================================================================
|
||||
struct FallbackDownloadTask : public URL::DownloadTask,
|
||||
public Thread
|
||||
{
|
||||
FallbackDownloadTask (FileOutputStream* outputStreamToUse,
|
||||
size_t bufferSizeToUse,
|
||||
WebInputStream* streamToUse,
|
||||
URL::DownloadTask::Listener* listenerToUse)
|
||||
: Thread ("DownloadTask thread"),
|
||||
fileStream (outputStreamToUse),
|
||||
bufferSize (bufferSizeToUse),
|
||||
buffer (bufferSize),
|
||||
stream (streamToUse),
|
||||
listener (listenerToUse)
|
||||
{
|
||||
contentLength = stream->getTotalLength();
|
||||
httpCode = stream->getStatusCode();
|
||||
|
||||
startThread ();
|
||||
}
|
||||
|
||||
~FallbackDownloadTask()
|
||||
{
|
||||
signalThreadShouldExit();
|
||||
stream->cancel();
|
||||
waitForThreadToExit (-1);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void run() override
|
||||
{
|
||||
while (! stream->isExhausted() && ! stream->isError() && ! threadShouldExit())
|
||||
{
|
||||
if (listener != nullptr)
|
||||
listener->progress (this, downloaded, contentLength);
|
||||
|
||||
const int max = jmin ((int) bufferSize, contentLength < 0 ? std::numeric_limits<int>::max()
|
||||
: static_cast<int> (contentLength - downloaded));
|
||||
|
||||
const int actual = stream->read (buffer.getData(), max);
|
||||
|
||||
if (threadShouldExit() || stream->isError())
|
||||
break;
|
||||
|
||||
if (! fileStream->write (buffer.getData(), static_cast<size_t> (actual)))
|
||||
{
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
|
||||
downloaded += actual;
|
||||
}
|
||||
|
||||
if (threadShouldExit() || (stream != nullptr && stream->isError()))
|
||||
error = true;
|
||||
|
||||
finished = true;
|
||||
|
||||
if (listener != nullptr && ! threadShouldExit())
|
||||
listener->finished (this, ! error);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
ScopedPointer<FileOutputStream> fileStream;
|
||||
size_t bufferSize;
|
||||
HeapBlock<char> buffer;
|
||||
ScopedPointer<WebInputStream> stream;
|
||||
URL::DownloadTask::Listener* listener;
|
||||
};
|
||||
|
||||
void URL::DownloadTask::Listener::progress (DownloadTask*, int64, int64) {}
|
||||
URL::DownloadTask::Listener::~Listener() {}
|
||||
|
||||
//==============================================================================
|
||||
URL::DownloadTask* URL::DownloadTask::createFallbackDownloader (const URL& urlToUse,
|
||||
const File& targetFileToUse,
|
||||
const String& extraHeadersToUse,
|
||||
Listener* listenerToUse)
|
||||
{
|
||||
const size_t bufferSize = 0x8000;
|
||||
targetFileToUse.deleteFile();
|
||||
|
||||
if (ScopedPointer<FileOutputStream> outputStream = targetFileToUse.createOutputStream (bufferSize))
|
||||
{
|
||||
ScopedPointer<WebInputStream> stream = new WebInputStream (urlToUse, false);
|
||||
stream->withExtraHeaders (extraHeadersToUse);
|
||||
|
||||
if (stream->connect (nullptr))
|
||||
return new FallbackDownloadTask (outputStream.release(), bufferSize, stream.release(), listenerToUse);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
URL::DownloadTask::DownloadTask() : contentLength (-1), downloaded (0), finished (false), error (false), httpCode (-1) {}
|
||||
URL::DownloadTask::~DownloadTask() {}
|
||||
|
||||
//==============================================================================
|
||||
URL::URL()
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,6 +322,81 @@ public:
|
|||
int numRedirectsToFollow = 5,
|
||||
String httpRequestCmd = String()) const;
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a download task.
|
||||
|
||||
Returned by downloadToFile to allow querying and controling the download task.
|
||||
*/
|
||||
class DownloadTask
|
||||
{
|
||||
public:
|
||||
struct Listener
|
||||
{
|
||||
virtual ~Listener();
|
||||
|
||||
/** Called when the download has finished. Be aware that this callback may
|
||||
come on an arbitrary thread. */
|
||||
virtual void finished (DownloadTask* task, bool success) = 0;
|
||||
|
||||
/** Called periodically by the OS to indicate download progress.
|
||||
Beware that this callback may come on an arbitrary thread.
|
||||
*/
|
||||
virtual void progress (DownloadTask* task, int64 bytesDownloaded, int64 totalLength);
|
||||
};
|
||||
|
||||
|
||||
/** Releases the resources of the download task, unregisters the listener
|
||||
and cancels the download if necessary. */
|
||||
virtual ~DownloadTask();
|
||||
|
||||
/** Returns the total length of the download task. This may return -1 if the length
|
||||
was not returned by the server. */
|
||||
inline int64 getTotalLength() const { return contentLength; }
|
||||
|
||||
/** Returns the number of bytes that have been downloaded so far. */
|
||||
inline int64 getLengthDownloaded() const { return downloaded; }
|
||||
|
||||
/** Returns true if the download finished or there was an error. */
|
||||
inline bool isFinished() const { return finished; }
|
||||
|
||||
/** Returns the status code of the server's response. This will only be valid
|
||||
after the download has finished.
|
||||
|
||||
@see isFinished
|
||||
*/
|
||||
inline int statusCode() const { return httpCode; }
|
||||
|
||||
/** Returns true if there was an error. */
|
||||
inline bool hadError() const { return error; }
|
||||
|
||||
protected:
|
||||
int64 contentLength, downloaded;
|
||||
bool finished, error;
|
||||
int httpCode;
|
||||
|
||||
DownloadTask ();
|
||||
|
||||
private:
|
||||
friend class URL;
|
||||
|
||||
static DownloadTask* createFallbackDownloader (const URL&, const File&, const String&, Listener*);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DownloadTask)
|
||||
};
|
||||
|
||||
/** Download the URL to a file.
|
||||
|
||||
This method attempts to download the URL to a given file location.
|
||||
|
||||
Using this method to download files on mobile is less flexible but more reliable
|
||||
than using createInputStream or WebInputStreams as it will attempt to download the file
|
||||
using a native OS background network task. Such tasks automatically deal with
|
||||
network re-connections and continuing your download while your app is suspended but are
|
||||
limited to simple GET requests.
|
||||
*/
|
||||
DownloadTask* downloadToFile (const File& targetLocation,
|
||||
String extraHeaders = String(),
|
||||
DownloadTask::Listener* listener = nullptr);
|
||||
|
||||
//==============================================================================
|
||||
/** Tries to download the entire contents of this URL into a binary data block.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ WebInputStream& WebInputStream::withNumRedirectsToFollow (int num) {
|
|||
StringPairArray WebInputStream::getRequestHeaders() const { return pimpl->getRequestHeaders(); }
|
||||
StringPairArray WebInputStream::getResponseHeaders() { connect (nullptr); return pimpl->getResponseHeaders(); }
|
||||
bool WebInputStream::isError() const { return pimpl->isError(); }
|
||||
void WebInputStream::cancel() { pimpl->cancel(); }
|
||||
bool WebInputStream::isExhausted() { return pimpl->isExhausted(); }
|
||||
int64 WebInputStream::getPosition() { return pimpl->getPosition(); }
|
||||
int64 WebInputStream::getTotalLength() { connect (nullptr); return pimpl->getTotalLength(); }
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ class JUCE_API WebInputStream : public InputStream
|
|||
/** Returns true if there was an error during the connection attempt */
|
||||
bool isError() const;
|
||||
|
||||
/** Will cancel a blocking read. */
|
||||
void cancel();
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the total number of bytes available for reading in this stream.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue