From 16b0654a6a83e6d5d49b4014013941588ddc147a Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 1 Jun 2022 11:30:01 +0200 Subject: [PATCH] Pause / continue download --- src/downloader/Download.cpp | 12 +++ src/downloader/Download.hpp | 7 +- src/downloader/DownloaderApp.cpp | 133 +++++++++++++++++++++++++-- src/downloader/DownloaderApp.hpp | 24 ++++- src/downloader/FileGet.cpp | 141 ++++++++++++++++++++--------- src/downloader/FileGet.hpp | 2 + src/downloader/FromSlicer/Http.cpp | 12 +++ src/downloader/FromSlicer/Http.hpp | 2 + 8 files changed, 277 insertions(+), 56 deletions(-) diff --git a/src/downloader/Download.cpp b/src/downloader/Download.cpp index f45a3246af..3745f44c65 100644 --- a/src/downloader/Download.cpp +++ b/src/downloader/Download.cpp @@ -32,4 +32,16 @@ void Download::cancel() m_state = DownloadState::DownloadStopped; m_file_get->cancel(); } +void Download::pause() +{ + assert(m_state == DownloadState::DownloadOngoing); + m_state = DownloadState::DownloadPaused; + m_file_get->pause(); +} +void Download::resume() +{ + assert(m_state == DownloadState::DownloadPaused); + m_state = DownloadState::DownloadOngoing; + m_file_get->resume(); +} } \ No newline at end of file diff --git a/src/downloader/Download.hpp b/src/downloader/Download.hpp index 5dc3bf6fe7..31762b5685 100644 --- a/src/downloader/Download.hpp +++ b/src/downloader/Download.hpp @@ -9,12 +9,12 @@ namespace Downloader { enum DownloadState { - DownloadPending, + DownloadPending = 0, DownloadOngoing, DownloadStopped, DownloadDone, DownloadError, - + DownloadPaused }; class Download { @@ -22,7 +22,8 @@ public: Download(int ID, std::string url, wxEvtHandler* evt_handler,const boost::filesystem::path& dest_folder); void start(); void cancel(); -// void pause(); + void pause(); + void resume(); int get_id() const { return m_id; } boost::filesystem::path get_final_path() const { return m_final_path; } diff --git a/src/downloader/DownloaderApp.cpp b/src/downloader/DownloaderApp.cpp index 87386c4ed8..aca9a69d80 100644 --- a/src/downloader/DownloaderApp.cpp +++ b/src/downloader/DownloaderApp.cpp @@ -181,6 +181,15 @@ DownloadFrame::DownloadFrame(const wxString& title, const wxPoint& pos, const wx btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_cancel_button(evt); }); btn_sizer->Add(btn_cancel, 0, wxLEFT | wxALIGN_CENTER_VERTICAL); + wxButton* btn_pause = new wxButton(this, wxID_EDIT, "Pause"); + btn_pause->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_pause_button(evt); }); + btn_sizer->Add(btn_pause, 0, wxLEFT | wxALIGN_CENTER_VERTICAL); + + + wxButton* btn_resume = new wxButton(this, wxID_EDIT, "Resume"); + btn_resume->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) { on_resume_button(evt); }); + btn_sizer->Add(btn_resume, 0, wxLEFT | wxALIGN_CENTER_VERTICAL); + //main_sizer->Add(data_sizer); //main_sizer->Add(btn_sizer); @@ -245,25 +254,53 @@ void DownloadFrame::log(const wxString& msg) void DownloadFrame::on_progress(wxCommandEvent& event) { - //log(std::to_string(event.GetInt()) + ": " + event.GetString()); - m_dataview->SetValue(std::stoi(boost::nowide::narrow(event.GetString())), event.GetInt() - 1, 2); - m_dataview->SetValue("Downloading", event.GetInt() -1, 3); - + + for (size_t i = 0; i < m_dataview->GetItemCount(); i++) { + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(i, 0))); + if (id == event.GetInt()) { + if (!is_in_state(id, DownloadState::DownloadOngoing)) + return; + m_dataview->SetValue(std::stoi(boost::nowide::narrow(event.GetString())), i, 2); + m_dataview->SetValue("Downloading", i, 3); + return; + } + } } void DownloadFrame::on_error(wxCommandEvent& event) { set_download_state(event.GetInt(), DownloadState::DownloadError); - m_dataview->SetValue("Error", event.GetInt() - 1, 3); + + for (size_t i = 0; i < m_dataview->GetItemCount(); i++) { + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(i, 0))); + if (id == event.GetInt()) { + m_dataview->SetValue("Error - " + event.GetString(), i, 3); + return; + } + } } void DownloadFrame::on_complete(wxCommandEvent& event) { set_download_state(event.GetInt(), DownloadState::DownloadDone); - m_dataview->SetValue("Done", event.GetInt() - 1, 3); + + for (size_t i = 0; i < m_dataview->GetItemCount(); i++) { + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(i, 0))); + if (id == event.GetInt()) { + m_dataview->SetValue("Done", i, 3); + return; + } + } + start_next(); } void DownloadFrame::on_name_change(wxCommandEvent& event) { - m_dataview->SetValue(event.GetString(), event.GetInt() - 1, 1); + for (size_t i = 0; i < m_dataview->GetItemCount(); i++) { + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(i, 0))); + if (id == event.GetInt()) { + m_dataview->SetValue(event.GetString(), i, 1); + return; + } + } } void DownloadFrame::start_next() { @@ -313,7 +350,29 @@ void DownloadFrame::on_cancel_button(wxCommandEvent& event) int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(selected, 0))); if(cancel_download(id)) - m_dataview->SetValue("Canceled", id - 1, 3); + m_dataview->SetValue("Canceled", selected, 3); + if (delete_download(id)) + m_dataview->DeleteItem(selected); +} + +void DownloadFrame::on_pause_button(wxCommandEvent& event) +{ + int selected = m_dataview->GetSelectedRow(); + if (selected == wxNOT_FOUND) { return; } + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(selected, 0))); + + if (pause_download(id)) + m_dataview->SetValue("Paused", selected, 3); +} + +void DownloadFrame::on_resume_button(wxCommandEvent& event) +{ + int selected = m_dataview->GetSelectedRow(); + if (selected == wxNOT_FOUND) { return; } + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(selected, 0))); + + if (resume_download(id)) + m_dataview->SetValue("Paused", selected, 3); } void DownloadFrame::set_download_state(int id, DownloadState state) @@ -326,7 +385,15 @@ void DownloadFrame::set_download_state(int id, DownloadState state) } } -bool DownloadFrame::is_in_state(int id, DownloadState state) const +void DownloadFrame::update_state_labels() +{ + for (size_t i = 0; i < m_dataview->GetItemCount(); i++) { + int id = std::stoi(boost::nowide::narrow(m_dataview->GetTextValue(i, 0))); + m_dataview->SetValue(c_state_labels.at(get_download_state(id)), i, 1); + } +} + +DownloadState DownloadFrame::get_download_state(int id) const { for (size_t i = 0; i < m_downloads.size(); ++i) { if (m_downloads[i]->get_id() == id) { @@ -335,6 +402,15 @@ bool DownloadFrame::is_in_state(int id, DownloadState state) const } } +bool DownloadFrame::is_in_state(int id, DownloadState state) const +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + return m_downloads[i]->get_state() == state; + } + } +} + bool DownloadFrame::cancel_download(int id) { for (size_t i = 0; i < m_downloads.size(); ++i) { @@ -349,6 +425,45 @@ bool DownloadFrame::cancel_download(int id) return false; } +bool DownloadFrame::pause_download(int id) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + if (m_downloads[i]->get_state() == DownloadState::DownloadOngoing) { + m_downloads[i]->pause(); + return true; + } + return false; + } + } + return false; +} + +bool DownloadFrame::resume_download(int id) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + if (m_downloads[i]->get_state() == DownloadState::DownloadPaused) { + m_downloads[i]->resume(); + return true; + } + return false; + } + } + return false; +} + +bool DownloadFrame::delete_download(int id) +{ + for (size_t i = 0; i < m_downloads.size(); ++i) { + if (m_downloads[i]->get_id() == id) { + m_downloads.erase(m_downloads.begin() + i); + return true; + } + } + return false; +} + wxString DownloadFrame::get_path_of(int id) const { for (size_t i = 0; i < m_downloads.size(); ++i) { diff --git a/src/downloader/DownloaderApp.hpp b/src/downloader/DownloaderApp.hpp index f86b3dffa1..2267135e98 100644 --- a/src/downloader/DownloaderApp.hpp +++ b/src/downloader/DownloaderApp.hpp @@ -4,6 +4,7 @@ #include "InstanceSend.hpp" #include "Download.hpp" #include +#include #include #include @@ -29,12 +30,18 @@ private: void on_open_in_new_slicer(wxCommandEvent& event); void on_open_in_explorer(wxCommandEvent& event); void on_cancel_button(wxCommandEvent& event); - - + void on_pause_button(wxCommandEvent& event); + void on_resume_button(wxCommandEvent& event); + + void update_state_labels(); void start_next(); void set_download_state(int id, DownloadState state); bool is_in_state(int id, DownloadState state) const; + DownloadState get_download_state(int id) const; bool cancel_download(int id); + bool pause_download(int id); + bool resume_download(int id); + bool delete_download(int id); wxString get_path_of(int id) const; wxString get_folder_path_of(int id) const; @@ -55,6 +62,19 @@ private: boost::filesystem::path m_dest_folder; std::vector> m_downloads; + /* DownloadPending = 0, + DownloadOngoing, + DownloadStopped, + DownloadDone, + DownloadError, + DownloadPaused*/ + const std::map c_state_labels = { + {DownloadPending, "Pending"}, + {DownloadStopped, "Canceled"}, + {DownloadDone, "Done"}, + {DownloadError, "Error"}, + {DownloadPaused, "Paused"}, + }; }; class DownloadApp : public wxApp diff --git a/src/downloader/FileGet.cpp b/src/downloader/FileGet.cpp index c161348e61..89f8782d5a 100644 --- a/src/downloader/FileGet.cpp +++ b/src/downloader/FileGet.cpp @@ -55,7 +55,10 @@ struct FileGet::priv std::thread m_io_thread; wxEvtHandler* m_evt_handler; boost::filesystem::path m_dest_folder; - std::atomic_bool m_cancel = false; + boost::filesystem::path m_tmp_path; // path when ongoing download + std::atomic_bool m_cancel { false }; + std::atomic_bool m_pause { false }; + size_t m_written { 0 }; priv(int ID, std::string&& url, const std::string& filename, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder); void get_perform(); @@ -79,68 +82,91 @@ void FileGet::priv::get_perform() assert(boost::filesystem::is_directory(m_dest_folder)); // open dest file - boost::filesystem::path dest_path = m_dest_folder / m_filename; - std::string extension = boost::filesystem::extension(dest_path); - std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size()); - std::string final_filename = just_filename; - - size_t version = 0; - while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download"))) + if (m_written == 0) { - ++version; - final_filename = just_filename + "(" + std::to_string(version) + ")"; + boost::filesystem::path dest_path = m_dest_folder / m_filename; + std::string extension = boost::filesystem::extension(dest_path); + std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size()); + std::string final_filename = just_filename; + + size_t version = 0; + while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download"))) + { + ++version; + final_filename = just_filename + "(" + std::to_string(version) + ")"; + } + m_filename = final_filename + extension; + + m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download"); + + wxCommandEvent* evt = new wxCommandEvent(EVT_FILE_NAME_CHANGE); + evt->SetString(boost::nowide::widen(m_filename)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); } - m_filename = final_filename + extension; - - boost::filesystem::path tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download"); - dest_path = m_dest_folder / m_filename; - - wxCommandEvent* evt = new wxCommandEvent(EVT_FILE_NAME_CHANGE); - evt->SetString(boost::nowide::widen(m_filename)); - evt->SetInt(m_id); - m_evt_handler->QueueEvent(evt); - + + boost::filesystem::path dest_path = m_dest_folder / m_filename; + FILE* file; // open file for writting - FILE* file = fopen(tmp_path.string().c_str(), "wb"); - size_t written = 0; + if (m_written == 0) + file = fopen(m_tmp_path.string().c_str(), "wb"); + else + file = fopen(m_tmp_path.string().c_str(), "a"); + std:: string range_string = std::to_string(m_written) + "-"; + + size_t written_previously = m_written; + size_t written_this_session = 0; Downloader::Http::get(m_url) .size_limit(DOWNLOAD_SIZE_LIMIT) //more? + .set_range(range_string) .on_progress([&](Downloader::Http::Progress progress, bool& cancel) { if (m_cancel) { fclose(file); // remove canceled file - std::remove(tmp_path.string().c_str()); + std::remove(m_tmp_path.string().c_str()); + m_written = 0; cancel = true; return; // TODO: send canceled event? - } + } + if (m_pause) { + fclose(file); + cancel = true; + return; + } + wxCommandEvent* evt = new wxCommandEvent(EVT_FILE_PROGRESS); - if (progress.dlnow == 0) + /*if (progress.dlnow == 0 && m_written == 0) { evt->SetString("0"); - else { - if (progress.dlnow - written > DOWNLOAD_MAX_CHUNK_SIZE || progress.dlnow == progress.dltotal) { + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); + } else*/ + if (progress.dlnow != 0) { + if (progress.dlnow - written_this_session > DOWNLOAD_MAX_CHUNK_SIZE || progress.dlnow == progress.dltotal) { try { - std::string part_for_write = progress.buffer.substr(written, progress.dlnow); + std::string part_for_write = progress.buffer.substr(written_this_session, progress.dlnow); fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); } - catch (const std::exception&) + catch (const std::exception& e) { // fclose(file); do it? wxCommandEvent* evt = new wxCommandEvent(EVT_FILE_ERROR); - evt->SetString("Failed to write progress."); + evt->SetString(e.what()); evt->SetInt(m_id); m_evt_handler->QueueEvent(evt); cancel = true; return; } - written = progress.dlnow; + written_this_session = progress.dlnow; + m_written = written_previously + written_this_session; } evt->SetString(std::to_string(progress.dlnow * 100 / progress.dltotal)); + evt->SetInt(m_id); + m_evt_handler->QueueEvent(evt); } - evt->SetInt(m_id); - m_evt_handler->QueueEvent(evt); + }) .on_error([&](std::string body, std::string error, unsigned http_status) { fclose(file); @@ -158,14 +184,16 @@ void FileGet::priv::get_perform() //} try { - if (written < body.size()) + /* + if (m_written < body.size()) { // this code should never be entered. As there should be on_progress call after last bit downloaded. - std::string part_for_write = body.substr(written); + std::string part_for_write = body.substr(m_written); fwrite(part_for_write.c_str(), 1, part_for_write.size(), file); } + */ fclose(file); - boost::filesystem::rename(tmp_path, dest_path); + boost::filesystem::rename(m_tmp_path, dest_path); } catch (const std::exception& e) { @@ -196,18 +224,25 @@ FileGet::FileGet(FileGet&& other) : p(std::move(other.p)) {} FileGet::~FileGet() { if (p && p->m_io_thread.joinable()) { - p->m_io_thread.detach(); + p->m_cancel = true; + p->m_io_thread.join(); } } void FileGet::get() { - if (p) { - auto io_thread = std::thread([&priv = p]() { - priv->get_perform(); - }); - p->m_io_thread = std::move(io_thread); + assert(p); + if (p->m_io_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_io_thread.join(); } + p->m_cancel = false; + p->m_pause = false; + p->m_io_thread = std::thread([this]() { + p->get_perform(); + }); } void FileGet::cancel() @@ -217,4 +252,26 @@ void FileGet::cancel() } } +void FileGet::pause() +{ + if (p) { + p->m_pause = true; + } +} +void FileGet::resume() +{ + assert(p); + if (p->m_io_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_io_thread.join(); + } + p->m_cancel = false; + p->m_pause = false; + p->m_io_thread = std::thread([this]() { + p->get_perform(); + }); +} + } diff --git a/src/downloader/FileGet.hpp b/src/downloader/FileGet.hpp index 7c1b5356b4..dd4ab24e42 100644 --- a/src/downloader/FileGet.hpp +++ b/src/downloader/FileGet.hpp @@ -20,6 +20,8 @@ public: void get(); void cancel(); + void pause(); + void resume(); static std::string escape_url(const std::string& url); private: std::unique_ptr p; diff --git a/src/downloader/FromSlicer/Http.cpp b/src/downloader/FromSlicer/Http.cpp index 9934054210..e96ea18e3e 100644 --- a/src/downloader/FromSlicer/Http.cpp +++ b/src/downloader/FromSlicer/Http.cpp @@ -144,6 +144,7 @@ struct Http::priv void set_post_body(const fs::path &path); void set_post_body(const std::string &body); void set_put_body(const fs::path &path); + void set_range(const std::string& range); std::string curl_error(CURLcode curlcode); std::string body_size_error(); @@ -313,6 +314,11 @@ void Http::priv::set_put_body(const fs::path &path) } } +void Http::priv::set_range(const std::string& range) +{ + ::curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str()); +} + std::string Http::priv::curl_error(CURLcode curlcode) { return (boost::format("%1%:\n%2%\n[Error %3%]") @@ -438,6 +444,12 @@ Http& Http::size_limit(size_t sizeLimit) return *this; } +Http& Http::set_range(const std::string& range) +{ + if (p) { p->set_range(range); } + return *this; +} + Http& Http::header(std::string name, const std::string &value) { if (!p) { return * this; } diff --git a/src/downloader/FromSlicer/Http.hpp b/src/downloader/FromSlicer/Http.hpp index 86b91ee79c..9d8d2080c5 100644 --- a/src/downloader/FromSlicer/Http.hpp +++ b/src/downloader/FromSlicer/Http.hpp @@ -67,6 +67,8 @@ public: // Sets a maximum size of the data that can be received. // A value of zero sets the default limit, which is is 5MB. Http& size_limit(size_t sizeLimit); + // range of donloaded bytes. example: curl_easy_setopt(curl, CURLOPT_RANGE, "0-199"); + Http& set_range(const std::string& range); // Sets a HTTP header field. Http& header(std::string name, const std::string &value); // Removes a header field.