NAM A2 Sync

This commit is contained in:
Robin E.R. Davies
2026-05-12 12:31:21 -04:00
parent 44db94a955
commit c456e5187b
39 changed files with 5086 additions and 2206 deletions
+442 -221
View File
@@ -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<std::string>&headers)
static void logHeaders(const std::vector<std::string> &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<uint8_t> &output,
std::vector<std::string> *headersOpt
) {
TemporaryFile tempFile { WEB_TEMP_DIR};
int rc = CurlGet(url,tempFile.Path(), headersOpt);
std::vector<std::string> *outputHeadersOpt,
const std::vector<std::string> *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<std::string> &output,
std::vector<std::string>*headersOpt
)
std::vector<std::string> *outputHeadersOpt,
const std::vector<std::string> *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<std::string> *headersOpt
)
const std::filesystem::path &outputPath,
std::vector<std::string> *outputHeadersOpt,
const std::vector<std::string> *inputHeadersOpt)
{
TemporaryFile headersFile { WEB_TEMP_DIR};
TemporaryFile headersFile{WEB_TEMP_DIR};
std::vector<std::string> 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<std::string> &arguments, // commandline arguments.
const std::function<bool (const std::string &string)> &onStdoutLine,
std::string&stdErrOutput
)
const std::function<bool(const std::string &string)> &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<char*> argv;
argv.push_back(const_cast<char*>(commandPath.c_str()));
for (const auto& arg : arguments)
std::vector<char *> argv;
argv.push_back(const_cast<char *>(commandPath.c_str()));
for (const auto &arg : arguments)
{
argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(const_cast<char *>(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<CurlDownloadRequest> &request,
const std::vector<CurlDownloadRequest> &request,
const std::function<void(size_t completed, size_t total)> &progressCallback,
std::vector<std::string>*headersOpt
)
std::vector<std::string> *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<std::string> 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<std::string> *outputHeadersOpt,
std::vector<std::string> *inputHeadersOpt)
{
TemporaryFile headersFile{WEB_TEMP_DIR};
std::vector<std::string> 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<std::string> *outputHeadersOpt,
std::vector<std::string> *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<char>(outputStream),
std::istreambuf_iterator<char>());
}
return result;
}
}
+25 -4
View File
@@ -34,17 +34,21 @@ namespace pipedal {
extern int CurlGet(
const std::string &url,
const std::filesystem::path&outputFile,
std::vector<std::string> *headersOpt = nullptr
std::vector<std::string> *outputHeadersOpt = nullptr,
const std::vector<std::string> *inputHeadersOpt = nullptr
);
extern int CurlGet(
const std::string &url,
std::vector<uint8_t> &output,
std::vector<std::string> *headersOpt = nullptr
std::vector<std::string> *outputHeadersOpt = nullptr,
const std::vector<std::string> *inputHeadersOpt = nullptr
);
extern int CurlGet(
const std::string&url,
std::vector<std::string> &output,
std::vector<std::string> *headersOpt = nullptr
std::vector<std::string> *outputHeadersOpt = nullptr,
const std::vector<std::string> *inputHeadersOpt = nullptr
);
struct CurlDownloadRequest{
@@ -54,7 +58,24 @@ namespace pipedal {
extern int CurlGet(
const std::vector<CurlDownloadRequest> &request,
const std::function<void(size_t completed, size_t total)> &progressCallback,
std::vector<std::string>*headersOpt
std::vector<std::string>*outputHeadersOpt
);
extern int CurlPostFile(
const std::string &url,
const std::filesystem::path&inputFile,
const std::filesystem::path&outputFile,
std::vector<std::string> *outputHeadersOpt = nullptr,
std::vector<std::string> *inputHeadersOpt = nullptr
);
extern int CurlPostStrings(
const std::string &url,
const std::string&body,
std::string&responseBody,
std::vector<std::string> *outputHeadersOpt = nullptr,
std::vector<std::string> *inputHeadersOpt = nullptr
);
}
+12 -7
View File
@@ -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<std::recursive_mutex> 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";
}
+5 -3
View File
@@ -43,6 +43,7 @@
#include "ChannelRouterSettings.hpp"
#include <unordered_map>
#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,
+283 -291
View File
File diff suppressed because it is too large Load Diff
+46 -39
View File
@@ -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<ChannelRouterSettings>();
}
@@ -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<ChannelRouterSettings>();
if (jackChannelSelection.isValid())
if (jackChannelSelection.isValid())
{
channelRouterSettings->configured(true);
channelRouterSettings->mainInputChannels().resize(2);
const std::vector<std::string>& oldInputs = jackChannelSelection.GetInputAudioPorts();
std::vector<int64_t>& newInputs = channelRouterSettings->mainInputChannels();
const std::vector<std::string> &oldInputs = jackChannelSelection.GetInputAudioPorts();
std::vector<int64_t> &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<std::string>& oldOutputs = jackChannelSelection.GetOutputAudioPorts();
const std::vector<std::string> &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)
+116
View File
@@ -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 <memory>
#include <queue>
#include <condition_variable>
#include <functional>
namespace pipedal
{
template <typename T>
class ThreadedQueue
{
public:
~ThreadedQueue() { Close(); }
void Put(std::shared_ptr<T> value)
{
{
std::lock_guard lock(mutex);
if (!closed)
{
queue.push(value);
}
}
read_cv.notify_one();
}
std::shared_ptr<T> 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<bool(std::shared_ptr<T> queueEntry)> predicate)
{
std::lock_guard lock{mutex};
std::queue<std::shared_ptr<T>> newQueue;
while (!queue.empty())
{
auto item = queue.front();
queue.pop();
if (!predicate(item))
{
newQueue.push(item);
}
}
queue = newQueue;
}
private:
bool closed = 0;
std::queue<std::shared_ptr<T>> queue;
std::mutex mutex;
std::condition_variable read_cv;
};
}
+17 -760
View File
@@ -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<bool()> &isCancelled)
{
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
while (true)
{
if (std::chrono::steady_clock::now() - start >= duration)
{
break;
}
if (isCancelled())
{
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
static 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 << "&lt;";
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<std::string, std::string> &enumValues)
{
auto ff = enumValues.find(value);
if (ff == enumValues.end())
{
return value;
}
return ff->second;
}
static std::string mdEnumList(
const std::vector<std::string> &values,
const std::map<std::string, std::string> &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<std::string, std::string> sizeEnumValues =
{
{"standard", "Standard"},
{"lite", "Lite"},
{"feather", "Feather"},
{"nano", "Nano"},
{"custom", "Custom"}};
static std::string mdSizes(const std::vector<std::string> &sizes)
{
return mdEnumList(sizes, sizeEnumValues);
}
static std::map<std::string, std::string> 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<LicenseInfo> 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<std::string, LicenseInfo> makeLicenseEnum()
{
std::map<std::string, LicenseInfo> result;
for (const auto &license : licenses)
{
result[license.key] = license;
}
return result;
}
static std::map<std::string, LicenseInfo> 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 << "<img id='cc_img' src='" << url << "'/>";
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 <br> 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>{LicenseFlags::Cc, LicenseFlags::By, LicenseFlags::Cc0, LicenseFlags::Nc, LicenseFlags::Sa, LicenseFlags::Nd})
{
if (licenseInfo.licenseFlags & licenseFlag)
{
os << mdLicenseIcon(licenseFlag);
}
}
os << " ";
}
if (licenseInfo.url != "")
{
os << "<a href="
<< mdHref(licenseInfo.url)
<< " target='_blank' rel='noopener noreferrer' >" << mdSanitize(licenseInfo.displayName) << "</a>";
}
else
{
os << mdSanitize(licenseInfo.displayName);
}
return os.str();
}
// static std::string mdTestLicenses()
// {
// std::ostringstream os;
// for (auto license: licenseEnum)
// {
// os << "<br/>" << mdLicense(license.first) << "\n";
// }
// return os.str();
// }
static std::map<std::string, std::string> platformEnumValues =
{
{"nam", "NAM"},
{"ir", "I/R"},
{"aida-x", "Aida X"},
{"aa-snapshot", "aa-snapshot"},
{"proteus", "Proteus"}};
static std::string mdPlatform(const std::string &platform)
{
return mdEnum(platform, platformEnumValues);
}
static std::string mdUser(const tone3000::Tone3000User &user)
{
// clang-format off
std::ostringstream os;
os <<
"<a target='_blank' rel='noopener noreferrer' href='"
<< mdSanitize(user.url(),"'")
<< "'>" << mdSanitize(user.username()) << "</a>";
return os.str();
// clang-format on
}
static void mdLink(std::ostream &f, const std::string &label, const std::string &url)
{
f << "[" << mdSanitize(label, "]") << "](" << mdSanitize(url, ")") << ")";
}
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 <<
"<div id='tone3000_float' >\n";
if (!thumbnailUrl.empty())
{
of << " <img id='tone3000_thumbnail' src='"
<< thumbnailUrl << "' alt='' />\n";
}
of << " <div id='tone3000_float_content' >\n"
<< " <h3 id='tone3000_title'>" << mdSanitize(download.title()) << "</h3>\n"
<< " <div>\n"
<< " <p id='tone3000_subtitle'>\n"
<< mdDate(download.updated_at())
<< " "
<< mdUser(download.user())
<< " <br/>\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 << "<br/>\n";
if (download.license() != "") {
of << " License: " << mdLicense(download.license()) << "\n";
}
of
// << " " << mdTestLicenses()
<< " </p>\n"
<< " </div>\n"
<< " </div>\n"
<< "</div>\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<CurlDownloadRequest> &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<std::string> &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<bool()> &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<void(const Tone3000DownloadProgress &progress)> progressCallback,
std::function<bool()> isCancelled)
{
TemporaryFile downloadTemporaryFile{WEB_TEMP_DIR};
fs::path downloadPath = downloadTemporaryFile.Path();
std::vector<std::string> 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<CurlDownloadRequest> 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<CurlDownloadRequest> 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<std::string> 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();
+1 -1
View File
@@ -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,
+89 -96
View File
@@ -22,6 +22,7 @@
*/
#pragma once
#include <cstdint>
#include <string>
#include <vector>
@@ -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<std::string> size_;
std::string user_id_;
private:
int64_t id_ = -1;
std::string name_;
std::string model_url_;
tone3000_time_point created_at_;
std::optional<std::string> 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<std::string> avatar_url_;
std::string username_;
std::string url_;
class Tone3000User
{
private:
std::string id_;
std::optional<std::string> 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<std::vector<std::string>> images_;
bool is_public_ = true;
std::vector<std::string> links_;
int64_t model_count_ = 0;
int64_t favorites_count_ = 0;
std::string license_;
std::optional<std::vector<std::string>> sizes_;
Tone3000User user_;
std::vector<Tone3000Model> 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<std::string> images_;
bool is_public_ = true;
std::vector<std::string> links_;
std::string platform_;
int64_t models_count_ = 0;
int64_t favorites_count_ = 0;
int64_t downloads_count_ = 0;
std::string license_;
std::optional<std::vector<std::string>> 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<Tone3000Model> 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<void(const Tone3000DownloadProgress&progress)> progressCallback,
std::function<bool()> isCancelled
);
}
}
}
+2 -2
View File
@@ -26,8 +26,8 @@
namespace pipedal {
enum class Tone3000DownloadType {
Nam,
CabIr
Nam = 0,
CabIr = 1
};
Tone3000DownloadType StringToTone3000DownloadType(const std::string &value);
+1030 -103
View File
File diff suppressed because it is too large Load Diff
+110 -28
View File
@@ -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 <memory>
#include <cstdint>
#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<Tone3000ModelInfo> models_;
// std::string updated_at_;
// std::string user_;
// std::vector<std::string> sizes_;
// std::string platform_;
// std::string gear_;
// std::string license_;
// std::vector<std::string> 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<self> 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;
};
}
+153 -34
View File
@@ -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 <memory>
#include <cstdint>
@@ -31,55 +31,173 @@
#include <condition_variable>
#include <vector>
#include <functional>
#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<std::string> tone_id_;
std::optional<std::string> 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<bool> cancelled = false;
};
void DownloadTone3000ToneBg(
std::shared_ptr<DownloadRequest> request);
void OnTone3000DownloadError(int64_t handle, const std::string &error)
{
if (listener)
{
listener->OnTone3000DownloadError(handle, error);
}
}
std::shared_ptr<Tone3000Download> 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<bool()> isCancelled
);
struct DownloadRequest {
Tone3000DownloadType downloadType;
std::atomic<bool> cancelled = false;
handle_t handle;
const std::string downloadPath;
const std::string downloadUrl;
// std::string PerformFileDownloads(
// const Tone3000FileDownloadRequest &downloadRequets,
// Tone3000DownloadProgress &progress,
// std::function<void(Tone3000DownloadProgress &progress)> updateProgress,
// std::function<bool()> isCancelled);
struct PostResult
{
bool ok = false;
int errorCode = 0;
std::string error;
std::string body;
};
PostResult Post(
const uri &requestedUri,
std::vector<std::string> headers,
const std::string &body);
std::string Tone3000GetText(
const uri &requestedUri
);
struct OAuthCallbackResult
{
std::optional<int64_t> toneId;
std::optional<int64_t> modelId;
};
OAuthCallbackResult handleOAuthCallback(
const uri &callbackUri,
const Tone3000PkceParams &pkce);
std::atomic<bool> closed = false;
handle_t nextHandle = 1;
handle_t NextHandle();
std::vector<std::shared_ptr<DownloadRequest>> requestQueue;
ThreadedQueue<DownloadRequest> requestQueue;
Tone3000DownloadProgress fgDownloadProgress;
void bgUpdateDownloadProgress(const Tone3000DownloadProgress &progress);
@@ -87,9 +205,10 @@ namespace pipedal {
std::shared_ptr<DownloadRequest> activeRequest;
std::mutex mutex;
std::condition_variable thread_cv;
std::unique_ptr<std::jthread> thread;
std::shared_ptr<Tone3000AccessTokens> accessTokens;
std::string GetBearerToken() const;
};
}
}
+162 -109
View File
@@ -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<std::string> 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<std::string>();
}
}
while (p != query_end && *p != '&' && *p != '#')
{
++p;
}
if (p != query_end && *p == '&')
if (p != query_end && *p == '&')
{
++p;
}
}
return std::optional<std::string>();
}
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<std::string> 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();
}
}
+209 -184
View File
@@ -24,214 +24,239 @@
#include <stdexcept>
#include <sstream>
#include <cstdlib>
#include <optional>
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<std::string> segments();
const std::vector<std::string> 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<std::string> segments_;
std::vector<query_segment> 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<std::string> segments();
const std::vector<std::string> 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<std::string> 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<std::string> segments_;
int query_count() const { return (int)(queries_.size());}
std::vector<query_segment> 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<std::string> queries(const std::string &key) const {
std::vector<std::string> 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<std::string> queries(const std::string &key) const
{
std::vector<std::string> 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.
+12
View File
@@ -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(); }
+1
View File
@@ -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;
+229 -18
View File
@@ -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 &param)
{
if (param.empty())
@@ -128,8 +139,8 @@ private:
std::vector<std::string> 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 "<html>\n"
"<head>\n"
"<script>\n"
" window.close();"
"</script>\n"
"</head>\n"
"<body>\n"
"</body>\n"
"</html>\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<std::string> 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.