mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-08-05 17:10:45 +08:00
Pause / continue download
This commit is contained in:
parent
648e56d8bf
commit
16b0654a6a
@ -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();
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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) {
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "InstanceSend.hpp"
|
||||
#include "Download.hpp"
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <wx/wx.h>
|
||||
#include <wx/dataview.h>
|
||||
|
||||
@ -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<std::unique_ptr<Download>> m_downloads;
|
||||
/* DownloadPending = 0,
|
||||
DownloadOngoing,
|
||||
DownloadStopped,
|
||||
DownloadDone,
|
||||
DownloadError,
|
||||
DownloadPaused*/
|
||||
const std::map<DownloadState, wxString> c_state_labels = {
|
||||
{DownloadPending, "Pending"},
|
||||
{DownloadStopped, "Canceled"},
|
||||
{DownloadDone, "Done"},
|
||||
{DownloadError, "Error"},
|
||||
{DownloadPaused, "Paused"},
|
||||
};
|
||||
};
|
||||
|
||||
class DownloadApp : public wxApp
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<priv> p;
|
||||
|
@ -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; }
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user