DotNetDupe 4.0.6
C++17/20 Implementation of the .NET Base Class Library (BCL)
Loading...
Searching...
No Matches
FileDownloader.cpp
Go to the documentation of this file.
1#include "pch.h"
10#include "System/Console.h"
11#include "System/Uri.h"
12#include "System/Convert.h"
14#include "System/IO/File.h"
16#include <chrono>
17#include <cstdlib>
18#include <atomic>
19#include <string>
20
21namespace DotNetDupe {
22 namespace System {
23 namespace Net {
24 namespace Http {
25
26 struct FileDownloader::Impl : public Object {
27 Collections::Generic::Dictionary<String, String> m_customHeaders;
28 String m_sUrl;
29 String m_sDestinationPath;
30
33
34 std::atomic<DownloadStatus> m_status{ DownloadStatus::NotStarted };
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 };
39
40 HttpClient m_httpClient;
41 SmartPointer<Threading::Thread> m_pWorkerThread;
42
43 Impl() = default;
44
45 long long CheckExistingFileSize() {
46 if (!IO::File::Exists(m_sDestinationPath)) return 0;
47 try {
48 IO::FileStream existingFile(m_sDestinationPath, 2); // FileMode::Open
49 return existingFile.GetLength();
50 } catch (const IO::IOException& ex) {
51 Console::WriteLine(String("[FileDownloader] Error checking file size: ") + ex.What());
52 return 0;
53 } catch (const SystemException& ex) {
54 Console::WriteLine(String("[FileDownloader] System Exception checking file: ") + ex.What());
55 return 0;
56 }
57 }
58
59 long long ParseContentLengthFromResponse(const HttpResponseMessagePtr& pResponse) {
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());
65 }
66 }
67 return -1;
68 }
69
70 long long FetchContentLength() {
71 try {
72 Console::WriteLine(String("[FileDownloader] Sending HEAD request to URL: ") + m_sUrl);
73 auto pRequest = HttpRequestMessagePtr::NewShared(HttpMethod("HEAD"), Uri(m_sUrl));
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) {
80 Console::WriteLine(String("[FileDownloader] HEAD request failed: ") + ex.What());
81 return -1;
82 } catch (const Sockets::SocketException& ex) {
83 Console::WriteLine(String("[FileDownloader] Socket error on HEAD request: ") + ex.What());
84 return -1;
85 } catch (const SystemException& ex) {
86 Console::WriteLine(String("[FileDownloader] HEAD Exception: ") + ex.What());
87 return -1;
88 }
89 }
90
91 HttpRequestMessagePtr CreateGetRequest(long long llRangeStart) {
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");
97
98 for (auto const& [sKey, sVal] : m_customHeaders) {
99 pRequest->GetHeaders()[sKey] = sVal;
100 }
101
102 if (llRangeStart > 0) {
103 pRequest->GetHeaders().Add("Range", String("bytes=") + Convert::ToString(llRangeStart) + "-");
104 }
105 return pRequest;
106 }
107
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;
113 }
114 }
115
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();
123 return progress;
124 }
125
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);
132 }
133
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);
136 outFile.Flush();
137 m_llDownloadedBytes += iRead;
138 llBytesSession += iRead;
139 UpdateRate(llBytesSession, timeStart);
140 FireProgressEvent();
141 }
142
143 bool ReadAndWriteData(const SmartPointer<IO::Stream>& pStream, IO::FileStream& outFile) {
144 char pBuffer[8192];
145 auto timeStart = std::chrono::steady_clock::now();
146 long long llBytesSession = 0;
147 try {
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);
152 }
153 } catch (const Exception& ex) {
154 Console::WriteLine(String("[FileDownloader] Stream error: ") + ex.What());
155 return false;
156 }
157 return !m_bPauseRequested.load();
158 }
159
160 void NotifyStateChange(DownloadStatus status, const String& sError = "") {
161 m_status = status;
162 m_dDownloadRate = 0.0;
163 FireProgressEvent();
164 if (status == DownloadStatus::Completed || status == DownloadStatus::Failed) {
165 bool bSuccess = (status == DownloadStatus::Completed);
166 bool bCancelled = m_bPauseRequested.load();
167 DownloadCompletedEventArgs args(bSuccess, bCancelled, sError);
168 DownloadCompleted.Invoke(this, args);
169 }
170 }
171
172 void ExecuteDownload(long long llExistingBytes) {
173 auto pRequest = CreateGetRequest(llExistingBytes);
174 HttpResponseMessagePtr pResponse;
175 Console::WriteLine(String("[FileDownloader] Requesting URL: ") + m_sUrl);
176 try {
177 pResponse = m_httpClient.Send(pRequest, HttpCompletionOption::ResponseHeadersRead);
178 } catch (const HttpRequestException& ex) {
179 Console::WriteLine(String("[FileDownloader] HTTP request failed: ") + ex.What());
180 return NotifyStateChange(DownloadStatus::Failed, ex.What());
181 } catch (const Sockets::SocketException& ex) {
182 Console::WriteLine(String("[FileDownloader] Socket connection failed: ") + ex.What());
183 return NotifyStateChange(DownloadStatus::Failed, ex.What());
184 } catch (const SystemException& ex) {
185 Console::WriteLine(String("[FileDownloader] HTTP client exception: ") + ex.What());
186 return NotifyStateChange(DownloadStatus::Failed, ex.What());
187 }
188
189 if (pResponse.IsNull()) return NotifyStateChange(DownloadStatus::Failed, "Null response received.");
190
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") {
196 m_sUrl = sVal;
197 return ExecuteDownload(llExistingBytes);
198 }
199 }
200 }
201
202 int iFileMode = 1; // FileMode::Create
203 long long llRangeContentLen = ParseContentLengthFromResponse(pResponse);
204 if (iStatusCode == 206) {
205 iFileMode = 5; // FileMode::Append
206 } else if (iStatusCode == 200) {
207 iFileMode = 1; // FileMode::Create
208 llExistingBytes = 0;
209 m_llDownloadedBytes = 0;
210 } else {
211 Console::WriteLine(String("[FileDownloader] Server returned error status code: ") + Convert::ToString(iStatusCode));
212 return NotifyStateChange(DownloadStatus::Failed, String("HTTP Status ") + Convert::ToString(iStatusCode));
213 }
214 if (llRangeContentLen >= 0) m_llTotalBytes = llExistingBytes + llRangeContentLen;
215
216 auto pStream = pResponse->GetContent()->ReadAsStream();
217 if (pStream.IsNull()) return NotifyStateChange(DownloadStatus::Failed, "Content stream is null.");
218
219 try {
220 bool bCompleted = false;
221 {
222 IO::FileStream outFile(m_sDestinationPath, iFileMode);
223 bCompleted = ReadAndWriteData(pStream, outFile);
224 }
225 NotifyStateChange(bCompleted ? DownloadStatus::Completed : DownloadStatus::Paused);
226 } catch (const IO::IOException& ex) {
227 Console::WriteLine(String("[FileDownloader] File I/O exception: ") + ex.What());
228 NotifyStateChange(DownloadStatus::Failed, ex.What());
229 }
230 }
231
232 void DownloadLoop() {
233 long long llExistingBytes = CheckExistingFileSize();
234 m_llDownloadedBytes = llExistingBytes;
235
236 long long llTotalContentLen = FetchContentLength();
237 if (llTotalContentLen >= 0) m_llTotalBytes = llTotalContentLen;
238 else if (m_llTotalBytes.load() == 0) m_llTotalBytes = llExistingBytes;
239
240 try {
241 ExecuteDownload(llExistingBytes);
242 } catch (const Exception& ex) {
243 Console::WriteLine(String("[FileDownloader] Download loop failed: ") + ex.What());
244 NotifyStateChange(DownloadStatus::Failed, ex.What());
245 } catch (const std::exception& ex) {
246 UnknownException unk(ex.what());
247 Console::WriteLine(String("[FileDownloader] Download loop failed: ") + unk.What());
248 NotifyStateChange(DownloadStatus::Failed, unk.What());
249 } catch (...) {
250 UnknownException unk("An unknown error occurred during download.");
251 Console::WriteLine(String("[FileDownloader] Download loop failed: ") + unk.What());
252 NotifyStateChange(DownloadStatus::Failed, unk.What());
253 }
254 }
255 };
256
257 FileDownloader::FileDownloader(const String& sUrl, const String& sDestinationPath)
258 : m_pImpl(SmartPointer<Impl>::NewShared()),
262 m_pImpl->m_sUrl = sUrl;
263 m_pImpl->m_sDestinationPath = sDestinationPath;
264 if (sUrl.IsEmpty()) throw ArgumentException("sUrl cannot be empty.");
265 if (sDestinationPath.IsEmpty()) throw ArgumentException("sDestinationPath cannot be empty.");
266 String sLower = sUrl.ToLower();
267 if (!sLower.StartsWith("https://", false) && !sLower.StartsWith("http://", false)) {
268 throw ArgumentException("Only HTTP/HTTPS URLs are supported.");
269 }
270 }
271
274 m_pImpl->m_bPauseRequested = true;
275 }
276
279 return m_pImpl->GetProgress();
280 }
281
284 return m_pImpl->m_status.load();
285 }
286
289 if (m_pImpl->m_status.load() == DownloadStatus::Downloading) {
290 m_pImpl->m_bPauseRequested = true;
291 Console::WriteLine("[FileDownloader] Pause requested.");
292 }
293 }
294
297 if (m_pImpl->m_status.load() == DownloadStatus::Downloading) return false;
298
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;
304 m_pImpl->m_status = DownloadStatus::Downloading;
305 Console::WriteLine(String("[FileDownloader] Starting download from ") + m_pImpl->m_sUrl);
306
308 auto pImpl = m_pImpl;
309 m_pImpl->m_pWorkerThread = SmartPointer<Threading::Thread>::NewShared(Threading::ThreadStart([pImpl]() { pImpl->DownloadLoop(); }));
310 m_pImpl->m_pWorkerThread->Start();
311 return true;
312 }
313
316 DownloadStatus currentStatus = m_pImpl->m_status.load();
317 if (currentStatus == DownloadStatus::Downloading || currentStatus == DownloadStatus::Completed) return false;
318
320 m_pImpl->m_bPauseRequested = false;
321 m_pImpl->m_status = DownloadStatus::Downloading;
322 Console::WriteLine(String("[FileDownloader] Resuming download for ") + m_pImpl->m_sDestinationPath);
323
325 auto pImpl = m_pImpl;
326 m_pImpl->m_pWorkerThread = SmartPointer<Threading::Thread>::NewShared(Threading::ThreadStart([pImpl]() { pImpl->DownloadLoop(); }));
327 m_pImpl->m_pWorkerThread->Start();
328 return true;
329 }
330
333 for (auto const& [sKey, sVal] : headers) {
334 m_pImpl->m_customHeaders[sKey] = sVal;
335 }
336 }
337
338 void FileDownloader::SetUserAgent(const String& sUserAgent) {
340 m_pImpl->m_customHeaders["User-Agent"] = sUserAgent;
341 }
342
343 }
344 }
345 }
346}
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.
Definition Dictionary.h:66
static String ToString(bool value)
Definition Convert.cpp:253
static bool Exists(const String &sPath)
Determines whether the specified file exists.
Definition File.cpp:31
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.
Definition HttpMethod.h:49
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.
Definition String.h:74
String ToLower() const
Definition String.cpp:672
bool StartsWith(const String &sPrefix) const
Definition String.cpp:610
String()
Initializes a new instance of the String class to an empty string.
Definition String.cpp:64
Uri(const String &uriString)
Initializes a new instance of the Uri class with the specified URI string.
Definition Uri.cpp:9
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.
Definition HttpClient.h:31
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.