#include "DownloadManager.hpp" #include "GUI_App.hpp" #include "libslic3r/Utils.hpp" #include #include #include #include #include #include namespace Slic3r { namespace GUI { // ============================================================================ // Helper Functions // ============================================================================ std::string DownloadManager::get_unique_file_path(const boost::filesystem::path& file_path) { // file_path should be the complete absolute path: directory + filename std::string original_path = file_path.string(); BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: Checking path '%1%'") % original_path; // Check if file exists, if not return original path if (!boost::filesystem::exists(file_path)) { BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: File does not exist, returning original path '%1%'") % original_path; return original_path; } BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: File exists, generating unique name"); boost::filesystem::path parent_dir = file_path.parent_path(); std::string filename = file_path.filename().string(); std::string extension = file_path.extension().string(); std::string name_without_ext; if (extension.empty()) { name_without_ext = filename; BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: No extension found, filename='%1%'") % filename; } else { name_without_ext = filename.substr(0, filename.size() - extension.size()); BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: filename='%1%', extension='%2%', name_without_ext='%3%'") % filename % extension % name_without_ext; } // Generate unique filename with Windows-style numbering: filename(1).ext, filename(2).ext, etc. size_t version = 1; boost::filesystem::path unique_path; do { std::string new_filename; if (extension.empty()) { // No extension: filename(1), filename(2), etc. new_filename = name_without_ext + "(" + std::to_string(version) + ")"; } else { // Has extension: filename(1).ext, filename(2).ext, etc. new_filename = name_without_ext + "(" + std::to_string(version) + ")" + extension; } unique_path = parent_dir / new_filename; BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: Trying version %1%: '%2%'") % version % unique_path.string(); version++; } while (boost::filesystem::exists(unique_path) && version < 10000); // Safety limit if (version >= 10000) { // If we hit the limit, log a warning and return a timestamp-based name BOOST_LOG_TRIVIAL(warning) << boost::format("DownloadManager::get_unique_file_path: Too many duplicate files for '%1%', using timestamp-based name") % original_path; std::string timestamp = std::to_string(std::time(nullptr)); std::string new_filename; if (extension.empty()) { new_filename = name_without_ext + "_" + timestamp; } else { new_filename = name_without_ext + "_" + timestamp + extension; } unique_path = parent_dir / new_filename; } std::string result = unique_path.string(); BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: Final unique path: '%1%'") % result; return result; } // ============================================================================ // WCP Download Interface (for Web-to-PC communication) // ============================================================================ size_t DownloadManager::start_wcp_download(const std::string& file_url, const std::string& file_name, std::shared_ptr wcp_instance, bool use_original_event_id) { std::lock_guard lock(m_tasks_mutex); size_t task_id = m_next_task_id++; auto downloadPath = wxGetApp().app_config->get("download_path"); boost::filesystem::path dest_folder(downloadPath); boost::filesystem::create_directories(dest_folder); boost::filesystem::path dest_file = dest_folder / file_name; // Generate unique file path if file already exists std::string dest_path = get_unique_file_path(dest_file); // Update file_name if it was changed due to duplicate std::string actual_file_name = boost::filesystem::path(dest_path).filename().string(); auto task = std::make_shared(task_id, file_url, actual_file_name, dest_path, wcp_instance, use_original_event_id); task->state = DownloadTaskState::Downloading; m_tasks[task_id] = task; start_download_impl(task); return task_id; } // ============================================================================ // Internal Download Interface (for PC internal use) // ============================================================================ size_t DownloadManager::start_internal_download(const std::string& file_url, const std::string& file_name, const std::string& dest_path, DownloadCallbacks callbacks) { std::lock_guard lock(m_tasks_mutex); size_t task_id = m_next_task_id++; boost::filesystem::path dest_path_obj(dest_path); boost::filesystem::path dest_file_path; if (boost::filesystem::is_directory(dest_path_obj) || dest_path_obj.filename().empty()) { // dest_path is a directory, need to append file_name BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::start_internal_download: dest_path '%1%' is a directory, appending file_name '%2%'") % dest_path % file_name; dest_file_path = dest_path_obj / file_name; } else { // dest_path is already a complete file path (directory + filename) BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::start_internal_download: dest_path '%1%' is a complete file path") % dest_path; dest_file_path = dest_path_obj; } boost::filesystem::create_directories(dest_file_path.parent_path()); std::string unique_dest_path = get_unique_file_path(dest_file_path); auto task = std::make_shared(task_id, file_url, file_name, unique_dest_path, std::move(callbacks)); task->state = DownloadTaskState::Downloading; m_tasks[task_id] = task; start_download_impl(task); return task_id; } size_t DownloadManager::start_internal_download(const std::string& file_url, const std::string& file_name, DownloadCallbacks callbacks) { // Get default download path auto downloadPath = wxGetApp().app_config->get("download_path"); boost::filesystem::path dest_folder(downloadPath); boost::filesystem::create_directories(dest_folder); boost::filesystem::path dest_file = dest_folder / file_name; // Generate unique file path if file already exists std::string dest_path = get_unique_file_path(dest_file); return start_internal_download(file_url, file_name, dest_path, std::move(callbacks)); } void DownloadManager::start_download_impl(std::shared_ptr task) { wxGetApp().CallAfter([this, task]() { try { // Step 1: Create Http object Http http = Http::get(task->file_url); http.timeout_max(0); // Step 2: Set progress callback http.on_progress([this, task](Http::Progress progress, bool& cancel) { // Check if task is canceled or already cleaned up { std::lock_guard lock(m_tasks_mutex); if (m_tasks.find(task->task_id) == m_tasks.end()) { // Task has been cleaned up, cancel the download cancel = true; return; } } if (task->state == DownloadTaskState::Canceled) { cancel = true; return; } int percent = 0; if (progress.dltotal > 0) { percent = (int)(progress.dlnow * 100 / progress.dltotal); } task->percent = percent; // Throttle progress updates: update every 5% or every second std::lock_guard lock(m_tasks_mutex); // Double-check task still exists after acquiring lock if (m_tasks.find(task->task_id) == m_tasks.end()) { cancel = true; return; } auto& last_pct = m_last_percent[task->task_id]; auto& last_upd = m_last_update[task->task_id]; auto now = std::chrono::steady_clock::now(); bool should_update = false; if (percent - last_pct >= 5) { should_update = true; last_pct = percent; } else if (now - last_upd >= std::chrono::seconds(1)) { should_update = true; } if (should_update) { last_upd = now; wxGetApp().CallAfter([this, task, percent, progress]() { // Check if task still exists before sending update std::lock_guard lock(m_tasks_mutex); if (m_tasks.find(task->task_id) != m_tasks.end() && task->state != DownloadTaskState::Canceled) { send_progress_update(task, percent, progress.dlnow, progress.dltotal); } }); } }); // Step 3: Set complete callback http.on_complete([this, task](std::string body, unsigned status) { wxGetApp().CallAfter([this, task, body]() { // Check if task still exists and is not canceled (without lock to avoid deadlock with cleanup_task) if (task->state == DownloadTaskState::Canceled) { // Task has been canceled, ignore completion BOOST_LOG_TRIVIAL(debug) << "DownloadManager: Ignoring complete callback for canceled task " << task->task_id; return; } try { // Save file boost::nowide::ofstream file(task->dest_path, std::ios::binary); if (!file.is_open()) { std::string error_msg = "Failed to open file for writing: " + task->dest_path; BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg; // Flush logs immediately for critical errors Slic3r::flush_logs(); send_error_update(task, error_msg); cleanup_task(task->task_id); return; } file.write(body.c_str(), body.size()); if (file.fail()) { std::string error_msg = "Failed to write file: " + task->dest_path; BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg << ", body size: " << body.size(); // Flush logs immediately for critical errors Slic3r::flush_logs(); file.close(); send_error_update(task, error_msg); cleanup_task(task->task_id); return; } file.close(); task->state = DownloadTaskState::Completed; task->percent = 100; send_complete_update(task, task->dest_path); cleanup_task(task->task_id); } catch (std::exception& e) { std::string error_msg = std::string("File write exception: ") + e.what(); BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg << ", file: " << task->dest_path; // Flush logs immediately for critical errors Slic3r::flush_logs(); send_error_update(task, error_msg); cleanup_task(task->task_id); } }); }); // Step 4: Set error callback http.on_error([this, task](std::string body, std::string error, unsigned status) { wxGetApp().CallAfter([this, task, error, status]() { // Check if task was canceled (without lock to avoid deadlock with cleanup_task) if (task->state == DownloadTaskState::Canceled) { // Task was canceled, ignore error callback (cancel already handled cleanup) BOOST_LOG_TRIVIAL(debug) << "DownloadManager: Ignoring error callback for canceled task " << task->task_id; return; } std::string error_msg = boost::str(boost::format("HTTP error: %1% (status: %2%)") % error % status); BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg << ", URL: " << task->file_url << ", file: " << task->file_name << ", dest: " << task->dest_path; // Flush logs immediately for critical errors to ensure they are written Slic3r::flush_logs(); task->state = DownloadTaskState::Error; task->error_message = error; send_error_update(task, error); cleanup_task(task->task_id); }); }); // Step 5: Start download and save Http::Ptr for cancellation task->http_object = http.perform(); } catch (std::exception& e) { std::string error_msg = std::string("Download exception: ") + e.what(); BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg << ", URL: " << task->file_url << ", file: " << task->file_name << ", dest: " << task->dest_path; task->state = DownloadTaskState::Error; task->error_message = e.what(); send_error_update(task, e.what()); cleanup_task(task->task_id); } }); } //this function not currently in use bool DownloadManager::cancel_download(size_t task_id) { std::shared_ptr wcp_to_destroy; { std::lock_guard lock(m_tasks_mutex); auto it = m_tasks.find(task_id); if (it == m_tasks.end()) { return false; } auto task = it->second; if (task->state == DownloadTaskState::Downloading) { task->state = DownloadTaskState::Canceled; if (task->http_object) { task->http_object->cancel(); } // Only for WCP downloads if (task->is_wcp_download()) { wcp_to_destroy = task->wcp_instance.lock(); } else { task->callbacks.on_error = nullptr; task->callbacks.on_progress = nullptr; task->callbacks.on_complete = nullptr; } // Cleanup task directly (already holding the lock, don't call cleanup_task) m_tasks.erase(task_id); m_last_percent.erase(task_id); m_last_update.erase(task_id); } else { return false; } } if (wcp_to_destroy) { wcp_to_destroy->finish_job(); } return true; } // this function not currently in use bool DownloadManager::pause_download(size_t task_id) { std::lock_guard lock(m_tasks_mutex); auto it = m_tasks.find(task_id); if (it != m_tasks.end() && it->second->state == DownloadTaskState::Downloading) { it->second->state = DownloadTaskState::Paused; return true; } return false; } // this function not currently in use bool DownloadManager::resume_download(size_t task_id) { std::lock_guard lock(m_tasks_mutex); auto it = m_tasks.find(task_id); if (it != m_tasks.end() && it->second->state == DownloadTaskState::Paused) { return false; } return false; } // this function not currently in use DownloadTaskState DownloadManager::get_task_state(size_t task_id) { std::lock_guard lock(m_tasks_mutex); auto it = m_tasks.find(task_id); if (it != m_tasks.end()) { return it->second->state; } return DownloadTaskState::Error; } // this function not currently in use std::shared_ptr DownloadManager::get_task(size_t task_id) { std::lock_guard lock(m_tasks_mutex); auto it = m_tasks.find(task_id); if (it != m_tasks.end()) { return it->second; } return nullptr; } // this function not currently in use std::vector> DownloadManager::get_all_tasks() { std::lock_guard lock(m_tasks_mutex); std::vector> result; result.reserve(m_tasks.size()); for (const auto& pair : m_tasks) { result.push_back(pair.second); } return result; } // ============================================================================ // Progress/Complete/Error Update Handlers // ============================================================================ void DownloadManager::send_progress_update(std::shared_ptr task, int percent, size_t downloaded, size_t total) { if (task->is_wcp_download()) { send_wcp_progress_update(task, percent, downloaded, total); } else { call_internal_progress_callback(task, percent, downloaded, total); } } void DownloadManager::send_wcp_progress_update(std::shared_ptr task, int percent, size_t downloaded, size_t total) { if (!task->use_original_event_id) { return; } if (auto wcp = task->wcp_instance.lock()) { json progress_data; progress_data["task_id"] = task->task_id; progress_data["percent"] = percent; progress_data["downloaded"] = downloaded; progress_data["total"] = total; progress_data["state"] = "downloading"; wcp->m_res_data = progress_data; wcp->m_status = 0; wcp->m_msg = "Download progress"; json header; if (task->use_original_event_id) { header["event_id"] = wcp->m_event_id; } else { header["event_id"] = wcp->m_event_id + "_progress"; } header["command"] = "download_progress"; wcp->m_header = header; wcp->send_to_js(); } } void DownloadManager::call_internal_progress_callback(std::shared_ptr task, int percent, size_t downloaded, size_t total) { if (task->callbacks.on_progress && task->state != DownloadTaskState::Canceled) { task->callbacks.on_progress(task->task_id, percent, downloaded, total); } } void DownloadManager::send_complete_update(std::shared_ptr task, const std::string& file_path) { if (task->is_wcp_download()) { send_wcp_complete_update(task, file_path); } else { call_internal_complete_callback(task, file_path); } } void DownloadManager::send_wcp_complete_update(std::shared_ptr task, const std::string& file_path) { if (!task->use_original_event_id) { return; } if (auto wcp = task->wcp_instance.lock()) { json complete_data; complete_data["task_id"] = task->task_id; complete_data["file_path"] = file_path; complete_data["file_name"] = task->file_name; complete_data["percent"] = 100; complete_data["state"] = "completed"; wcp->m_res_data = complete_data; wcp->m_status = 0; wcp->m_msg = "Download completed"; wcp->send_to_js(); wcp->finish_job(); } } void DownloadManager::call_internal_complete_callback(std::shared_ptr task, const std::string& file_path) { if (task->callbacks.on_complete && task->state != DownloadTaskState::Canceled) { task->callbacks.on_complete(task->task_id, file_path); } } void DownloadManager::send_error_update(std::shared_ptr task, const std::string& error) { if (task->is_wcp_download()) { send_wcp_error_update(task, error); } else { call_internal_error_callback(task, error); } } void DownloadManager::send_wcp_error_update(std::shared_ptr task, const std::string& error) { if (!task->use_original_event_id) { return; } if (auto wcp = task->wcp_instance.lock()) { json error_data; error_data["task_id"] = task->task_id; error_data["error"] = error; error_data["state"] = "error"; wcp->m_res_data = error_data; wcp->m_status = -1; wcp->m_msg = error; wcp->send_to_js(); wcp->finish_job(); } } void DownloadManager::call_internal_error_callback(std::shared_ptr task, const std::string& error) { if (task->callbacks.on_error && task->state != DownloadTaskState::Canceled) { task->callbacks.on_error(task->task_id, error); } } void DownloadManager::cleanup_task(size_t task_id) { std::lock_guard lock(m_tasks_mutex); m_tasks.erase(task_id); m_last_percent.erase(task_id); m_last_update.erase(task_id); } }} // namespace Slic3r::GUI