Fix webui font download
This commit is contained in:
@@ -1589,6 +1589,8 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string&
|
|||||||
|
|
||||||
for (const auto& file : family.files) {
|
for (const auto& file : family.files) {
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
|
yield();
|
||||||
|
delay(500); // allow network stack to clean up sockets
|
||||||
|
|
||||||
std::string localFilename = file.name;
|
std::string localFilename = file.name;
|
||||||
std::string familyPrefix = family.name + "/";
|
std::string familyPrefix = family.name + "/";
|
||||||
@@ -1740,6 +1742,7 @@ void CrossPointWebServer::handleFontDownload() {
|
|||||||
server->send(400, "application/json", "{\"ok\":false,\"error\":\"Invalid request\"}");
|
server->send(400, "application/json", "{\"ok\":false,\"error\":\"Invalid request\"}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
body.clear(); // Free memory early!
|
||||||
|
|
||||||
const bool installAll = req["all"] | false;
|
const bool installAll = req["all"] | false;
|
||||||
const std::string requestedFamily = req["family"] | "";
|
const std::string requestedFamily = req["family"] | "";
|
||||||
@@ -1784,14 +1787,26 @@ void CrossPointWebServer::handleFontDownload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<RemoteManifestFamily> targetCopies;
|
||||||
|
for (auto* f : targets) {
|
||||||
|
targetCopies.push_back(*f);
|
||||||
|
}
|
||||||
|
families.clear();
|
||||||
|
families.shrink_to_fit();
|
||||||
|
|
||||||
size_t installedCount = 0;
|
size_t installedCount = 0;
|
||||||
for (auto* family : targets) {
|
for (auto& family : targetCopies) {
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
if (!installRemoteFamily(*family, baseUrl, installer, error)) {
|
yield();
|
||||||
|
delay(500); // allow network stack to clean up sockets
|
||||||
|
|
||||||
|
LOG_DBG("WEB", "Installing font family: %s", family.name.c_str());
|
||||||
|
|
||||||
|
if (!installRemoteFamily(family, baseUrl, installer, error)) {
|
||||||
JsonDocument errDoc;
|
JsonDocument errDoc;
|
||||||
errDoc["ok"] = false;
|
errDoc["ok"] = false;
|
||||||
errDoc["error"] = error;
|
errDoc["error"] = error;
|
||||||
errDoc["family"] = family->name;
|
errDoc["family"] = family.name;
|
||||||
errDoc["installedCount"] = static_cast<unsigned>(installedCount);
|
errDoc["installedCount"] = static_cast<unsigned>(installedCount);
|
||||||
String out;
|
String out;
|
||||||
serializeJson(errDoc, out);
|
serializeJson(errDoc, out);
|
||||||
|
|||||||
@@ -66,9 +66,11 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent, const
|
|||||||
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
|
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
|
||||||
|
|
||||||
http.begin(*client, url.c_str());
|
http.begin(*client, url.c_str());
|
||||||
|
http.setReuse(false);
|
||||||
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
||||||
http.setTimeout(30000);
|
http.setTimeout(30000);
|
||||||
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
|
http.addHeader("Connection", "close");
|
||||||
|
|
||||||
if (!username.empty() || !password.empty()) {
|
if (!username.empty() || !password.empty()) {
|
||||||
std::string credentials = username + ":" + password;
|
std::string credentials = username + ":" + password;
|
||||||
@@ -80,12 +82,18 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent, const
|
|||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
|
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
|
||||||
http.end();
|
http.end();
|
||||||
|
if (client) {
|
||||||
|
client->stop();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
http.writeToStream(&outContent);
|
http.writeToStream(&outContent);
|
||||||
|
|
||||||
http.end();
|
http.end();
|
||||||
|
if (client) {
|
||||||
|
client->stop();
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DBG("HTTP", "Fetch success");
|
LOG_DBG("HTTP", "Fetch success");
|
||||||
return true;
|
return true;
|
||||||
@@ -118,9 +126,11 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
|
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
|
||||||
|
|
||||||
http.begin(*client, url.c_str());
|
http.begin(*client, url.c_str());
|
||||||
|
http.setReuse(false);
|
||||||
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
||||||
http.setTimeout(65535); // max uint16_t (~65s) — HTTPClient::setTimeout takes uint16_t ms
|
http.setTimeout(65535); // max uint16_t (~65s) — HTTPClient::setTimeout takes uint16_t ms
|
||||||
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||||
|
http.addHeader("Connection", "close");
|
||||||
|
|
||||||
if (!username.empty() || !password.empty()) {
|
if (!username.empty() || !password.empty()) {
|
||||||
std::string credentials = username + ":" + password;
|
std::string credentials = username + ":" + password;
|
||||||
@@ -132,6 +142,9 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
LOG_ERR("HTTP", "Download failed: %d", httpCode);
|
LOG_ERR("HTTP", "Download failed: %d", httpCode);
|
||||||
http.end();
|
http.end();
|
||||||
|
if (client) {
|
||||||
|
client->stop();
|
||||||
|
}
|
||||||
return HTTP_ERROR;
|
return HTTP_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +169,51 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
return FILE_ERROR;
|
return FILE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int writeResult = -1;
|
||||||
|
size_t downloaded = 0;
|
||||||
|
bool writeOk = true;
|
||||||
|
|
||||||
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
|
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
|
||||||
|
// For known sizes (Content-Length), we can stream it manually to save RAM!
|
||||||
|
// HTTPClient::writeToStream allocates a 4096-byte chunk on the heap which can
|
||||||
|
// fail (-8 / HTTPC_ERROR_TOO_LESS_RAM) if the heap is fragmented or depleted.
|
||||||
|
if (contentLength > 0) {
|
||||||
|
NetworkClient& stream = http.getStream();
|
||||||
|
uint8_t buffer[1024];
|
||||||
|
writeResult = 1;
|
||||||
|
while (http.connected() && downloaded < contentLength) {
|
||||||
|
size_t available = stream.available();
|
||||||
|
if (available > 0) {
|
||||||
|
size_t toRead = available > sizeof(buffer) ? sizeof(buffer) : available;
|
||||||
|
if (downloaded + toRead > contentLength) {
|
||||||
|
toRead = contentLength - downloaded;
|
||||||
|
}
|
||||||
|
int readSize = stream.readBytes(buffer, toRead);
|
||||||
|
if (readSize > 0) {
|
||||||
|
if (file.write(buffer, readSize) != static_cast<size_t>(readSize)) {
|
||||||
|
writeOk = false;
|
||||||
|
writeResult = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
downloaded += readSize;
|
||||||
|
if (progress) progress(downloaded, contentLength);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (downloaded != contentLength) {
|
||||||
|
writeResult = -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Chunked or unknown length fallback
|
||||||
FileWriteStream fileStream(file, contentLength, progress);
|
FileWriteStream fileStream(file, contentLength, progress);
|
||||||
const int writeResult = http.writeToStream(&fileStream);
|
writeResult = http.writeToStream(&fileStream);
|
||||||
|
downloaded = fileStream.downloaded();
|
||||||
|
writeOk = fileStream.ok();
|
||||||
|
}
|
||||||
|
|
||||||
// Flush before closing to ensure data is written to the SD card.
|
// Flush before closing to ensure data is written to the SD card.
|
||||||
// Without this, Storage.exists() might return false immediately after
|
// Without this, Storage.exists() might return false immediately after
|
||||||
@@ -166,6 +221,9 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
file.flush();
|
file.flush();
|
||||||
file.close();
|
file.close();
|
||||||
http.end();
|
http.end();
|
||||||
|
if (client) {
|
||||||
|
client->stop();
|
||||||
|
}
|
||||||
|
|
||||||
if (writeResult < 0) {
|
if (writeResult < 0) {
|
||||||
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
|
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
|
||||||
@@ -173,11 +231,10 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
return HTTP_ERROR;
|
return HTTP_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t downloaded = fileStream.downloaded();
|
|
||||||
LOG_DBG("HTTP", "Downloaded %zu bytes", downloaded);
|
LOG_DBG("HTTP", "Downloaded %zu bytes", downloaded);
|
||||||
|
|
||||||
// Guard against partial writes even if HTTPClient completes.
|
// Guard against partial writes even if HTTPClient completes.
|
||||||
if (!fileStream.ok()) {
|
if (!writeOk) {
|
||||||
LOG_ERR("HTTP", "Write failed during download");
|
LOG_ERR("HTTP", "Write failed during download");
|
||||||
Storage.remove(destPath.c_str());
|
Storage.remove(destPath.c_str());
|
||||||
return FILE_ERROR;
|
return FILE_ERROR;
|
||||||
|
|||||||
@@ -148,6 +148,46 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.download-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 1fr) 280px;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.status-panel {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.status-panel h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.status-panel p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#remoteStatus {
|
||||||
|
margin-top: 8px;
|
||||||
|
min-height: 80px;
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--notice-bg);
|
||||||
|
border: 1px solid var(--notice-border);
|
||||||
|
color: var(--notice-color);
|
||||||
|
}
|
||||||
|
.status-ok {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
}
|
||||||
|
.status-err {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -171,8 +211,13 @@
|
|||||||
<button id="refreshManifest" class="btn btn-primary" type="button">Refresh Catalog</button>
|
<button id="refreshManifest" class="btn btn-primary" type="button">Refresh Catalog</button>
|
||||||
<button id="downloadAllRemote" class="btn btn-primary" type="button">Download All Updates</button>
|
<button id="downloadAllRemote" class="btn btn-primary" type="button">Download All Updates</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="download-layout">
|
||||||
<div id="remoteFamilies"><p class="empty">Catalog not loaded</p></div>
|
<div id="remoteFamilies"><p class="empty">Catalog not loaded</p></div>
|
||||||
<div id="remoteStatus"></div>
|
<div class="status-panel">
|
||||||
|
<h3>Download Status</h3>
|
||||||
|
<div id="remoteStatus" class="status-message"><p>Waiting for action...</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user