Line data Source code
1 : #include "NetworkDownloadCore.h"
2 : #include "FangNetworkAccessManager.h"
3 :
4 : #include "../utilities/FangLogging.h"
5 :
6 260 : NetworkDownloadCore::NetworkDownloadCore(NetworkDownloadConfig config,
7 : QObject* parent,
8 260 : QNetworkAccessManager* networkManager)
9 : : FangObject(parent)
10 260 : , manager(networkManager ? networkManager : new FangNetworkAccessManager(this))
11 260 : , ownsManager(networkManager == nullptr)
12 260 : , config(config)
13 260 : , currentReply(nullptr)
14 260 : , redirectCount(0)
15 260 : , currentAttemptCount(0)
16 520 : , lastError(QNetworkReply::NoError)
17 : {
18 260 : inactivityTimer.setSingleShot(true);
19 260 : retryTimer.setSingleShot(true);
20 :
21 260 : connect(manager, &QNetworkAccessManager::finished, this, &NetworkDownloadCore::onRequestFinished);
22 260 : connect(&inactivityTimer, &QTimer::timeout, this, &NetworkDownloadCore::onTimeout);
23 260 : connect(&retryTimer, &QTimer::timeout, this, &NetworkDownloadCore::onRetryTimer);
24 260 : }
25 :
26 494 : NetworkDownloadCore::~NetworkDownloadCore()
27 : {
28 : // Disconnect from manager to avoid receiving signals during destruction. This prevents
29 : // signals from being emitted on the object as its being destructed.
30 260 : if (manager) {
31 236 : disconnect(manager, &QNetworkAccessManager::finished, this, &NetworkDownloadCore::onRequestFinished);
32 : }
33 :
34 260 : retryTimer.stop();
35 :
36 260 : if (currentReply) {
37 0 : currentReply->abort();
38 0 : currentReply->deleteLater();
39 0 : currentReply = nullptr;
40 : }
41 494 : }
42 :
43 112 : void NetworkDownloadCore::download(const QUrl& url)
44 : {
45 : // Reset counters for new download.
46 112 : redirectCount = 0;
47 112 : currentAttemptCount = 1; // First attempt
48 112 : originalUrl = url;
49 112 : lastError = QNetworkReply::NoError;
50 112 : lastErrorString.clear();
51 :
52 : // Stop any pending retries.
53 112 : retryTimer.stop();
54 :
55 112 : downloadInternal(url);
56 112 : }
57 :
58 142 : void NetworkDownloadCore::downloadInternal(const QUrl& url)
59 : {
60 : // Clean up existing reply if needed.
61 142 : if (currentReply) {
62 : // Clear currentReply BEFORE aborting to prevent onRequestFinished from processing.
63 25 : QNetworkReply* oldReply = currentReply;
64 25 : currentReply = nullptr;
65 25 : oldReply->disconnect(this);
66 25 : oldReply->abort();
67 25 : oldReply->deleteLater();
68 : }
69 :
70 : // Stop any existing timer.
71 142 : inactivityTimer.stop();
72 :
73 : // Validate URL.
74 142 : if (url.isRelative()) {
75 2 : emit error(originalUrl, "Relative URLs are not allowed");
76 2 : return;
77 : }
78 :
79 : // Create and send request.
80 140 : QNetworkRequest request(url);
81 :
82 : // Configure timeout based on mode.
83 140 : if (!config.useInactivityTimeout) {
84 1 : request.setTransferTimeout(config.timeoutMs);
85 : }
86 :
87 140 : currentReply = manager->get(request);
88 :
89 : // Connect progress signal for inactivity timeout reset.
90 140 : if (config.useInactivityTimeout) {
91 139 : connect(currentReply, &QNetworkReply::downloadProgress,
92 139 : this, &NetworkDownloadCore::onDownloadProgress);
93 139 : inactivityTimer.start(config.timeoutMs);
94 : }
95 140 : }
96 :
97 2 : void NetworkDownloadCore::abort()
98 : {
99 2 : inactivityTimer.stop();
100 2 : retryTimer.stop();
101 :
102 2 : if (currentReply) {
103 : // Clear currentReply BEFORE aborting to prevent onRequestFinished from processing.
104 1 : QNetworkReply* reply = currentReply;
105 1 : currentReply = nullptr;
106 :
107 1 : reply->abort();
108 1 : reply->deleteLater();
109 : }
110 2 : }
111 :
112 359 : void NetworkDownloadCore::onRequestFinished(QNetworkReply* reply)
113 : {
114 : // Sanity check: Only process *our* reply.
115 359 : if (reply != currentReply) {
116 279 : return;
117 : }
118 :
119 135 : inactivityTimer.stop();
120 :
121 : // Check for network errors.
122 135 : if (reply->error() != QNetworkReply::NoError) {
123 29 : lastError = reply->error();
124 29 : lastErrorString = reply->errorString();
125 58 : qCDebug(logNetwork) << "NetworkDownloadCore error:" << lastErrorString;
126 :
127 : // Clean up the reply.
128 29 : currentReply = nullptr;
129 29 : reply->deleteLater();
130 :
131 : // Check if we should retry.
132 35 : if (config.retryPolicy.shouldRetry(currentAttemptCount) &&
133 6 : config.retryPolicy.isRetryable(lastError)) {
134 5 : scheduleRetry();
135 5 : return;
136 : }
137 :
138 : // No more retries - emit error.
139 24 : emit error(originalUrl, lastErrorString);
140 24 : return;
141 : }
142 :
143 : // Check for HTTP redirect.
144 106 : QVariant redirectVariant = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
145 106 : if (!redirectVariant.isNull()) {
146 26 : if (redirectCount >= config.maxRedirects) {
147 2 : currentReply = nullptr; // Clean up before emitting signal.
148 2 : reply->deleteLater();
149 2 : emit error(originalUrl, "Maximum HTTP redirects exceeded");
150 2 : return;
151 : }
152 :
153 24 : redirectCount++;
154 24 : QUrl redirectUrl = redirectVariant.toUrl();
155 :
156 : // Handle relative redirects.
157 24 : if (redirectUrl.isRelative()) {
158 2 : redirectUrl = reply->url().resolved(redirectUrl);
159 : }
160 :
161 24 : downloadInternal(redirectUrl);
162 24 : return;
163 24 : }
164 :
165 : // Success!
166 80 : QUrl finalUrl = reply->url();
167 80 : QByteArray data = reply->readAll();
168 80 : currentReply = nullptr; // Clean up before emitting signal.
169 80 : reply->deleteLater();
170 80 : emit finished(finalUrl, data);
171 106 : }
172 :
173 134 : void NetworkDownloadCore::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
174 : {
175 : // Reset inactivity timer on progress.
176 134 : inactivityTimer.start(config.timeoutMs);
177 :
178 : // Forward progress signal.
179 134 : if (currentReply) {
180 134 : emit progress(currentReply->url(), bytesReceived, bytesTotal);
181 : }
182 134 : }
183 :
184 3 : void NetworkDownloadCore::onTimeout()
185 : {
186 3 : if (currentReply) {
187 : // Clear currentReply BEFORE aborting to prevent onRequestFinished from processing.
188 3 : QNetworkReply* reply = currentReply;
189 3 : currentReply = nullptr;
190 :
191 3 : reply->abort();
192 3 : reply->deleteLater();
193 :
194 3 : lastError = QNetworkReply::TimeoutError;
195 3 : lastErrorString = "Download timeout";
196 :
197 : // Check if we should retry.
198 4 : if (config.retryPolicy.shouldRetry(currentAttemptCount) &&
199 1 : config.retryPolicy.isRetryable(lastError)) {
200 1 : scheduleRetry();
201 1 : return;
202 : }
203 :
204 : // No more retries - emit error.
205 2 : emit error(originalUrl, lastErrorString);
206 : }
207 : }
208 :
209 6 : void NetworkDownloadCore::scheduleRetry()
210 : {
211 6 : int delay = config.retryPolicy.calculateDelay(currentAttemptCount);
212 18 : qCDebug(logNetwork) << "NetworkDownloadCore: scheduling retry" << (currentAttemptCount + 1)
213 12 : << "in" << delay << "ms for" << originalUrl;
214 6 : emit retrying(currentAttemptCount + 1, delay);
215 6 : retryTimer.start(delay);
216 6 : }
217 :
218 6 : void NetworkDownloadCore::onRetryTimer()
219 : {
220 6 : currentAttemptCount++;
221 6 : redirectCount = 0; // Reset redirect count for new attempt.
222 6 : downloadInternal(originalUrl);
223 6 : }
|