Checkpoint

This commit is contained in:
Robin E.R. Davies
2026-02-05 11:51:43 -05:00
parent e5beb97466
commit aec88fa9f1
24 changed files with 949 additions and 306 deletions
+1 -1
View File
@@ -204,7 +204,7 @@ else()
endif()
set (PIPEDAL_SOURCES
Tone3000DownloadProgress.cpp Tone3000DownloadProgress.hpp
Tone3000Download.cpp Tone3000Download.hpp
Tone3000Downloader.cpp Tone3000Downloader.hpp
+54 -2
View File
@@ -239,9 +239,26 @@ int64_t PiPedalModel::DownloadModelsFromTone3000(
{
std::lock_guard<std::recursive_mutex> lock(mutex);
// Validate the download path
std::filesystem::path path{downloadPath};
if (!IsInUploadsDirectory(downloadPath))
{
throw PiPedalException("Invalid path: not in uploads directory.");
}
// Check for ".." in path components (security check)
for (const auto &component : path)
{
if (component == "..")
{
throw PiPedalException("Invalid path: contains '..' component.");
}
}
if (!tone3000Downloader)
{
tone3000Downloader = Tone3000Downloader::Create();
tone3000Downloader->SetListener(this);
}
return tone3000Downloader->RequestDownload(
downloadPath,
@@ -255,13 +272,49 @@ void PiPedalModel::CancelTone3000Download(
)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
if (tone3000Downloader)
{
tone3000Downloader->CancelDownload(downloadHandle);
}
}
// Tone3000Downloader::Listener implementation
void PiPedalModel::OnStartTone3000Download(int64_t handle, const std::string &title)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
{
subscriber->OnTone3000DownloadStarted(handle, title);
}
}
void PiPedalModel::OnTone3000Progress(const Tone3000DownloadProgress &progress)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
{
subscriber->OnTone3000DownloadProgress(progress);
}
}
void PiPedalModel::OnTone3000DownloadComplete(int64_t handle, const std::string&resultPath)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
{
subscriber->OnTone3000DownloadComplete(handle,resultPath);
}
}
void PiPedalModel::OnTone3000DownloadError(int64_t handle, const std::string &errorMessage)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
for (auto subscriber : this->subscribers)
{
subscriber->OnTone3000DownloadError(handle, errorMessage);
}
}
void PiPedalModel::LoadLv2PluginInfo()
{
@@ -317,7 +370,6 @@ void PiPedalModel::LoadLv2PluginInfo()
}
storage.SetPluginPresetIndexVersion(1);
}
void PiPedalModel::Load()
{
this->webRoot = configuration.GetWebRoot();
+13 -2
View File
@@ -41,6 +41,7 @@
#include "AtomConverter.hpp"
#include "FileEntry.hpp"
#include <unordered_map>
#include "Tone3000Downloader.hpp"
namespace pipedal
{
@@ -51,7 +52,6 @@ namespace pipedal
class Updater;
class AvahiService;
class Lv2PluginState;
class Tone3000Downloader;
class IPiPedalModelSubscriber
{
@@ -92,6 +92,11 @@ namespace pipedal
// virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0;
virtual void OnErrorMessage(const std::string &message) = 0;
virtual void OnTone3000DownloadStarted(int64_t handle, const std::string &title) = 0;
virtual void OnTone3000DownloadProgress(const Tone3000DownloadProgress &progress) = 0;
virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) = 0;
virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) = 0;
virtual void OnLv2PluginsChanging() = 0;
virtual void OnNetworkChanging(bool hotspotConnected) = 0;
@@ -102,7 +107,7 @@ namespace pipedal
class HotspotManager;
class PiPedalModel : private IAudioHostCallbacks
class PiPedalModel : private IAudioHostCallbacks, private Tone3000Downloader::Listener
{
public:
using clock = std::chrono::steady_clock;
@@ -114,6 +119,12 @@ namespace pipedal
private:
PedalboardItem *GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty);
// Tone3000Downloader::Listener implementation
virtual void OnStartTone3000Download(int64_t handle, const std::string &title) override;
virtual void OnTone3000Progress(const Tone3000DownloadProgress &progress) override;
virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override;
virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override;
std::shared_ptr<Tone3000Downloader> tone3000Downloader;
void CancelAudioRetry();
clock::time_point lastRestartTime = clock::time_point::min();
+72 -5
View File
@@ -24,6 +24,7 @@
#include "json.hpp"
#include "viewstream.hpp"
#include "PiPedalVersion.hpp"
#include "Tone3000Downloader.hpp"
#include <atomic>
#include <limits>
#include "Lv2Log.hpp"
@@ -46,6 +47,18 @@
using namespace std;
using namespace pipedal;
class DownloadModelsFromTone3000Body {
public:
std::string downloadPath_;
std::string tone3000Url_;
DECLARE_JSON_MAP(DownloadModelsFromTone3000Body);
};
JSON_MAP_BEGIN(DownloadModelsFromTone3000Body)
JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadPath)
JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, tone3000Url)
JSON_MAP_END()
class CopyPresetsToBankBody {
public:
int64_t bankInstanceId_;
@@ -92,6 +105,32 @@ JSON_MAP_REFERENCE(DownloadTone3000Body, handle)
JSON_MAP_REFERENCE(DownloadTone3000Body, tone3000DownloadUrl)
JSON_MAP_END()
class Tone3000DownloadStartedBody {
public:
int64_t handle_;
std::string title_;
DECLARE_JSON_MAP(Tone3000DownloadStartedBody);
};
JSON_MAP_BEGIN(Tone3000DownloadStartedBody)
JSON_MAP_REFERENCE(Tone3000DownloadStartedBody, handle)
JSON_MAP_REFERENCE(Tone3000DownloadStartedBody, title)
JSON_MAP_END()
class Tone3000DownloadErrorBody {
public:
int64_t handle_;
std::string errorMessage_;
DECLARE_JSON_MAP(Tone3000DownloadErrorBody);
};
JSON_MAP_BEGIN(Tone3000DownloadErrorBody)
JSON_MAP_REFERENCE(Tone3000DownloadErrorBody, handle)
JSON_MAP_REFERENCE(Tone3000DownloadErrorBody, errorMessage)
JSON_MAP_END()
class PathPatchPropertyChangedBody
{
@@ -1861,10 +1900,6 @@ public:
}
else if (message == "downloadModelsFromTone3000")
{
struct DownloadModelsFromTone3000Body {
std::string downloadPath_;
std::string tone3000Url_;
};
DownloadModelsFromTone3000Body args;
pReader->read(&args);
auto result = this->model.DownloadModelsFromTone3000(
@@ -1873,6 +1908,11 @@ public:
args.tone3000Url_
);
this->Reply(replyTo,"downloadModelsFromTone3000",result);
} else if (message == "cancelTone3000Download")
{
int64_t handle = -1;
pReader->read(&handle);
model.CancelTone3000Download(clientId,handle);
}
else
{
@@ -1995,6 +2035,33 @@ private:
{
Send("onErrorMessage", message);
}
virtual void OnTone3000DownloadStarted(int64_t handle, const std::string &title) override
{
Tone3000DownloadStartedBody body;
body.handle_ = handle;
body.title_ = title;
Send("onTone3000DownloadStarted", body);
}
virtual void OnTone3000DownloadProgress(const Tone3000DownloadProgress &progress) override
{
Send("onTone3000DownloadProgress", progress);
}
virtual void OnTone3000DownloadComplete(int64_t handle, const std::string &resultPath) override
{
Send("onTone3000DownloadComplete", resultPath);
}
virtual void OnTone3000DownloadError(int64_t handle, const std::string &errorMessage) override
{
Tone3000DownloadErrorBody body;
body.handle_ = handle;
body.errorMessage_ = errorMessage;
Send("onTone3000DownloadError", body);
}
// virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value)
// {
// PatchPropertyChangedBody body;
@@ -2376,7 +2443,7 @@ public:
}
virtual std::shared_ptr<SocketHandler> CreateHandler(const uri &request)
{
return std::make_shared<PiPedalSocketHandler>(model);
return std::shared_ptr<PiPedalSocketHandler>(new PiPedalSocketHandler(model));
}
};
+284 -173
View File
@@ -27,6 +27,9 @@
#include <array>
#include <ctime>
#include "util.hpp"
#include "Tone3000DownloadProgress.hpp"
#include <thread>
#include "HtmlHelper.hpp"
using namespace pipedal;
using namespace pipedal::tone3000;
@@ -75,6 +78,7 @@ JSON_MAP_END()
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
static bool enableLogging = true;
static std::string shellEscape(const std::string &input)
{
// Escape string for safe use in shell commands by using single quotes
@@ -117,57 +121,107 @@ static std::string getCurlErrorFromExitCode(int exitCode)
return SS("Failed to download file (exit code: " << (exitCode / 256) << ")");
}
}
static void downloadFile(const std::string &url, const fs::path &path)
static void cancellableSleep(std::chrono::steady_clock::duration duration, std::function<bool()> &isCancelled)
{
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
while (true)
{
if (std::chrono::steady_clock::now() - start >= duration)
{
break;
}
if (isCancelled())
{
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
static void downloadFile(
const std::string &url,
const fs::path &path,
std::function<bool()> &isCancelled)
{
// Create temporary file for HTTP status code
TemporaryFile httpCodeFile{WEB_TEMP_DIR};
fs::path httpCodePath = httpCodeFile.Path();
// Build curl command with error detection and proper escaping
std::ostringstream os;
os << "/usr/bin/curl -f -s -S -L --max-time 300 --max-filesize 104857600";
os << " -w '%{http_code}' -o ";
os << shellEscape(path.string());
os << " ";
os << shellEscape(url);
os << " > ";
os << shellEscape(httpCodePath.string());
TemporaryFile headersFile{WEB_TEMP_DIR};
fs::path headersFilePath = headersFile.Path();
std::string command = os.str();
// Execute curl command
int exitCode = std::system(command.c_str());
// Read HTTP status code
int httpCode = 0;
if (fs::exists(httpCodePath))
for (size_t retry = 0; retry < 10; ++retry)
{
std::ifstream httpCodeStream(httpCodePath);
std::string httpCodeStr;
std::getline(httpCodeStream, httpCodeStr);
try
{
httpCode = std::stoi(httpCodeStr);
}
catch (...)
{
// If parsing fails, keep httpCode as 0
}
}
if (exitCode != 0)
{
if (exitCode == 22 * 256) // HTTP error
// Build curl command with error detection and proper escaping
std::ostringstream os;
os << "/usr/bin/curl -f -s -S -L --max-time 60 --retry 5 --retry-delay 1 --retry-max-time 10 --max-filesize 104857600";
os << " -w '%{http_code}' -o ";
os << shellEscape(path.string());
os << " -D " << shellEscape(headersFilePath);
os << " ";
os << shellEscape(url);
os << " > ";
os << shellEscape(httpCodePath.string());
std::string command = os.str();
// Execute curl command
int exitCode = std::system(command.c_str());
// Read HTTP status code
int httpCode = 0;
if (fs::exists(httpCodePath))
{
throw std::runtime_error(
SS("Server was unabled to download the file. Http error: " << httpCode << " url: " << url));
std::ifstream httpCodeStream(httpCodePath);
std::string httpCodeStr;
std::getline(httpCodeStream, httpCodeStr);
try
{
httpCode = std::stoi(httpCodeStr);
}
catch (...)
{
// If parsing fails, keep httpCode as 0
}
}
if (exitCode != 0)
{
if (exitCode == 22 * 256) // HTTP error
{
std::ifstream headersStream(headersFilePath);
std::string headerStr;
while (std::getline(headersStream, headerStr))
{
std::cout << headerStr << std::endl;
}
if (httpCode == 429 || httpCode == 403 || httpCode == 503)
{
cancellableSleep(std::chrono::milliseconds(2000), isCancelled);
if (isCancelled())
{
return;
}
continue;
}
throw std::runtime_error(
SS("Server was unabled to download the file. Http error: "
<< HtmlHelper::httpErrorString(httpCode)));
}
else
{
throw std::runtime_error(
SS("Server was unable to download the file. "
<< getCurlErrorFromExitCode(exitCode)
<< " " << "url: " << url));
}
}
else
{
throw std::runtime_error(
SS("Server was unable to download the file. "
<< getCurlErrorFromExitCode(exitCode)
<< " " << "url: " << url));
break;
}
}
@@ -178,7 +232,6 @@ static void downloadFile(const std::string &url, const fs::path &path)
}
}
static std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "")
{
std::ostringstream os;
@@ -201,7 +254,6 @@ static std::string mdSanitize(const std::string &str, const std::string &illegal
return os.str();
}
static std::string mdDate(const tone3000_time_point &date_)
{
std::ostringstream os;
@@ -323,54 +375,40 @@ namespace
}
static std::vector<LicenseInfo> licenses{
{
.key = "t3k",
.displayName = "t3k",
.url = "",
.licenseFlags = LicenseFlags::None
},
{
.key = "cc-by",
.displayName = "CC BY 4.0",
.url = "https://creativecommons.org/licenses/by/4.0/",
.licenseFlags = LicenseFlags::CcBy
},
{
.key = "cc-by-sa",
.displayName = "CC BY-SA 4.0",
.url = "https://creativecommons.org/licenses/by-sa/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa
},
{
.key = "cc-by-nc",
.displayName = "CC BY-NC 4.0",
.url = "https://creativecommons.org/licenses/by-nc/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc
},
{
.key = "cc-by-nc-sa",
.displayName = "CC BY-NC-SA 4.0",
.url = "https://creativecommons.org/licenses/by-nc-sa/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa
},
{
.key = "cc-by-nd",
.displayName = "CC BY-ND",
.url = "https://creativecommons.org/licenses/by-nd/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd
},
{
.key = "cc-by-nc-nd",
.displayName = "CC BY-NC-ND",
.url = "https://creativecommons.org/licenses/by-nc-nd/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd
},
{.key = "t3k",
.displayName = "T3K",
.url = "licenses/t3k_license.html",
.licenseFlags = LicenseFlags::None},
{.key = "cc-by",
.displayName = "CC BY 4.0",
.url = "https://creativecommons.org/licenses/by/4.0/",
.licenseFlags = LicenseFlags::CcBy},
{.key = "cc-by-sa",
.displayName = "CC BY-SA 4.0",
.url = "https://creativecommons.org/licenses/by-sa/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Sa},
{.key = "cc-by-nc",
.displayName = "CC BY-NC 4.0",
.url = "https://creativecommons.org/licenses/by-nc/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc},
{.key = "cc-by-nc-sa",
.displayName = "CC BY-NC-SA 4.0",
.url = "https://creativecommons.org/licenses/by-nc-sa/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Sa},
{.key = "cc-by-nd",
.displayName = "CC BY-ND 4.0",
.url = "https://creativecommons.org/licenses/by-nd/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nd},
{.key = "cc-by-nc-nd",
.displayName = "CC BY-NC-ND 4.0",
.url = "https://creativecommons.org/licenses/by-nc-nd/4.0/",
.licenseFlags = LicenseFlags::CcBy | LicenseFlags::Nc | LicenseFlags::Nd},
{
.key = "cco",
.displayName = "CC0",
.url = "https://creativecommons.org/publicdomain/zero/1.0/",
.licenseFlags = LicenseFlags::Cc | LicenseFlags::Cc0,
},
};
@@ -391,93 +429,33 @@ static std::string mdLicenseIcon(LicenseFlags licenseFlag)
std::ostringstream os;
std::string url;
switch (licenseFlag)
switch (licenseFlag)
{
case LicenseFlags::Cc:
url = "img/cc.svg";
break;
case LicenseFlags::By:
url = "img/by.svg";
break;
case LicenseFlags::Sa:
url = "img/sa.svg";
break;
case LicenseFlags::Nc:
url = "img/nc.svg";
break;
case LicenseFlags::Nd:
url = "img/nd.svg";
break;
case LicenseFlags::Cc0:
url = "img/cc0.svg";
break;
case LicenseFlags::Cc:
url = "img/cc.svg";
break;
case LicenseFlags::By:
url = "img/by.svg";
break;
case LicenseFlags::Sa:
url = "img/sa.svg";
break;
case LicenseFlags::Nc:
url = "img/nc.svg";
break;
case LicenseFlags::Nd:
url = "img/nd.svg";
break;
case LicenseFlags::Cc0:
url = "img/cc0.svg";
break;
case LicenseFlags::None:
throw std::runtime_error("Invalid argument.");
case LicenseFlags::None:
throw std::runtime_error("Invalid argument.");
}
os << "<img id='cc_img' src='" << url << "'/>";
return os.str();
}
static std::string mdLicense(const std::string &license)
{
auto ff = licenseEnum.find(license);
LicenseInfo licenseInfo;
if (ff != licenseEnum.end())
{
licenseInfo = ff->second;
}
else
{
licenseInfo.displayName = license;
}
std::ostringstream os;
if (licenseInfo.licenseFlags != LicenseFlags::None) {
for (LicenseFlags licenseFlag: std::vector<LicenseFlags>{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd})
{
if (licenseInfo.licenseFlags & licenseFlag)
{
os << mdLicenseIcon(licenseFlag);
}
}
os << " ";
}
if (licenseInfo.url != "")
{
os << "<a target='_blank' rel='noopener noreferrer' href='"
<< licenseInfo.url
<< "'>" << mdSanitize(licenseInfo.displayName) << "</a>";
}
else
{
os << mdSanitize(licenseInfo.displayName);
}
return os.str();
}
static std::map<std::string, std::string> platformEnumValues =
{
{"nam", "NAM"},
{"ir", "I/R"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}};
static std::string mdPlatform(const std::string &platform)
{
return mdEnum(platform, platformEnumValues);
}
static std::string mdUser(const tone3000::Tone3000User &user)
{
// clang-format off
std::ostringstream os;
os <<
"<a target='_blank' rel='noopener noreferrer' href='"
<< mdSanitize(user.url(),"'")
<< "'>" << mdSanitize(user.username()) << "</a>";
return os.str();
// clang-format on
}
static void mdEscapeString(std::ostream &f, const std::string &str)
{
@@ -503,6 +481,95 @@ static void mdEscapeString(std::ostream &f, const std::string &str)
}
}
static std::string mdHref(const std::string &url)
{
std::ostringstream os;
os << '\'';
for (char c: url)
{
if (c == '\'')
{
continue;
}
os << c;
}
os << '\'';
return os.str();
}
static std::string mdLicense(const std::string &license)
{
auto ff = licenseEnum.find(license);
LicenseInfo licenseInfo;
if (ff != licenseEnum.end())
{
licenseInfo = ff->second;
}
else
{
licenseInfo.displayName = license;
}
std::ostringstream os;
if (licenseInfo.licenseFlags != LicenseFlags::None)
{
for (LicenseFlags licenseFlag : std::vector<LicenseFlags>{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd})
{
if (licenseInfo.licenseFlags & licenseFlag)
{
os << mdLicenseIcon(licenseFlag);
}
}
os << " ";
}
if (licenseInfo.url != "")
{
os << "<a href="
<< mdHref(licenseInfo.url)
<< " target='_blank' rel='noopener noreferrer' >" << mdSanitize(licenseInfo.displayName) << "</a>";
}
else
{
os << mdSanitize(licenseInfo.displayName);
}
return os.str();
}
// static std::string mdTestLicenses()
// {
// std::ostringstream os;
// for (auto license: licenseEnum)
// {
// os << "<br/>" << mdLicense(license.first) << "\n";
// }
// return os.str();
// }
static std::map<std::string, std::string> platformEnumValues =
{
{"nam", "NAM"},
{"ir", "I/R"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}
};
static std::string mdPlatform(const std::string &platform)
{
return mdEnum(platform, platformEnumValues);
}
static std::string mdUser(const tone3000::Tone3000User &user)
{
// clang-format off
std::ostringstream os;
os <<
"<a target='_blank' rel='noopener noreferrer' href='"
<< mdSanitize(user.url(),"'")
<< "'>" << mdSanitize(user.username()) << "</a>";
return os.str();
// clang-format on
}
static void mdLink(std::ostream &f, const std::string &label, const std::string &url)
{
f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")";
@@ -541,11 +608,14 @@ static void writeReadme(const fs::path &path, const Tone3000Download &download)
<< mdUser(download.user())
<< " <br/>\n"
<< " " << mdSizes(download.sizes()) << ", "
<< mdSizes(download.sizes()) << ", "
<< mdGear(download.gear()) << ", "
<< mdPlatform(download.platform())
<< "<br/>\n"
<< " License: xxx" << mdLicense(download.license()) << "\n"
<< "<br/>\n";
if (download.license() != "") {
of << " License: " << mdLicense(download.license()) << "\n";
}
of
// << " " << mdTestLicenses()
<< " </p>\n"
<< " </div>\n"
<< " </div>\n"
@@ -566,14 +636,28 @@ static void writeReadme(const fs::path &path, const Tone3000Download &download)
}
}
void pipedal::tone3000::DownloadTone3000Bundle(
std::string pipedal::tone3000::DownloadTone3000Bundle(
const std::filesystem::path &destinationFolder,
const std::string &downloadUrl)
const std::string &downloadUrl,
int64_t handle,
std::function<void(const Tone3000DownloadProgress &progress)> progressCallback,
std::function<bool()> isCancelled)
{
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
fs::path downloadPath = downloadTemporaryFile.Path();
downloadFile(downloadUrl, downloadPath);
std::string resultDirectory;
Tone3000DownloadProgress progress;
progress.handle(handle);
progressCallback(progress);
downloadFile(downloadUrl, downloadPath, isCancelled);
if (isCancelled())
{
return resultDirectory;
}
std::ifstream f{downloadPath};
if (!f.is_open())
@@ -585,21 +669,48 @@ void pipedal::tone3000::DownloadTone3000Bundle(
Tone3000Download tone3000Download;
reader.read(&tone3000Download);
if (isCancelled())
{
return resultDirectory;
}
progress.title(tone3000Download.title());
progress.total(tone3000Download.models().size());
progressCallback(progress);
if (tone3000Download.platform() != "nam")
{
throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")"));
}
fs::path bundlePath = destinationFolder / stringToSafeFilename(tone3000Download.title());
resultDirectory = bundlePath;
fs::create_directories(bundlePath);
uint64_t nModel = 0;
for (auto &model : tone3000Download.models())
{
if (isCancelled())
{
return resultDirectory;
}
fs::path modelPath = bundlePath / SS(stringToSafeFilename(model.name()) << ".nam");
downloadFile(model.model_url(), modelPath);
downloadFile(model.model_url(), modelPath, isCancelled);
if (isCancelled())
{
return resultDirectory;
}
progress.progress(++nModel);
progressCallback(progress);
}
if (isCancelled())
{
return resultDirectory;
}
fs::path readmePath = bundlePath / "README.md";
writeReadme(readmePath, tone3000Download);
return resultDirectory;
}
Tone3000DownloadResult::Tone3000DownloadResult()
+9 -3
View File
@@ -25,9 +25,12 @@
#include <cstdint>
#include <string>
#include <vector>
#include <optional>
#include "json.hpp"
#include <filesystem>
#include <chrono>
#include <functional>
#include "Tone3000DownloadProgress.hpp"
namespace pipedal
{
@@ -59,7 +62,7 @@ namespace pipedal
{
private:
std::string id_;
std::string avatar_url_;
std::optional<std::string> avatar_url_;
std::string username_;
std::string url_;
@@ -128,10 +131,13 @@ namespace pipedal
DECLARE_JSON_MAP(Tone3000DownloadResult);
};
extern void DownloadTone3000Bundle
extern std::string DownloadTone3000Bundle
(
const std::filesystem::path&destinationFolder,
const std::string &downloadUrl
const std::string &downloadUrl,
int64_t downloadHandle,
std::function<void(const Tone3000DownloadProgress&progress)> progressCallback,
std::function<bool()> isCancelled
);
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2026 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "Tone3000DownloadProgress.hpp"
using namespace pipedal;
JSON_MAP_BEGIN(Tone3000DownloadProgress)
JSON_MAP_REFERENCE(Tone3000DownloadProgress, handle)
JSON_MAP_REFERENCE(Tone3000DownloadProgress, title)
JSON_MAP_REFERENCE(Tone3000DownloadProgress, progress)
JSON_MAP_REFERENCE(Tone3000DownloadProgress, total)
JSON_MAP_END()
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#include <string>
#include <cstdint>
#include "json.hpp"
namespace pipedal {
class Tone3000DownloadProgress {
private:
int64_t handle_ = -1;
std::string title_;
uint64_t progress_ = 0;
uint64_t total_ = 0;
public:
JSON_GETTER_SETTER(handle);
JSON_GETTER_SETTER_REF(title);
JSON_GETTER_SETTER(progress);
JSON_GETTER_SETTER(total);
DECLARE_JSON_MAP(Tone3000DownloadProgress);
};
}
+127 -27
View File
@@ -22,6 +22,10 @@
*/
#include "Tone3000DownloaderImpl.hpp"
#include "Tone3000DownloadProgress.hpp"
#include "Tone3000Download.hpp"
#include "Uri.hpp"
#include <functional>
using namespace pipedal;
@@ -67,9 +71,10 @@ Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload(
this->requestQueue.push_back(request);
request = nullptr;
if (!this->thread)
if (!this->thread)
{
this->thread = std::make_unique<std::jthread>([this]{ this->ThreadProc(); });
this->thread = std::make_unique<std::jthread>([this]
{ this->ThreadProc(); });
}
}
this->thread_cv.notify_one();
@@ -97,7 +102,8 @@ void Tone3000DownloaderImpl::CancelDownload(
}
}
void Tone3000DownloaderImpl::Close() {
void Tone3000DownloaderImpl::Close()
{
bool notify = false;
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
@@ -109,7 +115,7 @@ void Tone3000DownloaderImpl::Close() {
this->requestQueue.resize(0);
DownloadProgress progress;
Tone3000DownloadProgress progress;
fgDownloadProgress = progress;
if (listener)
@@ -121,12 +127,14 @@ void Tone3000DownloaderImpl::Close() {
activeRequest->cancelled = true;
}
}
if (notify) {
if (notify)
{
thread_cv.notify_one();
}
}
Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() {
DownloadProgress result;
Tone3000DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus()
{
Tone3000DownloadProgress result;
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
result = fgDownloadProgress;
@@ -134,47 +142,139 @@ Tone3000DownloaderImpl::DownloadProgress Tone3000DownloaderImpl::GetDownloadStat
return result;
}
void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const DownloadProgress &progress)
void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress)
{
std::lock_guard<std::mutex> lockGuard{this->mutex};
if (closed)
if (closed)
{
return;
}
this->fgDownloadProgress = progress;
if (listener)
if (activeRequest != nullptr && !activeRequest->cancelled)
{
listener->OnTone3000Progress(this->fgDownloadProgress);
this->fgDownloadProgress = progress;
if (listener)
{
listener->OnTone3000Progress(this->fgDownloadProgress);
}
}
}
std::string Tone3000DownloaderImpl::PerformDownload(
const std::string &downloadPath,
const std::string &downloadUrl,
int64_t downloadHandle,
std::function<bool()> isCancelled)
{
std::string downloadedDirectory;
// Validate Tone3000 URL
uri downloadUri{downloadUrl};
if (!downloadUri.authority().ends_with(".tone3000.com"))
{
throw std::runtime_error("Invalid Tone3000 URL address.");
}
// Check for cancellation before starting
if (isCancelled())
{
return downloadedDirectory;
}
Tone3000DownloadProgress progress;
this->bgUpdateDownloadProgress(progress);
// Perform the download using existing code
downloadedDirectory = tone3000::DownloadTone3000Bundle(
downloadPath,
downloadUrl,
downloadHandle,
[this](const Tone3000DownloadProgress &progress)
{
this->bgUpdateDownloadProgress(progress);
},
[isCancelled]()
{
return isCancelled();
});
// Check for cancellation after download
if (isCancelled())
{
// maybe there's a partial result.
return downloadedDirectory;
}
return downloadedDirectory;
}
void Tone3000DownloaderImpl::ThreadProc()
{
while (true)
{
std::shared_ptr<DownloadRequest> downloadRequest;
std::shared_ptr<DownloadRequest> request;
Listener *currentListener = nullptr;
Tone3000DownloadProgress progress;
{
std::unique_lock lock { this->mutex};
this->activeRequest = nullptr;
std::unique_lock<std::mutex> lock{this->mutex};
thread_cv.wait(lock, [this]()
{ return this->closed || this->requestQueue.size() != 0; });
this->thread_cv.wait(lock,[this] { return this->closed || this->requestQueue.size() != 0;});
if (this->closed) {
if (closed)
{
return;
}
if (this->requestQueue.size() != 0)
if (requestQueue.empty())
{
this->activeRequest = this->requestQueue[0];
this->requestQueue.erase(requestQueue.begin());
continue;
}
downloadRequest = this->activeRequest;
}
if (!downloadRequest)
{
continue;
// Dequeue the next request
request = requestQueue.front();
requestQueue.erase(requestQueue.begin());
activeRequest = request;
currentListener = listener;
}
// Notify download started
if (currentListener)
{
currentListener->OnStartTone3000Download(request->handle, request->downloadUrl);
}
// Create cancellation predicate that checks the atomic flag
auto isCancelled = [request]() -> bool
{
return request->cancelled.load();
};
try
{
std::string outputPath = PerformDownload(request->downloadPath, request->downloadUrl, request->handle, isCancelled);
// Notify completion if not cancelled
{
std::lock_guard lockGuard{mutex};
if (!request->cancelled.load() && currentListener)
{
currentListener->OnTone3000DownloadComplete(request->handle,outputPath);
}
}
}
catch (const std::exception &e)
{
{
std::lock_guard lockGuard{mutex};
// Notify error
if (!request->cancelled.load() && currentListener)
{
currentListener->OnTone3000DownloadError(request->handle, e.what());
}
}
}
{
std::lock_guard<std::mutex> lock{this->mutex};
activeRequest = nullptr;
}
}
}
+10 -14
View File
@@ -25,6 +25,7 @@
#include <memory>
#include <cstdint>
#include "Tone3000DownloadProgress.hpp"
namespace pipedal {
@@ -33,20 +34,9 @@ namespace pipedal {
Tone3000Downloader();
public:
using handle_t = int64_t;
static constexpr handle_t INVALID_HANDLE = (handle_t)(int64_t)-1;
virtual ~Tone3000Downloader();
class DownloadProgress {
public:
private:
handle_t handle_ = INVALID_HANDLE;
std::string title_;
uint64_t progress_ = 0;
uint64_t total_ = 0;
};
class Listener {
public:
virtual void OnStartTone3000Download
@@ -55,10 +45,16 @@ namespace pipedal {
const std::string &title
) = 0;
virtual void OnTone3000Progress(
const DownloadProgress& downloadProgress
const Tone3000DownloadProgress& downloadProgress
) = 0;
virtual void OnTone3000DownloadComplete(
handle_t handle
handle_t handle,
const std::string&resultPath
) = 0;
virtual void OnTone3000DownloadError(
handle_t handle,
const std::string &errorMessage
) = 0;
};
@@ -79,6 +75,6 @@ namespace pipedal {
) = 0;
virtual void Close() = 0;
virtual DownloadProgress GetDownloadStatus() = 0;
virtual Tone3000DownloadProgress GetDownloadStatus() = 0;
};
}
+13 -3
View File
@@ -30,6 +30,7 @@
#include <mutex>
#include <condition_variable>
#include <vector>
#include <functional>
namespace pipedal {
@@ -49,12 +50,21 @@ namespace pipedal {
) override;
virtual void Close() override;
virtual DownloadProgress GetDownloadStatus() override;
virtual Tone3000DownloadProgress GetDownloadStatus() override;
private:
Listener *listener = nullptr;
void ThreadProc();
// Performs the actual download with cancellation support
std::string PerformDownload(
const std::string &downloadPath,
const std::string &downloadUrl,
int64_t downloadHandle,
std::function<bool()> isCancelled
);
struct DownloadRequest {
std::atomic<bool> cancelled = false;
handle_t handle;
@@ -68,8 +78,8 @@ namespace pipedal {
std::vector<std::shared_ptr<DownloadRequest>> requestQueue;
DownloadProgress fgDownloadProgress;
void bgUpdateDownloadProgress(const DownloadProgress &progress);
Tone3000DownloadProgress fgDownloadProgress;
void bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress);
std::shared_ptr<DownloadRequest> activeRequest;
-43
View File
@@ -40,7 +40,6 @@
#include "util.hpp"
#include "HtmlHelper.hpp"
#include "WebServerMod.hpp"
#include "Tone3000Download.hpp"
#define OLD_PRESET_EXTENSION ".piPreset"
#define PRESET_EXTENSION ".piPreset"
@@ -147,10 +146,6 @@ public:
}
std::string segment = request_uri.segment(1);
if (segment == "tone3000Upload")
{
return true;
}
if (segment == "downloadMediaFile")
{
return true;
@@ -437,44 +432,6 @@ public:
{
std::string segment = request_uri.segment(1);
if (segment == "tone3000Upload")
{
tone3000::Tone3000DownloadResult result;
try
{
fs::path downloadPath = request_uri.query("path");
if (downloadPath.empty())
{
throw std::runtime_error("Invalid query parameters.");
}
if (!this->model->IsInUploadsDirectory(downloadPath) || HasDotDot(downloadPath))
{
throw std::runtime_error("Invalid path.");
}
std::string downloadUrl = request_uri.query("url");
uri downloadUri{downloadUrl};
if (!downloadUri.authority().ends_with(".tone3000.com"))
{
throw std::runtime_error("Invalid Tone3000 URL address.");
}
tone3000::DownloadTone3000Bundle(downloadPath, downloadUrl);
}
catch (const std::exception &e)
{
result = tone3000::Tone3000DownloadResult(e);
}
std::ostringstream os;
json_writer writer(os);
writer.write(&result);
std::string strResult = os.str();
res.set(HttpField::content_type,"application/json");
res.set(HttpField::cache_control, "no-cache");
res.setBody(strResult);
return;
}
if (segment == "downloadMediaFile")
{
fs::path path = request_uri.query("path");