From c456e5187bb2760c19cb6e43fe424f82346a4638 Mon Sep 17 00:00:00 2001 From: "Robin E.R. Davies" Date: Tue, 12 May 2026 12:31:21 -0400 Subject: [PATCH] NAM A2 Sync --- PiPedalCommon/src/HtmlHelper.cpp | 50 +- PiPedalCommon/src/include/HtmlHelper.hpp | 88 +- src/Curl.cpp | 663 ++++++---- src/Curl.hpp | 29 +- src/PiPedalModel.cpp | 19 +- src/PiPedalModel.hpp | 8 +- src/PiPedalSocket.cpp | 574 ++++----- src/Storage.cpp | 85 +- src/ThreadedQueue.hpp | 116 ++ src/Tone3000Download.cpp | 777 +---------- src/Tone3000Download.cpp.bak | 2 +- src/Tone3000Download.hpp | 185 ++- src/Tone3000DownloadType.hpp | 4 +- src/Tone3000Downloader.cpp | 1133 +++++++++++++++-- src/Tone3000Downloader.hpp | 138 +- src/Tone3000DownloaderImpl.hpp | 187 ++- src/Uri.cpp | 271 ++-- src/Uri.hpp | 393 +++--- src/WebServer.cpp | 12 + src/WebServer.hpp | 1 + src/WebServerConfig.cpp | 247 +++- vite/public/handleTone3000download.html | 42 - vite/public/html/t3k_response.html | 27 + vite/public/t3k/handleTone3000download.html | 148 +++ vite/src/pipedal/AppThemed.tsx | 13 +- vite/src/pipedal/FilePropertyDialog.tsx | 19 +- vite/src/pipedal/PiPedalModel.tsx | 125 +- vite/src/pipedal/SafeFilename.tsx | 4 +- vite/src/pipedal/Tone3000Dialog.tsx | 185 +-- vite/src/pipedal/Tone3000DownloadProgress.tsx | 23 +- vite/src/pipedal/Tone3000DownloadType.tsx | 4 +- vite/src/pipedal/Tone3000Downloader.tsx | 546 ++++++++ vite/src/pipedal/Tone3000Downloader.tsx.bak | 222 ++++ vite/src/pipedal/t3k/config.ts | 20 + vite/src/pipedal/t3k/tone3000-client.ts | 713 +++++++++++ vite/src/pipedal/t3k/types.ts | 152 +++ vite/src/t3k_response.tsx | 42 + vite/t3k_response.html | 17 + vite/vite.config.ts | 8 +- 39 files changed, 5086 insertions(+), 2206 deletions(-) create mode 100644 src/ThreadedQueue.hpp delete mode 100644 vite/public/handleTone3000download.html create mode 100644 vite/public/html/t3k_response.html create mode 100644 vite/public/t3k/handleTone3000download.html create mode 100644 vite/src/pipedal/Tone3000Downloader.tsx create mode 100644 vite/src/pipedal/Tone3000Downloader.tsx.bak create mode 100644 vite/src/pipedal/t3k/config.ts create mode 100644 vite/src/pipedal/t3k/tone3000-client.ts create mode 100644 vite/src/pipedal/t3k/types.ts create mode 100644 vite/src/t3k_response.tsx create mode 100644 vite/t3k_response.html diff --git a/PiPedalCommon/src/HtmlHelper.cpp b/PiPedalCommon/src/HtmlHelper.cpp index 8d618a7..e0de787 100644 --- a/PiPedalCommon/src/HtmlHelper.cpp +++ b/PiPedalCommon/src/HtmlHelper.cpp @@ -247,8 +247,30 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string &name) #define MAX_FILE_NAME_LENGHT 96 -const std::string SF_SPECIALS = " <>@;:\"\'/[]?=+"; +const std::string SF_SPECIALS = "<>@;:\"\'/[]?="; +const std::string ISF_SPECIALS = "\\:"; + +bool HtmlHelper::IsSafeFileName(const std::filesystem::path &path) +{ + for (const auto&segment: path) + { + if (segment.string() == "..") + { + return false; + } + for (char c: segment.string()) + { + unsigned char uc = (unsigned char)c; + if (uc < 0x20 || uc >= 0x80 || ISF_SPECIALS.find(c) != std::string::npos) + { + return false; + } + + } + } + return true; +} std::string HtmlHelper::SafeFileName(const std::string &name) { std::stringstream s; @@ -456,8 +478,30 @@ std::string HtmlHelper::httpErrorString(int errorCode) auto ff = httpErrorStrings.find(errorCode); if (ff != httpErrorStrings.end()) { - return SS(errorCode << " " << ff->second); + return SS(errorCode << ": " << ff->second); } - return SS(errorCode << " Unknown error"); + return SS(errorCode << ": Unknown error"); } + +std::string HtmlFormBuilder::build() +{ + std::ostringstream ss; + bool firstTime = true; + for (const auto &entry: entries_) + { + if (!firstTime) + { + ss << "&"; + } + ss << entry.key(); + if (entry.value().has_value()) + { + + ss << "=" << HtmlHelper::encode_url_segment(entry.value().value(),true); + } + firstTime = false; + + } + return ss.str(); +} diff --git a/PiPedalCommon/src/include/HtmlHelper.hpp b/PiPedalCommon/src/include/HtmlHelper.hpp index 07a0ec4..b3be4cf 100644 --- a/PiPedalCommon/src/include/HtmlHelper.hpp +++ b/PiPedalCommon/src/include/HtmlHelper.hpp @@ -24,47 +24,75 @@ #include #include -namespace pipedal { +namespace pipedal +{ -class HtmlHelper { - -public: - - static std::string timeToHttpDate(); - static std::string timeToHttpDate(time_t time); - static std::string timeToHttpDate(std::filesystem::file_time_type time); - - - static std::string encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false); - static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) { - return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment); - } - static void encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment = false); - static void encode_url_segment(std::ostream&os, const std::string&segment, bool isQuerySegment = false) + class HtmlHelper { - const char*p = segment.c_str(); - encode_url_segment(os,p, p + segment.length(),isQuerySegment); - } + public: + static std::string timeToHttpDate(); + static std::string timeToHttpDate(time_t time); + static std::string timeToHttpDate(std::filesystem::file_time_type time); - static std::string decode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment = false); + static std::string encode_url_segment(const char *pStart, const char *pEnd, bool isQuerySegment = false); + static std::string encode_url_segment(const std::string &segment, bool isQuerySegment = false) + { + return encode_url_segment(segment.c_str(), segment.c_str() + segment.length(), isQuerySegment); + } + static void encode_url_segment(std::ostream &os, const char *pStart, const char *pEnd, bool isQuerySegment = false); + static void encode_url_segment(std::ostream &os, const std::string &segment, bool isQuerySegment = false) + { + const char *p = segment.c_str(); + encode_url_segment(os, p, p + segment.length(), isQuerySegment); + } - static std::string decode_url_segment(const char*text, bool isQuerySegment = false); + static std::string decode_url_segment(const char *pStart, const char *pEnd, bool isQuerySegment = false); - static void utf32_to_utf8_stream(std::ostream &s, uint32_t uc); + static std::string decode_url_segment(const char *text, bool isQuerySegment = false); - static std::string Rfc5987EncodeFileName(const std::string&name); + static void utf32_to_utf8_stream(std::ostream &s, uint32_t uc); - static std::string SafeFileName(const std::string &name); - static std::string HtmlEncode(const std::string& text); + static std::string Rfc5987EncodeFileName(const std::string &name); - static uint64_t crc64(uint8_t *data, size_t length,uint64_t crc = 0); - static uint64_t crc64(const std::string&value, uint64_t crc = 0); + static std::string SafeFileName(const std::string &name); + static bool IsSafeFileName(const std::filesystem::path &path); + static std::string HtmlEncode(const std::string &text); - static std::string generateEtag(const std::filesystem::path &path); + static uint64_t crc64(uint8_t *data, size_t length, uint64_t crc = 0); + static uint64_t crc64(const std::string &value, uint64_t crc = 0); - static std::string httpErrorString(int errorCode); + static std::string generateEtag(const std::filesystem::path &path); -}; + static std::string httpErrorString(int errorCode); + }; + + class HtmlFormBuilder + { + + public: + class FormEntry + { + public: + FormEntry() = default; + FormEntry(const std::string &key) : key_(key) {} + FormEntry(const std::string &key, const std::string &value) : key_(key), value_(value) {} + + const std::string&key() const { return key_; } + const std::optional&value() const { return value_; } + private: + std::string key_; + std::optional value_; + }; + HtmlFormBuilder() = default; + HtmlFormBuilder(const std::vector &&entries) : entries_(std::move(entries)) {} + void Add(const std::string &key) { entries_.push_back(FormEntry(key)); } + void Add(const std::string &key, const std::string &value) { entries_.push_back(FormEntry(key, value)); } + + std::string build(); + + private: + std::vector entries_; + }; } // namespace. \ No newline at end of file diff --git a/src/Curl.cpp b/src/Curl.cpp index 82f3ecb..0fe9349 100644 --- a/src/Curl.cpp +++ b/src/Curl.cpp @@ -41,16 +41,15 @@ static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; static bool enableLogging = true; - namespace pipedal { - static void logHeaders(const std::vector&headers) + static void logHeaders(const std::vector &headers) { if (enableLogging) { - std::ofstream os {"/tmp/PipedalCurl.log"}; - for (const auto&header: headers) + std::ofstream os{"/tmp/PipedalCurl.log"}; + for (const auto &header : headers) { os << header << "\n"; } @@ -62,92 +61,178 @@ namespace pipedal // https://curl.se/libcurl/c/libcurl-errors.html switch (exitCode) { - case 0: return "OK"; // CURLE_OK - All fine - case 1: return "Unsupported protocol."; - case 2: return "Failed to initialize."; - case 3: return "URL malformed."; - case 4: return "Feature not built-in."; - case 5: return "Couldn't resolve proxy."; - case 6: return "Couldn't resolve host."; - case 7: return "Failed to connect to host."; - case 8: return "Weird server reply."; - case 9: return "Access denied to remote resource."; - case 10: return "FTP accept failed."; - case 11: return "FTP weird PASS reply."; - case 12: return "FTP accept timeout."; - case 13: return "FTP weird PASV reply."; - case 14: return "FTP weird 227 format."; - case 15: return "FTP can't get host."; - case 16: return "HTTP/2 error."; - case 17: return "FTP couldn't set type."; - case 18: return "Partial file transfer."; - case 19: return "FTP couldn't retrieve file."; - case 21: return "FTP quote error."; - case 22: return "HTTP page not retrieved."; - case 23: return "Write error."; - case 25: return "Upload failed."; - case 26: return "Read error."; - case 27: return "Out of memory."; - case 28: return "Operation timeout."; - case 30: return "FTP PORT failed."; - case 31: return "FTP couldn't use REST."; - case 33: return "Range error."; - case 35: return "SSL connect error."; - case 36: return "Bad download resume."; - case 37: return "FILE couldn't read file."; - case 38: return "LDAP cannot bind."; - case 39: return "LDAP search failed."; - case 42: return "Aborted by callback."; - case 43: return "Bad function argument."; - case 45: return "Interface failed."; - case 47: return "Too many redirects."; - case 48: return "Unknown option."; - case 49: return "Setopt option syntax error."; - case 52: return "Got nothing from server."; - case 53: return "SSL engine not found."; - case 54: return "SSL engine set failed."; - case 55: return "Failed sending network data."; - case 56: return "Failure receiving network data."; - case 58: return "Problem with local client certificate."; - case 59: return "Couldn't use specified SSL cipher."; - case 60: return "Peer SSL certificate or SSH fingerprint verification failed."; - case 61: return "Unrecognized transfer encoding."; - case 63: return "Maximum file size exceeded."; - case 64: return "Requested FTP SSL level failed."; - case 65: return "Send failed to rewind."; - case 66: return "SSL engine initialization failed."; - case 67: return "Login denied."; - case 68: return "TFTP file not found."; - case 69: return "TFTP permission problem."; - case 70: return "Remote disk full."; - case 71: return "TFTP illegal operation."; - case 72: return "TFTP unknown transfer ID."; - case 73: return "Remote file already exists."; - case 74: return "TFTP no such user."; - case 77: return "Problem reading SSL CA cert."; - case 78: return "Remote file not found."; - case 79: return "SSH error."; - case 80: return "SSL shutdown failed."; - case 82: return "Failed to load CRL file."; - case 83: return "SSL issuer check failed."; - case 84: return "FTP PRET command failed."; - case 85: return "RTSP CSeq mismatch."; - case 86: return "RTSP session ID mismatch."; - case 87: return "Unable to parse FTP file list."; - case 88: return "Chunk callback error."; - case 89: return "No connection available."; - case 90: return "SSL pinned public key mismatch."; - case 91: return "SSL invalid certificate status."; - case 92: return "HTTP/2 stream error."; - case 93: return "Recursive API call."; - case 94: return "Authentication error."; - case 95: return "HTTP/3 error."; - case 96: return "QUIC connection error."; - case 97: return "Proxy handshake error."; - case 98: return "SSL client certificate required."; - case 99: return "Unrecoverable poll error."; - case 100: return "Value or data field too large."; - case 101: return "ECH failed."; + case 0: + return "OK"; // CURLE_OK - All fine + case 1: + return "Unsupported protocol."; + case 2: + return "Failed to initialize."; + case 3: + return "URL malformed."; + case 4: + return "Feature not built-in."; + case 5: + return "Couldn't resolve proxy."; + case 6: + return "Couldn't resolve host."; + case 7: + return "Failed to connect to host."; + case 8: + return "Weird server reply."; + case 9: + return "Access denied to remote resource."; + case 10: + return "FTP accept failed."; + case 11: + return "FTP weird PASS reply."; + case 12: + return "FTP accept timeout."; + case 13: + return "FTP weird PASV reply."; + case 14: + return "FTP weird 227 format."; + case 15: + return "FTP can't get host."; + case 16: + return "HTTP/2 error."; + case 17: + return "FTP couldn't set type."; + case 18: + return "Partial file transfer."; + case 19: + return "FTP couldn't retrieve file."; + case 21: + return "FTP quote error."; + case 22: + return "HTTP page not retrieved."; + case 23: + return "Write error."; + case 25: + return "Upload failed."; + case 26: + return "Read error."; + case 27: + return "Out of memory."; + case 28: + return "Operation timeout."; + case 30: + return "FTP PORT failed."; + case 31: + return "FTP couldn't use REST."; + case 33: + return "Range error."; + case 35: + return "SSL connect error."; + case 36: + return "Bad download resume."; + case 37: + return "FILE couldn't read file."; + case 38: + return "LDAP cannot bind."; + case 39: + return "LDAP search failed."; + case 42: + return "Aborted by callback."; + case 43: + return "Bad function argument."; + case 45: + return "Interface failed."; + case 47: + return "Too many redirects."; + case 48: + return "Unknown option."; + case 49: + return "Setopt option syntax error."; + case 52: + return "Got nothing from server."; + case 53: + return "SSL engine not found."; + case 54: + return "SSL engine set failed."; + case 55: + return "Failed sending network data."; + case 56: + return "Failure receiving network data."; + case 58: + return "Problem with local client certificate."; + case 59: + return "Couldn't use specified SSL cipher."; + case 60: + return "Peer SSL certificate or SSH fingerprint verification failed."; + case 61: + return "Unrecognized transfer encoding."; + case 63: + return "Maximum file size exceeded."; + case 64: + return "Requested FTP SSL level failed."; + case 65: + return "Send failed to rewind."; + case 66: + return "SSL engine initialization failed."; + case 67: + return "Login denied."; + case 68: + return "TFTP file not found."; + case 69: + return "TFTP permission problem."; + case 70: + return "Remote disk full."; + case 71: + return "TFTP illegal operation."; + case 72: + return "TFTP unknown transfer ID."; + case 73: + return "Remote file already exists."; + case 74: + return "TFTP no such user."; + case 77: + return "Problem reading SSL CA cert."; + case 78: + return "Remote file not found."; + case 79: + return "SSH error."; + case 80: + return "SSL shutdown failed."; + case 82: + return "Failed to load CRL file."; + case 83: + return "SSL issuer check failed."; + case 84: + return "FTP PRET command failed."; + case 85: + return "RTSP CSeq mismatch."; + case 86: + return "RTSP session ID mismatch."; + case 87: + return "Unable to parse FTP file list."; + case 88: + return "Chunk callback error."; + case 89: + return "No connection available."; + case 90: + return "SSL pinned public key mismatch."; + case 91: + return "SSL invalid certificate status."; + case 92: + return "HTTP/2 stream error."; + case 93: + return "Recursive API call."; + case 94: + return "Authentication error."; + case 95: + return "HTTP/3 error."; + case 96: + return "QUIC connection error."; + case 97: + return "Proxy handshake error."; + case 98: + return "SSL client certificate required."; + case 99: + return "Unrecoverable poll error."; + case 100: + return "Value or data field too large."; + case 101: + return "ECH failed."; default: return SS("Failed with exit code " << exitCode << "."); } @@ -155,7 +240,8 @@ namespace pipedal static void throwCurlExitCode(int exitCode) { - if (exitCode == 0) return; + if (exitCode == 0) + return; std::string message = getCurlExitCodeString(exitCode); throw std::runtime_error(SS("Server download failed. " << message)); } @@ -167,48 +253,57 @@ namespace pipedal throw std::runtime_error("Download failed. Invalid curl response: no headers."); } - const std::string&httpResponse = headers[0]; - // verify that the string starts with HTTP/, and then parse the numeric error code in the result, throwing a std::runtime_error if it doesn't parse. - // e.g. "HTTP/2 200" - if (!httpResponse.starts_with("HTTP/")) - { - throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse)); - } + int returnCode = 0; - size_t codeStart = httpResponse.find(' '); - if (codeStart == std::string::npos) + for (const auto &httpResponse : headers) { - throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse)); - } - ++codeStart; + if (httpResponse.starts_with("HTTP/")) + { + size_t codeStart = httpResponse.find(' '); + if (codeStart == std::string::npos) + { + throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse)); + } + ++codeStart; - size_t codeEnd = httpResponse.find(' ', codeStart); - if (codeEnd == std::string::npos) - { - codeEnd = httpResponse.length(); - } + size_t codeEnd = httpResponse.find(' ', codeStart); + if (codeEnd == std::string::npos) + { + codeEnd = httpResponse.length(); + } - int statusCode = 0; - try - { - statusCode = std::stoi(httpResponse.substr(codeStart, codeEnd - codeStart)); + int statusCode = 0; + try + { + statusCode = std::stoi(httpResponse.substr(codeStart, codeEnd - codeStart)); + if (statusCode >= 200 && statusCode < 300) // a completion code. + { + return statusCode; + } + if (statusCode >= 400) // an error + { + return statusCode; + } + // otherwise it's a redirect of some kind, keep going. + returnCode = statusCode; // return the last one if there's not 200 or 400+ result. + } + catch (const std::exception &) + { + // ignored. Weird. but ignored. + } + } } - catch (const std::exception &) - { - throw std::runtime_error(SS("Download failed. Invalid curl response: " << httpResponse)); - } - - return statusCode; + return returnCode; } - int CurlGet( const std::string &url, std::vector &output, - std::vector *headersOpt - ) { - TemporaryFile tempFile { WEB_TEMP_DIR}; - int rc = CurlGet(url,tempFile.Path(), headersOpt); + std::vector *outputHeadersOpt, + const std::vector *inputHeadersOpt) + { + TemporaryFile tempFile{WEB_TEMP_DIR}; + int rc = CurlGet(url, tempFile.Path(), outputHeadersOpt, inputHeadersOpt); if (rc == 200) { std::ifstream inputStream(tempFile.Path(), std::ios::binary); @@ -226,16 +321,15 @@ namespace pipedal } } return rc; - } int CurlGet( - const std::string&url, + const std::string &url, std::vector &output, - std::vector*headersOpt - ) + std::vector *outputHeadersOpt, + const std::vector *inputHeadersOpt) { - TemporaryFile tempFile {WEB_TEMP_DIR}; - int rc = CurlGet(url,tempFile.Path(), headersOpt); + TemporaryFile tempFile{WEB_TEMP_DIR}; + int rc = CurlGet(url, tempFile.Path(), outputHeadersOpt, inputHeadersOpt); std::ifstream inputStream(tempFile.Path(), std::ios::binary); if (!inputStream) @@ -253,36 +347,48 @@ namespace pipedal output.push_back(line); } return rc; - } - int CurlGet( const std::string &url, - const std::filesystem::path&outputPath, - std::vector *headersOpt - ) + const std::filesystem::path &outputPath, + std::vector *outputHeadersOpt, + const std::vector *inputHeadersOpt) { - TemporaryFile headersFile { WEB_TEMP_DIR}; + TemporaryFile headersFile{WEB_TEMP_DIR}; std::vector defaultHeaders; - if (headersOpt == nullptr) + if (outputHeadersOpt == nullptr) { - headersOpt = &defaultHeaders; + outputHeadersOpt = &defaultHeaders; } bool bResult = true; - std::string args = SS("-s -L -D " << ShellEscape(headersFile.Path().c_str()) << " " << ShellEscape(url) << " -o " << ShellEscape(outputPath.c_str()) ) ; + std::stringstream ssArgs; + + if (inputHeadersOpt) + { + for (const std::string &header : *inputHeadersOpt) + { + ssArgs << "-H " << ShellEscape(header) << " "; + } + } + + ssArgs << "-s -L -D " << ShellEscape(headersFile.Path().c_str()) + << " " << ShellEscape(url) + << " -o " << ShellEscape(outputPath.c_str()); + + std::string args = ssArgs.str(); auto curlOutput = sysExecForOutput("/usr/bin/curl", args); - if (headersOpt != nullptr) + if (outputHeadersOpt != nullptr) { std::ifstream headersStream(headersFile.Path()); if (headersStream) { - headersOpt->clear(); + outputHeadersOpt->clear(); std::string line; while (std::getline(headersStream, line)) { @@ -293,13 +399,13 @@ namespace pipedal } if (!line.empty()) { - headersOpt->push_back(line); + outputHeadersOpt->push_back(line); } } } } - logHeaders(*headersOpt); + logHeaders(*outputHeadersOpt); if (curlOutput.exitCode == 512) { throw std::runtime_error("Invalid curl arguments."); @@ -322,37 +428,36 @@ namespace pipedal } } - - int errorCode = checkCurlHttpResponse(*headersOpt); - + int errorCode = checkCurlHttpResponse(*outputHeadersOpt); return errorCode; } - static int curlExec( - const std::string&commandPath, // path to executable. + const std::string &commandPath, // path to executable. const std::vector &arguments, // commandline arguments. - const std::function &onStdoutLine, - std::string&stdErrOutput - ) + const std::function &onStdoutLine, + std::string &stdErrOutput) { // Unexpected errors throw std::runtime_error. - // create linux socket pairs for stdout and stderr. - int stdoutPipe[2] = {-1,-1}; - int stderrPipe[2] = {-1,-1}; + // create linux socket pairs for stdout and stderr. + int stdoutPipe[2] = {-1, -1}; + int stderrPipe[2] = {-1, -1}; - Finally ffPipes { - [&stdoutPipe, &stderrPipe]() + Finally ffPipes{ + [&stdoutPipe, &stderrPipe]() { - if (stdoutPipe[0] != -1) close(stdoutPipe[0]); - if (stdoutPipe[1] != -1) close(stdoutPipe[1]); - if (stderrPipe[0] != -1) close(stdoutPipe[0]); - if (stderrPipe[1] != -1) close(stdoutPipe[1]); - } - }; - + if (stdoutPipe[0] != -1) + close(stdoutPipe[0]); + if (stdoutPipe[1] != -1) + close(stdoutPipe[1]); + if (stderrPipe[0] != -1) + close(stdoutPipe[0]); + if (stderrPipe[1] != -1) + close(stdoutPipe[1]); + }}; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, stdoutPipe) == -1) { throw std::runtime_error("Failed to create stdout socket pair"); @@ -368,21 +473,20 @@ namespace pipedal { throw std::runtime_error("Failed to fork process"); } - + if (pid == 0) { // Child process close(stdoutPipe[0]); // Close read end close(stderrPipe[0]); // Close read end - + // Redirect stdout and stderr dup2(stdoutPipe[1], STDOUT_FILENO); dup2(stderrPipe[1], STDERR_FILENO); - + close(stdoutPipe[1]); close(stderrPipe[1]); - // stdin to /dev/null. int devNull = open("/dev/null", O_RDONLY); if (devNull == -1) @@ -398,49 +502,48 @@ namespace pipedal } close(devNull); - // Build argv array - std::vector argv; - argv.push_back(const_cast(commandPath.c_str())); - for (const auto& arg : arguments) + std::vector argv; + argv.push_back(const_cast(commandPath.c_str())); + for (const auto &arg : arguments) { - argv.push_back(const_cast(arg.c_str())); + argv.push_back(const_cast(arg.c_str())); } argv.push_back(nullptr); - + execv(commandPath.c_str(), argv.data()); - + // If execv returns, it failed std::cerr << "Failed to execute: " << commandPath << std::endl; exit(EXIT_FAILURE); } - + // Parent process close(stdoutPipe[1]); // Close write end stdoutPipe[1] = -1; close(stderrPipe[1]); // Close write end stderrPipe[1] = -1; - // Read from stderr and stdout. - // stderr output gets appended to the stdErrOutput argument. + // Read from stderr and stdout. + // stderr output gets appended to the stdErrOutput argument. // stdin output gets read line by line, and onStdOutLine is called for each line of text that's read. - + // Set non-blocking mode for both pipes fcntl(stdoutPipe[0], F_SETFL, O_NONBLOCK); fcntl(stderrPipe[0], F_SETFL, O_NONBLOCK); - + std::string stdoutBuffer; std::string stderrBuffer; - + bool stdoutOpen = true; bool stderrOpen = true; - + while (stdoutOpen || stderrOpen) { fd_set readfds; FD_ZERO(&readfds); int maxfd = -1; - + if (stdoutOpen) { FD_SET(stdoutPipe[0], &readfds); @@ -451,13 +554,13 @@ namespace pipedal FD_SET(stderrPipe[0], &readfds); maxfd = std::max(maxfd, stderrPipe[0]); } - + struct timeval timeout; timeout.tv_sec = 1; timeout.tv_usec = 0; - + int ret = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout); - + if (ret > 0) { // Read from stdout @@ -468,7 +571,7 @@ namespace pipedal if (n > 0) { stdoutBuffer.append(buffer, n); - + // Process complete lines size_t pos; while ((pos = stdoutBuffer.find('\n')) != std::string::npos) @@ -491,7 +594,7 @@ namespace pipedal { stdoutOpen = false; close(stdoutPipe[0]); - + // Process any remaining data if (!stdoutBuffer.empty()) { @@ -503,7 +606,7 @@ namespace pipedal { if (!onStdoutLine(stdoutBuffer)) { - // abort the child process + // abort the child process kill(pid, SIGINT); // Curl: SIGINT= graceful shutdown. SIGTERM=abrupt shutdown. // but wait for output conforming the cancellation // i.e. don't waitpid -- that will be done on cleanup, and don't abort the loop. @@ -512,7 +615,7 @@ namespace pipedal } } } - + // Read from stderr if (stderrOpen && FD_ISSET(stderrPipe[0], &readfds)) { @@ -531,14 +634,15 @@ namespace pipedal } } - // Return the procesess's exit code. + // Return the procesess's exit code. int status; waitpid(pid, &status, 0); - + if (WIFEXITED(status)) { auto exitCode = WEXITSTATUS(status); - if (exitCode == EXIT_SUCCESS) { + if (exitCode == EXIT_SUCCESS) + { return EXIT_SUCCESS; } throwCurlExitCode(exitCode); @@ -548,14 +652,13 @@ namespace pipedal { return -1; } - + return -2; } int CurlGet( - const std::vector &request, + const std::vector &request, const std::function &progressCallback, - std::vector*headersOpt - ) + std::vector *outputHeadersOpt) { if (request.empty()) { @@ -565,10 +668,10 @@ namespace pipedal size_t currentDownload = 0; size_t totalDownloads = request.size(); int lastStatusCode = 200; - + // Create temporary file for curl config TemporaryFile configFile{WEB_TEMP_DIR}; - + // Write curl config file with URL and output pairs { std::ofstream configStream(configFile.Path()); @@ -576,24 +679,24 @@ namespace pipedal { throw std::runtime_error("Failed to create curl config file"); } - - for (const auto& req : request) + + for (const auto &req : request) { configStream << "url = \"" << req.url << "\"\n"; configStream << "output = \"" << req.outputFile.string() << "\"\n"; } } - + // Build curl arguments std::vector curlArgs; - curlArgs.push_back("-s"); // Silent mode - curlArgs.push_back("-L"); // Follow redirects + curlArgs.push_back("-s"); // Silent mode + curlArgs.push_back("-L"); // Follow redirects // curlArgs.push_back("-w"); // Write format - // curlArgs.push_back("URL: %{url_effective}\\n"); + // curlArgs.push_back("URL: %{url_effective}\\n"); curlArgs.push_back("--parallel-max"); curlArgs.push_back("1"); - curlArgs.push_back("-D"); // Dump headers to stdout - curlArgs.push_back("-"); // Write headers to stdout + curlArgs.push_back("-D"); // Dump headers to stdout + curlArgs.push_back("-"); // Write headers to stdout curlArgs.push_back("--no-progress-meter"); // Suppress progress curlArgs.push_back("--retry-delay"); curlArgs.push_back("2"); @@ -602,16 +705,17 @@ namespace pipedal curlArgs.push_back("-K"); curlArgs.push_back(configFile.Path().string()); curlArgs.push_back("--fail-early"); // quit as soon as one transfer fails. - + std::string stderrOutput; std::string savedHttpHeader; - + // Process stdout lines - auto onStdoutLine = [&](const std::string& line) -> bool { - if (headersOpt != nullptr) + auto onStdoutLine = [&](const std::string &line) -> bool + { + if (outputHeadersOpt != nullptr) { - headersOpt->push_back(line); + outputHeadersOpt->push_back(line); } // Empty line indicates start of new download or end of headers if (line.empty()) @@ -630,12 +734,12 @@ namespace pipedal { codeEnd = lastHeader.length(); } - + std::string codeStr = lastHeader.substr(codeStart, codeEnd - codeStart); try { int statusCode = std::stoi(codeStr); - + // Handle different status codes if (statusCode == 200) { @@ -656,8 +760,8 @@ namespace pipedal { // Throttling - save position and expect exit lastStatusCode = 503; - Lv2Log::warning("%s","curl: Received 503 response."); - return false; + Lv2Log::warning("%s", "curl: Received 503 response."); + return false; } else if (statusCode >= 300 && statusCode < 400) { @@ -671,14 +775,15 @@ namespace pipedal return false; // Stop processing } } - catch (const std::exception&) + catch (const std::exception &) { // Failed to parse status code - continue return true; } } } - } else if (line.starts_with("HTTP/")) + } + else if (line.starts_with("HTTP/")) { // Delay processing until we get a blank line. savedHttpHeader = line; @@ -686,21 +791,137 @@ namespace pipedal // Other lines are headers - ignore unless we need to save them return true; }; - + // Execute curl int exitCode = curlExec("/usr/bin/curl", curlArgs, onStdoutLine, stderrOutput); - logHeaders(*headersOpt); + logHeaders(*outputHeadersOpt); if (exitCode != EXIT_SUCCESS && exitCode != -1) { throwCurlExitCode(exitCode); } - + // Return appropriate status code - + return lastStatusCode; } -} + int CurlPostFile( + const std::string &url, + const std::filesystem::path &inputPath, + const std::filesystem::path &outputPath, + std::vector *outputHeadersOpt, + std::vector *inputHeadersOpt) + { + TemporaryFile headersFile{WEB_TEMP_DIR}; + std::vector defaultHeaders; + if (outputHeadersOpt == nullptr) + { + outputHeadersOpt = &defaultHeaders; + } + + bool bResult = true; + + std::stringstream ssArgs; + + if (inputHeadersOpt) + { + for (const std::string &header : *inputHeadersOpt) + { + ssArgs << "-H " << ShellEscape(header) << " "; + } + } + ssArgs << "-s -L -X POST --data-binary " + << SS("@" << inputPath.c_str()).c_str() + << " -D " << ShellEscape(headersFile.Path().c_str()); + + std::string args = ssArgs.str(); + + + args += SS(" " << ShellEscape(url) + << " -o " << ShellEscape(outputPath.c_str())); + + auto curlOutput = sysExecForOutput("/usr/bin/curl", args); + + if (outputHeadersOpt != nullptr) + { + std::ifstream headersStream(headersFile.Path()); + if (headersStream) + { + outputHeadersOpt->clear(); + std::string line; + while (std::getline(headersStream, line)) + { + // Remove trailing carriage return if present + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + if (!line.empty()) + { + outputHeadersOpt->push_back(line); + } + } + } + } + + logHeaders(*outputHeadersOpt); + if (curlOutput.exitCode == 512) + { + throw std::runtime_error("Invalid curl arguments."); + } + if (curlOutput.exitCode != EXIT_SUCCESS && curlOutput.exitCode != 22 * 256) + { + if (WIFEXITED(curlOutput.exitCode)) + { + auto exitCode = WEXITSTATUS(curlOutput.exitCode); + throwCurlExitCode(exitCode); + return -99; + } + else if (WIFSIGNALED(curlOutput.exitCode)) + { + throw std::runtime_error("No internet access."); + } + else + { + throw std::runtime_error(SS("Unable to exec curl. (exit status = " << curlOutput.exitCode << ")")); + } + } + + int errorCode = checkCurlHttpResponse(*outputHeadersOpt); + + return errorCode; + } + + int CurlPostStrings( + const std::string &url, + const std::string &body, + std::string &outputBody, + std::vector *outputHeadersOpt, + std::vector *inputHeadersOpt) + { + TemporaryFile inputFile(WEB_TEMP_DIR); + { + std::ofstream f{inputFile.Path()}; + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open file " << inputFile.Path())); + } + f << body; + } + TemporaryFile outputFile(WEB_TEMP_DIR); + int result = CurlPostFile(url, inputFile.Path(), outputFile.Path(), outputHeadersOpt, inputHeadersOpt); + + std::ifstream outputStream(outputFile.Path()); + if (outputStream) + { + outputBody.assign( + std::istreambuf_iterator(outputStream), + std::istreambuf_iterator()); + } + + return result; + } +} diff --git a/src/Curl.hpp b/src/Curl.hpp index a1aec8e..922d3b2 100644 --- a/src/Curl.hpp +++ b/src/Curl.hpp @@ -34,17 +34,21 @@ namespace pipedal { extern int CurlGet( const std::string &url, const std::filesystem::path&outputFile, - std::vector *headersOpt = nullptr + std::vector *outputHeadersOpt = nullptr, + const std::vector *inputHeadersOpt = nullptr ); extern int CurlGet( const std::string &url, std::vector &output, - std::vector *headersOpt = nullptr + std::vector *outputHeadersOpt = nullptr, + const std::vector *inputHeadersOpt = nullptr + ); extern int CurlGet( const std::string&url, std::vector &output, - std::vector *headersOpt = nullptr + std::vector *outputHeadersOpt = nullptr, + const std::vector *inputHeadersOpt = nullptr ); struct CurlDownloadRequest{ @@ -54,7 +58,24 @@ namespace pipedal { extern int CurlGet( const std::vector &request, const std::function &progressCallback, - std::vector*headersOpt + std::vector*outputHeadersOpt ); + + extern int CurlPostFile( + const std::string &url, + const std::filesystem::path&inputFile, + const std::filesystem::path&outputFile, + std::vector *outputHeadersOpt = nullptr, + std::vector *inputHeadersOpt = nullptr + ); + extern int CurlPostStrings( + const std::string &url, + const std::string&body, + std::string&responseBody, + std::vector *outputHeadersOpt = nullptr, + std::vector *inputHeadersOpt = nullptr + ); + + } \ No newline at end of file diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 838bcfb..f7834a0 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -238,16 +238,17 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) } int64_t PiPedalModel::DownloadModelsFromTone3000( - int64_t clientId, - Tone3000DownloadType downloadType, + const std::string &uri, + const Tone3000PkceParams&pckeParams, const std::string &downloadPath, - const std::string &tone3000Url) + Tone3000DownloadType downloadType +) { std::lock_guard lock(mutex); // Validate the download path std::filesystem::path path{downloadPath}; - if (!IsInUploadsDirectory(downloadPath)) + if (!IsInUploadsDirectory(path)) { throw PiPedalException("Invalid path: not in uploads directory."); } @@ -266,10 +267,12 @@ int64_t PiPedalModel::DownloadModelsFromTone3000( tone3000Downloader = Tone3000Downloader::Create(); tone3000Downloader->SetListener(this); } - return tone3000Downloader->RequestDownload( - downloadType, + return tone3000Downloader->RequestTone3000Download( + uri, + pckeParams, downloadPath, - tone3000Url); + downloadType + ); } void PiPedalModel::CancelTone3000Download( @@ -3447,3 +3450,5 @@ std::string PiPedalModel::Tone3000ThumbnailDirectory() { return "/var/pipedal/tone3000_thumbnails"; } + + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 107bb7e..b24cfd6 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -43,6 +43,7 @@ #include "ChannelRouterSettings.hpp" #include #include "Tone3000Downloader.hpp" +#include "Uri.hpp" namespace pipedal { @@ -310,10 +311,11 @@ namespace pipedal void PreviousPreset() { NextPreset(Direction::Decrease); } int64_t DownloadModelsFromTone3000( - int64_t clientId, - Tone3000DownloadType downloadType, + const std::string&responseuri, + const Tone3000PkceParams& pkce, const std::string &downloadPath, - const std::string &tone3000Url); + Tone3000DownloadType downloadType + ); void CancelTone3000Download( int64_t clientId, diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 9d6457b..6bf91c4 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -35,6 +35,7 @@ #include "Ipv6Helpers.hpp" #include "Promise.hpp" #include +#include "Tone3000Downloader.hpp" #include "AdminClient.hpp" #include "WifiConfigSettings.hpp" @@ -44,26 +45,14 @@ #include "PiPedalAlsa.hpp" #include #include "FileEntry.hpp" +#include "Tone3000Downloader.hpp" using namespace std; using namespace pipedal; -class DownloadModelsFromTone3000Body { +class CopyPresetsToBankBody +{ public: - std::string downloadPath_; - std::string tone3000Url_; - std::string downloadType_; - DECLARE_JSON_MAP(DownloadModelsFromTone3000Body); -}; -JSON_MAP_BEGIN(DownloadModelsFromTone3000Body) -JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadPath) -JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, tone3000Url) -JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadType) -JSON_MAP_END() - - -class CopyPresetsToBankBody { - public: int64_t bankInstanceId_; std::vector presets_; DECLARE_JSON_MAP(CopyPresetsToBankBody); @@ -71,11 +60,32 @@ class CopyPresetsToBankBody { JSON_MAP_BEGIN(CopyPresetsToBankBody) JSON_MAP_REFERENCE(CopyPresetsToBankBody, bankInstanceId) JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets) -JSON_MAP_END() +JSON_MAP_END(); +class DownloadModelsFromTone3000Body +{ +public: + std::string responseUri_; + Tone3000PkceParams tone3000PckceParams_; + std::string downloadPath_; + int64_t downloadType_ = -1; -class ImportPresetsFromBankBody { - public: + Tone3000DownloadType downloadType() const { return (Tone3000DownloadType)downloadType_; } + void downloadType(Tone3000DownloadType value) { downloadType_ = (int64_t)value; } + + DECLARE_JSON_MAP(DownloadModelsFromTone3000Body); +}; + +JSON_MAP_BEGIN(DownloadModelsFromTone3000Body) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, responseUri) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, tone3000PckceParams) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadPath) +JSON_MAP_REFERENCE(DownloadModelsFromTone3000Body, downloadType) +JSON_MAP_END(); + +class ImportPresetsFromBankBody +{ +public: int64_t bankInstanceId_; std::vector presets_; DECLARE_JSON_MAP(ImportPresetsFromBankBody); @@ -85,7 +95,8 @@ JSON_MAP_REFERENCE(ImportPresetsFromBankBody, bankInstanceId) JSON_MAP_REFERENCE(ImportPresetsFromBankBody, presets) JSON_MAP_END() -class RequestBankPresetsBody { +class RequestBankPresetsBody +{ public: int64_t bankInstanceId_; DECLARE_JSON_MAP(RequestBankPresetsBody); @@ -94,21 +105,8 @@ JSON_MAP_BEGIN(RequestBankPresetsBody) JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId) JSON_MAP_END() - -class DownloadTone3000Body { -public: - int64_t handle_; - std::string tone3000DownloadUrl_; - - DECLARE_JSON_MAP(DownloadTone3000Body); -}; - -JSON_MAP_BEGIN(DownloadTone3000Body) -JSON_MAP_REFERENCE(DownloadTone3000Body, handle) -JSON_MAP_REFERENCE(DownloadTone3000Body, tone3000DownloadUrl) -JSON_MAP_END() - -class Tone3000DownloadStartedBody { +class Tone3000DownloadStartedBody +{ public: int64_t handle_; std::string title_; @@ -121,7 +119,8 @@ JSON_MAP_REFERENCE(Tone3000DownloadStartedBody, handle) JSON_MAP_REFERENCE(Tone3000DownloadStartedBody, title) JSON_MAP_END() -class Tone3000DownloadErrorBody { +class Tone3000DownloadErrorBody +{ public: int64_t handle_; std::string errorMessage_; @@ -134,7 +133,6 @@ JSON_MAP_REFERENCE(Tone3000DownloadErrorBody, handle) JSON_MAP_REFERENCE(Tone3000DownloadErrorBody, errorMessage) JSON_MAP_END() - class PathPatchPropertyChangedBody { public: @@ -233,7 +231,7 @@ public: JSON_MAP_BEGIN(GetFilePropertyDirectoryTreeArgs) JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, fileProperty) JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, selectedPath) -JSON_MAP_END() +JSON_MAP_END(); class Lv2StateChangedBody { @@ -246,7 +244,7 @@ public: JSON_MAP_BEGIN(Lv2StateChangedBody) JSON_MAP_REFERENCE(Lv2StateChangedBody, instanceId) JSON_MAP_REFERENCE(Lv2StateChangedBody, state) -JSON_MAP_END() +JSON_MAP_END(); class SetPatchPropertyBody { @@ -278,9 +276,6 @@ JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, colorKey) JSON_MAP_END() - - - class NotifyMidiListenerBody { public: @@ -312,7 +307,7 @@ JSON_MAP_REFERENCE(NotifyAtomOutputBody, clientHandle) JSON_MAP_REFERENCE(NotifyAtomOutputBody, instanceId) JSON_MAP_REFERENCE(NotifyAtomOutputBody, propertyUri) JSON_MAP_REFERENCE(NotifyAtomOutputBody, atomJson) -JSON_MAP_END() +JSON_MAP_END(); class ListenForMidiEventBody { @@ -463,7 +458,6 @@ JSON_MAP_REFERENCE(RenameBankBody, bankId) JSON_MAP_REFERENCE(RenameBankBody, newName) JSON_MAP_END() - class RenamePresetBody { public: @@ -537,7 +531,6 @@ JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, instanceId) JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, useModUi) JSON_MAP_END() - class UpdateCurrentPedalboardBody { public: @@ -580,7 +573,6 @@ JSON_MAP_REFERENCE(SetSelectedPedalboardPluginBody, clientId) JSON_MAP_REFERENCE(SetSelectedPedalboardPluginBody, pluginInstanceId) JSON_MAP_END() - class SnapshotModifiedBody { public: @@ -599,11 +591,9 @@ class ChannelRouterSettingsChangedBody { public: ChannelRouterSettingsChangedBody( - int64_t clientId, - const ChannelRouterSettings &channelRouterSettings - ): clientId_(clientId), channelRouterSettings_(channelRouterSettings) + int64_t clientId, + const ChannelRouterSettings &channelRouterSettings) : clientId_(clientId), channelRouterSettings_(channelRouterSettings) { - } int64_t clientId_ = -1; ChannelRouterSettings channelRouterSettings_; @@ -684,12 +674,12 @@ JSON_MAP_REFERENCE(Vst3ControlChangedBody, state) JSON_MAP_END() class PiPedalSocketHandler; -namespace { +namespace +{ using PfnMessageHandler = void (PiPedalSocketHandler::*)(int replyTo, json_reader *pReader); inline static unordered_map socket_messageHandlers; - } class PiPedalSocketHandler : public SocketHandler, public IPiPedalModelSubscriber, public std::enable_shared_from_this @@ -1171,176 +1161,179 @@ public: /***********************/ - class MessageRegistration { + class MessageRegistration + { public: - MessageRegistration(const std::string&messageName,PfnMessageHandler pfnMessageHandler ) { + MessageRegistration(const std::string &messageName, PfnMessageHandler pfnMessageHandler) + { socket_messageHandlers[messageName] = pfnMessageHandler; } }; - #define REGISTER_MESSAGE_HANDLER(MESSAGE_NAME) \ - static inline MessageRegistration r_##MESSAGE_NAME{#MESSAGE_NAME,&PiPedalSocketHandler::handle_##MESSAGE_NAME}; +#define REGISTER_MESSAGE_HANDLER(MESSAGE_NAME) \ + static inline MessageRegistration r_##MESSAGE_NAME{#MESSAGE_NAME, &PiPedalSocketHandler::handle_##MESSAGE_NAME}; - void handle_setControl(int replyTo, json_reader*pReader) { + void handle_setControl(int replyTo, json_reader *pReader) + { ControlChangedBody message; pReader->read(&message); this->model.SetControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } REGISTER_MESSAGE_HANDLER(setControl) - void handle_previewControl(int replyTo, json_reader*pReader) { + void handle_previewControl(int replyTo, json_reader *pReader) + { ControlChangedBody message; pReader->read(&message); this->model.PreviewControl(message.clientId_, message.instanceId_, message.symbol_, message.value_); - } REGISTER_MESSAGE_HANDLER(previewControl) - void handle_setInputVolume(int replyTo, json_reader*pReader) { + void handle_setInputVolume(int replyTo, json_reader *pReader) + { float value; pReader->read(&value); this->model.SetInputVolume(value); - } REGISTER_MESSAGE_HANDLER(setInputVolume) - void handle_setOutputVolume(int replyTo, json_reader*pReader) { + void handle_setOutputVolume(int replyTo, json_reader *pReader) + { float value; pReader->read(&value); this->model.SetOutputVolume(value); - } REGISTER_MESSAGE_HANDLER(setOutputVolume) - void handle_previewInputVolume(int replyTo, json_reader*pReader) { + void handle_previewInputVolume(int replyTo, json_reader *pReader) + { float value; pReader->read(&value); this->model.PreviewInputVolume(value); - } REGISTER_MESSAGE_HANDLER(previewInputVolume) - void handle_previewOutputVolume(int replyTo, json_reader*pReader) { + void handle_previewOutputVolume(int replyTo, json_reader *pReader) + { float value; pReader->read(&value); this->model.PreviewOutputVolume(value); - } REGISTER_MESSAGE_HANDLER(previewOutputVolume) - void handle_listenForMidiEvent(int replyTo, json_reader*pReader) { + void handle_listenForMidiEvent(int replyTo, json_reader *pReader) + { ListenForMidiEventBody body; pReader->read(&body); this->model.ListenForMidiEvent(this->clientId, body.handle_); - } REGISTER_MESSAGE_HANDLER(listenForMidiEvent) - void handle_cancelListenForMidiEvent(int replyTo, json_reader*pReader) { + void handle_cancelListenForMidiEvent(int replyTo, json_reader *pReader) + { uint64_t handle; pReader->read(&handle); this->model.CancelListenForMidiEvent(this->clientId, handle); - } REGISTER_MESSAGE_HANDLER(cancelListenForMidiEvent) - void handle_monitorPatchProperty(int replyTo, json_reader*pReader) { + void handle_monitorPatchProperty(int replyTo, json_reader *pReader) + { MonitorPatchPropertyBody body; pReader->read(&body); this->model.MonitorPatchProperty(this->clientId, body.clientHandle_, body.instanceId_, body.propertyUri_); - } REGISTER_MESSAGE_HANDLER(monitorPatchProperty) - void handle_cancelMonitorPatchProperty(int replyTo, json_reader*pReader) { + void handle_cancelMonitorPatchProperty(int replyTo, json_reader *pReader) + { int64_t handle; pReader->read(&handle); this->model.CancelMonitorPatchProperty(this->clientId, handle); - } REGISTER_MESSAGE_HANDLER(cancelMonitorPatchProperty) - void handle_getUpdateStatus(int replyTo, json_reader*pReader) { + void handle_getUpdateStatus(int replyTo, json_reader *pReader) + { UpdateStatus updateStatus = model.GetUpdateStatus(); this->Reply(replyTo, "getUpdateStatus", updateStatus); - } REGISTER_MESSAGE_HANDLER(getUpdateStatus) - void handle_getHasWifi(int replyTo, json_reader*pReader) { + void handle_getHasWifi(int replyTo, json_reader *pReader) + { bool result = model.GetHasWifi(); this->Reply(replyTo, "getHasWifi", result); - } REGISTER_MESSAGE_HANDLER(getHasWifi) - void handle_updateNow(int replyTo, json_reader*pReader) { + void handle_updateNow(int replyTo, json_reader *pReader) + { std::string updateUrl; pReader->read(&updateUrl); model.UpdateNow(updateUrl); bool result = true; this->Reply(replyTo, "updateNow", result); - } REGISTER_MESSAGE_HANDLER(updateNow) - void handle_getJackStatus(int replyTo, json_reader*pReader) { + void handle_getJackStatus(int replyTo, json_reader *pReader) + { JackHostStatus status = model.GetJackStatus(); this->Reply(replyTo, "getJackStatus", status); - } REGISTER_MESSAGE_HANDLER(getJackStatus) - void handle_getAlsaDevices(int replyTo, json_reader*pReader) { + void handle_getAlsaDevices(int replyTo, json_reader *pReader) + { std::vector devices = model.GetAlsaDevices(); this->Reply(replyTo, "getAlsaDevices", devices); - } REGISTER_MESSAGE_HANDLER(getAlsaDevices) - void handle_getKnownWifiNetworks(int replyTo, json_reader*pReader) { + void handle_getKnownWifiNetworks(int replyTo, json_reader *pReader) + { std::vector channels = this->model.GetKnownWifiNetworks(); this->Reply(replyTo, "getWifiChannels", channels); - } REGISTER_MESSAGE_HANDLER(getKnownWifiNetworks) - void handle_getWifiChannels(int replyTo, json_reader*pReader) { + void handle_getWifiChannels(int replyTo, json_reader *pReader) + { std::string country; pReader->read(&country); std::vector channels = pipedal::getWifiChannelSelectors(country.c_str()); this->Reply(replyTo, "getWifiChannels", channels); - } REGISTER_MESSAGE_HANDLER(getWifiChannels) - void handle_getPluginPresets(int replyTo, json_reader*pReader) { + void handle_getPluginPresets(int replyTo, json_reader *pReader) + { std::string uri; pReader->read(&uri); this->Reply(replyTo, "getPluginPresets", this->model.GetPluginUiPresets(uri)); - } REGISTER_MESSAGE_HANDLER(getPluginPresets) - void handle_loadPluginPreset(int replyTo, json_reader*pReader) { + void handle_loadPluginPreset(int replyTo, json_reader *pReader) + { LoadPluginPresetBody body; pReader->read(&body); this->model.LoadPluginPreset(body.pluginInstanceId_, body.presetInstanceId_); - } REGISTER_MESSAGE_HANDLER(loadPluginPreset) - void handle_setJackServerSettings(int replyTo, json_reader*pReader) { + void handle_setJackServerSettings(int replyTo, json_reader *pReader) + { JackServerSettings jackServerSettings; pReader->read(&jackServerSettings); this->model.SetJackServerSettings(jackServerSettings); this->Reply(replyTo, "setJackserverSettings"); - } REGISTER_MESSAGE_HANDLER(setJackServerSettings) - void handle_setGovernorSettings(int replyTo, json_reader*pReader) { + void handle_setGovernorSettings(int replyTo, json_reader *pReader) + { std::string governor; pReader->read(&governor); std::string fromAddress = this->getFromAddress(); @@ -1350,11 +1343,11 @@ public: // } this->model.SetGovernorSettings(governor); this->Reply(replyTo, "setGovernorSettings"); - } REGISTER_MESSAGE_HANDLER(setGovernorSettings) - void handle_setWifiConfigSettings(int replyTo, json_reader*pReader) { + void handle_setWifiConfigSettings(int replyTo, json_reader *pReader) + { WifiConfigSettings wifiConfigSettings; pReader->read(&wifiConfigSettings); if (!GetAdminClient().CanUseAdminClient()) @@ -1369,17 +1362,17 @@ public: this->model.SetWifiConfigSettings(wifiConfigSettings); this->Reply(replyTo, "setWifiConfigSettings"); - } REGISTER_MESSAGE_HANDLER(setWifiConfigSettings) - void handle_getWifiConfigSettings(int replyTo, json_reader*pReader) { + void handle_getWifiConfigSettings(int replyTo, json_reader *pReader) + { this->Reply(replyTo, "getWifiConfigSettings", model.GetWifiConfigSettings()); - } REGISTER_MESSAGE_HANDLER(getWifiConfigSettings) - void handle_setWifiDirectConfigSettings(int replyTo, json_reader*pReader) { + void handle_setWifiDirectConfigSettings(int replyTo, json_reader *pReader) + { WifiDirectConfigSettings wifiDirectConfigSettings; pReader->read(&wifiDirectConfigSettings); if (!GetAdminClient().CanUseAdminClient()) @@ -1394,248 +1387,248 @@ public: this->model.SetWifiDirectConfigSettings(wifiDirectConfigSettings); this->Reply(replyTo, "setWifiDirectConfigSettings"); - } REGISTER_MESSAGE_HANDLER(setWifiDirectConfigSettings) - void handle_getWifiDirectConfigSettings(int replyTo, json_reader*pReader) { + void handle_getWifiDirectConfigSettings(int replyTo, json_reader *pReader) + { this->Reply(replyTo, "getWifiDirectConfigSettings", model.GetWifiDirectConfigSettings()); - } REGISTER_MESSAGE_HANDLER(getWifiDirectConfigSettings) - void handle_getGovernorSettings(int replyTo, json_reader*pReader) { + void handle_getGovernorSettings(int replyTo, json_reader *pReader) + { this->Reply(replyTo, "getGovernorSettings", model.GetGovernorSettings()); - } REGISTER_MESSAGE_HANDLER(getGovernorSettings) - void handle_getJackServerSettings(int replyTo, json_reader*pReader) { + void handle_getJackServerSettings(int replyTo, json_reader *pReader) + { this->Reply(replyTo, "getJackServerSettings", model.GetJackServerSettings()); - } REGISTER_MESSAGE_HANDLER(getJackServerSettings) - void handle_getBankIndex(int replyTo, json_reader*pReader) { + void handle_getBankIndex(int replyTo, json_reader *pReader) + { BankIndex bankIndex = model.GetBankIndex(); this->Reply(replyTo, "getBankIndex", bankIndex); - } REGISTER_MESSAGE_HANDLER(getBankIndex) - void handle_getJackConfiguration(int replyTo, json_reader*pReader) { + void handle_getJackConfiguration(int replyTo, json_reader *pReader) + { JackConfiguration configuration = this->model.GetJackConfiguration(); this->Reply(replyTo, "getJackConfiguration", configuration); - } REGISTER_MESSAGE_HANDLER(getJackConfiguration) - void handle_getJackSettings(int replyTo, json_reader*pReader) { + void handle_getJackSettings(int replyTo, json_reader *pReader) + { JackChannelSelection selection = this->model.GetJackChannelSelection(); this->Reply(replyTo, "getJackSettings", selection); - } REGISTER_MESSAGE_HANDLER(getJackSettings) - void handle_saveCurrentPreset(int replyTo, json_reader*pReader) { + void handle_saveCurrentPreset(int replyTo, json_reader *pReader) + { this->model.SaveCurrentPreset(this->clientId); - } REGISTER_MESSAGE_HANDLER(saveCurrentPreset) - void handle_saveCurrentPresetAs(int replyTo, json_reader*pReader) { + void handle_saveCurrentPresetAs(int replyTo, json_reader *pReader) + { SaveCurrentPresetAsBody body; pReader->read(&body); - int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.bankInstanceId_,body.name_, body.saveAfterInstanceId_); + int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.bankInstanceId_, body.name_, body.saveAfterInstanceId_); Reply(replyTo, "saveCurrentPresetsAs", result); - } REGISTER_MESSAGE_HANDLER(saveCurrentPresetAs) - void handle_setSelectedPedalboardPlugin(int replyTo, json_reader*pReader) { + void handle_setSelectedPedalboardPlugin(int replyTo, json_reader *pReader) + { SetSelectedPedalboardPluginBody body; pReader->read(&body); - this->model.SetSelectedPedalboardPlugin(body.clientId_,body.pluginInstanceId_); - + this->model.SetSelectedPedalboardPlugin(body.clientId_, body.pluginInstanceId_); } REGISTER_MESSAGE_HANDLER(setSelectedPedalboardPlugin) - void handle_savePluginPresetAs(int replyTo, json_reader*pReader) { + void handle_savePluginPresetAs(int replyTo, json_reader *pReader) + { SavePluginPresetAsBody body; pReader->read(&body); int64_t result = this->model.SavePluginPresetAs(body.instanceId_, body.name_); Reply(replyTo, "saveCurrentPresetsAs", result); - } REGISTER_MESSAGE_HANDLER(savePluginPresetAs) - void handle_getPresets(int replyTo, json_reader*pReader) { + void handle_getPresets(int replyTo, json_reader *pReader) + { PresetIndex presets; this->model.GetPresets(&presets); Reply(replyTo, "getPresets", presets); - } REGISTER_MESSAGE_HANDLER(getPresets) - void handle_setPedalboardItemEnable(int replyTo, json_reader*pReader) { + void handle_setPedalboardItemEnable(int replyTo, json_reader *pReader) + { PedalboardItemEnabledBody body; pReader->read(&body); model.SetPedalboardItemEnable(body.clientId_, body.instanceId_, body.enabled_); - } REGISTER_MESSAGE_HANDLER(setPedalboardItemEnable) - void handle_setPedalboardItemUseModUi(int replyTo, json_reader*pReader) { + void handle_setPedalboardItemUseModUi(int replyTo, json_reader *pReader) + { PedalboardItemUseModGuiBody body; pReader->read(&body); model.SetPedalboardItemUseModUi(body.clientId_, body.instanceId_, body.useModUi_); - } REGISTER_MESSAGE_HANDLER(setPedalboardItemUseModUi) - void handle_updateCurrentPedalboard(int replyTo, json_reader*pReader) { + void handle_updateCurrentPedalboard(int replyTo, json_reader *pReader) + { UpdateCurrentPedalboardBody body; pReader->read(&body); this->model.UpdateCurrentPedalboard(body.clientId_, body.pedalboard_); - } REGISTER_MESSAGE_HANDLER(updateCurrentPedalboard) - void handle_setSnapshot(int replyTo, json_reader*pReader) { + void handle_setSnapshot(int replyTo, json_reader *pReader) + { int64_t snapshotIndex = -1; pReader->read(&snapshotIndex); this->model.SetSnapshot(snapshotIndex); - } REGISTER_MESSAGE_HANDLER(setSnapshot) - void handle_setSnapshots(int replyTo, json_reader*pReader) { + void handle_setSnapshots(int replyTo, json_reader *pReader) + { SetSnapshotsBody body; pReader->read(&body); this->model.SetSnapshots(body.snapshots_, body.selectedSnapshot_); - } REGISTER_MESSAGE_HANDLER(setSnapshots) - void handle_currentPedalboard(int replyTo, json_reader*pReader) { + void handle_currentPedalboard(int replyTo, json_reader *pReader) + { auto pedalboard = model.GetCurrentPedalboardCopy(); Reply(replyTo, "currentPedalboard", pedalboard); - } REGISTER_MESSAGE_HANDLER(currentPedalboard) - void handle_plugins(int replyTo, json_reader*pReader) { + void handle_plugins(int replyTo, json_reader *pReader) + { auto ui_plugins = model.GetPluginHost().GetUiPlugins(); Reply(replyTo, "plugins", ui_plugins); - } REGISTER_MESSAGE_HANDLER(plugins) - void handle_pluginClasses(int replyTo, json_reader*pReader) { + void handle_pluginClasses(int replyTo, json_reader *pReader) + { auto classes = model.GetPluginHost().GetLv2PluginClass(); Reply(replyTo, "pluginClasses", classes); - } REGISTER_MESSAGE_HANDLER(pluginClasses) - void handle_hello(int replyTo, json_reader*pReader) { + void handle_hello(int replyTo, json_reader *pReader) + { this->model.AddNotificationSubscription(shared_from_this()); Reply(replyTo, "ehlo", clientId); - } REGISTER_MESSAGE_HANDLER(hello) - void handle_setShowStatusMonitor(int replyTo, json_reader*pReader) { + void handle_setShowStatusMonitor(int replyTo, json_reader *pReader) + { bool showStatusMonitor; pReader->read(&showStatusMonitor); this->model.SetShowStatusMonitor(showStatusMonitor); - } REGISTER_MESSAGE_HANDLER(setShowStatusMonitor) - void handle_getShowStatusMonitor(int replyTo, json_reader*pReader) { + void handle_getShowStatusMonitor(int replyTo, json_reader *pReader) + { Reply(replyTo, "getShowStatusMonitor", this->model.GetShowStatusMonitor()); - } REGISTER_MESSAGE_HANDLER(getShowStatusMonitor) - void handle_version(int replyTo, json_reader*pReader) { + void handle_version(int replyTo, json_reader *pReader) + { PiPedalVersion version(this->model); Reply(replyTo, "version", version); - } REGISTER_MESSAGE_HANDLER(version) - void handle_loadPreset(int replyTo, json_reader*pReader) { + void handle_loadPreset(int replyTo, json_reader *pReader) + { int64_t instanceId = 0; pReader->read(&instanceId); model.LoadPreset(this->clientId, instanceId); - } REGISTER_MESSAGE_HANDLER(loadPreset) - void handle_updatePresets(int replyTo, json_reader*pReader) { + void handle_updatePresets(int replyTo, json_reader *pReader) + { PresetIndex newIndex; pReader->read(&newIndex); bool result = model.UpdatePresets(this->clientId, newIndex); this->Reply(replyTo, "updatePresets", result); - } REGISTER_MESSAGE_HANDLER(updatePresets) - void handle_updatePluginPresets(int replyTo, json_reader*pReader) { + void handle_updatePluginPresets(int replyTo, json_reader *pReader) + { PluginUiPresets pluginPresets; pReader->read(&pluginPresets); model.UpdatePluginPresets(pluginPresets); this->Reply(replyTo, "updatePluginPresets", true); - } REGISTER_MESSAGE_HANDLER(updatePluginPresets) - void handle_moveBank(int replyTo, json_reader*pReader) { + void handle_moveBank(int replyTo, json_reader *pReader) + { FromToBody body; pReader->read(&body); model.MoveBank(this->clientId, body.from_, body.to_); this->Reply(replyTo, "moveBank"); - } REGISTER_MESSAGE_HANDLER(moveBank) - void handle_shutdown(int replyTo, json_reader*pReader) { + void handle_shutdown(int replyTo, json_reader *pReader) + { model.RequestShutdown(false); this->Reply(replyTo, "shutdown"); - } REGISTER_MESSAGE_HANDLER(shutdown) - void handle_restart(int replyTo, json_reader*pReader) { + void handle_restart(int replyTo, json_reader *pReader) + { model.RequestShutdown(true); this->Reply(replyTo, "restart"); - } REGISTER_MESSAGE_HANDLER(restart) - void handle_deletePresetItems(int replyTo, json_reader*pReader) { + void handle_deletePresetItems(int replyTo, json_reader *pReader) + { std::vector items; pReader->read(&items); int64_t result = model.DeletePresets(this->clientId, items); this->Reply(replyTo, "deletePresetItems", result); - } REGISTER_MESSAGE_HANDLER(deletePresetItems) - void handle_deleteBankItem(int replyTo, json_reader*pReader) { + void handle_deleteBankItem(int replyTo, json_reader *pReader) + { int64_t instanceId = 0; pReader->read(&instanceId); uint64_t result = model.DeleteBank(this->clientId, instanceId); this->Reply(replyTo, "deleteBankItem", result); - } REGISTER_MESSAGE_HANDLER(deleteBankItem) - void handle_renameBank(int replyTo, json_reader*pReader) { + void handle_renameBank(int replyTo, json_reader *pReader) + { RenameBankBody body; pReader->read(&body); @@ -1659,11 +1652,11 @@ public: { this->SendError(replyTo, std::string(e.what())); } - } REGISTER_MESSAGE_HANDLER(renameBank) - void handle_openBank(int replyTo, json_reader*pReader) { + void handle_openBank(int replyTo, json_reader *pReader) + { int64_t bankId = -1; pReader->read(&bankId); try @@ -1676,11 +1669,11 @@ public: { this->SendError(replyTo, std::string(e.what())); } - } REGISTER_MESSAGE_HANDLER(openBank) - void handle_saveBankAs(int replyTo, json_reader*pReader) { + void handle_saveBankAs(int replyTo, json_reader *pReader) + { RenameBankBody body; pReader->read(&body); try @@ -1692,81 +1685,81 @@ public: { this->SendError(replyTo, std::string(e.what())); } - } REGISTER_MESSAGE_HANDLER(saveBankAs) - void handle_nextBank(int replyTo, json_reader*pReader) { + void handle_nextBank(int replyTo, json_reader *pReader) + { model.NextBank(); - } REGISTER_MESSAGE_HANDLER(nextBank) - void handle_previousBank(int replyTo, json_reader*pReader) { + void handle_previousBank(int replyTo, json_reader *pReader) + { model.PreviousBank(); - } REGISTER_MESSAGE_HANDLER(previousBank) - void handle_nextPreset(int replyTo, json_reader*pReader) { + void handle_nextPreset(int replyTo, json_reader *pReader) + { model.NextPreset(); - } REGISTER_MESSAGE_HANDLER(nextPreset) - void handle_previousPreset(int replyTo, json_reader*pReader) { + void handle_previousPreset(int replyTo, json_reader *pReader) + { model.PreviousPreset(); - } REGISTER_MESSAGE_HANDLER(previousPreset) - void handle_renamePresetItem(int replyTo, json_reader*pReader) { + void handle_renamePresetItem(int replyTo, json_reader *pReader) + { RenamePresetBody body; pReader->read(&body); bool result = model.RenamePreset(body.clientId_, body.instanceId_, body.name_); this->Reply(replyTo, "renamePresetItem", result); - } REGISTER_MESSAGE_HANDLER(renamePresetItem) - void handle_copyPreset(int replyTo, json_reader*pReader) { + void handle_copyPreset(int replyTo, json_reader *pReader) + { CopyPresetBody body; pReader->read(&body); int64_t result = model.CopyPreset(body.clientId_, body.fromId_, body.toId_); this->Reply(replyTo, "copyPreset", result); - } REGISTER_MESSAGE_HANDLER(copyPreset) - void handle_copyPluginPreset(int replyTo, json_reader*pReader) { + void handle_copyPluginPreset(int replyTo, json_reader *pReader) + { CopyPluginPresetBody body; pReader->read(&body); uint64_t result = model.CopyPluginPreset(body.pluginUri_, body.instanceId_); this->Reply(replyTo, "copyPluginPreset", result); - } REGISTER_MESSAGE_HANDLER(copyPluginPreset) - void handle_setPatchProperty(int replyTo, json_reader*pReader) { + void handle_setPatchProperty(int replyTo, json_reader *pReader) + { SetPatchPropertyBody body; pReader->read(&body); model.SendSetPatchProperty(clientId, body.instanceId_, body.propertyUri_, body.value_, [this, replyTo]() { this->JsonReply(replyTo, "setPatchProperty", "true"); }, [this, replyTo](const std::string &error) { this->SendError(replyTo, error.c_str()); }); - } REGISTER_MESSAGE_HANDLER(setPatchProperty) - void handle_setPedalboardItemTitle(int replyTo, json_reader*pReader) { + void handle_setPedalboardItemTitle(int replyTo, json_reader *pReader) + { SetPedalboardItemTitleBody body; pReader->read(&body); model.SetPedalboardItemTitle(body.instanceId_, body.title_, body.colorKey_); - } REGISTER_MESSAGE_HANDLER(setPedalboardItemTitle) - void handle_getPatchProperty(int replyTo, json_reader*pReader) { + void handle_getPatchProperty(int replyTo, json_reader *pReader) + { GetPatchPropertyBody body; pReader->read(&body); @@ -1782,20 +1775,20 @@ public: { this->SendError(replyTo, error.c_str()); }); - } REGISTER_MESSAGE_HANDLER(getPatchProperty) - void handle_monitorPort(int replyTo, json_reader*pReader) { + void handle_monitorPort(int replyTo, json_reader *pReader) + { MonitorPortBody body; pReader->read(&body); MonitorPort(replyTo, body); - } REGISTER_MESSAGE_HANDLER(monitorPort) - void handle_unmonitorPort(int replyTo, json_reader*pReader) { + void handle_unmonitorPort(int replyTo, json_reader *pReader) + { int64_t subscriptionHandle; pReader->read(&subscriptionHandle); { @@ -1815,11 +1808,11 @@ public: } model.UnmonitorPort(subscriptionHandle); } - } REGISTER_MESSAGE_HANDLER(unmonitorPort) - void handle_addVuSubscription(int replyTo, json_reader*pReader) { + void handle_addVuSubscription(int replyTo, json_reader *pReader) + { int64_t instanceId = -1; pReader->read(&instanceId); @@ -1831,11 +1824,11 @@ public: activeVuSubscriptions.push_back(VuSubscription{subscriptionHandle, instanceId}); } this->Reply(replyTo, "addVuSubscription", subscriptionHandle); - } REGISTER_MESSAGE_HANDLER(addVuSubscription) - void handle_removeVuSubscription(int replyTo, json_reader*pReader) { + void handle_removeVuSubscription(int replyTo, json_reader *pReader) + { int64_t subscriptionHandle = -1; pReader->read(&subscriptionHandle); { @@ -1851,124 +1844,124 @@ public: } } model.RemoveVuSubscription(subscriptionHandle); - } REGISTER_MESSAGE_HANDLER(removeVuSubscription) - void handle_imageList(int replyTo, json_reader*pReader) { + void handle_imageList(int replyTo, json_reader *pReader) + { this->Reply(replyTo, "imageList", imageList); - } REGISTER_MESSAGE_HANDLER(imageList) - void handle_getFavorites(int replyTo, json_reader*pReader) { + void handle_getFavorites(int replyTo, json_reader *pReader) + { std::map favorites = this->model.GetFavorites(); this->Reply(replyTo, "getFavorites", favorites); - } REGISTER_MESSAGE_HANDLER(getFavorites) - void handle_setFavorites(int replyTo, json_reader*pReader) { + void handle_setFavorites(int replyTo, json_reader *pReader) + { std::map favorites; pReader->read(&favorites); this->model.SetFavorites(favorites); - } REGISTER_MESSAGE_HANDLER(setFavorites) - void handle_setUpdatePolicy(int replyTo, json_reader*pReader) { + void handle_setUpdatePolicy(int replyTo, json_reader *pReader) + { int iPolicy; pReader->read(&iPolicy); this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy); - } REGISTER_MESSAGE_HANDLER(setUpdatePolicy) - void handle_forceUpdateCheck(int replyTo, json_reader*pReader) { + void handle_forceUpdateCheck(int replyTo, json_reader *pReader) + { this->model.ForceUpdateCheck(); - } REGISTER_MESSAGE_HANDLER(forceUpdateCheck) - void handle_setSystemMidiBindings(int replyTo, json_reader*pReader) { + void handle_setSystemMidiBindings(int replyTo, json_reader *pReader) + { std::vector bindings; pReader->read(&bindings); this->model.SetSystemMidiBindings(bindings); - } REGISTER_MESSAGE_HANDLER(setSystemMidiBindings) - void handle_getSystemMidiBindings(int replyTo, json_reader*pReader) { + void handle_getSystemMidiBindings(int replyTo, json_reader *pReader) + { std::vector bindings = this->model.GetSystemMidiBidings(); this->Reply(replyTo, "getSystemMidiBindings", bindings); - } REGISTER_MESSAGE_HANDLER(getSystemMidiBindings) - void handle_requestFileList(int replyTo, json_reader*pReader) { + void handle_requestFileList(int replyTo, json_reader *pReader) + { throw std::runtime_error("No longer implemented."); - } REGISTER_MESSAGE_HANDLER(requestFileList) - void handle_requestFileList2(int replyTo, json_reader*pReader) { + void handle_requestFileList2(int replyTo, json_reader *pReader) + { FileRequestArgs requestArgs; pReader->read(&requestArgs); FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); this->Reply(replyTo, "requestFileList2", result); - } REGISTER_MESSAGE_HANDLER(requestFileList2) - void handle_newPreset(int replyTo, json_reader*pReader) { + void handle_newPreset(int replyTo, json_reader *pReader) + { int64_t presetId = this->model.CreateNewPreset(); this->Reply(replyTo, "newPreset", presetId); - } REGISTER_MESSAGE_HANDLER(newPreset) - void handle_deleteUserFile(int replyTo, json_reader*pReader) { + void handle_deleteUserFile(int replyTo, json_reader *pReader) + { std::string fileName; pReader->read(&fileName); this->model.DeleteSampleFile(fileName); this->Reply(replyTo, "deleteUserFile", true); - } REGISTER_MESSAGE_HANDLER(deleteUserFile) - void handle_createNewSampleDirectory(int replyTo, json_reader*pReader) { + void handle_createNewSampleDirectory(int replyTo, json_reader *pReader) + { CreateNewSampleDirectoryArgs args; pReader->read(&args); std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_, args.uiFileProperty_); this->Reply(replyTo, "createNewSampleDirectory", newFileName); - } REGISTER_MESSAGE_HANDLER(createNewSampleDirectory) - void handle_renameFilePropertyFile(int replyTo, json_reader*pReader) { + void handle_renameFilePropertyFile(int replyTo, json_reader *pReader) + { RenameSampleFileArgs args; pReader->read(&args); std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_); this->Reply(replyTo, "renameFilePropertyFile", newFileName); - } REGISTER_MESSAGE_HANDLER(renameFilePropertyFile) - void handle_copyFilePropertyFile(int replyTo, json_reader*pReader) { + void handle_copyFilePropertyFile(int replyTo, json_reader *pReader) + { CopySampleFileArgs args; pReader->read(&args); std::string newFileName = this->model.CopyFilePropertyFile(args.oldRelativePath_, args.newRelativePath_, args.uiFileProperty_, args.overwrite_); this->Reply(replyTo, "copyFilePropertyFile", newFileName); - } REGISTER_MESSAGE_HANDLER(copyFilePropertyFile) - void handle_getFilePropertyDirectoryTree(int replyTo, json_reader*pReader) { + void handle_getFilePropertyDirectoryTree(int replyTo, json_reader *pReader) + { GetFilePropertyDirectoryTreeArgs args; pReader->read(&args); FilePropertyDirectoryTree::ptr result = @@ -1976,133 +1969,132 @@ public: args.fileProperty_, args.selectedPath_); this->Reply(replyTo, "GetFilePropertydirectoryTree", result); - } REGISTER_MESSAGE_HANDLER(getFilePropertyDirectoryTree) - void handle_moveAudioFile(int replyTo, json_reader*pReader) { + void handle_moveAudioFile(int replyTo, json_reader *pReader) + { MoveAudioFileArgs args; pReader->read(&args); this->model.MoveAudioFile(args.path_, args.from_, args.to_); bool result = true; - this->Reply(replyTo,"moveAudioFile", result); - + this->Reply(replyTo, "moveAudioFile", result); } REGISTER_MESSAGE_HANDLER(moveAudioFile) - void handle_setOnboarding(int replyTo, json_reader*pReader) { + void handle_setOnboarding(int replyTo, json_reader *pReader) + { bool value; pReader->read(&value); this->model.SetOnboarding(value); - } REGISTER_MESSAGE_HANDLER(setOnboarding) - void handle_getWifiRegulatoryDomains(int replyTo, json_reader*pReader) { + void handle_getWifiRegulatoryDomains(int replyTo, json_reader *pReader) + { auto regulatoryDomains = this->model.GetWifiRegulatoryDomains(); this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains); - } REGISTER_MESSAGE_HANDLER(getWifiRegulatoryDomains) - void handle_setAlsaSequencerConfiguration(int replyTo, json_reader*pReader) { + void handle_setAlsaSequencerConfiguration(int replyTo, json_reader *pReader) + { AlsaSequencerConfiguration config; pReader->read(&config); this->model.SetAlsaSequencerConfiguration(config); this->Reply(replyTo, "setAlsaSequencerConfiguration"); - } REGISTER_MESSAGE_HANDLER(setAlsaSequencerConfiguration) - void handle_getAlsaSequencerConfiguration(int replyTo, json_reader*pReader) { + void handle_getAlsaSequencerConfiguration(int replyTo, json_reader *pReader) + { AlsaSequencerConfiguration config = this->model.GetAlsaSequencerConfiguration(); this->Reply(replyTo, "getAlsaSequencerConfiguration", config); - } REGISTER_MESSAGE_HANDLER(getAlsaSequencerConfiguration) - void handle_getAlsaSequencerPorts(int replyTo, json_reader*pReader) { + void handle_getAlsaSequencerPorts(int replyTo, json_reader *pReader) + { std::vector result = model.GetAlsaSequencerPorts(); - this->Reply(replyTo,"getAlsaSequencerPorts", result); - + this->Reply(replyTo, "getAlsaSequencerPorts", result); } REGISTER_MESSAGE_HANDLER(getAlsaSequencerPorts) - void handle_requestBankPresets(int replyTo, json_reader*pReader) { - RequestBankPresetsBody args; + void handle_requestBankPresets(int replyTo, json_reader *pReader) + { + RequestBankPresetsBody args; pReader->read(&args); auto result = this->model.RequestBankPresets(args.bankInstanceId_); - this->Reply(replyTo,"requestBankPresets",result); - + this->Reply(replyTo, "requestBankPresets", result); } REGISTER_MESSAGE_HANDLER(requestBankPresets) - void handle_importPresetsFromBank(int replyTo, json_reader*pReader) { + void handle_importPresetsFromBank(int replyTo, json_reader *pReader) + { ImportPresetsFromBankBody args; pReader->read(&args); auto result = this->model.ImportPresetsFromBank(args.bankInstanceId_, args.presets_); - this->Reply(replyTo,"importPresetsFromBank",result); - + this->Reply(replyTo, "importPresetsFromBank", result); } REGISTER_MESSAGE_HANDLER(importPresetsFromBank) - void handle_copyPresetsToBank(int replyTo, json_reader*pReader) { + void handle_copyPresetsToBank(int replyTo, json_reader *pReader) + { CopyPresetsToBankBody args; pReader->read(&args); auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); - this->Reply(replyTo,"copyPresetsToBank",result); - + this->Reply(replyTo, "copyPresetsToBank", result); } REGISTER_MESSAGE_HANDLER(copyPresetsToBank) - void handle_getChannelRouterSettings(int replyTo, json_reader*pReader) { + void handle_getChannelRouterSettings(int replyTo, json_reader *pReader) + { ChannelRouterSettings::ptr result = this->model.GetChannelRouterSettings(); this->Reply(replyTo, "getChannelRouterSettings", result); - } REGISTER_MESSAGE_HANDLER(getChannelRouterSettings) - void handle_setChannelRouterSettings(int replyTo, json_reader*pReader) { + void handle_setChannelRouterSettings(int replyTo, json_reader *pReader) + { ChannelRouterSettings::ptr args; pReader->read(&args); this->model.SetChannelRouterSettings(this->clientId, args); - } REGISTER_MESSAGE_HANDLER(setChannelRouterSettings) - void handle_downloadModelsFromTone3000(int replyTo, json_reader*pReader) { - DownloadModelsFromTone3000Body args; - pReader->read(&args); + void handle_DownloadModelsFromTone3000(int replyTo, json_reader *pReader) + { + + DownloadModelsFromTone3000Body body; + pReader->read(&body); auto result = this->model.DownloadModelsFromTone3000( - this->clientId, - StringToTone3000DownloadType(args.downloadType_), - args.downloadPath_, - args.tone3000Url_ - ); - this->Reply(replyTo,"downloadModelsFromTone3000",result); + body.responseUri_, + body.tone3000PckceParams_, + body.downloadPath_, + body.downloadType()); + this->Reply(replyTo, "downloadModelsFromTone3000", result); } - REGISTER_MESSAGE_HANDLER(downloadModelsFromTone3000) + REGISTER_MESSAGE_HANDLER(DownloadModelsFromTone3000) - void handle_cancelTone3000Download(int replyTo, json_reader*pReader) { + void handle_cancelTone3000Download(int replyTo, json_reader *pReader) + { int64_t handle = -1; pReader->read(&handle); - model.CancelTone3000Download(clientId,handle); - + model.CancelTone3000Download(clientId, handle); } REGISTER_MESSAGE_HANDLER(cancelTone3000Download) - void handle_pingTone3000Server(int replyTo, json_reader*pReader) { + void handle_pingTone3000Server(int replyTo, json_reader *pReader) + { std::shared_ptr this_ = shared_from_this(); - model.Post([this_, replyTo] () { + model.Post([this_, replyTo]() + { bool result = this_->PingTone3000Server(); - this_->Reply(replyTo,"pingTone3000Server",result); - }); - + this_->Reply(replyTo,"pingTone3000Server",result); }); } REGISTER_MESSAGE_HANDLER(pingTone3000Server) - void handleMessage(int reply, int replyTo, const std::string &message, json_reader *pReader) { if (reply != -1) @@ -2145,9 +2137,9 @@ public: } auto ffHandler = socket_messageHandlers.find(message); - if (ffHandler != socket_messageHandlers.end()) + if (ffHandler != socket_messageHandlers.end()) { - (this->*(ffHandler->second))(replyTo,pReader); + (this->*(ffHandler->second))(replyTo, pReader); return; } Lv2Log::error("Unknown message received: %s", message.c_str()); @@ -2268,7 +2260,7 @@ private: { Send("onErrorMessage", message); } - + virtual void OnTone3000DownloadStarted(int64_t handle, const std::string &title) override { Tone3000DownloadStartedBody body; @@ -2276,17 +2268,17 @@ private: 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; @@ -2294,7 +2286,7 @@ private: 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; @@ -2646,7 +2638,6 @@ private: body.enabled_ = enabled; Send("onUseItemModUiChanged", body); } - }; std::atomic PiPedalSocketHandler::nextClientId = 0; @@ -2683,18 +2674,19 @@ std::shared_ptr pipedal::MakePiPedalSocketFactory(PiPedalModel & return std::make_shared(model); } - bool PiPedalSocketHandler::PingTone3000Server() { - constexpr const char* TONE3000_PING_URL = "https://www.tone3000.com/robots.txt"; + constexpr const char *TONE3000_PING_URL = "https://www.tone3000.com/robots.txt"; std::vector output; std::vector headers; - try { + try + { int result = CurlGet(TONE3000_PING_URL, output, &headers); return result == 200; - } catch(const std::exception&e) + } + catch (const std::exception &e) { - Lv2Log::error("PingTone3000Server: %s",e.what()); + Lv2Log::error("PingTone3000Server: %s", e.what()); return false; } } \ No newline at end of file diff --git a/src/Storage.cpp b/src/Storage.cpp index c3dbdae..8ea253a 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -435,7 +435,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const return this->dataRoot / "currentPreset.json"; } - std::filesystem::path Storage::GetChannelSelectionFileName() { return this->dataRoot / "JackChannelSelection.json"; @@ -2083,26 +2082,29 @@ static bool ensureNoDotDot(const std::filesystem::path &path) return true; } -static bool isInfoFile(const std::string &fileName) +bool isInfoFile(const fs::path &path) { - if (strcasecmp(fileName.c_str(), "license.txt") == 0) + auto extension = path.extension(); + if (extension == ".pdf") { return true; } - if (strcasecmp(fileName.c_str(), "license.md") == 0) + auto filename = path.filename(); + if (filename == "LICENSE.txt" || filename == "README.txt") { return true; } - if (strcasecmp(fileName.c_str(), "readme.txt") == 0) + if (filename == "LICENSE.md" || filename == "README.md") { return true; } - if (strcasecmp(fileName.c_str(), "readme.md") == 0) + if (filename == "license.txt" || filename == "readme.txt") { return true; } return false; } + static bool isInfoFile(const FileEntry &l) { return isInfoFile(l.displayName_); @@ -2333,10 +2335,8 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons { rootModDirectory = modDirectoryInfo; modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; - result.breadcrumbs_.push_back({ - modDirectoryPath.string(), - (modDirectoryInfo->displayName)} - ); + result.breadcrumbs_.push_back({modDirectoryPath.string(), + (modDirectoryInfo->displayName)}); break; } } @@ -2346,9 +2346,8 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons if (IsSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory())) { modDirectoryPath = uploadsDirectory / fileProperty.directory(); - result.breadcrumbs_.push_back({ - modDirectoryPath.string(), - SafeFilenameToString(fs::path(fileProperty.directory()).filename().string())}); + result.breadcrumbs_.push_back({modDirectoryPath.string(), + SafeFilenameToString(fs::path(fileProperty.directory()).filename().string())}); } else { @@ -2991,8 +2990,6 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex() return pluginPresetIndex; } - - std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath) { if (stringPath.empty()) @@ -3054,15 +3051,18 @@ ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() // See UpgradeChannelRouterSettings(), which will upgrade from old settings // or create a default instance. return nullptr; // will be upgraded later. - } - try { + } + try + { std::ifstream is(path); json_reader reader(is); ChannelRouterSettings::ptr result; reader.read(&result); this->channelSelection = ChannelSelection(*result); - return result; - } catch (const std::exception &e) { + return result; + } + catch (const std::exception &e) + { Lv2Log::error("Failed to load Channel Router settings: %s", e.what()); return std::make_shared(); } @@ -3071,55 +3071,62 @@ ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() static int64_t GetUpgradedPortIndex(const std::string &portName) { auto nPos = portName.find_last_of('_'); - if (nPos != std::string::npos) + if (nPos != std::string::npos) { std::string channelStr = portName.substr(nPos + 1); - try + try { int64_t channelIndex = std::stoll(channelStr); return channelIndex; - } catch (const std::exception &) { + } + catch (const std::exception &) + { return -1; } - } + } return -1; -} -void Storage::UpgradeChannelRouterSettings() +} +void Storage::UpgradeChannelRouterSettings() { - if (channelRouterSettings == nullptr) + if (channelRouterSettings == nullptr) { channelRouterSettings = std::make_shared(); - if (jackChannelSelection.isValid()) + if (jackChannelSelection.isValid()) { channelRouterSettings->configured(true); channelRouterSettings->mainInputChannels().resize(2); - const std::vector& oldInputs = jackChannelSelection.GetInputAudioPorts(); - std::vector& newInputs = channelRouterSettings->mainInputChannels(); + const std::vector &oldInputs = jackChannelSelection.GetInputAudioPorts(); + std::vector &newInputs = channelRouterSettings->mainInputChannels(); - if (oldInputs.size() ==1) + if (oldInputs.size() == 1) { int64_t leftIndex = GetUpgradedPortIndex(oldInputs[0]); newInputs.resize(2); newInputs[0] = leftIndex; newInputs[1] = leftIndex; - } else if (oldInputs.size() == 2) { + } + else if (oldInputs.size() == 2) + { newInputs.resize(2); - for (size_t i = 0; i < std::min(oldInputs.size(), newInputs.size()); ++i) + for (size_t i = 0; i < std::min(oldInputs.size(), newInputs.size()); ++i) { int64_t portIndex = GetUpgradedPortIndex(oldInputs[i]); newInputs[i] = portIndex; } } - const std::vector& oldOutputs = jackChannelSelection.GetOutputAudioPorts(); + const std::vector &oldOutputs = jackChannelSelection.GetOutputAudioPorts(); auto &newMainOutputs = channelRouterSettings->mainOutputChannels(); - if (oldOutputs.size() == 1) { + if (oldOutputs.size() == 1) + { int64_t leftIndex = GetUpgradedPortIndex(oldOutputs[0]); newMainOutputs.resize(2); newMainOutputs[0] = leftIndex; newMainOutputs[1] = leftIndex; - } else if (oldOutputs.size() == 2) { + } + else if (oldOutputs.size() == 2) + { newMainOutputs.resize(2); - for (size_t i = 0; i < std::min(oldOutputs.size(), newMainOutputs.size()); ++i) + for (size_t i = 0; i < std::min(oldOutputs.size(), newMainOutputs.size()); ++i) { int64_t portIndex = GetUpgradedPortIndex(oldOutputs[i]); newMainOutputs[i] = portIndex; @@ -3135,16 +3142,16 @@ void Storage::UpgradeChannelRouterSettings() newAuxOutputs[0] = -1; newAuxOutputs[1] = -1; SaveChannelRouterSettings(channelRouterSettings); - } + } channelSelection = ChannelSelection(*channelRouterSettings); } } -const ChannelSelection& Storage::GetChannelSelection() const { +const ChannelSelection &Storage::GetChannelSelection() const +{ return channelSelection; } - JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/ThreadedQueue.hpp b/src/ThreadedQueue.hpp new file mode 100644 index 0000000..15701ea --- /dev/null +++ b/src/ThreadedQueue.hpp @@ -0,0 +1,116 @@ +/* + * 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 +#include +#include +#include + +namespace pipedal +{ + template + class ThreadedQueue + { + public: + ~ThreadedQueue() { Close(); } + + void Put(std::shared_ptr value) + { + { + std::lock_guard lock(mutex); + if (!closed) + { + queue.push(value); + } + } + read_cv.notify_one(); + } + std::shared_ptr Take() + { + while (true) + { + { + std::unique_lock lock(mutex); + read_cv.wait(lock, + [this]() {return !queue.empty() || closed;}); + if (!queue.empty()) + { + auto result = queue.front(); + queue.pop(); + return result; + } + else + { + if (closed) + { + return nullptr; + } + } + } + } + } + void Close() + { + { + std::lock_guard lock{mutex}; + closed = true; + } + read_cv.notify_all(); + } + void CloseAndClear() + { + { + std::lock_guard lock{mutex}; + closed = true; + while (!queue.empty()) + { + queue.pop(); + } + } + read_cv.notify_all(); + } + void Erase(std::function queueEntry)> predicate) + { + std::lock_guard lock{mutex}; + std::queue> newQueue; + while (!queue.empty()) + { + auto item = queue.front(); + queue.pop(); + if (!predicate(item)) + { + newQueue.push(item); + } + } + queue = newQueue; + } + + private: + bool closed = 0; + std::queue> queue; + std::mutex mutex; + std::condition_variable read_cv; + }; +} \ No newline at end of file diff --git a/src/Tone3000Download.cpp b/src/Tone3000Download.cpp index ee0c695..d0b4680 100644 --- a/src/Tone3000Download.cpp +++ b/src/Tone3000Download.cpp @@ -35,8 +35,13 @@ #include "PiPedalModel.hpp" using namespace pipedal; -using namespace pipedal::tone3000; -namespace fs = std::filesystem; + +Tone3000Download::Tone3000Download(const std::string &json) +{ + std::istringstream ss(json); + json_reader reader(ss); + reader.read(this); +} JSON_MAP_BEGIN(Tone3000Model) JSON_MAP_REFERENCE(Tone3000Model, id) @@ -61,770 +66,22 @@ JSON_MAP_REFERENCE(Tone3000Download, title) JSON_MAP_REFERENCE(Tone3000Download, description) JSON_MAP_REFERENCE(Tone3000Download, created_at) JSON_MAP_REFERENCE(Tone3000Download, updated_at) -JSON_MAP_REFERENCE(Tone3000Download, platform) JSON_MAP_REFERENCE(Tone3000Download, gear) JSON_MAP_REFERENCE(Tone3000Download, images) JSON_MAP_REFERENCE(Tone3000Download, is_public) JSON_MAP_REFERENCE(Tone3000Download, links) -JSON_MAP_REFERENCE(Tone3000Download, model_count) +JSON_MAP_REFERENCE(Tone3000Download, platform) +JSON_MAP_REFERENCE(Tone3000Download, models_count) JSON_MAP_REFERENCE(Tone3000Download, favorites_count) +JSON_MAP_REFERENCE(Tone3000Download, downloads_count) JSON_MAP_REFERENCE(Tone3000Download, license) JSON_MAP_REFERENCE(Tone3000Download, sizes) +JSON_MAP_REFERENCE(Tone3000Download, a1_models_count) +JSON_MAP_REFERENCE(Tone3000Download, a2_models_count) +JSON_MAP_REFERENCE(Tone3000Download, irs_count) +JSON_MAP_REFERENCE(Tone3000Download, custom_models_count) +JSON_MAP_REFERENCE(Tone3000Download, user) +JSON_MAP_REFERENCE(Tone3000Download, url) JSON_MAP_REFERENCE(Tone3000Download, user) JSON_MAP_REFERENCE(Tone3000Download, models) -JSON_MAP_END() - -JSON_MAP_BEGIN(Tone3000DownloadResult) -JSON_MAP_REFERENCE(Tone3000DownloadResult, success) -JSON_MAP_REFERENCE(Tone3000DownloadResult, errorMessage) -JSON_MAP_END() - -static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; -static const std::filesystem::path TONE3000_THUMBNAIL_PATH{"/var/pipedal/tone3000_thumbnails"}; - -static std::string getCurlErrorFromExitCode(int exitCode) -{ - - switch (exitCode) - { - case 3 * 256: // malformed URL - return "Invalid URL."; - case 6 * 256: // could not resolve hostname - return "Could not resolve hostname. Check that the PiPedal server is connected to the Internet."; - case 7 * 256: // failed to connect to host - return "Failed to connect to host."; - case 22 * 256: // HTTP error - return "HTTP error downloading file."; - case 28 * 256: // timeout - return "Download timeout."; - case 63 * 256: // file too large - return "File too large."; - default: - return SS("Failed to download file (exit code: " << (exitCode / 256) << ")"); - } -} - -static void cancellableSleep(std::chrono::steady_clock::duration duration, std::function &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 std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") -{ - std::ostringstream os; - for (char c : str) - { - if ((unsigned char)c < 0x20) - continue; - if (c == '<') - { - os << "<"; - continue; - } - auto npos = illegalCharacters.find_first_of(c); - if (npos != std::string::npos) - { - continue; - } - os << c; - } - return os.str(); -} - -static std::string mdDate(const tone3000_time_point &date_) -{ - std::ostringstream os; - // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" - std::string date = date_; - - // Remove timezone offset if present and replace with Z - size_t plusPos = date.find('+'); - if (plusPos != std::string::npos) - { - date = date.substr(0, plusPos) + "Z"; - } - // Parse ISO 8601 date string into tm - std::tm tm = {}; - std::istringstream ss(date); - ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); - if (ss.fail()) - { - os << date; - return os.str(); - } - - os << std::put_time(&tm, "%x"); - return os.str(); -} - -static std::string mdEnum(const std::string &value, const std::map &enumValues) -{ - auto ff = enumValues.find(value); - if (ff == enumValues.end()) - { - return value; - } - return ff->second; -} - -static std::string mdEnumList( - const std::vector &values, - const std::map &enumValues) -{ - if (values.size() == 1) - { - return mdEnum(values[0], enumValues); - } - std::ostringstream os; - os << '['; - bool first = true; - for (const auto &value : values) - { - if (first) - { - first = false; - } - else - { - os << ", "; - } - os << mdEnum(value, enumValues); - } - os << ']'; - return os.str(); -} - -static std::map sizeEnumValues = - { - {"standard", "Standard"}, - {"lite", "Lite"}, - {"feather", "Feather"}, - {"nano", "Nano"}, - {"custom", "Custom"}}; -static std::string mdSizes(const std::vector &sizes) -{ - return mdEnumList(sizes, sizeEnumValues); -} - -static std::map gearEnumValues = - { - {"amp", "Amp only"}, - {"full-rig", "Full rig"}, - {"pedal", "Pedal"}, - {"outboard", "Outboard"}, - {"ir", "I/R"}}; - -static std::string mdGear(const std::string &gear) -{ - return mdEnum(gear, gearEnumValues); -} - -namespace -{ - enum class LicenseFlags - { - None = 0, - Cc = 1, - By = 2, - CcBy = 3, // = Cc | By - Sa = 4, - Nc = 8, - Nd = 16, - Cc0 = 32, - }; - - static bool operator&(LicenseFlags v1, LicenseFlags v2) - { - return (((int)v1) & ((int)v2)) != 0; - } - static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) - { - return (LicenseFlags)(((int)(v1)) | ((int)(v2))); - } - - struct LicenseInfo - { - std::string key; - std::string displayName; - std::string url; - LicenseFlags licenseFlags; - }; -} - -static std::vector licenses{ - {.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, - - }, -}; - -static std::map makeLicenseEnum() -{ - std::map result; - for (const auto &license : licenses) - { - result[license.key] = license; - } - return result; -} - -static std::map licenseEnum = makeLicenseEnum(); - -static std::string mdLicenseIcon(LicenseFlags licenseFlag) -{ - std::ostringstream os; - std::string url; - - 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::None: - throw std::runtime_error("Invalid argument."); - } - os << ""; - return os.str(); -} - -static void mdEscapeString(std::ostream &f, const std::string &str) -{ - size_t ix = 0; - while (ix < str.size()) - { - char c = str[ix++]; - if (c == '\n') - { - if (ix < str.size() && str[ix] == '\n') - { - f << "\n\n"; - } - else - { - f << " \n"; // a
in md. - } - } - else - { - f << c; - } - } -} - -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::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) - { - if (licenseInfo.licenseFlags & licenseFlag) - { - os << mdLicenseIcon(licenseFlag); - } - } - os << " "; - } - - if (licenseInfo.url != "") - { - os << "" << mdSanitize(licenseInfo.displayName) << ""; - } - else - { - os << mdSanitize(licenseInfo.displayName); - } - return os.str(); -} -// static std::string mdTestLicenses() -// { -// std::ostringstream os; -// for (auto license: licenseEnum) -// { -// os << "
" << mdLicense(license.first) << "\n"; -// } -// return os.str(); -// } -static std::map 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 << - "" << mdSanitize(user.username()) << ""; - 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, ")") << ")"; -} - -static void writeReadme(const fs::path &path, const std::string &thumbnailUrl, const Tone3000Download &download) -{ - std::ofstream of{path}; - if (!of.is_open()) - { - throw std::runtime_error(SS("Unable to create file " << path)); - } - - // clang-format off - of << - - "
\n"; - if (!thumbnailUrl.empty()) - { - - of << " \n"; - } - of << "
\n" - << "

