Skip to content

Commit 8355c66

Browse files
Sunrisepeakclaude
andcommitted
feat: add isCancelled support to download_to_file
Check cancellation callback after each 8KB block during body read. Allows callers to abort long downloads promptly (e.g. ESC in TUI). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5c2cf35 commit 8355c66

File tree

1 file changed

+25
-6
lines changed

1 file changed

+25
-6
lines changed

src/http.cppm

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -762,12 +762,15 @@ public:
762762

763763
// Download URL to file with streaming progress.
764764
// Follows redirects. Calls onProgress periodically during body read.
765+
// isCancelled is checked after each block — return true to abort.
765766
DownloadToFileResult download_to_file(
766767
const std::string& url,
767768
const std::filesystem::path& destFile,
768-
DownloadProgressFn onProgress = nullptr)
769+
DownloadProgressFn onProgress = nullptr,
770+
std::function<bool()> isCancelled = nullptr)
769771
{
770-
return download_to_file_impl(url, destFile, std::move(onProgress), 0);
772+
return download_to_file_impl(url, destFile, std::move(onProgress),
773+
std::move(isCancelled), 0);
771774
}
772775

773776
HttpClientConfig& config() { return config_; }
@@ -778,6 +781,7 @@ private:
778781
const std::string& url,
779782
const std::filesystem::path& destFile,
780783
DownloadProgressFn onProgress,
784+
std::function<bool()> isCancelled,
781785
int redirectCount)
782786
{
783787
DownloadToFileResult result;
@@ -907,7 +911,7 @@ private:
907911
location;
908912
}
909913
return download_to_file_impl(location, destFile, std::move(onProgress),
910-
redirectCount + 1);
914+
std::move(isCancelled), redirectCount + 1);
911915
}
912916

913917
if (result.statusCode < 200 || result.statusCode >= 300) {
@@ -929,9 +933,23 @@ private:
929933
std::int64_t totalBytes = contentLength > 0 ? contentLength : 0;
930934
std::int64_t downloaded = 0;
931935

936+
// Check cancellation between blocks
937+
auto cancelled = [&]() -> bool {
938+
if (isCancelled && isCancelled()) {
939+
result.error = "cancelled";
940+
ofs.close();
941+
result.bytesWritten = downloaded;
942+
sock->close();
943+
pool_.erase(poolKey);
944+
return true;
945+
}
946+
return false;
947+
};
948+
932949
// Read body and stream to file
933950
if (chunked) {
934951
while (true) {
952+
if (cancelled()) return result;
935953
std::string sizeLine = read_line(*sock, config_.readTimeoutMs);
936954
auto semi = sizeLine.find(';');
937955
if (semi != std::string::npos) sizeLine = sizeLine.substr(0, semi);
@@ -944,10 +962,10 @@ private:
944962
break;
945963
}
946964

947-
// Read chunk in sub-blocks for progress
948965
int remaining = chunkSize;
949966
char buf[8192];
950967
while (remaining > 0) {
968+
if (cancelled()) return result;
951969
int toRead = remaining > static_cast<int>(sizeof(buf))
952970
? static_cast<int>(sizeof(buf)) : remaining;
953971
if (!read_exact(*sock, buf, toRead, config_.readTimeoutMs)) {
@@ -961,12 +979,13 @@ private:
961979
remaining -= toRead;
962980
if (onProgress) onProgress(totalBytes, downloaded);
963981
}
964-
read_line(*sock, config_.readTimeoutMs); // trailing \r\n
982+
read_line(*sock, config_.readTimeoutMs);
965983
}
966984
} else if (contentLength > 0) {
967985
char buf[8192];
968986
std::int64_t remaining = contentLength;
969987
while (remaining > 0) {
988+
if (cancelled()) return result;
970989
int toRead = remaining > static_cast<std::int64_t>(sizeof(buf))
971990
? static_cast<int>(sizeof(buf))
972991
: static_cast<int>(remaining);
@@ -982,10 +1001,10 @@ private:
9821001
if (onProgress) onProgress(totalBytes, downloaded);
9831002
}
9841003
} else {
985-
// Read until connection close
9861004
connectionClose = true;
9871005
char buf[8192];
9881006
while (true) {
1007+
if (cancelled()) return result;
9891008
if (!sock->wait_readable(config_.readTimeoutMs)) break;
9901009
int ret = sock->read(buf, sizeof(buf));
9911010
if (ret <= 0) break;

0 commit comments

Comments
 (0)