2026-05-11 23:06:39 +03:00
|
|
|
/*
|
|
|
|
|
* InfiniPaint
|
|
|
|
|
* Copyright (C) 2025-2026 Yousef Khadadeh
|
|
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-10-09 13:04:39 +03:00
|
|
|
#include "FileDownloader.hpp"
|
|
|
|
|
|
2025-10-11 13:02:07 +03:00
|
|
|
#ifndef __EMSCRIPTEN__
|
|
|
|
|
|
2025-10-01 17:54:23 +03:00
|
|
|
#include <curl/multi.h>
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <curl/curl.h>
|
2025-11-19 08:53:49 +03:00
|
|
|
#include <algorithm>
|
2025-10-01 17:54:23 +03:00
|
|
|
|
|
|
|
|
std::mutex FileDownloader::newDownloadsMutex;
|
|
|
|
|
std::vector<FileDownloader::DownloadHandler> FileDownloader::newDownloads;
|
|
|
|
|
std::atomic<bool> FileDownloader::shutdownThread;
|
2025-10-07 12:41:21 +03:00
|
|
|
CURLM* FileDownloader::mHandle;
|
2025-10-01 17:54:23 +03:00
|
|
|
std::vector<FileDownloader::DownloadHandler> FileDownloader::currentDownloads;
|
|
|
|
|
std::unique_ptr<std::thread> FileDownloader::downloadThread;
|
|
|
|
|
|
|
|
|
|
void FileDownloader::init() {
|
|
|
|
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
|
|
|
|
shutdownThread = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-11 13:02:07 +03:00
|
|
|
std::shared_ptr<FileDownloader::DownloadData> FileDownloader::download_data_from_url(const std::string& url) {
|
2025-10-01 17:54:23 +03:00
|
|
|
DownloadHandler h;
|
|
|
|
|
auto downloadData = std::make_shared<DownloadData>();
|
|
|
|
|
h.data = downloadData;
|
2025-10-07 12:41:21 +03:00
|
|
|
h.eHandle = curl_easy_init();
|
2025-10-11 13:02:07 +03:00
|
|
|
downloadData->fileName = get_potential_filename_from_url(url);
|
|
|
|
|
|
2025-10-07 12:41:21 +03:00
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_URL, url.data());
|
|
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_NOSIGNAL, 1);
|
2025-10-09 16:56:05 +03:00
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_FOLLOWLOCATION, CURLFOLLOW_ALL);
|
2025-10-11 13:02:07 +03:00
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_USERAGENT, "InfiniPaint/0.2.0");
|
2025-10-09 17:58:12 +03:00
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_NOPROGRESS, 0);
|
|
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_XFERINFODATA, h.data.get());
|
|
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_XFERINFOFUNCTION, download_progress_callback);
|
2025-10-11 13:02:07 +03:00
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_HEADERFUNCTION, header_callback);
|
|
|
|
|
curl_easy_setopt(h.eHandle, CURLOPT_HEADERDATA, h.data.get());
|
|
|
|
|
|
2025-10-07 12:41:21 +03:00
|
|
|
disable_ssl_verification(h.eHandle);
|
|
|
|
|
save_to_string(h.eHandle, &h.data->str);
|
2025-10-01 17:54:23 +03:00
|
|
|
|
|
|
|
|
{
|
|
|
|
|
std::scoped_lock newDownloadsLock(newDownloadsMutex);
|
|
|
|
|
newDownloads.emplace_back(std::move(h));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!downloadThread)
|
|
|
|
|
downloadThread = std::make_unique<std::thread>(download_thread_update);
|
|
|
|
|
|
|
|
|
|
return downloadData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FileDownloader::add_new_handlers_to_multi() {
|
|
|
|
|
std::scoped_lock newDownloadsLock(newDownloadsMutex);
|
|
|
|
|
for(auto& handler : newDownloads) {
|
2025-10-07 12:41:21 +03:00
|
|
|
curl_multi_add_handle(mHandle, handler.eHandle);
|
2025-10-01 17:54:23 +03:00
|
|
|
currentDownloads.emplace_back(std::move(handler));
|
|
|
|
|
}
|
|
|
|
|
newDownloads.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FileDownloader::download_thread_update() {
|
|
|
|
|
int stillRunning = 0;
|
|
|
|
|
|
2025-10-07 12:41:21 +03:00
|
|
|
mHandle = curl_multi_init();
|
2025-10-01 17:54:23 +03:00
|
|
|
|
2025-10-09 12:03:03 +03:00
|
|
|
for(;;) {
|
2025-10-01 17:54:23 +03:00
|
|
|
if(shutdownThread)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
CURLMcode mc;
|
|
|
|
|
int numfds;
|
|
|
|
|
|
2025-10-09 12:03:03 +03:00
|
|
|
add_new_handlers_to_multi();
|
2025-10-07 12:41:21 +03:00
|
|
|
mc = curl_multi_perform(mHandle, &stillRunning);
|
2025-10-01 17:54:23 +03:00
|
|
|
|
|
|
|
|
for(;;) {
|
|
|
|
|
int msgCount;
|
2025-10-07 12:41:21 +03:00
|
|
|
CURLMsg* eHandleMsg = curl_multi_info_read(mHandle, &msgCount);
|
2025-10-01 17:54:23 +03:00
|
|
|
if(!eHandleMsg)
|
|
|
|
|
break;
|
|
|
|
|
if(eHandleMsg->msg == CURLMSG_DONE) {
|
|
|
|
|
auto it = std::find_if(currentDownloads.begin(), currentDownloads.end(), [eHandleMsg](auto& d) {
|
2025-10-07 12:41:21 +03:00
|
|
|
return eHandleMsg->easy_handle == d.eHandle;
|
2025-10-01 17:54:23 +03:00
|
|
|
});
|
|
|
|
|
if(it != currentDownloads.end()) {
|
|
|
|
|
DownloadHandler& download = *it;
|
|
|
|
|
download.data->status = eHandleMsg->data.result == CURLE_OK ? DownloadData::Status::SUCCESS : DownloadData::Status::FAILURE;
|
2025-10-07 12:41:21 +03:00
|
|
|
curl_multi_remove_handle(mHandle, download.eHandle);
|
|
|
|
|
curl_easy_cleanup(download.eHandle);
|
2025-10-01 17:54:23 +03:00
|
|
|
currentDownloads.erase(it);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(mc == CURLM_OK)
|
2025-10-07 12:41:21 +03:00
|
|
|
curl_multi_poll(mHandle, nullptr, 0, 1000, &numfds);
|
2025-10-01 17:54:23 +03:00
|
|
|
else {
|
|
|
|
|
std::cout << "curl_multi failed" << std::endl;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-10-09 12:03:03 +03:00
|
|
|
}
|
2025-10-01 17:54:23 +03:00
|
|
|
|
2025-10-07 12:41:21 +03:00
|
|
|
for(auto& download : currentDownloads) {
|
2025-10-01 17:54:23 +03:00
|
|
|
download.data->status = DownloadData::Status::FAILURE;
|
2025-10-07 12:41:21 +03:00
|
|
|
curl_multi_remove_handle(mHandle, download.eHandle);
|
|
|
|
|
curl_easy_cleanup(download.eHandle);
|
|
|
|
|
}
|
2025-10-09 12:03:03 +03:00
|
|
|
|
2025-10-01 17:54:23 +03:00
|
|
|
currentDownloads.clear();
|
2025-10-07 12:41:21 +03:00
|
|
|
curl_multi_cleanup(mHandle);
|
2025-10-01 17:54:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FileDownloader::save_to_string(CURL* curl, std::string* str) {
|
|
|
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_string);
|
|
|
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(str));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FileDownloader::disable_ssl_verification(CURL* curl) {
|
|
|
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
|
|
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t FileDownloader::write_to_string(void* contents, size_t size, size_t nmemb, void* userp) {
|
|
|
|
|
size_t realsize = size * nmemb;
|
|
|
|
|
auto str = reinterpret_cast<std::string*>(userp);
|
|
|
|
|
str->append(reinterpret_cast<const char*>(contents), realsize);
|
|
|
|
|
return realsize;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 17:58:12 +03:00
|
|
|
int FileDownloader::download_progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
|
|
|
|
|
DownloadData* downData = static_cast<DownloadData*>(clientp);
|
|
|
|
|
if(dltotal == 0)
|
|
|
|
|
downData->progress = 0.0f;
|
|
|
|
|
else
|
|
|
|
|
downData->progress = static_cast<float>(dlnow) / static_cast<float>(dltotal);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-11 13:02:07 +03:00
|
|
|
size_t FileDownloader::header_callback(char* buffer, size_t size, size_t nitems, void* userData) {
|
|
|
|
|
DownloadData* downData = static_cast<DownloadData*>(userData);
|
|
|
|
|
std::string header(buffer, nitems);
|
|
|
|
|
std::string newFilename = parse_http_headers_for_filename(header);
|
|
|
|
|
if(!newFilename.empty())
|
|
|
|
|
downData->fileName = newFilename;
|
|
|
|
|
|
|
|
|
|
return nitems;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 17:54:23 +03:00
|
|
|
void FileDownloader::cleanup() {
|
|
|
|
|
if(downloadThread && downloadThread->joinable()) {
|
|
|
|
|
shutdownThread = true;
|
|
|
|
|
downloadThread->join();
|
|
|
|
|
}
|
2025-10-09 12:03:03 +03:00
|
|
|
downloadThread = nullptr;
|
2025-10-01 17:54:23 +03:00
|
|
|
|
|
|
|
|
curl_global_cleanup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif
|
2025-10-11 13:02:07 +03:00
|
|
|
|
|
|
|
|
std::string FileDownloader::parse_http_headers_for_filename(std::string header) {
|
|
|
|
|
std::string headerLower = header;
|
|
|
|
|
std::transform(headerLower.begin(), headerLower.end(), headerLower.begin(), ::tolower);
|
|
|
|
|
std::string contentDispositionHeader = "content-disposition:";
|
|
|
|
|
size_t contentDispositionLoc = headerLower.find(contentDispositionHeader);
|
|
|
|
|
if(contentDispositionLoc < headerLower.size()) {
|
|
|
|
|
size_t newlineHeaderLoc = std::min<size_t>(headerLower.find('\n', contentDispositionLoc), headerLower.size());
|
|
|
|
|
|
|
|
|
|
std::string filenameHeader = "filename=";
|
|
|
|
|
size_t filenameLoc = headerLower.find(filenameHeader, contentDispositionLoc);
|
|
|
|
|
if(filenameLoc < newlineHeaderLoc) {
|
|
|
|
|
header = header.substr(filenameLoc + filenameHeader.size());
|
|
|
|
|
header = header.substr(0, header.find(';'));
|
|
|
|
|
if(header.size() >= 2 && header.front() == '"' && header.back() == '"') {
|
|
|
|
|
header.erase(0, 1);
|
|
|
|
|
header.pop_back();
|
|
|
|
|
}
|
|
|
|
|
return header;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string FileDownloader::get_potential_filename_from_url(const std::string& url) {
|
|
|
|
|
size_t fileNameStart = url.find_last_of('/');
|
|
|
|
|
if(fileNameStart >= (url.size() - 1))
|
|
|
|
|
return "";
|
|
|
|
|
std::string fileName = url.substr(fileNameStart + 1);
|
|
|
|
|
return fileName.substr(0, fileName.find('?'));
|
|
|
|
|
}
|