Files
crosspoint-reader-mod/src/network/HttpDownloader.cpp
Arthur Tazhitdinov 45a228a645 fix: use HTTPClient::writeToStream for downloading files from OPDS (#1207)
## Summary

* Refactored `HttpDownloader::downloadToFile` to use `FileWriteStream`
and `HTTPClient::writeToStream`, removing manual chunked downloading
logic, which was error-prone.
* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/632

## Additional Context

* Tested downloading files from OPDS with a size up to 10 mb.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
2026-02-28 13:39:09 +03:00

197 lines
5.8 KiB
C++

#include "HttpDownloader.h"
#include <HTTPClient.h>
#include <Logging.h>
#include <NetworkClient.h>
#include <NetworkClientSecure.h>
#include <StreamString.h>
#include <base64.h>
#include <cstring>
#include <memory>
#include <utility>
#include "CrossPointSettings.h"
#include "util/UrlUtils.h"
namespace {
class FileWriteStream final : public Stream {
public:
FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress)
: file_(file), total_(total), progress_(std::move(progress)) {}
size_t write(uint8_t byte) override { return write(&byte, 1); }
size_t write(const uint8_t* buffer, size_t size) override {
// Write-through stream for HTTPClient::writeToStream with progress tracking.
const size_t written = file_.write(buffer, size);
if (written != size) {
writeOk_ = false;
}
downloaded_ += written;
if (progress_ && total_ > 0) {
progress_(downloaded_, total_);
}
return written;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override { file_.flush(); }
size_t downloaded() const { return downloaded_; }
bool ok() const { return writeOk_; }
private:
FsFile& file_;
size_t total_;
size_t downloaded_ = 0;
bool writeOk_ = true;
HttpDownloader::ProgressCallback progress_;
};
} // namespace
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
secureClient->setInsecure();
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
}
HTTPClient http;
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
http.begin(*client, url.c_str());
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
}
const int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
http.end();
return false;
}
http.writeToStream(&outContent);
http.end();
LOG_DBG("HTTP", "Fetch success");
return true;
}
bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
StreamString stream;
if (!fetchUrl(url, stream)) {
return false;
}
outContent = stream.c_str();
return true;
}
HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& url, const std::string& destPath,
ProgressCallback progress) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
secureClient->setInsecure();
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
}
HTTPClient http;
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
http.begin(*client, url.c_str());
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
}
const int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Download failed: %d", httpCode);
http.end();
return HTTP_ERROR;
}
const int64_t reportedLength = http.getSize();
const size_t contentLength = reportedLength > 0 ? static_cast<size_t>(reportedLength) : 0;
if (contentLength > 0) {
LOG_DBG("HTTP", "Content-Length: %zu", contentLength);
} else {
LOG_DBG("HTTP", "Content-Length: unknown");
}
// Remove existing file if present
if (Storage.exists(destPath.c_str())) {
Storage.remove(destPath.c_str());
}
// Open file for writing
FsFile file;
if (!Storage.openFileForWrite("HTTP", destPath.c_str(), file)) {
LOG_ERR("HTTP", "Failed to open file for writing");
http.end();
return FILE_ERROR;
}
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
FileWriteStream fileStream(file, contentLength, progress);
const int writeResult = http.writeToStream(&fileStream);
file.close();
http.end();
if (writeResult < 0) {
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
Storage.remove(destPath.c_str());
return HTTP_ERROR;
}
const size_t downloaded = fileStream.downloaded();
LOG_DBG("HTTP", "Downloaded %zu bytes", downloaded);
// Guard against partial writes even if HTTPClient completes.
if (!fileStream.ok()) {
LOG_ERR("HTTP", "Write failed during download");
Storage.remove(destPath.c_str());
return FILE_ERROR;
}
if (contentLength == 0 && downloaded == 0) {
LOG_ERR("HTTP", "Download failed: no data received");
Storage.remove(destPath.c_str());
return HTTP_ERROR;
}
// Verify download size if known
if (contentLength > 0 && downloaded != contentLength) {
LOG_ERR("HTTP", "Size mismatch: got %zu, expected %zu", downloaded, contentLength);
Storage.remove(destPath.c_str());
return HTTP_ERROR;
}
return OK;
}