" << mdSanitize(download.title()) << "

\n" - << "
\n" - << "

\n" - << mdDate(download.updated_at()) - << " " - << mdUser(download.user()) - << "
\n" - << " "; - if (download.sizes().has_value()) { - of - << mdSizes(download.sizes().value()) << ", " - ; - } - if (download.platform() == "ir") - { - of - << (mdPlatform(download.platform())); - } else { - of - << mdGear(download.gear()) << ", " - << mdPlatform(download.platform()); - } - of << "
\n"; - if (download.license() != "") { - of << " License: " << mdLicense(download.license()) << "\n"; - } - of -// << " " << mdTestLicenses() - << "

\n" - << "
\n" - << "
\n" - << "
\n"; - // clang-format on - of << "\n\n"; - mdEscapeString(of, download.description()); - - if (download.links().size() > 0) - { - of << std::endl; - of << "## Links" << std::endl - << std::endl; - for (auto &link : download.links()) - { - of << "- " << "[" << link << "](" << link << ")" << std::endl; - } - } -} - -static void CleanUpFailedDownload(const std::filesystem::path &folder, const std::vector &requests) -{ - - std::error_code ec; // do NOT throw errors. - - if (fs::exists(folder)) - { - for (const auto &request : requests) - { - if (fs::exists(request.outputFile)) - { - fs::remove(request.outputFile, ec); // ignoring errors. - } - } - // If there's a readme file, remove that too. - fs::path readmePath = folder / "README.md"; - if (fs::exists(readmePath)) - { - fs::remove(readmePath, ec); // ignoring errors - } - - // if the folder directory is empty, remove the the folder too. - try - { - if (fs::is_empty(folder)) - { - fs::remove(folder, ec); // ignoring errors. - } - } - catch (const std::exception &) - { - } - } -} - -static std::string GetHeader(const std::string &headerName, const std::vector &htmlHeaders) -{ - std::string needle = headerName; - std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char c) - { return std::tolower(c); }); - - for (const auto &header : htmlHeaders) - { - auto pos = header.find(':'); - if (pos == std::string::npos) - { - continue; - } - std::string name = header.substr(0, pos); - std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) - { return std::tolower(c); }); - if (name != needle) - { - continue; - } - std::string value = header.substr(pos + 1); - auto first = value.find_first_not_of(" \t\r\n"); - if (first == std::string::npos) - { - return ""; - } - auto last = value.find_last_not_of(" \t\r\n"); - return value.substr(first, last - first + 1); - } - return ""; -} - -static bool CancellableSleep(std::chrono::steady_clock::duration duration, std::function &isCancelled) -{ - using clock_t = std::chrono::steady_clock; - - auto start = clock_t::now(); - while (true) - { - auto now = clock_t::now(); - auto elapsed = now - start; - if (elapsed > duration) - { - return true; - } - if (isCancelled()) - { - return false; - } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } -} -std::string pipedal::tone3000::DownloadTone3000Bundle( - const std::filesystem::path &destinationFolder, - const std::string &downloadUrl, - int64_t handle, - std::function progressCallback, - std::function isCancelled) -{ - TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR}; - fs::path downloadPath = downloadTemporaryFile.Path(); - std::vector headers; - - std::string resultDirectory; - Tone3000DownloadProgress progress; - progress.handle(handle); - progressCallback(progress); - - int httpResult = CurlGet(downloadUrl, downloadPath, &headers); - if (httpResult != 200) - { - throw std::runtime_error(SS("Download failed: HTTP " << HtmlHelper::httpErrorString(httpResult))); - } - - if (isCancelled()) - { - - return resultDirectory; - } - - std::ifstream f{downloadPath}; - if (!f.is_open()) - { - throw std::runtime_error(SS("Can't open file " << downloadPath)); - } - json_reader reader(f); - - Tone3000Download tone3000Download; - reader.read(&tone3000Download); - - if (isCancelled()) - { - return resultDirectory; - } - progress.title(tone3000Download.title()); - progress.total(tone3000Download.models().size()); - progressCallback(progress); - - if (tone3000Download.platform() != "nam" && tone3000Download.platform() != "ir") - { - throw std::runtime_error(SS("Unexpected file format. (platform=\"" << tone3000Download.platform() << "\")")); - } - fs::path bundlePath = destinationFolder / StringToSafeFilename(tone3000Download.title()); - resultDirectory = bundlePath; - - fs::create_directories(bundlePath); - - std::vector requests; - - for (auto &model : tone3000Download.models()) - { - fs::path modelPath; - if (tone3000Download.platform() == "ir") - { - modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".wav"); - } - else - { - modelPath = bundlePath / SS(StringToSafeFilename(model.name()) << ".nam"); - } - requests.push_back(CurlDownloadRequest{url : model.model_url(), outputFile : modelPath}); - } - try - { - size_t downloadIndex = 0; - while (downloadIndex < requests.size()) - { - if (isCancelled()) - { - throw std::runtime_error("Cancelled"); - } - size_t thisTime = requests.size() - downloadIndex; - thisTime = Tone3000Throttler::instance().ReserveDownloadSlots(thisTime); - if (thisTime != 0) - { - std::vector requestsThisTime; - for (size_t i = 0; i < thisTime; ++i) - { - requestsThisTime.push_back(requests[downloadIndex + i]); - } - - int result = CurlGet( - requestsThisTime, - [&](size_t completed, size_t total) - { - if (isCancelled()) - { - return false; - } - progress.progress(completed + downloadIndex); - progress.total(requests.size()); - progressCallback(progress); - return true; - }, - &headers); - - if (isCancelled()) - { - throw std::runtime_error("Cancelled"); - } - - if (result != 200) - { - throw std::runtime_error(SS("Server download failed. HTTP Error " << HtmlHelper::httpErrorString(result))); - } - - downloadIndex += thisTime; - } - if (thisTime <= 1) - { - CancellableSleep(std::chrono::milliseconds(1000), isCancelled); - } - } - - fs::path readmePath = bundlePath / "README.md"; - - std::string thumbnailUrl; - if (tone3000Download.images().has_value() && tone3000Download.images().value().size() != 0) - { - - TemporaryFile imageFile{TONE3000_THUMBNAIL_PATH}; - - std::string imageUrl = tone3000Download.images().value()[0]; - std::vector imageHeaders; - try - { - size_t reserveCount; - while (true) - { - reserveCount = Tone3000Throttler::instance().ReserveDownloadSlots(1); - if (reserveCount != 0) - { - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - if (isCancelled()) - { - throw std::runtime_error("Cancelled"); - } - } - if (CurlGet(imageUrl, imageFile.Path(), &imageHeaders) == 200) - { - std::string mediaType = GetHeader("Content-Type", imageHeaders); - std::string extension; - if (mediaType == "image/jpeg") - { - extension = ".jpeg"; - } - else if (mediaType == "image/webp") - { - extension = ".webp"; - } - else if (mediaType == "image/png") - { - extension = ".png"; - } - else - { - Lv2Log::warning("Tone3000 thumbnail has an unexpected media type of '%s'", mediaType.c_str()); - throw std::runtime_error("Invalid thumbnail media type."); - } - if (mediaType == "image/jpeg") - { - fs::path thumnbailTempPath = imageFile.Detach(); - fs::path finalPath = TONE3000_THUMBNAIL_PATH / SS(tone3000Download.id() << extension); - if (fs::exists(finalPath)) - { - fs::remove(finalPath); - } - fs::create_directories(finalPath.parent_path()); - fs::rename(thumnbailTempPath, finalPath); - thumbnailUrl = SS("var/tone3000_thumbnail?id=" << tone3000Download.id()); - // thumbnailUrl = mdDataUrl("image/jpeg", finalPath); - } - } - } - catch (const std::exception &) - { - // ignore. - } - } - writeReadme(readmePath, thumbnailUrl, tone3000Download); - } - catch (const std::exception &e) - { - CleanUpFailedDownload(bundlePath, requests); - if (isCancelled()) - { - return ""; - } - throw; - } - - return resultDirectory; -} - -Tone3000DownloadResult::Tone3000DownloadResult() - : success_(true) -{ -} -Tone3000DownloadResult::Tone3000DownloadResult(const std::exception &e) - : success_(false), errorMessage_(e.what()) -{ -} +JSON_MAP_END(); diff --git a/src/Tone3000Download.cpp.bak b/src/Tone3000Download.cpp.bak index e6dcde9..0d8b722 100644 --- a/src/Tone3000Download.cpp.bak +++ b/src/Tone3000Download.cpp.bak @@ -618,7 +618,7 @@ static void writeReadme(const fs::path &path, const Tone3000Download &download) } } -std::string pipedal::tone3000::DownloadTone3000Bundle( +std::string pipedal::tone3000::DownloadTone3000File( const std::filesystem::path &destinationFolder, const std::string &downloadUrl, int64_t handle, diff --git a/src/Tone3000Download.hpp b/src/Tone3000Download.hpp index c42d1bc..5dd6e6d 100644 --- a/src/Tone3000Download.hpp +++ b/src/Tone3000Download.hpp @@ -22,6 +22,7 @@ */ #pragma once + #include #include #include @@ -36,111 +37,103 @@ namespace pipedal { - namespace tone3000 + class Tone3000FileDownloadRequest; + + using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones) + class Tone3000Model { - using tone3000_time_point = std::string; // Date in ISO 8601 format. (C++ doesn't handle timezones) - class Tone3000Model - { - private: - int64_t id_ = -1; - std::string name_; - std::string model_url_; - tone3000_time_point created_at_; - std::optional size_; - std::string user_id_; + private: + int64_t id_ = -1; + std::string name_; + std::string model_url_; + tone3000_time_point created_at_; + std::optional size_; + std::string user_id_; - public: - JSON_GETTER_SETTER(id) - JSON_GETTER_SETTER_REF(name) - JSON_GETTER_SETTER_REF(model_url) - JSON_GETTER_SETTER_REF(created_at) - JSON_GETTER_SETTER_REF(size) - JSON_GETTER_SETTER_REF(user_id) + public: + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(name) + JSON_GETTER_SETTER_REF(model_url) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(size) + JSON_GETTER_SETTER_REF(user_id) - DECLARE_JSON_MAP(Tone3000Model); - }; + DECLARE_JSON_MAP(Tone3000Model); + }; - class Tone3000User - { - private: - std::string id_; - std::optional avatar_url_; - std::string username_; - std::string url_; + class Tone3000User + { + private: + std::string id_; + std::optional avatar_url_; + std::string username_; + std::string url_; - public: - JSON_GETTER_SETTER_REF(id) - JSON_GETTER_SETTER(username) - JSON_GETTER_SETTER(url) + public: + JSON_GETTER_SETTER_REF(id) + JSON_GETTER_SETTER(username) + JSON_GETTER_SETTER(url) - DECLARE_JSON_MAP(Tone3000User); - }; + DECLARE_JSON_MAP(Tone3000User); + }; - class Tone3000Download - { - private: - int64_t id_ = -1; - std::string user_id_; - std::string title_; - std::string platform_; - std::string description_; - tone3000_time_point created_at_; - tone3000_time_point updated_at_; - std::string gear_; - std::optional> images_; - bool is_public_ = true; - std::vector links_; - int64_t model_count_ = 0; - int64_t favorites_count_ = 0; - std::string license_; - std::optional> sizes_; - Tone3000User user_; - std::vector models_; + class Tone3000Download + { + private: + int64_t id_ = -1; + std::string user_id_; + std::string title_; + std::string description_; + tone3000_time_point created_at_; + tone3000_time_point updated_at_; + std::string gear_; + std::vector images_; + bool is_public_ = true; + std::vector links_; + std::string platform_; + int64_t models_count_ = 0; + int64_t favorites_count_ = 0; + int64_t downloads_count_ = 0; + std::string license_; + std::optional> sizes_; + int64_t a1_models_count_ = 0; + int64_t a2_models_count_ =0; + int64_t irs_count_ =0; + int64_t custom_models_count_ = 0; + Tone3000User user_; + std::string url_; - public: - JSON_GETTER_SETTER(id) - JSON_GETTER_SETTER_REF(user_id) - JSON_GETTER_SETTER_REF(title) - JSON_GETTER_SETTER_REF(platform) - JSON_GETTER_SETTER_REF(description) - JSON_GETTER_SETTER_REF(created_at) - JSON_GETTER_SETTER_REF(updated_at) - JSON_GETTER_SETTER_REF(gear) - JSON_GETTER_SETTER_REF(images) - JSON_GETTER_SETTER(is_public) - JSON_GETTER_SETTER_REF(links) - JSON_GETTER_SETTER(model_count) - JSON_GETTER_SETTER(favorites_count) - JSON_GETTER_SETTER_REF(license) - JSON_GETTER_SETTER_REF(sizes) - JSON_GETTER_SETTER_REF(user) - JSON_GETTER_SETTER_REF(models) + std::vector models_; - DECLARE_JSON_MAP(Tone3000Download); - }; + public: + Tone3000Download() = default; + Tone3000Download(const std::string&json); - class Tone3000DownloadResult { - private: - bool success_; - std::string errorMessage_; - public: - JSON_GETTER_SETTER(success); - JSON_GETTER_SETTER_REF(errorMessage); + JSON_GETTER_SETTER(id) + JSON_GETTER_SETTER_REF(user_id) + JSON_GETTER_SETTER_REF(title) + JSON_GETTER_SETTER_REF(description) + JSON_GETTER_SETTER_REF(created_at) + JSON_GETTER_SETTER_REF(updated_at) + JSON_GETTER_SETTER_REF(gear) + JSON_GETTER_SETTER_REF(images) + JSON_GETTER_SETTER(is_public) + JSON_GETTER_SETTER_REF(links) + JSON_GETTER_SETTER_REF(platform) + JSON_GETTER_SETTER(models_count) + JSON_GETTER_SETTER(favorites_count) + JSON_GETTER_SETTER(downloads_count) + JSON_GETTER_SETTER_REF(license) + JSON_GETTER_SETTER_REF(sizes) + JSON_GETTER_SETTER_REF(a1_models_count) + JSON_GETTER_SETTER_REF(a2_models_count) + JSON_GETTER_SETTER_REF(irs_count) + JSON_GETTER_SETTER_REF(custom_models_count) + JSON_GETTER_SETTER_REF(user) + JSON_GETTER_SETTER_REF(url) + JSON_GETTER_SETTER_REF(models) - Tone3000DownloadResult(); - Tone3000DownloadResult(const std::exception &e); + DECLARE_JSON_MAP(Tone3000Download); + }; - DECLARE_JSON_MAP(Tone3000DownloadResult); - }; - - extern std::string DownloadTone3000Bundle - ( - const std::filesystem::path&destinationFolder, - const std::string &downloadUrl, - int64_t downloadHandle, - std::function progressCallback, - std::function isCancelled - ); - } - -} \ No newline at end of file +} diff --git a/src/Tone3000DownloadType.hpp b/src/Tone3000DownloadType.hpp index 3d24738..6a1ef93 100644 --- a/src/Tone3000DownloadType.hpp +++ b/src/Tone3000DownloadType.hpp @@ -26,8 +26,8 @@ namespace pipedal { enum class Tone3000DownloadType { - Nam, - CabIr + Nam = 0, + CabIr = 1 }; Tone3000DownloadType StringToTone3000DownloadType(const std::string &value); diff --git a/src/Tone3000Downloader.cpp b/src/Tone3000Downloader.cpp index ed4c28b..cb15224 100644 --- a/src/Tone3000Downloader.cpp +++ b/src/Tone3000Downloader.cpp @@ -26,8 +26,34 @@ #include "Tone3000Download.hpp" #include "Uri.hpp" #include +#include "HtmlHelper.hpp" +#include "Curl.hpp" +#include +#include +#include +#include +#include +#include "TemporaryFile.hpp" +#include "HtmlHelper.hpp" using namespace pipedal; +namespace fs = std::filesystem; + +static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"}; +static const std::filesystem::path TONE3000_THUMBNAIL_PATH{"/var/pipedal/tone3000_thumbnails"}; + +class Tone3000CancelledError : std::exception +{ +public: + Tone3000CancelledError() = default; + virtual ~Tone3000CancelledError() = default; + + virtual const char *what() const noexcept override + { + std::exception::what(); + return "Cancelled."; + } +}; std::shared_ptr Tone3000Downloader::Create() { @@ -42,6 +68,9 @@ Tone3000Downloader::~Tone3000Downloader() { } +Tone3000DownloaderImpl::Tone3000DownloaderImpl() +{ +} Tone3000DownloaderImpl::~Tone3000DownloaderImpl() { Close(); @@ -59,42 +88,13 @@ Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::NextHandle() handle_t result = this->nextHandle++; return result; } -Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestDownload( - Tone3000DownloadType downloadType, - const std::string &path, - const std::string &url) -{ - handle_t handle; - { - std::lock_guard lockGuard{this->mutex}; - handle = NextHandle(); - std::shared_ptr request = - std::make_shared(downloadType, false, handle, path, url); - - this->requestQueue.push_back(request); - request = nullptr; - if (!this->thread) - { - this->thread = std::make_unique([this] - { this->ThreadProc(); }); - } - } - this->thread_cv.notify_one(); - return handle; -} void Tone3000DownloaderImpl::CancelDownload( handle_t handle) { std::lock_guard lockGuard{this->mutex}; - for (auto it = requestQueue.begin(); it != requestQueue.end(); ++it) - { - if ((*it)->handle == handle) - { - requestQueue.erase(it); - return; - } - } + requestQueue.Erase([handle](std::shared_ptr entry) + { return entry->handle = handle; }); if (activeRequest) { if (activeRequest->handle == handle) @@ -115,8 +115,6 @@ void Tone3000DownloaderImpl::Close() notify = true; } - this->requestQueue.resize(0); - Tone3000DownloadProgress progress; fgDownloadProgress = progress; @@ -129,10 +127,7 @@ void Tone3000DownloaderImpl::Close() activeRequest->cancelled = true; } } - if (notify) - { - thread_cv.notify_one(); - } + this->requestQueue.CloseAndClear(); } Tone3000DownloadProgress Tone3000DownloaderImpl::GetDownloadStatus() { @@ -162,79 +157,54 @@ void Tone3000DownloaderImpl::bgUpdateDownloadProgress(const Tone3000DownloadProg } } -std::string Tone3000DownloaderImpl::PerformDownload( - Tone3000DownloadType downloadType, - const std::string &downloadPath, - const std::string &downloadUrl, - int64_t downloadHandle, - std::function isCancelled) +static void ValidateTone3000Url(const std::string &url) { - std::string downloadedDirectory; - // Validate Tone3000 URL - uri downloadUri{downloadUrl}; + uri downloadUri{url}; if (!downloadUri.authority().ends_with(".tone3000.com")) { throw std::runtime_error("Invalid Tone3000 URL address."); } +} - // Check for cancellation before starting - if (isCancelled()) +namespace +{ + template + static T ParseIntegerOrThrow(const std::string &text, const char *fieldName) { - return downloadedDirectory; - } + static_assert(std::is_integral_v, "ParseIntegerOrThrow requires an integral type."); - Tone3000DownloadProgress progress; - this->bgUpdateDownloadProgress(progress); - // Perform the download using existing code - downloadedDirectory = tone3000::DownloadTone3000Bundle( - downloadPath, - downloadUrl, - downloadHandle, - [this](const Tone3000DownloadProgress &progress) + T value{}; + const char *begin = text.data(); + const char *end = begin + text.size(); + auto [ptr, ec] = std::from_chars(begin, end, value); + if (ec == std::errc::invalid_argument || ptr != end) { - this->bgUpdateDownloadProgress(progress); - }, - [isCancelled]() + throw std::runtime_error(SS("Tone3000 Auth: invalid " << fieldName << ".")); + } + if (ec == std::errc::result_out_of_range) { - return isCancelled(); - }); - - // Check for cancellation after download - if (isCancelled()) - { - // maybe there's a partial result. - return downloadedDirectory; + throw std::runtime_error(SS("Tone3000 Auth: " << fieldName << " is out of range.")); + } + return value; } - return downloadedDirectory; } void Tone3000DownloaderImpl::ThreadProc() { while (true) { - std::shared_ptr request; Listener *currentListener = nullptr; Tone3000DownloadProgress progress; + auto request = requestQueue.Take(); + if (request == nullptr) + { + return; + } + { std::unique_lock lock{this->mutex}; - thread_cv.wait(lock, [this]() - { return this->closed || this->requestQueue.size() != 0; }); - - if (closed) - { - return; - } - - if (requestQueue.empty()) - { - continue; - } - - // Dequeue the next request - request = requestQueue.front(); - requestQueue.erase(requestQueue.begin()); activeRequest = request; currentListener = listener; } @@ -242,7 +212,7 @@ void Tone3000DownloaderImpl::ThreadProc() // Notify download started if (currentListener) { - currentListener->OnStartTone3000Download(request->handle, request->downloadUrl); + currentListener->OnStartTone3000Download(request->handle, ""); } // Create cancellation predicate that checks the atomic flag @@ -253,21 +223,31 @@ void Tone3000DownloaderImpl::ThreadProc() try { - std::string outputPath = PerformDownload( - request->downloadType, - request->downloadPath, - request->downloadUrl, - request->handle, - isCancelled); + this->DownloadTone3000ToneBg( + request); + // std::string outputPath; - // Notify completion - { - std::lock_guard lockGuard{mutex}; - if (currentListener) - { - currentListener->OnTone3000DownloadComplete(request->handle, outputPath); - } - } + // Tone3000DownloadProgress progress; + // progress.total(request->tone3000DownloadRequest.models().size()); + // progress.handle(request->handle); + + // this->bgUpdateDownloadProgress(progress); + + // outputPath = PerformFileDownloads( + // request->tone3000DownloadRequest, + // progress, + // [this](Tone3000DownloadProgress &progress) + // { bgUpdateDownloadProgress(progress); }, + // isCancelled); + + // // Notify completion + // { + // std::lock_guard lockGuard{mutex}; + // if (currentListener) + // { + // currentListener->OnTone3000DownloadComplete(request->handle, outputPath); + // } + // } } catch (const std::exception &e) { @@ -286,4 +266,951 @@ void Tone3000DownloaderImpl::ThreadProc() activeRequest = nullptr; } } -} \ No newline at end of file +} + +void Tone3000DownloaderImpl::DownloadTone3000ToneBg( + std::shared_ptr request) +{ + handle_t handle = request->handle; + uri requestUri{request->requestUri}; + const Tone3000PkceParams &pkceParams = request->pkceParams; + const std::string &downloadPath = request->downloadPath; + + Tone3000DownloadProgress progress; + progress.handle(handle); + + try + { + if (listener) + { + listener->OnStartTone3000Download(progress.handle(), "Authenticating..."); + } + progress.title("Authenticating..."); + bgUpdateDownloadProgress(progress); + + auto oAuthResult = handleOAuthCallback(requestUri, pkceParams); + if (!oAuthResult.toneId.has_value()) + { + throw std::runtime_error("Tone3000 Auth failed. tone ID not received."); + } + std::shared_ptr download = GetTone3000Tone(oAuthResult.toneId.value()); + } + catch (const Tone3000CancelledError &e) + { + OnTone3000DownloadCancelled(progress.handle()); + return; + } + catch (const std::exception &e) + { + OnTone3000DownloadError(progress.handle(), e.what()); + return; + } + OnTone3000DownloadComplete(progress.handle(), ""); +} + +Tone3000DownloaderImpl::OAuthCallbackResult Tone3000DownloaderImpl::handleOAuthCallback( + const uri &callbackUri, + const Tone3000PkceParams &pkce) +{ + OAuthCallbackResult result; + + try + { + std::map params; + std::string code = callbackUri.requiredQuery("code"); + std::optional error = callbackUri.optionalQuery("error"); + std::string returnedState = callbackUri.requiredQuery("state"); + std::optional toneId = callbackUri.optionalQuery("tone_id"); + std::optional modelId = callbackUri.optionalQuery("model_id"); + bool canceled = callbackUri.query("canceled") == "true"; + if (error.has_value()) + { + throw std::runtime_error(error.value()); + } + + if (canceled) + { + throw Tone3000CancelledError(); + } + + std::string codeVerifier = pkce.codeVerifier(); + + // Access denied — e.g. model is private and user clicked "Back" + if (error.has_value()) + { + throw std::runtime_error(error.value()); + } + + if (code.empty() || codeVerifier.empty()) + { + throw std::runtime_error("Tone3000 Auth: missing code."); + } + + std::string body = HtmlFormBuilder({{"grant_type", "authorization_code"}, + {"code", code}, + {"code_verifier", codeVerifier}, + {"redirect_uri", pkce.redirectUrl()}, + {"client_id", pkce.publishableKey()}}) + .build(); + + auto postResult = Post( + uri("http://www.tone3000.com/api/v1/oauth/token"), + {{"Content-Type: application/x-www-form-urlencoded"}}, + body); + if (!postResult.ok) + { + throw std::runtime_error(postResult.error); + } + Tone3000AuthResponse authResponse(postResult.body); + this->accessTokens = std::make_shared(authResponse); + + OAuthCallbackResult result; + if (toneId.has_value()) + { + result.toneId = ParseIntegerOrThrow(toneId.value(), "tone_id"); + } + if (modelId.has_value()) + { + result.modelId = ParseIntegerOrThrow(modelId.value(), "model_id"); + } + return result; + } + catch (const std::exception &e) + { + throw; + } +} + + +std::string Tone3000DownloaderImpl::GetBearerToken() const +{ + if (!accessTokens) + { + throw std::runtime_error("Tone3000 auth failure. No access token."); + } + return accessTokens->access_token(); +} + + +std::string Tone3000DownloaderImpl::Tone3000GetText( + const uri &requestedUri +) +{ + TemporaryFile tempFile{WEB_TEMP_DIR}; + + std::vector inputHeaders { + SS("Authorization: Bearer " << this->GetBearerToken()) + }; + int httpCode = CurlGet(requestedUri.str(),tempFile.Path(),nullptr,&inputHeaders); + if (httpCode != 200) + { + throw std::runtime_error( + SS("Http error " << HtmlHelper::httpErrorString(httpCode))); + } + std::string result; + + std::ifstream ifs(tempFile.Path(), std::ios::binary); + if (!ifs) + { + throw std::runtime_error("Tone3000: failed to open response file."); + } + result.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + return result; +} + +Tone3000DownloaderImpl::PostResult Tone3000DownloaderImpl::Post( + const uri &requestedUri, + std::vector headers, + const std::string &body) +{ + PostResult result; + try + { + int response = CurlPostStrings( + requestedUri.str(), + body, + result.body, + nullptr, + &headers + + ); + result.errorCode = response; + if (response != 200) + { + result.ok = false; + result.error = SS("HTPP Error " << response); + } + else + { + result.ok = true; + } + } + catch (const std::exception &e) + { + result.ok = false; + result.error = e.what(); + } + return result; +} + +Tone3000AuthResponse::Tone3000AuthResponse(const std::string &responseBody) +{ + std::stringstream ss(responseBody); + json_reader reader(ss); + reader.read(this); + expires_at_ = clock_t::now() + std::chrono::seconds(expires_in_); +} + +Tone3000AccessTokens::Tone3000AccessTokens(const Tone3000AuthResponse &authResponse) + : access_token_(authResponse.access_token()), + expires_in_(authResponse.expires_in()), + refresh_token_(authResponse.refresh_token()), + token_type_(authResponse.token_type()) +{ +} + +Tone3000DownloaderImpl::handle_t Tone3000DownloaderImpl::RequestTone3000Download( + const uri &uri, + const Tone3000PkceParams &pkceParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) +{ + handle_t handle; + { + std::lock_guard lockGuard{this->mutex}; + handle = NextHandle(); + std::shared_ptr request = + std::make_shared(handle, uri.str(), pkceParams, downloadPath, downloadType); + + this->requestQueue.Put(request); + request = nullptr; + if (!this->thread) + { + this->thread = std::make_unique([this] + { this->ThreadProc(); }); + } + } + return handle; +} + + +static void cancellableSleep(std::chrono::steady_clock::duration duration, std::function &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 std::string mdSanitize(const std::string &str, const std::string &illegalCharacters = "") +{ + std::ostringstream os; + for (char c : str) + { + if ((unsigned char)c < 0x20) + continue; + if (c == '<') + { + os << "<"; + continue; + } + auto npos = illegalCharacters.find_first_of(c); + if (npos != std::string::npos) + { + continue; + } + os << c; + } + return os.str(); +} + +static std::string mdDate(const tone3000_time_point &date_) +{ + std::ostringstream os; + // C++ doesn't handle timezones. Just zap the timezone, and replace it with "Z" + std::string date = date_; + + // Remove timezone offset if present and replace with Z + size_t plusPos = date.find('+'); + if (plusPos != std::string::npos) + { + date = date.substr(0, plusPos) + "Z"; + } + // Parse ISO 8601 date string into tm + std::tm tm = {}; + std::istringstream ss(date); + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + os << date; + return os.str(); + } + + os << std::put_time(&tm, "%x"); + return os.str(); +} + +static std::string mdEnum(const std::string &value, const std::map &enumValues) +{ + auto ff = enumValues.find(value); + if (ff == enumValues.end()) + { + return value; + } + return ff->second; +} + +static std::string mdEnumList( + const std::vector &values, + const std::map &enumValues) +{ + if (values.size() == 1) + { + return mdEnum(values[0], enumValues); + } + std::ostringstream os; + os << '['; + bool first = true; + for (const auto &value : values) + { + if (first) + { + first = false; + } + else + { + os << ", "; + } + os << mdEnum(value, enumValues); + } + os << ']'; + return os.str(); +} + +static std::map sizeEnumValues = + { + {"standard", "Standard"}, + {"lite", "Lite"}, + {"feather", "Feather"}, + {"nano", "Nano"}, + {"custom", "Custom"}}; +static std::string mdSizes(const std::vector &sizes) +{ + return mdEnumList(sizes, sizeEnumValues); +} + +static std::map gearEnumValues = + { + {"amp", "Amp only"}, + {"full-rig", "Full rig"}, + {"pedal", "Pedal"}, + {"outboard", "Outboard"}, + {"ir", "I/R"}}; + +static std::string mdGear(const std::string &gear) +{ + return mdEnum(gear, gearEnumValues); +} + +namespace +{ + enum class LicenseFlags + { + None = 0, + Cc = 1, + By = 2, + CcBy = 3, // = Cc | By + Sa = 4, + Nc = 8, + Nd = 16, + Cc0 = 32, + }; + + static bool operator&(LicenseFlags v1, LicenseFlags v2) + { + return (((int)v1) & ((int)v2)) != 0; + } + static LicenseFlags operator|(LicenseFlags v1, LicenseFlags v2) + { + return (LicenseFlags)(((int)(v1)) | ((int)(v2))); + } + + struct LicenseInfo + { + std::string key; + std::string displayName; + std::string url; + LicenseFlags licenseFlags; + }; +} + +static std::vector licenses{ + {.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, + + }, +}; + +static std::map makeLicenseEnum() +{ + std::map result; + for (const auto &license : licenses) + { + result[license.key] = license; + } + return result; +} + +static std::map licenseEnum = makeLicenseEnum(); + +static std::string mdLicenseIcon(LicenseFlags licenseFlag) +{ + std::ostringstream os; + std::string url; + + 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::None: + throw std::runtime_error("Invalid argument."); + } + os << ""; + return os.str(); +} + +static void mdEscapeString(std::ostream &f, const std::string &str) +{ + size_t ix = 0; + while (ix < str.size()) + { + char c = str[ix++]; + if (c == '\n') + { + if (ix < str.size() && str[ix] == '\n') + { + f << "\n\n"; + } + else + { + f << " \n"; // a
in md. + } + } + else + { + f << c; + } + } +} + +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::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd}) + { + if (licenseInfo.licenseFlags & licenseFlag) + { + os << mdLicenseIcon(licenseFlag); + } + } + os << " "; + } + + if (licenseInfo.url != "") + { + os << "" << mdSanitize(licenseInfo.displayName) << ""; + } + else + { + os << mdSanitize(licenseInfo.displayName); + } + return os.str(); +} +// static std::string mdTestLicenses() +// { +// std::ostringstream os; +// for (auto license: licenseEnum) +// { +// os << "
" << mdLicense(license.first) << "\n"; +// } +// return os.str(); +// } +static std::map 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 std::string &user) +{ + return mdSanitize(user); +} + +static void mdLink(std::ostream &f, const std::string &label, const std::string &url) +{ + f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")"; +} + +static void writeReadme(const fs::path &path, const std::string &thumbnailUrl, const Tone3000FileDownloadRequest &download) +{ + // std::ofstream of{path}; + // if (!of.is_open()) + // { + // throw std::runtime_error(SS("Unable to create file " << path)); + // } + + // // clang-format off + // of << + + // "
\n"; + // if (!thumbnailUrl.empty()) + // { + + // of << " \n"; + // } + // of << "
\n" + // << "

