26 struct FileDownloader::Impl :
public Object {
27 Collections::Generic::Dictionary<String, String> m_customHeaders;
35 std::atomic<bool> m_bPauseRequested{
false };
36 std::atomic<long long> m_llTotalBytes{ 0 };
37 std::atomic<long long> m_llDownloadedBytes{ 0 };
38 std::atomic<double> m_dDownloadRate{ 0.0 };
40 HttpClient m_httpClient;
41 SmartPointer<Threading::Thread> m_pWorkerThread;
45 long long CheckExistingFileSize() {
48 IO::FileStream existingFile(m_sDestinationPath, 2);
49 return existingFile.GetLength();
50 }
catch (
const IO::IOException& ex) {
53 }
catch (
const SystemException& ex) {
60 if (pResponse.IsNull())
return -1;
61 auto& headers = pResponse->GetHeaders();
62 for (
auto const& [sKey, sVal] : headers) {
63 if (sKey.ToLower() ==
"content-length") {
64 return std::atoll(sVal.GetRawString());
70 long long FetchContentLength() {
74 pRequest->GetHeaders().Add(
"Accept",
"*/*");
75 pRequest->GetHeaders().Add(
"User-Agent",
"DotNetDupe-FileDownloader/1.0");
76 for (
auto const& [sKey, sVal] : m_customHeaders) pRequest->GetHeaders()[sKey] = sVal;
77 auto pResponse = m_httpClient.Send(pRequest);
78 return ParseContentLengthFromResponse(pResponse);
79 }
catch (
const HttpRequestException& ex) {
82 }
catch (
const Sockets::SocketException& ex) {
85 }
catch (
const SystemException& ex) {
93 pRequest->GetHeaders().Add(
"Accept",
"*/*");
94 pRequest->GetHeaders().Add(
"User-Agent",
"DotNetDupe-FileDownloader/1.0");
95 pRequest->GetHeaders().Add(
"Accept-Encoding",
"identity");
96 pRequest->GetHeaders().Add(
"Connection",
"keep-alive");
98 for (
auto const& [sKey, sVal] : m_customHeaders) {
99 pRequest->GetHeaders()[sKey] = sVal;
102 if (llRangeStart > 0) {
108 void UpdateRate(
long long llBytesSession,
const std::chrono::steady_clock::time_point& timeStart) {
109 auto timeNow = std::chrono::steady_clock::now();
110 double dElapsedSec = std::chrono::duration<double>(timeNow - timeStart).count();
111 if (dElapsedSec > 0.05) {
112 m_dDownloadRate =
static_cast<double>(llBytesSession) / dElapsedSec;
116 DownloadProgress GetProgress()
const {
117 DownloadProgress progress;
118 progress.TotalBytes = m_llTotalBytes.load();
119 progress.DownloadedBytes = m_llDownloadedBytes.load();
120 progress.RemainingBytes = progress.TotalBytes > progress.DownloadedBytes ? (progress.TotalBytes - progress.DownloadedBytes) : 0;
121 progress.DownloadRateBytesPerSec = m_dDownloadRate.load();
122 progress.Status = m_status.load();
126 void FireProgressEvent() {
127 long long llTotal = m_llTotalBytes.load();
128 long long llDownloaded = m_llDownloadedBytes.load();
129 double dPercent = (llTotal > 0) ? (
static_cast<double>(llDownloaded) /
static_cast<double>(llTotal) * 100.0) : 0.0;
130 DownloadProgressChangedEventArgs args(llDownloaded, llTotal, dPercent, m_dDownloadRate.load(), m_status.load());
131 DownloadProgressChanged.Invoke(
this, args);
134 void ProcessDownloadChunk(
int iRead,
const char* pBuffer, IO::FileStream& outFile,
long long& llBytesSession,
const std::chrono::steady_clock::time_point& timeStart) {
135 outFile.Write(pBuffer, 0, iRead);
137 m_llDownloadedBytes += iRead;
138 llBytesSession += iRead;
139 UpdateRate(llBytesSession, timeStart);
143 bool ReadAndWriteData(
const SmartPointer<IO::Stream>& pStream, IO::FileStream& outFile) {
145 auto timeStart = std::chrono::steady_clock::now();
146 long long llBytesSession = 0;
148 while (!m_bPauseRequested.load()) {
149 int iRead = pStream->Read(pBuffer, 0,
sizeof(pBuffer));
150 if (iRead <= 0)
break;
151 ProcessDownloadChunk(iRead, pBuffer, outFile, llBytesSession, timeStart);
153 }
catch (
const Exception& ex) {
157 return !m_bPauseRequested.load();
160 void NotifyStateChange(
DownloadStatus status,
const String& sError =
"") {
162 m_dDownloadRate = 0.0;
166 bool bCancelled = m_bPauseRequested.load();
167 DownloadCompletedEventArgs args(bSuccess, bCancelled, sError);
168 DownloadCompleted.Invoke(
this, args);
172 void ExecuteDownload(
long long llExistingBytes) {
173 auto pRequest = CreateGetRequest(llExistingBytes);
178 }
catch (
const HttpRequestException& ex) {
181 }
catch (
const Sockets::SocketException& ex) {
184 }
catch (
const SystemException& ex) {
191 int iStatusCode =
static_cast<int>(pResponse->GetStatusCode());
192 if (iStatusCode == 301 || iStatusCode == 302 || iStatusCode == 307 || iStatusCode == 308) {
193 auto& headers = pResponse->GetHeaders();
194 for (
auto const& [sKey, sVal] : headers) {
195 if (sKey.ToLower() ==
"location") {
197 return ExecuteDownload(llExistingBytes);
203 long long llRangeContentLen = ParseContentLengthFromResponse(pResponse);
204 if (iStatusCode == 206) {
206 }
else if (iStatusCode == 200) {
209 m_llDownloadedBytes = 0;
214 if (llRangeContentLen >= 0) m_llTotalBytes = llExistingBytes + llRangeContentLen;
216 auto pStream = pResponse->GetContent()->ReadAsStream();
220 bool bCompleted =
false;
222 IO::FileStream outFile(m_sDestinationPath, iFileMode);
223 bCompleted = ReadAndWriteData(pStream, outFile);
226 }
catch (
const IO::IOException& ex) {
232 void DownloadLoop() {
233 long long llExistingBytes = CheckExistingFileSize();
234 m_llDownloadedBytes = llExistingBytes;
236 long long llTotalContentLen = FetchContentLength();
237 if (llTotalContentLen >= 0) m_llTotalBytes = llTotalContentLen;
238 else if (m_llTotalBytes.load() == 0) m_llTotalBytes = llExistingBytes;
241 ExecuteDownload(llExistingBytes);
242 }
catch (
const Exception& ex) {
245 }
catch (
const std::exception& ex) {
246 UnknownException unk(ex.what());
250 UnknownException unk(
"An unknown error occurred during download.");
262 m_pImpl->m_sUrl = sUrl;
263 m_pImpl->m_sDestinationPath = sDestinationPath;
268 throw ArgumentException(
"Only HTTP/HTTPS URLs are supported.");
274 m_pImpl->m_bPauseRequested =
true;
279 return m_pImpl->GetProgress();
284 return m_pImpl->m_status.load();
290 m_pImpl->m_bPauseRequested =
true;
300 m_pImpl->m_bPauseRequested =
false;
301 m_pImpl->m_llDownloadedBytes = 0;
302 m_pImpl->m_llTotalBytes = 0;
303 m_pImpl->m_dDownloadRate = 0.0;
308 auto pImpl = m_pImpl;
310 m_pImpl->m_pWorkerThread->Start();
320 m_pImpl->m_bPauseRequested =
false;
325 auto pImpl = m_pImpl;
327 m_pImpl->m_pWorkerThread->Start();
333 for (
auto const& [sKey, sVal] : headers) {
334 m_pImpl->m_customHeaders[sKey] = sVal;
340 m_pImpl->m_customHeaders[
"User-Agent"] = sUserAgent;
Defines the exception thrown when an invalid argument is provided to a method.
Defines the exception thrown when a null reference is passed to a method that does not accept it.
Represents standard input, output, and error streams for console applications.
Converts a base data type to another base data type, and encodes/decodes Base64 data.
Provides static methods for the creation, copying, deletion, moving, and opening of a single file.
Resumable, multi-threaded HTTP file downloader with progress tracking and event notifications.
Exception thrown when an HTTP request or connection failure occurs.
The exception that is thrown when an I/O error occurs.
Defines the exception thrown when a method call is invalid for the object's current state.
Exception thrown when a network socket error occurs per RFC 793 / RFC 768.
Creates and controls a thread, sets its priority, and gets its status mirroring .NET System....
Represents an unknown or unmapped exception encountered during execution.
Provides an object representation of a Uniform Resource Identifier (URI) and easy access to its parts...
The exception that is thrown when one of the arguments provided to a method is not valid.
Represents a collection of keys and values.
static String ToString(bool value)
static bool Exists(const String &sPath)
Determines whether the specified file exists.
bool Resume()
Resumes a paused download using HTTP Range headers if supported.
void SetUserAgent(const String &sUserAgent)
Overrides the default User-Agent request header string.
FileDownloader(const String &sUrl, const String &sDestinationPath)
Initializes a new instance of FileDownloader with source URL and destination path.
DownloadProgress GetProgress() const
Queries instantaneous download telemetry metrics.
void Pause()
Suspends byte streaming without deleting partially received data.
EventHandler< DownloadCompletedEventArgs > & DownloadCompleted
Multicast event triggered when transfer completes or terminates.
~FileDownloader() override
Cancels active downloads and releases resources.
DownloadStatus GetStatus() const
Queries current operational status.
EventHandler< DownloadProgressChangedEventArgs > & DownloadProgressChanged
Multicast event triggered when chunk progress occurs.
void AddHeaders(const Collections::Generic::Dictionary< String, String > &headers)
Appends custom HTTP request headers to outgoing requests.
bool Start()
Begins the asynchronous download on a background worker thread.
static const HttpMethod Get
Represents an HTTP GET protocol method.
HttpMethod(const String &method)
Initializes a new instance of the HttpMethod class with a specific HTTP method name.
A unified smart pointer that supports both unique and shared ownership semantics.
static SmartPointer< HttpRequestMessage > NewShared()
Represents text as a sequence of UTF-8 code units with culture-invariant operations.
bool StartsWith(const String &sPrefix) const
String()
Initializes a new instance of the String class to an empty string.
Uri(const String &uriString)
Initializes a new instance of the Uri class with the specified URI string.
SmartPointer< HttpResponseMessage > HttpResponseMessagePtr
Type alias for reference-counted SmartPointer to HttpResponseMessage.
DownloadStatus
State machine values representing the lifecycle of an ongoing file download.
@ Completed
Transfer finished successfully and file was verified.
@ Downloading
Transfer is actively receiving bytes over the network.
@ Failed
Transfer encountered an unrecoverable network or disk I/O failure.
@ Paused
Transfer is temporarily suspended.
@ NotStarted
Transfer has not been initiated.
@ ResponseHeadersRead
Operation should complete as soon as a response is available and headers are read.
SmartPointer< HttpRequestMessage > HttpRequestMessagePtr
Type alias for reference-counted SmartPointer to HttpRequestMessage.
EventHandler() -> EventHandler< TEventArgs >
Snapshot of download telemetry metrics including transferred bytes and bandwidth speed.