" << mdSanitize(download.title()) << "

\n" + // << "
\n" + // << "

\n" + // << mdDate(download.updated_at()) + // << " " + // << mdUser(download.user()) + // << "
\n" + // << " "; + // if (download.sizes().size() != 0) { + // of + // << mdSizes(download.sizes()) << ", " + // ; + // } + // if (download.platform() == "ir") + // { + // of + // << (mdPlatform(download.platform())); + // } else { + // of + // << mdGear(download.gear()) << ", " + // << mdPlatform(download.platform()); + // } + // of << "
\n"; + // if (download.license() != "") { + // of << " License: " << mdLicense(download.license()) << "\n"; + // } + // of + // // << " " << mdTestLicenses() + // << "

\n" + // << "
\n" + // << "
\n" + // << "
\n"; + // // clang-format on + // of << "\n\n"; + // mdEscapeString(of, download.description()); + + // if (download.links().size() > 0) + // { + // of << std::endl; + // of << "## Links" << std::endl + // << std::endl; + // for (auto &link : download.links()) + // { + // of << "- " << "[" << link << "](" << link << ")" << std::endl; + // } + // } +} + +static void CleanUpFailedDownload(const std::filesystem::path &folder, const std::vector &requests) +{ + + std::error_code ec; // do NOT throw errors. + + if (fs::exists(folder)) + { + for (const auto &request : requests) + { + if (fs::exists(request.outputFile)) + { + fs::remove(request.outputFile, ec); // ignoring errors. + } + } + // If there's a readme file, remove that too. + fs::path readmePath = folder / "README.md"; + if (fs::exists(readmePath)) + { + fs::remove(readmePath, ec); // ignoring errors + } + + // if the folder directory is empty, remove the the folder too. + try + { + if (fs::is_empty(folder)) + { + fs::remove(folder, ec); // ignoring errors. + } + } + catch (const std::exception &) + { + } + } +} + +static std::string GetHeader(const std::string &headerName, const std::vector &htmlHeaders) +{ + std::string needle = headerName; + std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char c) + { return std::tolower(c); }); + + for (const auto &header : htmlHeaders) + { + auto pos = header.find(':'); + if (pos == std::string::npos) + { + continue; + } + std::string name = header.substr(0, pos); + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) + { return std::tolower(c); }); + if (name != needle) + { + continue; + } + std::string value = header.substr(pos + 1); + auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + { + return ""; + } + auto last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); + } + return ""; +} + +static bool CancellableSleep(std::chrono::steady_clock::duration duration, std::function &isCancelled) +{ + using clock_t = std::chrono::steady_clock; + + auto start = clock_t::now(); + while (true) + { + auto now = clock_t::now(); + auto elapsed = now - start; + if (elapsed > duration) + { + return true; + } + if (isCancelled()) + { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} +std::string Tone3000DownloaderImpl::DownloadTone3000Files( + const Tone3000FileDownloadRequest &request, + Tone3000DownloadProgress &progress) +{ + + // fs::path bundlePath = fs::path(request.downloadPath()) / StringToSafeFilename(request.title()); + // fs::path resultDirectory = bundlePath; + + // fs::create_directories(bundlePath); + + // std::vector requests; + + // auto&models = request.models(); + + // std::vector headers; // yyy: make sure we have a user agent header! + + // headers.push_back( + // SS("Authorization: Bearer " << request.authToken()) + // ); + // try + // { + // size_t downloadIndex = 0; + // while (downloadIndex < models.size()) + // { + // if (isCancelled()) + // { + // throw std::runtime_error("Cancelled"); + // } + // size_t thisTime = models.size() - downloadIndex; + // thisTime = Tone3000Throttler::instance().ReserveDownloadSlots(thisTime); + // if (thisTime != 0) + // { + // std::vector requestsThisTime; + // for (size_t i = 0; i < thisTime; ++i) + // { + // auto &model = models[downloadIndex+i]; + + // requestsThisTime.push_back( + // CurlDownloadRequest{ + // url: model.url(), + // outputFile: bundlePath / StringToSafeFilename(model.name()) + // }); + // } + + // int result = CurlGet( + // requestsThisTime, + // [&](size_t completed, size_t total) + // { + // if (isCancelled()) + // { + // return false; + // } + // progress.progress(completed + downloadIndex); + // progress.total(requests.size()); + // progressCallback(progress); + // return true; + // }, + // &headers); + + // if (isCancelled()) + // { + // throw std::runtime_error("Cancelled"); + // } + + // if (result != 200) + // { + // throw std::runtime_error(SS("Server download failed. HTTP Error " << HtmlHelper::httpErrorString(result))); + // } + + // downloadIndex += thisTime; + // } + // if (thisTime <= 1) + // { + // CancellableSleep(std::chrono::milliseconds(1000), isCancelled); + // } + // } + + // fs::path readmePath = bundlePath / "README.md"; + + // std::string thumbnailUrl; + // if (!request.imageUrl().empty()) + // { + + // TemporaryFile imageFile{TONE3000_THUMBNAIL_PATH}; + + // std::string imageUrl = request.imageUrl(); + // std::vector imageHeaders; + // try + // { + // size_t reserveCount; + // while (true) + // { + // reserveCount = Tone3000Throttler::instance().ReserveDownloadSlots(1); + // if (reserveCount != 0) + // { + // break; + // } + // std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + // if (isCancelled()) + // { + // throw std::runtime_error("Cancelled"); + // } + // } + // if (CurlGet(imageUrl, imageFile.Path(), &imageHeaders) == 200) + // { + // std::string mediaType = GetHeader("Content-Type", imageHeaders); + // std::string extension; + // if (mediaType == "image/jpeg") + // { + // extension = ".jpeg"; + // } + // else if (mediaType == "image/webp") + // { + // extension = ".webp"; + // } + // else if (mediaType == "image/png") + // { + // extension = ".png"; + // } + // else + // { + // Lv2Log::warning("Tone3000 thumbnail has an unexpected media type of '%s'", mediaType.c_str()); + // throw std::runtime_error("Invalid thumbnail media type."); + // } + // if (mediaType == "image/jpeg") + // { + // fs::path thumnbailTempPath = imageFile.Detach(); + // fs::path finalPath = TONE3000_THUMBNAIL_PATH / SS(request.id() << extension); + // if (fs::exists(finalPath)) + // { + // fs::remove(finalPath); + // } + // fs::create_directories(finalPath.parent_path()); + // fs::rename(thumnbailTempPath, finalPath); + // thumbnailUrl = SS("var/tone3000_thumbnail?id=" << request.id()); + // } + // } + // } + // catch (const std::exception &) + // { + // // ignore. + // } + // } + // writeReadme(readmePath, thumbnailUrl, request); + // } + // catch (const std::exception &e) + // { + // CleanUpFailedDownload(bundlePath, requests); + // if (isCancelled()) + // { + // return ""; + // } + // throw; + // } + + // return resultDirectory; + throw std::runtime_error("Not implemented"); +} + + +std::shared_ptr Tone3000DownloaderImpl::GetTone3000Tone( + int64_t toneId) +{ + std::string jsonResult = Tone3000GetText(SS("https://www.tone3000.com/api/v1/tones/" << toneId)); + std::shared_ptr tone = std::make_shared(jsonResult); + + return tone; +} + +////////////////////////////////////////////////////////////////////////////////// + +JSON_MAP_BEGIN(Tone3000PkceParams) +JSON_MAP_REFERENCE(Tone3000PkceParams, publishableKey) +JSON_MAP_REFERENCE(Tone3000PkceParams, redirectUrl) +JSON_MAP_REFERENCE(Tone3000PkceParams, codeVerifier) +JSON_MAP_REFERENCE(Tone3000PkceParams, codeChallenge) +JSON_MAP_REFERENCE(Tone3000PkceParams, state) +JSON_MAP_END(); + +JSON_MAP_BEGIN(Tone3000ModelInfo) +JSON_MAP_REFERENCE(Tone3000ModelInfo, url) +JSON_MAP_REFERENCE(Tone3000ModelInfo, name) +JSON_MAP_END(); + +// JSON_MAP_BEGIN(Tone3000FileDownloadRequest) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, downloadType) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, downloadPath) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, id) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, codeVerifier) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, state) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, codeChallenge) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, authToken) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, title) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, description) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, imageUrl) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, models) + +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, updated_at) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, user) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, sizes) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, platform) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, gear) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, license) +// JSON_MAP_REFERENCE(Tone3000FileDownloadRequest, links) +// JSON_MAP_END(); + +JSON_MAP_BEGIN(Tone3000AuthResponse) +JSON_MAP_REFERENCE(Tone3000AuthResponse, access_token) +JSON_MAP_REFERENCE(Tone3000AuthResponse, expires_in) +JSON_MAP_REFERENCE(Tone3000AuthResponse, refresh_token) +JSON_MAP_REFERENCE(Tone3000AuthResponse, token_type) +JSON_MAP_REFERENCE(Tone3000AuthResponse, tone_id) +JSON_MAP_REFERENCE(Tone3000AuthResponse, model_id) +JSON_MAP_REFERENCE(Tone3000AuthResponse, canceled) +JSON_MAP_END() diff --git a/src/Tone3000Downloader.hpp b/src/Tone3000Downloader.hpp index 3dea6a0..6690469 100644 --- a/src/Tone3000Downloader.hpp +++ b/src/Tone3000Downloader.hpp @@ -8,10 +8,10 @@ * 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 @@ -21,64 +21,146 @@ * SOFTWARE. */ -#pragma once +#pragma once #include #include #include "Tone3000DownloadProgress.hpp" #include "Tone3000DownloadType.hpp" +#include "json.hpp" +#include "Uri.hpp" -namespace pipedal { +namespace pipedal +{ - class Tone3000Downloader { + class Tone3000PkceParams { + private: + std::string publishableKey_; + std::string redirectUrl_; + std::string codeVerifier_; + std::string codeChallenge_; + std::string state_; + public: + JSON_GETTER_SETTER_REF(publishableKey); + JSON_GETTER_SETTER_REF(redirectUrl); + JSON_GETTER_SETTER_REF(codeVerifier); + JSON_GETTER_SETTER_REF(codeChallenge); + JSON_GETTER_SETTER_REF(state); + + DECLARE_JSON_MAP(Tone3000PkceParams); + }; + class Tone3000ModelInfo + { + private: + std::string url_; + std::string name_; + + public: + JSON_GETTER_SETTER_REF(url); + JSON_GETTER_SETTER_REF(name); + + DECLARE_JSON_MAP(Tone3000ModelInfo); + }; + + // class Tone3000DownloadRequest + // { + // private: + // int64_t downloadType_; + // std::string downloadPath_; + // std::string codeVerifier_; + // std::string state_; + // std::string codeChallenge_; + + // std::string authToken_; + // int64_t id_; + // std::string title_; + // std::string description_; + // std::string imageUrl_; + // std::vector models_; + + // std::string updated_at_; + // std::string user_; + // std::vector sizes_; + // std::string platform_; + // std::string gear_; + // std::string license_; + // std::vector links_; + + // public: + // Tone3000DownloadType downloadType() const { return (Tone3000DownloadType)downloadType_;} + // void downloadType(Tone3000DownloadType value) { downloadType_= (int64_t)value;} + // JSON_GETTER_SETTER_REF(downloadPath); + // JSON_GETTER_SETTER(id); + // JSON_GETTER_SETTER(codeVerifier); + // JSON_GETTER_SETTER(state); + // JSON_GETTER_SETTER(codeChallenge); + // JSON_GETTER_SETTER(authToken); + // JSON_GETTER_SETTER_REF(title); + // JSON_GETTER_SETTER_REF(description) + // JSON_GETTER_SETTER_REF(imageUrl); + // JSON_GETTER_SETTER_REF(models); + + // JSON_GETTER_SETTER_REF(updated_at); + // JSON_GETTER_SETTER_REF(user); + // JSON_GETTER_SETTER_REF(sizes); + // JSON_GETTER_SETTER_REF(platform); + // JSON_GETTER_SETTER_REF(gear); + // JSON_GETTER_SETTER_REF(license); + // JSON_GETTER_SETTER_REF(links); + + + // DECLARE_JSON_MAP(Tone3000DownloadRequest); + // }; + + class Tone3000Downloader + { protected: Tone3000Downloader(); + public: using handle_t = int64_t; virtual ~Tone3000Downloader(); - class Listener { + class Listener + { public: - virtual void OnStartTone3000Download - ( + virtual void OnStartTone3000Download( handle_t handle, - const std::string &title - ) = 0; + const std::string &title) = 0; virtual void OnTone3000Progress( - const Tone3000DownloadProgress& downloadProgress - ) = 0; + const Tone3000DownloadProgress &downloadProgress) = 0; virtual void OnTone3000DownloadComplete( handle_t handle, - const std::string&resultPath - ) = 0; - + const std::string &resultPath) = 0; + virtual void OnTone3000DownloadError( handle_t handle, - const std::string &errorMessage - ) = 0; - - virtual std::string Tone3000ThumbnailDirectory() = 0; + const std::string &errorMessage) = 0; + virtual std::string Tone3000ThumbnailDirectory() = 0; }; - using self = Tone3000Downloader; static std::shared_ptr Create(); - virtual void SetListener(Listener*listener) = 0; - virtual handle_t RequestDownload( - Tone3000DownloadType downloadType, - const std::string &path, - const std::string &url - ) = 0; + virtual void SetListener(Listener *listener) = 0; + virtual void CancelDownload( - handle_t handle - ) = 0; + handle_t handle) = 0; + + virtual handle_t RequestTone3000Download( + const uri &uri, + const Tone3000PkceParams &pkceParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) + = 0; virtual void Close() = 0; virtual Tone3000DownloadProgress GetDownloadStatus() = 0; + + }; } \ No newline at end of file diff --git a/src/Tone3000DownloaderImpl.hpp b/src/Tone3000DownloaderImpl.hpp index 571b9ee..f354eb3 100644 --- a/src/Tone3000DownloaderImpl.hpp +++ b/src/Tone3000DownloaderImpl.hpp @@ -8,10 +8,10 @@ * 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 @@ -21,7 +21,7 @@ * SOFTWARE. */ -#pragma once +#pragma once #include #include @@ -31,55 +31,173 @@ #include #include #include +#include "ThreadedQueue.hpp" +#include "Tone3000Download.hpp" -namespace pipedal { +namespace pipedal +{ + + struct CachedPkceParams + { + std::chrono::steady_clock::time_point time; + Tone3000PkceParams pkceParams; + }; + + class Tone3000AuthResponse + { + private: + std::string access_token_; + int64_t expires_in_ = -1; + std::string refresh_token_; + std::string token_type_; + std::optional tone_id_; + std::optional model_id_; + bool canceled_ = false; + + using clock_t = std::chrono::steady_clock; + clock_t::time_point expires_at_; - class Tone3000DownloaderImpl: public Tone3000Downloader { public: - virtual ~Tone3000DownloaderImpl(); - - virtual void SetListener(Listener*listener) override; + Tone3000AuthResponse(const std::string &responseBody); + JSON_GETTER_REF(access_token); + JSON_GETTER(expires_in); + JSON_GETTER_REF(refresh_token); + JSON_GETTER_REF(token_type); + JSON_GETTER_REF(expires_at); + JSON_GETTER_REF(tone_id); + JSON_GETTER_REF(model_id); + JSON_GETTER_SETTER(canceled); - virtual handle_t RequestDownload( - Tone3000DownloadType downloadType, - const std::string &path, - const std::string &url - ) override; + DECLARE_JSON_MAP(Tone3000AuthResponse); + }; + class Tone3000AccessTokens + { + private: + std::string access_token_; + int64_t expires_in_ = -1; + std::string refresh_token_; + std::string token_type_; + + using clock_t = std::chrono::steady_clock; + clock_t::time_point expires_at_; + + public: + Tone3000AccessTokens(const Tone3000AuthResponse &authResponse); + JSON_GETTER_REF(access_token); + JSON_GETTER(expires_in); + JSON_GETTER_REF(refresh_token); + JSON_GETTER_REF(token_type); + JSON_GETTER_REF(expires_at); + }; + + class Tone3000DownloaderImpl : public Tone3000Downloader + { + public: + Tone3000DownloaderImpl(); + + virtual ~Tone3000DownloaderImpl(); + + virtual void SetListener(Listener *listener) override; virtual void CancelDownload( - handle_t handle - ) override; + handle_t handle) override; virtual void Close() override; virtual Tone3000DownloadProgress GetDownloadStatus() override; + virtual handle_t RequestTone3000Download( + const uri &uri, + const Tone3000PkceParams &pkceParams, + const std::string &downloadPath, + Tone3000DownloadType downloadType) override; + private: + struct DownloadRequest + { + handle_t handle = -1; + std::string requestUri; + Tone3000PkceParams pkceParams; + std::string downloadPath; + Tone3000DownloadType downloadType = Tone3000DownloadType::Nam; + std::atomic cancelled = false; + }; + + void DownloadTone3000ToneBg( + std::shared_ptr request); + void OnTone3000DownloadError(int64_t handle, const std::string &error) + { + if (listener) + { + listener->OnTone3000DownloadError(handle, error); + } + } + std::shared_ptr GetTone3000Tone( + int64_t toneId); + std::string DownloadTone3000Files( + const Tone3000FileDownloadRequest &request, + Tone3000DownloadProgress &progress); + + void OnTone3000DownloadComplete(int64_t handle, const std::string &path) + { + if (listener) + { + listener->OnTone3000DownloadComplete(handle, path); + } + } + void OnTone3000DownloadCancelled(int64_t handle) + { + if (listener) + { + listener->OnTone3000DownloadComplete(handle, ""); + } + } + using clock_t = std::chrono::steady_clock; + std::recursive_mutex pkceCacheMutex; + Listener *listener = nullptr; void ThreadProc(); - + + // Performs the actual download with cancellation support - std::string PerformDownload( - Tone3000DownloadType downloadType, - const std::string &downloadPath, - const std::string &downloadUrl, - int64_t downloadHandle, - std::function isCancelled - ); - - struct DownloadRequest { - Tone3000DownloadType downloadType; - std::atomic cancelled = false; - handle_t handle; - const std::string downloadPath; - const std::string downloadUrl; + // std::string PerformFileDownloads( + // const Tone3000FileDownloadRequest &downloadRequets, + // Tone3000DownloadProgress &progress, + // std::function updateProgress, + // std::function isCancelled); + + struct PostResult + { + bool ok = false; + int errorCode = 0; + std::string error; + std::string body; }; + PostResult Post( + const uri &requestedUri, + std::vector headers, + const std::string &body); + + + std::string Tone3000GetText( + const uri &requestedUri + ); + struct OAuthCallbackResult + { + std::optional toneId; + std::optional modelId; + }; + + OAuthCallbackResult handleOAuthCallback( + const uri &callbackUri, + const Tone3000PkceParams &pkce); + std::atomic closed = false; handle_t nextHandle = 1; handle_t NextHandle(); - std::vector> requestQueue; + ThreadedQueue requestQueue; Tone3000DownloadProgress fgDownloadProgress; void bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress); @@ -87,9 +205,10 @@ namespace pipedal { std::shared_ptr activeRequest; std::mutex mutex; - std::condition_variable thread_cv; - std::unique_ptr thread; + std::shared_ptr accessTokens; + + std::string GetBearerToken() const; }; -} \ No newline at end of file +} diff --git a/src/Uri.cpp b/src/Uri.cpp index 9061189..5cac8e9 100644 --- a/src/Uri.cpp +++ b/src/Uri.cpp @@ -29,55 +29,61 @@ std::string uri::segment(int index) const const char *p = path_start; int segment = 0; - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; while (p != path_end && *p != '?' && *p != '#') { if (segment == index) { - const char*segmentStart = p; - while(p != path_end && *p != '/' && *p != '?' && *p != '#') + const char *segmentStart = p; + while (p != path_end && *p != '/' && *p != '?' && *p != '#') { ++p; } - return HtmlHelper::decode_url_segment(segmentStart,p); - } else { - while(p != path_end && *p != '/' && *p != '?' && *p != '#') + return HtmlHelper::decode_url_segment(segmentStart, p); + } + else + { + while (p != path_end && *p != '/' && *p != '?' && *p != '#') { ++p; } } - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; ++segment; } throw std::invalid_argument("Invalid segement number."); }; - -int uri::segment_count() const { +int uri::segment_count() const +{ const char *p = path_start; int segment = 0; - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; while (p != path_end && *p != '?' && *p != '#') { - while(p != path_end && *p != '/' && *p != '?' && *p != '#') + while (p != path_end && *p != '/' && *p != '?' && *p != '#') { ++p; } - if (p != path_end && *p == '/') ++p; + if (p != path_end && *p == '/') + ++p; ++segment; } return segment; } -void uri::set(const char*start, const char*end) +void uri::set(const char *start, const char *end) { - this->text = std::string(start,end); + this->text = std::string(start, end); set_(); } -void uri::set(const char*text) +void uri::set(const char *text) { this->text = text; set_(); @@ -85,10 +91,10 @@ void uri::set(const char*text) void uri::set_() { - const char*start = this->text.c_str(); - const char*end = start+this->text.size(); + const char *start = this->text.c_str(); + const char *end = start + this->text.size(); - const char* p = start; + const char *p = start; this->isRelative = true; @@ -96,7 +102,8 @@ void uri::set_() while (p != end) { char c = *p; - if (c == ':') { + if (c == ':') + { hasScheme = true; break; } @@ -111,12 +118,14 @@ void uri::set_() scheme_start = start; scheme_end = p; ++p; - } else { + } + else + { scheme_start = start; scheme_end = start; p = start; } - if (p[0] == '/' && p[1] == '/') + if (p[0] == '/' && p[1] == '/') { this->isRelative = false; // authority. @@ -127,20 +136,22 @@ void uri::set_() authority_start = p; authority_end = nullptr; - const char*port_start = nullptr; + const char *port_start = nullptr; while (p != end) { char c = *p; if (c == '@') { user_end = p; - authority_start = p+1; + authority_start = p + 1; port_start = nullptr; - } else if (c == ':') + } + else if (c == ':') { authority_end = p; - port_start = p+1; - } else if (c == ']') // ignore colon inside ipv6 address. + port_start = p + 1; + } + else if (c == ']') // ignore colon inside ipv6 address. { port_start = nullptr; } @@ -150,16 +161,22 @@ void uri::set_() } ++p; } - if (authority_end == nullptr) authority_end = p; + if (authority_end == nullptr) + authority_end = p; if (port_start != nullptr) { - char*intEnd; - port_ = (int)std::strtol(port_start,&intEnd,10); - if (intEnd != p) throwInvalid(); - } else { + char *intEnd; + port_ = (int)std::strtol(port_start, &intEnd, 10); + if (intEnd != p) + throwInvalid(); + } + else + { port_ = -1; } - } else { + } + else + { user_start = p; user_end = p; authority_start = p; @@ -168,7 +185,8 @@ void uri::set_() } path_start = p; - if (p != end && *p == '/') this->isRelative = false; + if (p != end && *p == '/') + this->isRelative = false; while (p != end && *p != '?' && *p != '#') { @@ -185,7 +203,9 @@ void uri::set_() ++p; } query_end = p; - } else { + } + else + { query_start = query_end = p; } if (p != end && *p == '#') @@ -193,62 +213,64 @@ void uri::set_() ++p; fragment_start = p; fragment_end = end; - - } else { + } + else + { fragment_start = fragment_end = p; } } -void uri::throwInvalid() { +void uri::throwInvalid() +{ throw std::invalid_argument("Invalid uri."); - } -static bool compare_name(const char*start, const char*end, const char*szName) +static bool compare_name(const char *start, const char *end, const char *szName) { while (start != end) { - if (*start != *szName) return false; - ++start; ++szName; + if (*start != *szName) + return false; + ++start; + ++szName; } return *szName == 0; } -int uri::query_count() const +int uri::query_count() const { int count = 0; - const char*p = query_start; + const char *p = query_start; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '#') { ++p; } - if (p != query_end && *p == '&') ++p; + if (p != query_end && *p == '&') + ++p; ++count; if (p == query_end || *p == '#') { break; - } } return count; - } -bool uri::has_query(const char*name) const +bool uri::has_query(const char *name) const { - const char*p = query_start; + const char *p = query_start; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; } // compare names. - if (compare_name(nameStart,p,name)) + if (compare_name(nameStart, p, name)) { return true; } @@ -256,111 +278,128 @@ bool uri::has_query(const char*name) const { ++p; } - if (p != query_end && *p == '&') ++p; + if (p != query_end && *p == '&') + ++p; } return false; - } query_segment uri::query(int index) const { - const char*p = query_start; + const char *p = query_start; int ix = 0; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; } // compare names. - //if (compare_name(nameStart,p,name)) + // if (compare_name(nameStart,p,name)) if (ix == index) { const char *nameEnd = p; if (p != query_end && *p == '=') { ++p; - const char*value_start = p; - while(p != query_end && *p != '&' && *p != '#') + const char *value_start = p; + while (p != query_end && *p != '&' && *p != '#') { ++p; } return query_segment( - std::string(nameStart,nameEnd), - std::string(value_start,p) - ); - - } else { + std::string(nameStart, nameEnd), + std::string(value_start, p)); + } + else + { return query_segment( - std::string(nameStart,nameEnd), - "" - ); + std::string(nameStart, nameEnd), + ""); } } while (p != query_end && *p != '&' && *p != '#') { ++p; } - if (p != query_end && *p == '&') + if (p != query_end && *p == '&') { ++p; } ++ix; - } throw PiPedalArgumentException("Index out of range."); } -std::string uri::query(const char*name) const +std::optional uri::optionalQuery(const char *name) const { - const char*p = query_start; + const char *p = query_start; while (p != query_end && *p != '#') { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; } // compare names. - if (compare_name(nameStart,p,name)) + if (compare_name(nameStart, p, name)) { if (p != query_end && *p == '=') { ++p; - const char*value_start = p; - while(p != query_end && *p != '&' && *p != '#') + const char *value_start = p; + while (p != query_end && *p != '&' && *p != '#') { ++p; } - return HtmlHelper::decode_url_segment(value_start,p); - } else { - return ""; + return HtmlHelper::decode_url_segment(value_start, p); + } + else + { + return std::optional(); } } while (p != query_end && *p != '&' && *p != '#') { ++p; } - if (p != query_end && *p == '&') + if (p != query_end && *p == '&') { ++p; } - + } + return std::optional(); +} +std::string uri::query(const char *name) const +{ + auto result = optionalQuery(name); + if (result.has_value()) + { + return result.value(); } return ""; } +std::string uri::requiredQuery(const char *name) const +{ + auto result = optionalQuery(name); + if (result.has_value()) + { + return result.value(); + } + throw std::runtime_error("Invalid url."); +} query_segment uri::query(int index) { - const char*p = query_start; + const char *p = query_start; int count = 0; while (p != query_end && *p != '#') { if (count == index) { - const char*nameStart = p; + const char *nameStart = p; while (p != query_end && *p != '&' && *p != '=' && *p != '#') { ++p; @@ -368,28 +407,32 @@ query_segment uri::query(int index) const char *nameEnd = p; const char *valueStart, *valueEnd; - if(p == query_end || *p != '=') + if (p == query_end || *p != '=') { valueStart = valueEnd = p; - } else { + } + else + { ++p; valueStart = p; - while(p != query_end && *p != '&' && *p != '#') + while (p != query_end && *p != '&' && *p != '#') { ++p; } valueEnd = p; } - return query_segment(std::string(nameStart,nameEnd), HtmlHelper::decode_url_segment(valueStart,valueEnd)); - } else { + return query_segment(std::string(nameStart, nameEnd), HtmlHelper::decode_url_segment(valueStart, valueEnd)); + } + else + { while (p != query_end && *p != '&' && *p != '#') { ++p; } - } - if (p != query_end && *p == '&') ++p; + if (p != query_end && *p == '&') + ++p; ++count; } throw std::invalid_argument("Argument out of range."); @@ -407,66 +450,76 @@ std::vector uri::segments() std::string uri::get_extension() const { - const char*p = this->path_end; + const char *p = this->path_end; while (p != this->path_start) { - char c = p[-1]; - if (c == '/') return ""; - if (c == '.') { - --p; - return std::string(p,this->path_end-p); - } - --p; + char c = p[-1]; + if (c == '/') + return ""; + if (c == '.') + { + --p; + return std::string(p, this->path_end - p); + } + --p; } return ""; } -std::string uri::to_canonical_form() const { +std::string uri::to_canonical_form() const +{ uri_builder builder(*this); return builder.str(); } -std::string uri_builder::str() const { +std::string uri_builder::str() const +{ std::stringstream s; if (scheme_.length() != 0) { s << scheme_ << ':'; } - if (authority_.length() != 0) { + if (authority_.length() != 0) + { s << "//"; - if (user_.length() != 0) + if (user_.length() != 0) { s << user_ << '@'; } s << authority_; - if (port_ != -1) { + if (port_ != -1) + { s << ':' << (uint16_t)port_; } } - if ( authority_.length() != 0 || !isRelative_) { // canonical root form is http://xyz/ not httP://xyz - + if (authority_.length() != 0 || !isRelative_) + { // canonical root form is http://xyz/ not httP://xyz + s << '/'; } for (size_t i = 0; i < segments_.size(); ++i) { - if (i != 0) s << '/'; - HtmlHelper::encode_url_segment(s,segments_[i],false); + if (i != 0) + s << '/'; + HtmlHelper::encode_url_segment(s, segments_[i], false); } for (size_t i = 0; i < queries_.size(); ++i) { - if (i == 0) + if (i == 0) { s << '?'; - } else { + } + else + { s << '&'; } s << queries_[i].key << "="; - HtmlHelper::encode_url_segment(s,queries_[i].value,true); + HtmlHelper::encode_url_segment(s, queries_[i].value, true); } if (fragment_.length() != 0) { s << "#" << fragment_; } return s.str(); +} -} \ No newline at end of file diff --git a/src/Uri.hpp b/src/Uri.hpp index 52951f8..3208e10 100644 --- a/src/Uri.hpp +++ b/src/Uri.hpp @@ -24,214 +24,239 @@ #include #include #include +#include -namespace pipedal { - - -class query_segment { - -public: - query_segment(const std::string &key, const std::string &value) - : key(key) - , value(value) { - - } - const std::string key; - const std::string value; - - bool operator==(const query_segment &other) - { - return key == other.key && value == other.value; - } -}; - - -class uri +namespace pipedal { -private: - std::string text; - const char*scheme_start, *scheme_end; - const char *user_start, *user_end; - const char* authority_start, *authority_end; - const char* path_start, *path_end; - bool isRelative; - int port_; - const char*query_start, *query_end; - const char*fragment_start, *fragment_end; -public: - uri() - { - set(""); - } - const std::string& str() const { - return text; - } - uri(const char*text) - { - set(text); - } - uri(const std::string&text) - { - set(text.c_str()); - } - void set(const char*text); - void set(const char*start, const char*end); -private: - void set_(); - static void throwInvalid(); -public: - bool has_scheme() const { return scheme_start != scheme_end; } - std::string scheme() const{ return std::string(scheme_start, scheme_end); } - bool has_user() const{ return user_start != user_end; } - std::string user() const{ return std::string(user_start,user_end); } - - bool has_authority() const{ return authority_start != authority_end; } - std::string authority() const{ return std::string(authority_start,authority_end);} - - bool has_port() const{ return port_ != -1; } - int port() const{ return port_; } - - bool is_relative() const { return isRelative; } - - std::string path() const { return std::string(path_start, path_end); } - - int segment_count() const; - - std::string segment(int n) const; - - std::vector segments(); - const std::vector segments() const; - - int query_count() const; - - query_segment query(int index); - - bool has_query(const char*name) const; - - std::string query(const char*name) const; - query_segment query(int index) const; - - std::string get_extension() const; - - std::string fragment() const { return std::string(fragment_start, fragment_end);} - - std::string to_canonical_form() const; - -}; - -class uri_builder { - -private: - std::string scheme_ = "http"; - std::string user_ = ""; - std::string authority_ = ""; - int port_ = -1; - bool isRelative_ = false; - std::vector segments_; - - std::vector queries_; - std::string fragment_; - -public: - uri_builder() + class query_segment { - } - - uri_builder(const uri&uri) - : scheme_(uri.scheme()), - user_(uri.user()), - authority_(uri.authority()), - port_(uri.port()), - isRelative_(uri.is_relative()), - fragment_(uri.fragment()) - { - for (int i = 0; i < uri.segment_count(); ++i) + public: + query_segment(const std::string &key, const std::string &value) + : key(key), value(value) { - segments_.push_back(uri.segment(i)); } - for (int i = 0; i < uri.query_count(); ++i) + const std::string key; + const std::string value; + + bool operator==(const query_segment &other) { - queries_.push_back(uri.query(i)); + return key == other.key && value == other.value; } - } - std::string str() const; + }; - const std::string & scheme() const { return scheme_; } - void set_scheme(const std::string &scheme_) { - this->scheme_ = scheme_; - } - const std::string & user() const { return this->user_;} - void set_user(const std::string&user) { this->user_ = user; } - - const std::string &authority() const { - return this->authority_; - } - void set_authority(const std::string &authority) { - this->authority_ = authority; - } - int port() const { return port_; } - void set_port(int port) { - this->port_ = port; - } - - bool is_relative() const { return this->isRelative_ && authority_.length() == 0; } - void set_is_relative(bool isRelative) + class uri { - this->isRelative_ = isRelative; - } - int segment_count() const { return (int)segments_.size(); } - const std::string& segment(int i) const { return segments_[i];} + private: + std::string text; + const char *scheme_start, *scheme_end; + const char *user_start, *user_end; + const char *authority_start, *authority_end; + const char *path_start, *path_end; + bool isRelative; + int port_; + const char *query_start, *query_end; + const char *fragment_start, *fragment_end; - void append_segment(const std::string & segment) { - segments_.push_back(segment); - } - void insert_segment(int position, std::string & segment) { - segments_.insert(segments_.begin()+position,segment); - } - void erase_segment(int position) + public: + uri() + { + set(""); + } + const std::string &str() const + { + return text; + } + uri(const char *text) + { + set(text); + } + uri(const std::string &text) + { + set(text.c_str()); + } + uri(const uri&other) + { + set(other.text.c_str()); + } + void set(const char *text); + void set(const char *start, const char *end); + + private: + void set_(); + static void throwInvalid(); + + public: + bool has_scheme() const { return scheme_start != scheme_end; } + std::string scheme() const { return std::string(scheme_start, scheme_end); } + + bool has_user() const { return user_start != user_end; } + std::string user() const { return std::string(user_start, user_end); } + + bool has_authority() const { return authority_start != authority_end; } + std::string authority() const { return std::string(authority_start, authority_end); } + + bool has_port() const { return port_ != -1; } + int port() const { return port_; } + + bool is_relative() const { return isRelative; } + + std::string path() const { return std::string(path_start, path_end); } + + int segment_count() const; + + std::string segment(int n) const; + + std::vector segments(); + const std::vector segments() const; + + int query_count() const; + + query_segment query(int index); + + bool has_query(const char *name) const; + + std::string query(const char *name) const; + std::optional optionalQuery(const char *name) const; + std::string requiredQuery(const char *name) const; + + query_segment query(int index) const; + + std::string get_extension() const; + + std::string fragment() const { return std::string(fragment_start, fragment_end); } + + std::string to_canonical_form() const; + }; + + class uri_builder { - segments_.erase(segments_.begin()+position); - } - void replace_segment(int position, std::string& segment) { - segments_[position] = segment; - } + private: + std::string scheme_ = "http"; + std::string user_ = ""; + std::string authority_ = ""; + int port_ = -1; + bool isRelative_ = false; + std::vector segments_; - int query_count() const { return (int)(queries_.size());} + std::vector queries_; + std::string fragment_; - bool has_query(const std::string &key) const { - for (size_t i = 0; i < queries_.size(); ++i) + public: + uri_builder() { - if (queries_[i].key == key) true; } - return false; - } - std::string query(const std::string &key) const { - for (size_t i = 0; i < queries_.size(); ++i) + uri_builder(const uri &uri) + : scheme_(uri.scheme()), + user_(uri.user()), + authority_(uri.authority()), + port_(uri.port()), + isRelative_(uri.is_relative()), + fragment_(uri.fragment()) { - if (queries_[i].key == key) return queries_[i].value; + for (int i = 0; i < uri.segment_count(); ++i) + { + segments_.push_back(uri.segment(i)); + } + for (int i = 0; i < uri.query_count(); ++i) + { + queries_.push_back(uri.query(i)); + } } - return ""; - } - std::vector queries(const std::string &key) const { - std::vector result; - for (size_t i = 0; i < queries_.size(); ++i) + std::string str() const; + + const std::string &scheme() const { return scheme_; } + void set_scheme(const std::string &scheme_) { - if (queries_[i].key == key) result.push_back(queries_[i].value); + this->scheme_ = scheme_; } - return result; - } - const query_segment & query(int index) { - return queries_[index]; - } + const std::string &user() const { return this->user_; } + void set_user(const std::string &user) { this->user_ = user; } - const std::string&fragment() { return this->fragment_; } - void set_fragment(const std::string & fragment) { this->fragment_ = fragment;} + const std::string &authority() const + { + return this->authority_; + } + void set_authority(const std::string &authority) + { + this->authority_ = authority; + } + int port() const { return port_; } + void set_port(int port) + { + this->port_ = port; + } + bool is_relative() const { return this->isRelative_ && authority_.length() == 0; } + void set_is_relative(bool isRelative) + { + this->isRelative_ = isRelative; + } + int segment_count() const { return (int)segments_.size(); } + const std::string &segment(int i) const { return segments_[i]; } -}; + void append_segment(const std::string &segment) + { + segments_.push_back(segment); + } + void insert_segment(int position, std::string &segment) + { + segments_.insert(segments_.begin() + position, segment); + } + void erase_segment(int position) + { + segments_.erase(segments_.begin() + position); + } + void replace_segment(int position, std::string &segment) + { + segments_[position] = segment; + } + + void set_path(const std::string &path); + + int query_count() const { return (int)(queries_.size()); } + + bool has_query(const std::string &key) const + { + for (size_t i = 0; i < queries_.size(); ++i) + { + if (queries_[i].key == key) + true; + } + return false; + } + std::string query(const std::string &key) const + { + for (size_t i = 0; i < queries_.size(); ++i) + { + if (queries_[i].key == key) + return queries_[i].value; + } + return ""; + } + std::vector queries(const std::string &key) const + { + std::vector result; + for (size_t i = 0; i < queries_.size(); ++i) + { + if (queries_[i].key == key) + result.push_back(queries_[i].value); + } + return result; + } + const query_segment &query(int index) + { + return queries_[index]; + } + void add_query(const std::string &key, const std::string &value) + { + queries_.push_back(query_segment(key, value)); + } + + const std::string &fragment() { return this->fragment_; } + void set_fragment(const std::string &fragment) { this->fragment_ = fragment; } + }; }; // namespace pipedal. - diff --git a/src/WebServer.cpp b/src/WebServer.cpp index b6f00ef..281664d 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -89,6 +89,7 @@ public: std::istream &get_body_input_stream(); const std::filesystem::path &get_body_input_file(); + void detach_body_input_file(); size_t content_length() const { return m_content_length; } /// Returns the full raw request (including the body) @@ -163,6 +164,13 @@ const std::filesystem::path &request_with_file_upload::get_body_input_file() throw std::runtime_error("Request does not have a body."); return this->m_temporaryFile->Path(); } +void request_with_file_upload::detach_body_input_file() +{ + if (this->m_temporaryFile) + { + this->m_temporaryFile->Detach(); + } +} std::istream &request_with_file_upload::get_body_input_stream() { if (!m_outputOpen) @@ -711,6 +719,10 @@ namespace pipedal return m_request.get_body_input_file(); } + virtual void detach_body_temporary_file() override { + return m_request.detach_body_input_file(); + } + virtual size_t content_length() const { return m_request.content_length(); } virtual const std::string &method() const { return m_request.get_method(); } diff --git a/src/WebServer.hpp b/src/WebServer.hpp index 1d9bd03..6b2dae3 100644 --- a/src/WebServer.hpp +++ b/src/WebServer.hpp @@ -30,6 +30,7 @@ public: //virtual const std::string&body() const = 0; virtual std::istream &get_body_input_stream() = 0; virtual const std::filesystem::path& get_body_temporary_file() = 0; + virtual void detach_body_temporary_file() = 0; virtual size_t content_length() const = 0; virtual const std::string &method() const = 0; virtual const std::string&get(const std::string&key) const = 0; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 9681552..e255b3f 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -81,6 +81,17 @@ static std::string GetMimeType(const std::filesystem::path &path) return result; } +static bool IsSafeMediaPath(const fs::path path) +{ + if (!HtmlHelper::IsSafeFileName(path)) { + return false; + } + if (!(path.string().starts_with("/var/pipedal/audio_uploads"))) + { + return false; + } + return true; +} int32_t ConvertThumbnailSize(const std::string ¶m) { if (param.empty()) @@ -128,8 +139,8 @@ private: std::vector extensions; }; - -static fs::path GetTone3000ThumbnailFile(const fs::path &thumbnailDirectory, const std::string & id) { +static fs::path GetTone3000ThumbnailFile(const fs::path &thumbnailDirectory, const std::string &id) +{ std::string stem = id; // find a file in thumbnailDirectory which has a filename of {stem}.*. if (!fs::exists(thumbnailDirectory) || !fs::is_directory(thumbnailDirectory)) @@ -150,8 +161,7 @@ static fs::path GetTone3000ThumbnailFile(const fs::path &thumbnailDirectory, con } } return {}; - -} +} class DownloadIntercept : public RequestHandler { @@ -172,6 +182,14 @@ public: } std::string segment = request_uri.segment(1); + if (segment == "t3k_uploadAsset") + { + return true; + } + if (segment == "t3k_response.html") + { + return true; + } if (segment == "tone3000_thumbnail") { return true; @@ -181,6 +199,11 @@ public: { return true; } + if (segment == "displayMediaFile") + { + return true; + } + if (segment == "uploadPluginPresets") { return true; @@ -330,7 +353,20 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "tone3000_thumbnail") + if (segment == "t3k_uploadAsset") + { + res.set(HttpField::content_length, "0"); + return; + } + if (segment == "t3k_response.html") + { + std::string mimeType = GetMimeType("x.html"); + auto text = T3kResponse(); + res.set(HttpField::content_type, mimeType); + res.set(HttpField::content_length, SS(text.size())); + return; + } + else if (segment == "tone3000_thumbnail") { std::string id = request_uri.query("id"); if (id == "") @@ -338,7 +374,7 @@ public: throw PiPedalException("Invalid request."); } fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id); - if (!fs::exists(path)) + if (!fs::exists(path)) { throw PiPedalException("File not found."); } @@ -353,7 +389,7 @@ public: return; } - if (segment == "downloadMediaFile") + if (segment == "downloadMediaFile" || segment == "displayMediaFile") { fs::path path = request_uri.query("path"); @@ -375,8 +411,11 @@ public: } res.set(HttpField::content_type, mimeType); res.set(HttpField::cache_control, "no-cache"); - std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); - res.set(HttpField::content_disposition, disposition); + if (segment == "downloadMediaFile") + { + std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); + res.set(HttpField::content_disposition, disposition); + } size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); return; @@ -465,18 +504,41 @@ public: bool isInfoFile(const fs::path &path) { auto extension = path.extension(); - if (extension == ".md" || extension == ".txt") + if (extension == ".pdf") { return true; } - auto filename = path.stem(); - if (filename == "LICENSE" || filename == "README") + auto filename = path.filename(); + if (filename == "LICENSE.txt" || filename == "README.txt") + { + return true; + } + if (filename == "LICENSE.md" || filename == "README.md") + { + return true; + } + if (filename == "license.txt" || filename == "readme.txt") { return true; } return false; } + std::string T3kResponse() + { + return "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; + } + void DownloadTone3000Tone(const uri &requestUri) + { + } virtual void get_response( const uri &request_uri, HttpRequest &req, @@ -487,11 +549,15 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "tone3000_thumbnail") + if (segment == "t3k_response.html") + { + throw std::runtime_error("Not implemented."); + } + if (segment == "tone3000_thumbnail") { std::string id = request_uri.query("id"); fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id); - if (!fs::exists(path)) + if (!fs::exists(path)) { throw PiPedalException("File not found."); } @@ -505,8 +571,8 @@ public: res.setContentLength(contentLength); res.setBodyFile(path, false); return; - - } else if (segment == "downloadMediaFile") + } + else if (segment == "downloadMediaFile" || segment == "displayMediaFile") { fs::path path = request_uri.query("path"); @@ -528,8 +594,11 @@ public: } res.set(HttpField::content_type, mimeType); res.set(HttpField::cache_control, "no-cache"); - std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); - res.set(HttpField::content_disposition, disposition); + if (segment == "downloadMediaFile") + { + std::string disposition = GetContentDispositionHeader(path.stem().string(), path.extension().string()); + res.set(HttpField::content_disposition, disposition); + } size_t contentLength = std::filesystem::file_size(path); res.setContentLength(contentLength); res.setBodyFile(path, false); @@ -826,6 +895,75 @@ public: return true; } + void UploadEmbeddedZipFile( + PiPedalModel *model, + pipedal::zip_file_input_stream &si, + const std::string &inputFileName, + fs::path directory, + ExtensionChecker &extensionChecker, + int64_t instanceId, + const std::string &patchProperty) + { + TemporaryFile tempFile{WEB_TEMP_DIR}; + // Extract the zip_file_input_stream to the temporary file. + { + std::ofstream out(tempFile.Path(), std::ios::binary); + if (!out) + { + throw std::runtime_error("Failed to open temporary file for writing."); + } + constexpr std::size_t bufferSize = 16384; + char buffer[bufferSize]; + while (si) + { + si.read(buffer, bufferSize); + std::streamsize bytesRead = si.gcount(); + if (bytesRead > 0) + { + out.write(buffer, bytesRead); + } + } + out.close(); + } + + // determine toplevel directory path. + auto zipFile = ZipFileReader::Create(tempFile.Path()); + std::vector files = zipFile->GetFiles(); + bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); + directory = directory / fs::path(inputFileName).parent_path(); + if (!hasSingleRootDirectory) + { + directory = (fs::path(directory) / fs::path(inputFileName).filename().stem()).string(); + } + for (const auto &inputFile : files) + { + if (!inputFile.ends_with("/")) // don't process directory entries. + { + fs::path inputPath{inputFile}; + std::string extension = inputPath.extension(); + if (extensionChecker.IsValidExtension(extension) || isInfoFile(inputFile)) + { + auto si = zipFile->GetFileInputStream(inputFile); + std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); + } + else if (extension == ".zip") + { + // recursively included zip file. :-/ + auto si = zipFile->GetFileInputStream(inputFile); + + UploadEmbeddedZipFile( + this->model, + si, + inputPath, + directory, + extensionChecker, + instanceId, + patchProperty); + } + } + } + } + virtual void post_response( const uri &request_uri, HttpRequest &req, @@ -836,6 +974,65 @@ public: { std::string segment = request_uri.segment(1); + if (segment == "t3k_uploadAsset") + { + std::string responseText; + try + { + fs::path targetPath = request_uri.query("path"); + if (targetPath.empty()) + { + throw std::runtime_error("Invalid path"); + } + if (!IsSafeMediaPath(targetPath)) + { + throw std::runtime_error("Unsafe path."); + } + fs::path filePath = req.get_body_temporary_file(); + if (filePath.empty()) + { + throw std::runtime_error("Unexpected."); + } + + try { + fs::create_directories(targetPath.parent_path()); + // try moving into place. + if (fs::exists(targetPath)) + { + fs::remove(targetPath); + } + fs::rename(req.get_body_temporary_file(), targetPath); + req.detach_body_temporary_file(); + + } catch (const std::exception &e) { + // ok Copy into place instead. + fs::copy(req.get_body_temporary_file(), targetPath); + } + // set target permissions to "pipedal_d:pipedald -rw-rw-r-- if we can. + try { + fs::permissions(targetPath, fs::perms::owner_read | fs::perms::owner_write | + fs::perms::group_read | fs::perms::group_write | + fs::perms::others_read, + fs::perm_options::replace); + } catch (const std::exception&) + { + // ignore. + } + + responseText = "{\"ok\": true}"; + } + catch (const std::exception &e) + { + std::string jsonString = json_writer::encode_string(e.what()); + std::ostringstream os; + os << "{\"ok\": false, \"error\": " << jsonString << "}"; + responseText = os.str(); + } + res.set(HttpField::content_length, SS(responseText.size())); + res.set(HttpField::content_type, "text/json"); + res.setBody(responseText); + return; + } if (segment == "uploadPluginPresets") { PluginPresets presets; @@ -1013,6 +1210,20 @@ public: auto si = zipFile->GetFileInputStream(inputFile); std::string path = this->model->UploadUserFile(directory, instanceId, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); } + else if (extension == ".zip") + { + // recursively included zip file. :-/ + auto si = zipFile->GetFileInputStream(inputFile); + + UploadEmbeddedZipFile( + this->model, + si, + inputPath, + directory, + extensionChecker, + instanceId, + patchProperty); + } } } // set outputPath to the file or folder we would like focus to go to. diff --git a/vite/public/handleTone3000download.html b/vite/public/handleTone3000download.html deleted file mode 100644 index f16cf52..0000000 --- a/vite/public/handleTone3000download.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - Tone3000 Download Received - - - - - - - \ No newline at end of file diff --git a/vite/public/html/t3k_response.html b/vite/public/html/t3k_response.html new file mode 100644 index 0000000..6143065 --- /dev/null +++ b/vite/public/html/t3k_response.html @@ -0,0 +1,27 @@ + + + + T3K Response + + + + \ No newline at end of file diff --git a/vite/public/t3k/handleTone3000download.html b/vite/public/t3k/handleTone3000download.html new file mode 100644 index 0000000..7c712a2 --- /dev/null +++ b/vite/public/t3k/handleTone3000download.html @@ -0,0 +1,148 @@ + + + + + + + Tone3000 Download Received + + + + + + + \ No newline at end of file diff --git a/vite/src/pipedal/AppThemed.tsx b/vite/src/pipedal/AppThemed.tsx index d97b177..55e08e3 100644 --- a/vite/src/pipedal/AppThemed.tsx +++ b/vite/src/pipedal/AppThemed.tsx @@ -58,7 +58,6 @@ import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo, wantsReloa import ZoomedUiControl from './ZoomedUiControl' import MainPage from './MainPage'; import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; import DialogActions from '@mui/material/DialogActions'; import ListSubheader from '@mui/material/ListSubheader'; import { BankIndex, BankIndexEntry } from './Banks'; @@ -1086,13 +1085,11 @@ export aria-describedby="alert-dialog-description" > - - - { - this.state.alertDialogMessage - } - - + + { + this.state.alertDialogMessage + } +