TooB ML support for large model. .zip file uploads.
This commit is contained in:
+2
-2
@@ -14,14 +14,14 @@
|
||||
"socketServerAddress": "0.0.0.0:80",
|
||||
|
||||
/* Number of threads to use for servicing websockets */
|
||||
"threads" : 10,
|
||||
"threads" : 5,
|
||||
|
||||
"logHttpRequests": false,
|
||||
/* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */
|
||||
"logLevel": 3,
|
||||
|
||||
/* Maximum filesize to allow when uploading */
|
||||
"maxUploadSize": 1048576,
|
||||
"maxUploadSize": 536870912,
|
||||
|
||||
/* Provide access point capture redirects on this gateway. -- provides automatic browser launching on Access Point access.
|
||||
(not implemented)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -117,7 +117,7 @@ and uses model files from the GuitarML Neural Pi project (https://github.com/Gui
|
||||
mod:brand "TooB";
|
||||
mod:label "TooB ML";
|
||||
lv2:requiredFeature urid:map, work:schedule ;
|
||||
lv2:optionalFeature lv2:hardRTCapable;
|
||||
lv2:optionalFeature state::freePath, state:makePath, state:mapPath ;
|
||||
lv2:extensionData state:interface, work:interface;
|
||||
|
||||
patch:readable
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"socket_server_port": 8080,
|
||||
"socket_server_address": "*",
|
||||
"debug": true,
|
||||
"max_upload_size": 1048576,
|
||||
"max_upload_size": 536870912,
|
||||
"fakeAndroid": false,
|
||||
"ui_plugins": []
|
||||
}
|
||||
@@ -428,6 +428,18 @@ export default withStyles(styles, { withTheme: true })(
|
||||
hasSelectedFileOrFolder(): boolean {
|
||||
return this.state.hasSelection && this.state.selectedFile !== "";
|
||||
}
|
||||
getFileExtensionList(uiFileProperty: UiFileProperty): string {
|
||||
let result = "";
|
||||
for (var fileType of uiFileProperty.fileTypes)
|
||||
{
|
||||
if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip")
|
||||
{
|
||||
if (result !== "") result = result + ",";
|
||||
result += fileType.fileExtension;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let columnWidth = this.state.columnWidth;
|
||||
@@ -573,9 +585,13 @@ export default withStyles(styles, { withTheme: true })(
|
||||
this.setState({ openUploadFileDialog: false });
|
||||
}
|
||||
}
|
||||
uploadPage={"uploadUserFile?directory=" + encodeURIComponent(
|
||||
pathConcat(this.props.fileProperty.directory,this.state.navDirectory)
|
||||
)}
|
||||
uploadPage={
|
||||
"uploadUserFile?directory=" + encodeURIComponent(
|
||||
pathConcat(this.props.fileProperty.directory,this.state.navDirectory)
|
||||
)
|
||||
+ "&ext="
|
||||
+ encodeURIComponent(this.getFileExtensionList(this.props.fileProperty))
|
||||
}
|
||||
onUploaded={() => { this.requestFiles(this.state.navDirectory); }}
|
||||
fileProperty={this.props.fileProperty}
|
||||
|
||||
|
||||
@@ -787,7 +787,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.androidHost = new FakeAndroidHost();
|
||||
}
|
||||
this.debug = !!data.debug;
|
||||
let { socket_server_port, socket_server_address } = data;
|
||||
let { socket_server_port, socket_server_address,max_upload_size } = data;
|
||||
if ((!socket_server_address) || socket_server_address === "*") {
|
||||
socket_server_address = window.location.hostname;
|
||||
}
|
||||
@@ -798,6 +798,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
this.socketServerUrl = socket_server;
|
||||
this.varServerUrl = var_server_url;
|
||||
this.maxFileUploadSize = parseInt(max_upload_size);
|
||||
|
||||
this.webSocket = new PiPedalSocket(
|
||||
this.socketServerUrl,
|
||||
|
||||
@@ -208,7 +208,11 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
||||
{
|
||||
throw new Error("Invalid file extension.");
|
||||
}
|
||||
let filename = await this.model.uploadFile(this.props.uploadPage, this.uploadList[i].file, "application/octet-stream", upload.abortController);
|
||||
let filename = await this.model.uploadFile(
|
||||
this.props.uploadPage,
|
||||
this.uploadList[i].file,
|
||||
"application/octet-stream",
|
||||
upload.abortController);
|
||||
this.props.onUploaded(filename);
|
||||
upload.status = FileUploadStatus.Uploaded;
|
||||
upload.statusMessage = "Uploaded.";
|
||||
|
||||
@@ -132,6 +132,7 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
WebServerConfig.cpp WebServerConfig.hpp
|
||||
Finally.hpp
|
||||
ZipFile.cpp ZipFile.hpp
|
||||
TemporaryFile.cpp TemporaryFile.hpp
|
||||
|
||||
@@ -2135,3 +2135,7 @@ void PiPedalModel::OnNotifyLv2RealtimeError(int64_t instanceId, const std::strin
|
||||
}
|
||||
delete[] t;
|
||||
}
|
||||
std::filesystem::path PiPedalModel::GetPluginUploadDirectory() const
|
||||
{
|
||||
return storage.GetPluginUploadDirectory();
|
||||
}
|
||||
@@ -196,6 +196,7 @@ namespace pipedal
|
||||
virtual ~PiPedalModel();
|
||||
|
||||
uint16_t GetWebPort() const { return webPort; }
|
||||
std::filesystem::path GetPluginUploadDirectory() const;
|
||||
void Close();
|
||||
|
||||
void SetOnboarding(bool value);
|
||||
|
||||
+2
-1
@@ -1441,7 +1441,8 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con
|
||||
if (plugin)
|
||||
{
|
||||
std::filesystem::path bundlePath = plugin->bundle_path();
|
||||
|
||||
if (!plugin->piPedalUI())
|
||||
return;
|
||||
const auto& fileProperties = plugin->piPedalUI()->fileProperties();
|
||||
if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri))
|
||||
{
|
||||
|
||||
+2
-6
@@ -1680,18 +1680,14 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
|
||||
{
|
||||
if (!ensureNoDotDot(directory))
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
throw std::logic_error(SS("Invalide filename: " << filename));
|
||||
}
|
||||
std::filesystem::path filePath{filename};
|
||||
|
||||
if (filePath.has_parent_path())
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
}
|
||||
std::filesystem::path result = this->GetPluginUploadDirectory() / directory / filename;
|
||||
if (!this->IsValidSampleFileName(result))
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
throw std::logic_error(SS("Invalid upload path: " << result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ public:
|
||||
|
||||
std::filesystem::path GetPluginUploadDirectory() const;
|
||||
|
||||
std::vector<std::string> GetPedalboards();
|
||||
//std::vector<std::string> GetPedalboards();
|
||||
|
||||
const BankIndex & GetBanks() const { return bankIndex; }
|
||||
|
||||
|
||||
+49
-30
@@ -56,7 +56,8 @@ const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
|
||||
|
||||
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
|
||||
|
||||
const size_t MAX_READ_SIZE = 512 * 1024 * 1024;;
|
||||
const size_t MAX_READ_SIZE = 512 * 1024 * 1024;
|
||||
;
|
||||
using namespace boost;
|
||||
|
||||
class request_with_file_upload : public websocketpp::http::parser::parser
|
||||
@@ -77,10 +78,10 @@ public:
|
||||
{
|
||||
return m_ready;
|
||||
}
|
||||
std::istream&get_body_input_stream();
|
||||
std::istream &get_body_input_stream();
|
||||
|
||||
const std::filesystem::path& get_body_input_file();
|
||||
size_t content_length() const { return m_content_length;}
|
||||
const std::filesystem::path &get_body_input_file();
|
||||
size_t content_length() const { return m_content_length; }
|
||||
|
||||
/// Returns the full raw request (including the body)
|
||||
std::string raw() const;
|
||||
@@ -148,33 +149,39 @@ public:
|
||||
std::stringstream m_stringInputStream;
|
||||
bool m_outputOpen = false;
|
||||
};
|
||||
const std::filesystem::path& request_with_file_upload::get_body_input_file()
|
||||
const std::filesystem::path &request_with_file_upload::get_body_input_file()
|
||||
{
|
||||
if (!this->m_temporaryFile)
|
||||
throw std::runtime_error("Request does not have a body.");
|
||||
return this->m_temporaryFile->Path();
|
||||
}
|
||||
std::istream&request_with_file_upload::get_body_input_stream()
|
||||
std::istream &request_with_file_upload::get_body_input_stream()
|
||||
{
|
||||
if (!m_outputOpen)
|
||||
{
|
||||
m_outputOpen = true;
|
||||
if (m_uploading_to_file)
|
||||
{
|
||||
m_inputStream.open(this->m_temporaryFile->Path(),std::ios_base::in | std::ios_base::binary);
|
||||
m_inputStream.open(this->m_temporaryFile->Path(), std::ios_base::in | std::ios_base::binary);
|
||||
return m_inputStream;
|
||||
}else {
|
||||
}
|
||||
else
|
||||
{
|
||||
auto body = super::get_body();
|
||||
m_stringInputStream.write(body.c_str(),body.length());
|
||||
m_stringInputStream.write(body.c_str(), body.length());
|
||||
m_stringInputStream.flush();
|
||||
m_stringInputStream.seekg(0);
|
||||
return m_stringInputStream;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_uploading_to_file)
|
||||
{
|
||||
return m_inputStream;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_stringInputStream;
|
||||
}
|
||||
}
|
||||
@@ -193,7 +200,7 @@ bool request_with_file_upload::prepare_body(std::error_code &ec)
|
||||
try
|
||||
{
|
||||
this->m_temporaryFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
|
||||
m_outputStream.open(this->m_temporaryFile->Path(),std::ios_base::trunc | std::ios_base::out | std::ios_base::binary);
|
||||
m_outputStream.open(this->m_temporaryFile->Path(), std::ios_base::trunc | std::ios_base::out | std::ios_base::binary);
|
||||
if (!m_outputStream)
|
||||
{
|
||||
throw std::runtime_error(SS("Unable to open file " << this->m_temporaryFile->Path()));
|
||||
@@ -221,10 +228,12 @@ inline size_t request_with_file_upload::process_body(char const *buf, size_t len
|
||||
if (m_body_encoding == body_encoding::plain)
|
||||
{
|
||||
size_t processed = (std::min)(m_body_bytes_needed, len);
|
||||
try {
|
||||
m_outputStream.write(buf,processed);
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_outputStream.write(buf, processed);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Can't write to web temporary file " << m_temporaryFile->Path() << ". " << e.what()));
|
||||
ec = error::make_error_code(error::istream_bad);
|
||||
return 0;
|
||||
@@ -507,7 +516,7 @@ public:
|
||||
typedef CustomPpConfig type;
|
||||
typedef websocketpp::config::asio base;
|
||||
|
||||
static const size_t max_http_body_size = MAX_READ_SIZE; // websocketpp::config::asio::max_http_body_size;
|
||||
static size_t max_http_body_size; // websocketpp::config::asio::max_http_body_size;
|
||||
typedef pipedal_elog elog_type;
|
||||
typedef pipedal_alog alog_type;
|
||||
|
||||
@@ -528,6 +537,8 @@ public:
|
||||
transport_type;
|
||||
};
|
||||
|
||||
size_t CustomPpConfig::max_http_body_size = MAX_READ_SIZE;
|
||||
|
||||
std::string
|
||||
pipedal::last_modified(const std::filesystem::path &path)
|
||||
{
|
||||
@@ -637,6 +648,7 @@ namespace pipedal
|
||||
int port = -1;
|
||||
std::filesystem::path rootPath;
|
||||
int threads = 1;
|
||||
size_t maxUploadSize = 512 * 1024 * 1024;
|
||||
|
||||
std::thread *pBgThread = nullptr;
|
||||
std::recursive_mutex io_mutex;
|
||||
@@ -658,13 +670,13 @@ namespace pipedal
|
||||
}
|
||||
|
||||
virtual std::istream &get_body_input_stream() override { return m_request.get_body_input_stream(); }
|
||||
virtual const std::filesystem::path& get_body_temporary_file()override {
|
||||
virtual const std::filesystem::path &get_body_temporary_file() override
|
||||
{
|
||||
return m_request.get_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(); }
|
||||
virtual const std::string &get(const std::string &key) const { return m_request.get_header(key); }
|
||||
virtual bool keepAlive() const
|
||||
@@ -1262,13 +1274,7 @@ namespace pipedal
|
||||
this->pBgThread = new std::thread(ThreadProc, this);
|
||||
}
|
||||
|
||||
WebServerImpl(const std::string &address, int port, const char *rootPath, int threads)
|
||||
: address(address),
|
||||
rootPath(rootPath),
|
||||
port(port),
|
||||
threads(threads)
|
||||
{
|
||||
}
|
||||
WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize);
|
||||
};
|
||||
} // namespace pipedal
|
||||
|
||||
@@ -1284,8 +1290,21 @@ std::shared_ptr<ISocketFactory> WebServerImpl::GetSocketFactory(const uri &reque
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<WebServer> pipedal::WebServer::create(const boost::asio::ip::address &address, int port, const char *rootPath, int threads)
|
||||
WebServerImpl::WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize)
|
||||
: address(address),
|
||||
rootPath(rootPath),
|
||||
port(port),
|
||||
threads(threads),
|
||||
maxUploadSize(maxUploadSize)
|
||||
{
|
||||
return std::shared_ptr<WebServer>(new WebServerImpl(address.to_string(), port, rootPath, threads));
|
||||
::CustomPpConfig::max_http_body_size = maxUploadSize;
|
||||
}
|
||||
|
||||
std::shared_ptr<WebServer> pipedal::WebServer::create(
|
||||
const boost::asio::ip::address &address,
|
||||
int port,
|
||||
const char *rootPath, int threads,
|
||||
size_t maxUploadSize)
|
||||
{
|
||||
return std::shared_ptr<WebServer>(new WebServerImpl(address.to_string(), port, rootPath, threads, maxUploadSize));
|
||||
}
|
||||
|
||||
+2
-1
@@ -236,7 +236,8 @@ public:
|
||||
const boost::asio::ip::address &address,
|
||||
int port,
|
||||
const char *rootPath,
|
||||
int threads);
|
||||
int threads,
|
||||
size_t maxUploadsize);
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "WebServerConfig.hpp"
|
||||
#include "WebServer.hpp"
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <filesystem>
|
||||
#include "PiPedalConfiguration.hpp"
|
||||
#include "PiPedalModel.hpp"
|
||||
#include "Banks.hpp"
|
||||
#include "Ipv6Helpers.hpp"
|
||||
#include <memory>
|
||||
#include "ZipFile.hpp"
|
||||
#include "PiPedalUI.hpp"
|
||||
|
||||
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
#define BANK_EXTENSION ".piBank"
|
||||
#define PLUGIN_PRESETS_EXTENSION ".piPluginPresets"
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace boost::system;
|
||||
|
||||
static std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
class ExtensionChecker {
|
||||
public:
|
||||
ExtensionChecker(const std::string&extensionList)
|
||||
:extensions(split(extensionList,','))
|
||||
{
|
||||
}
|
||||
bool IsValidExtension(const std::string&extension)
|
||||
{
|
||||
if (extensions.size() == 0)
|
||||
return true;
|
||||
for (const auto&ext: extensions)
|
||||
{
|
||||
if (ext == extension)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
std::vector<std::string> extensions;
|
||||
};
|
||||
|
||||
class DownloadIntercept : public RequestHandler
|
||||
{
|
||||
PiPedalModel *model;
|
||||
|
||||
public:
|
||||
DownloadIntercept(PiPedalModel *model)
|
||||
: RequestHandler("/var"),
|
||||
model(model)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool wants(const std::string &method, const uri &request_uri) const
|
||||
{
|
||||
if (request_uri.segment_count() != 2 || request_uri.segment(0) != "var")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
return true;
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetContentDispositionHeader(const std::string &name, const std::string &extension)
|
||||
{
|
||||
std::string fileName = name.substr(0, 64) + extension;
|
||||
std::stringstream s;
|
||||
|
||||
s << "attachment; filename*=" << HtmlHelper::Rfc5987EncodeFileName(fileName) << "; filename=\"" << HtmlHelper::SafeFileName(fileName) << "\"";
|
||||
std::string result = s.str();
|
||||
return result;
|
||||
}
|
||||
|
||||
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string pluginUri = request_uri.query("id");
|
||||
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
|
||||
*pName = plugin->name();
|
||||
|
||||
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(pluginPresets);
|
||||
*pContent = s.str();
|
||||
}
|
||||
void GetPreset(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
auto pedalboard = model->GetPreset(instanceId);
|
||||
|
||||
// a certain elegance to using same file format for banks and presets.
|
||||
BankFile file;
|
||||
file.name(pedalboard.name());
|
||||
int64_t newInstanceId = file.addPreset(pedalboard);
|
||||
file.selectedPreset(newInstanceId);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(file);
|
||||
*pContent = s.str();
|
||||
*pName = pedalboard.name();
|
||||
}
|
||||
void GetBank(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
BankFile bank;
|
||||
model->GetBank(instanceId, &bank);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s, true); // do what we can to reduce the file size.
|
||||
writer.write(bank);
|
||||
*pContent = s.str();
|
||||
*pName = bank.name();
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setContentLength(content.length());
|
||||
return;
|
||||
}
|
||||
throw PiPedalException("Not found.");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
static std::string GetFirstFolderOrFile(const std::vector<std::string>&fileNames)
|
||||
{
|
||||
for (const auto &fileName: fileNames)
|
||||
{
|
||||
size_t nPos = fileName.find('/');
|
||||
if (nPos != std::string::npos) {
|
||||
return fileName.substr(0,nPos);
|
||||
}
|
||||
}
|
||||
if (fileNames.size() == 0)
|
||||
{
|
||||
return 0;
|
||||
} else {
|
||||
return fileNames[0];
|
||||
}
|
||||
}
|
||||
virtual void post_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
PluginPresets presets;
|
||||
reader.read(&presets);
|
||||
model->UploadPluginPresets(presets);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << -1;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadPreset(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadBank(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
const std::string instanceId = request_uri.query("id");
|
||||
const std::string directory = request_uri.query("directory");
|
||||
const std::string filename = request_uri.query("filename");
|
||||
const std::string patchProperty = request_uri.query("property");
|
||||
|
||||
|
||||
if (patchProperty.length() == 0 && directory.length() == 0)
|
||||
{
|
||||
// yyy no throwing!
|
||||
throw PiPedalException("Malformed request.");
|
||||
|
||||
}
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
std::string outputFileName = std::filesystem::path(directory) / filename;
|
||||
|
||||
if (filename.ends_with(".zip"))
|
||||
{
|
||||
ExtensionChecker extensionChecker { request_uri.query("ext") };
|
||||
namespace fs = std::filesystem;
|
||||
try {
|
||||
auto zipFile = ZipFile::Create(req.get_body_temporary_file());
|
||||
std::vector<std::string> files = zipFile->GetFiles();
|
||||
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))
|
||||
{
|
||||
auto si = zipFile->GetFileInputStream(inputFile);
|
||||
std::string path = this->model->UploadUserFile(directory,patchProperty,inputFile, si,zipFile->GetFileSize(inputFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
// set outputPath to the file or folder we would like focus to go to.
|
||||
// almost always a single folder in the root.
|
||||
std::string returnPath = GetFirstFolderOrFile(files);
|
||||
outputFileName = this->model->GetPluginUploadDirectory() / directory / returnPath;
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Unzip failed. " << e.what()));
|
||||
throw;
|
||||
}
|
||||
|
||||
} else {
|
||||
outputFileName = this->model->UploadUserFile(directory,patchProperty,filename,req.get_body_input_stream(), req.content_length());
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(outputFileName);
|
||||
std::string response = ss.str();
|
||||
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* When hosting a react app, replace /var/config.json with
|
||||
data that will connect the react app with our socket server.
|
||||
*/
|
||||
|
||||
class InterceptConfig : public RequestHandler
|
||||
{
|
||||
private:
|
||||
uint64_t maxUploadSize;
|
||||
int portNumber;
|
||||
|
||||
public:
|
||||
InterceptConfig(int portNumber, uint64_t maxUploadSize)
|
||||
: RequestHandler("/var/config.json"),
|
||||
maxUploadSize(maxUploadSize),
|
||||
portNumber(portNumber)
|
||||
{
|
||||
}
|
||||
std::string GetConfig(const std::string &fromAddress)
|
||||
{
|
||||
#define LINK_LOCAL_WEB_SOCKET 1
|
||||
#if LINK_LOCAL_WEB_SOCKET
|
||||
std::string webSocketAddress = GetLinkLocalAddress(fromAddress);
|
||||
Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber));
|
||||
#else
|
||||
std::string webSocketAddress = "*";
|
||||
#endif
|
||||
|
||||
std::stringstream s;
|
||||
|
||||
s << "{ \"socket_server_port\": " << portNumber
|
||||
<< ", \"socket_server_address\": \"" << webSocketAddress <<
|
||||
"\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << " }";
|
||||
|
||||
return s.str();
|
||||
}
|
||||
virtual ~InterceptConfig() {}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
// intercepted. See the other overload.
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
return;
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
// intercepted. see the other overload.
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
};
|
||||
|
||||
void pipedal::ConfigureWebServer(
|
||||
WebServer&server,
|
||||
PiPedalModel&model,
|
||||
int port,
|
||||
size_t maxUploadSize)
|
||||
{
|
||||
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, maxUploadSize)};
|
||||
server.AddRequestHandler(interceptConfig);
|
||||
|
||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
||||
server.AddRequestHandler(downloadIntercept);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// 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 <cstdint>
|
||||
|
||||
namespace pipedal {
|
||||
class WebServer;
|
||||
class PiPedalModel;
|
||||
void ConfigureWebServer(WebServer&server,PiPedalModel&model,int port,size_t maxUploadSize);
|
||||
}
|
||||
+115
-31
@@ -23,25 +23,25 @@
|
||||
#include <map>
|
||||
#include "ss.hpp"
|
||||
#include "Finally.hpp"
|
||||
#include <cstring>
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
ZipFile::ZipFile()
|
||||
{
|
||||
|
||||
}
|
||||
ZipFile::~ZipFile()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
class ZipFileImpl: public ZipFile {
|
||||
class ZipFileImpl : public ZipFile
|
||||
{
|
||||
public:
|
||||
ZipFileImpl(const std::filesystem::path&path)
|
||||
:path(path)
|
||||
ZipFileImpl(const std::filesystem::path &path)
|
||||
: path(path)
|
||||
{
|
||||
int errorOp = 0;
|
||||
zipFile = zip_open(path.c_str(),ZIP_RDONLY,&errorOp);
|
||||
zipFile = zip_open(path.c_str(), ZIP_RDONLY, &errorOp);
|
||||
if (zipFile == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Can't open zip file.");
|
||||
@@ -49,12 +49,14 @@ public:
|
||||
}
|
||||
virtual ~ZipFileImpl();
|
||||
virtual std::vector<std::string> GetFiles() override;
|
||||
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) override;
|
||||
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path &path) override;
|
||||
virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) override;
|
||||
virtual size_t GetFileSize(const std::string&filename) override;
|
||||
|
||||
private:
|
||||
std::map<std::string,zip_int64_t> nameMap; // avoid o(2) extraction operations.
|
||||
std::map<std::string, zip_int64_t> nameMap; // avoid o(2) extraction operations.
|
||||
const std::filesystem::path path;
|
||||
zip_t*zipFile = nullptr;
|
||||
zip_t *zipFile = nullptr;
|
||||
};
|
||||
|
||||
ZipFile::ptr ZipFile::Create(const std::filesystem::path &path)
|
||||
@@ -70,13 +72,13 @@ ZipFileImpl::~ZipFileImpl()
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ZipFileImpl::GetFiles()
|
||||
std::vector<std::string> ZipFileImpl::GetFiles()
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
zip_int64_t nEntries = zip_get_num_entries(zipFile,0);
|
||||
zip_int64_t nEntries = zip_get_num_entries(zipFile, 0);
|
||||
for (zip_int64_t i = 0; i < nEntries; ++i)
|
||||
{
|
||||
const char*name = zip_get_name(zipFile,i,ZIP_FL_ENC_STRICT);
|
||||
const char *name = zip_get_name(zipFile, i, ZIP_FL_ENC_STRICT);
|
||||
if (name)
|
||||
{
|
||||
result.push_back(name);
|
||||
@@ -86,9 +88,10 @@ std::vector<std::string> ZipFileImpl::GetFiles()
|
||||
return result;
|
||||
}
|
||||
|
||||
void ZipFileImpl::ExtractTo(const std::string &zipName, const std::filesystem::path& path) {
|
||||
void ZipFileImpl::ExtractTo(const std::string &zipName, const std::filesystem::path &path)
|
||||
{
|
||||
auto fi = nameMap.find(zipName);
|
||||
|
||||
|
||||
if (fi == nameMap.end())
|
||||
{
|
||||
// must call GetFiles() firest.
|
||||
@@ -96,38 +99,119 @@ void ZipFileImpl::ExtractTo(const std::string &zipName, const std::filesystem::p
|
||||
}
|
||||
zip_int64_t fileIndex = fi->second;
|
||||
|
||||
zip_file_t*fIn = zip_fopen_index(this->zipFile,fileIndex,0);
|
||||
zip_file_t *fIn = zip_fopen_index(this->zipFile, fileIndex, 0);
|
||||
if (fIn == nullptr)
|
||||
{
|
||||
zip_error_t *error = zip_get_error(this->zipFile);
|
||||
zip_error_t *error = zip_get_error(this->zipFile);
|
||||
const char *strError = zip_error_strerror(error);
|
||||
|
||||
throw std::runtime_error(SS("Failed to read zip content file. " << strError));
|
||||
}
|
||||
Finally t{[fIn] () mutable{
|
||||
zip_fclose(fIn);
|
||||
}};
|
||||
Finally t{[fIn]() mutable
|
||||
{
|
||||
zip_fclose(fIn);
|
||||
}};
|
||||
|
||||
std::ofstream fo {path,std::ios_base::out |std::ios_base::trunc | std::ios_base::binary};
|
||||
std::ofstream fo{path, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary};
|
||||
|
||||
constexpr int BUFFER_SIZE = 64*1024;
|
||||
constexpr int BUFFER_SIZE = 64 * 1024;
|
||||
std::vector<char> vBuff(BUFFER_SIZE);
|
||||
char*pBuff = (char*)&(vBuff[0]);
|
||||
char *pBuff = (char *)&(vBuff[0]);
|
||||
while (true)
|
||||
{
|
||||
zip_int64_t nRead = zip_fread(fIn,pBuff, BUFFER_SIZE);
|
||||
if (nRead = 0) break;
|
||||
if (nRead == -1) {
|
||||
zip_error_t*error = zip_file_get_error(fIn);
|
||||
zip_int64_t nRead = zip_fread(fIn, pBuff, BUFFER_SIZE);
|
||||
if (nRead = 0)
|
||||
break;
|
||||
if (nRead == -1)
|
||||
{
|
||||
zip_error_t *error = zip_file_get_error(fIn);
|
||||
const char *strError = zip_error_strerror(error);
|
||||
throw std::runtime_error(SS("Error reading zip content file." << strError));
|
||||
}
|
||||
fo.write(pBuff,(std::streamsize)nRead);
|
||||
if (!fo) {
|
||||
fo.write(pBuff, (std::streamsize)nRead);
|
||||
if (!fo)
|
||||
{
|
||||
throw std::runtime_error(SS("Unable to write to " << path));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
zip_file_input_stream_buf::zip_file_input_stream_buf(zip_file_t *file, size_t buffer_size)
|
||||
: file(file), buffer(buffer_size + putback_size)
|
||||
{
|
||||
char *end = buffer.data() + buffer.size();
|
||||
setg(end, end, end);
|
||||
}
|
||||
zip_file_input_stream_buf::~zip_file_input_stream_buf()
|
||||
{
|
||||
zip_fclose(file);
|
||||
}
|
||||
|
||||
std::streamsize zip_file_input_stream_buf::xsgetn(char *s, std::streamsize n)
|
||||
{
|
||||
std::streamsize num_copied = 0;
|
||||
while (n > 0)
|
||||
{
|
||||
if (gptr() >= egptr())
|
||||
{
|
||||
if (underflow() == traits_type::eof())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::streamsize chunk = std::min(n, egptr() - gptr());
|
||||
std::memcpy(s, gptr(), chunk);
|
||||
s += chunk;
|
||||
n -= chunk;
|
||||
num_copied += chunk;
|
||||
gbump(chunk);
|
||||
}
|
||||
return num_copied;
|
||||
}
|
||||
zip_file_input_stream_buf::int_type zip_file_input_stream_buf::underflow()
|
||||
{
|
||||
if (gptr() < egptr())
|
||||
{
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
char *base = buffer.data();
|
||||
char *start = base;
|
||||
|
||||
if (eback() != base)
|
||||
{
|
||||
std::memmove(base, egptr() - putback_size, putback_size);
|
||||
start += putback_size;
|
||||
}
|
||||
|
||||
zip_int64_t n = zip_fread(file, start, (zip_uint64_t)(buffer.size() - (start - base)));
|
||||
if (n <= 0)
|
||||
{
|
||||
return traits_type::eof();
|
||||
}
|
||||
|
||||
setg(base, start, start + n);
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
zip_file_input_stream ZipFileImpl::GetFileInputStream(const std::string& filename, size_t bufferSize)
|
||||
{
|
||||
zip_file_t *f = zip_fopen(zipFile,filename.c_str(),0);
|
||||
if (!f) {
|
||||
throw std::runtime_error(SS("Failed to open zip content file " << filename));
|
||||
}
|
||||
return zip_file_input_stream(f,bufferSize);
|
||||
}
|
||||
size_t ZipFileImpl::GetFileSize(const std::string&filename)
|
||||
{
|
||||
zip_stat_t stat;
|
||||
if (zip_stat(zipFile,filename.c_str(),0,&stat) < 0)
|
||||
{
|
||||
throw std::runtime_error("File not found.");
|
||||
}
|
||||
if ((stat.valid & ZIP_STAT_SIZE) == 0)
|
||||
{
|
||||
throw std::runtime_error("Failed to get file size.");
|
||||
}
|
||||
return stat.size;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,35 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <zip.h>
|
||||
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class zip_file_input_stream_buf : public std::streambuf {
|
||||
private:
|
||||
zip_file_t* file;
|
||||
std::vector<char> buffer;
|
||||
static const size_t putback_size = 8;
|
||||
public:
|
||||
zip_file_input_stream_buf(zip_file_t* file, size_t buffer_size = 16384);
|
||||
~zip_file_input_stream_buf();
|
||||
protected:
|
||||
virtual int_type underflow() override;
|
||||
virtual std::streamsize xsgetn(char* s, std::streamsize n) override;
|
||||
};
|
||||
class zip_file_input_stream : public std::istream {
|
||||
private:
|
||||
zip_file_input_stream_buf buf;
|
||||
public:
|
||||
zip_file_input_stream(zip_file_t *file, size_t buff_size = 16384)
|
||||
: std::istream(nullptr), buf(file,buff_size)
|
||||
{
|
||||
rdbuf(&buf);
|
||||
|
||||
}
|
||||
};
|
||||
class ZipFile {
|
||||
protected:
|
||||
ZipFile();
|
||||
@@ -36,6 +64,8 @@ namespace pipedal {
|
||||
|
||||
virtual std::vector<std::string> GetFiles() = 0;
|
||||
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) = 0;
|
||||
virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) = 0;
|
||||
virtual size_t GetFileSize(const std::string&filename) = 0;
|
||||
|
||||
};
|
||||
}
|
||||
+5
-448
@@ -26,6 +26,7 @@
|
||||
#include "Lv2Log.hpp"
|
||||
#include "ServiceConfiguration.hpp"
|
||||
#include "AvahiService.hpp"
|
||||
#include "WebServerConfig.hpp"
|
||||
|
||||
#include "PiPedalSocket.hpp"
|
||||
#include "PluginHost.hpp"
|
||||
@@ -38,7 +39,6 @@
|
||||
#include <sys/stat.h>
|
||||
#include <boost/asio.hpp>
|
||||
#include "HtmlHelper.hpp"
|
||||
#include "Ipv6Helpers.hpp"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
@@ -47,11 +47,10 @@
|
||||
|
||||
#include <systemd/sd-daemon.h>
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
#define BANK_EXTENSION ".piBank"
|
||||
#define PLUGIN_PRESETS_EXTENSION ".piPluginPresets"
|
||||
|
||||
|
||||
|
||||
#ifdef __ARM_ARCH_ISA_A64
|
||||
@@ -94,443 +93,6 @@ void throwSystemError(int error)
|
||||
{
|
||||
|
||||
}
|
||||
using namespace boost::system;
|
||||
|
||||
class DownloadIntercept : public RequestHandler
|
||||
{
|
||||
PiPedalModel *model;
|
||||
|
||||
public:
|
||||
DownloadIntercept(PiPedalModel *model)
|
||||
: RequestHandler("/var"),
|
||||
model(model)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool wants(const std::string &method, const uri &request_uri) const
|
||||
{
|
||||
if (request_uri.segment_count() != 2 || request_uri.segment(0) != "var")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
return true;
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetContentDispositionHeader(const std::string &name, const std::string &extension)
|
||||
{
|
||||
std::string fileName = name.substr(0, 64) + extension;
|
||||
std::stringstream s;
|
||||
|
||||
s << "attachment; filename*=" << HtmlHelper::Rfc5987EncodeFileName(fileName) << "; filename=\"" << HtmlHelper::SafeFileName(fileName) << "\"";
|
||||
std::string result = s.str();
|
||||
return result;
|
||||
}
|
||||
|
||||
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string pluginUri = request_uri.query("id");
|
||||
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
|
||||
*pName = plugin->name();
|
||||
|
||||
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(pluginPresets);
|
||||
*pContent = s.str();
|
||||
}
|
||||
void GetPreset(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
auto pedalboard = model->GetPreset(instanceId);
|
||||
|
||||
// a certain elegance to using same file format for banks and presets.
|
||||
BankFile file;
|
||||
file.name(pedalboard.name());
|
||||
int64_t newInstanceId = file.addPreset(pedalboard);
|
||||
file.selectedPreset(newInstanceId);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(file);
|
||||
*pContent = s.str();
|
||||
*pName = pedalboard.name();
|
||||
}
|
||||
void GetBank(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
BankFile bank;
|
||||
model->GetBank(instanceId, &bank);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s, true); // do what we can to reduce the file size.
|
||||
writer.write(bank);
|
||||
*pContent = s.str();
|
||||
*pName = bank.name();
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setContentLength(content.length());
|
||||
return;
|
||||
}
|
||||
throw PiPedalException("Not found.");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
virtual void post_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
PluginPresets presets;
|
||||
reader.read(&presets);
|
||||
model->UploadPluginPresets(presets);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << -1;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadPreset(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadBank(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
const std::string &directory = request_uri.query("directory");
|
||||
const std::string &filename = request_uri.query("filename");
|
||||
const std::string &patchProperty = request_uri.query("property");
|
||||
|
||||
|
||||
if (patchProperty.length() == 0 && directory.length() == 0)
|
||||
{
|
||||
throw PiPedalException("Malformed request.");
|
||||
|
||||
}
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
std::string outputFileName = std::filesystem::path(directory) / filename;
|
||||
|
||||
std::string path = this->model->UploadUserFile(directory,patchProperty,filename,req.get_body_input_stream(), req.content_length());
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(outputFileName);
|
||||
std::string response = ss.str();
|
||||
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* When hosting a react app, replace /var/config.json with
|
||||
data that will connect the react app with our socket server.
|
||||
*/
|
||||
|
||||
class InterceptConfig : public RequestHandler
|
||||
{
|
||||
private:
|
||||
uint64_t maxUploadSize;
|
||||
int portNumber;
|
||||
|
||||
public:
|
||||
InterceptConfig(int portNumber, uint64_t maxUploadSize)
|
||||
: RequestHandler("/var/config.json"),
|
||||
maxUploadSize(maxUploadSize),
|
||||
portNumber(portNumber)
|
||||
{
|
||||
}
|
||||
std::string GetConfig(const std::string &fromAddress)
|
||||
{
|
||||
#define LINK_LOCAL_WEB_SOCKET 1
|
||||
#if LINK_LOCAL_WEB_SOCKET
|
||||
std::string webSocketAddress = GetLinkLocalAddress(fromAddress);
|
||||
Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber));
|
||||
#else
|
||||
std::string webSocketAddress = "*";
|
||||
#endif
|
||||
|
||||
std::stringstream s;
|
||||
|
||||
s << "{ \"socket_server_port\": " << portNumber
|
||||
<< ", \"socket_server_address\": \"" << webSocketAddress << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << " }";
|
||||
|
||||
return s.str();
|
||||
}
|
||||
virtual ~InterceptConfig() {}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
// intercepted. See the other overload.
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
return;
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
// intercepted. see the other overload.
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
};
|
||||
|
||||
static bool isJackServiceRunning()
|
||||
{
|
||||
@@ -652,7 +214,7 @@ int main(int argc, char *argv[])
|
||||
auto const threads = std::max<int>(1, configuration.GetThreads());
|
||||
|
||||
server = WebServer::create(
|
||||
address, port, web_root.c_str(), threads);
|
||||
address, port, web_root.c_str(), threads,configuration.GetMaxUploadSize());
|
||||
|
||||
Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads);
|
||||
|
||||
@@ -787,12 +349,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
server->AddSocketFactory(pipedalSocketFactory);
|
||||
|
||||
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, configuration.GetMaxUploadSize())};
|
||||
server->AddRequestHandler(interceptConfig);
|
||||
|
||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
||||
server->AddRequestHandler(downloadIntercept);
|
||||
|
||||
ConfigureWebServer(*server,model,port,configuration.GetMaxUploadSize());
|
||||
{
|
||||
server->RunInBackground(-1);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
- make sure lv2-dev shows up in packaging. Make sure dhcpcd shows up in packaging
|
||||
- Zip file upload in a subdirectory.
|
||||
- Turning off wifi-direct sould re-enable NetworkManager.
|
||||
- ToobML save/restore state.
|
||||
|
||||
|
||||
- make app use the same theme settings as the website.
|
||||
@@ -6,9 +8,7 @@
|
||||
- Shutdown/reboot midi bindings
|
||||
- versioning.
|
||||
- do we want to take a second crack at establishin multi-p2p connections with the driver_param?
|
||||
- property subscriptions don't get refreshed after a disconnect.
|
||||
|
||||
sudo apt install libsdbus-c++-dev libnm-dev
|
||||
|
||||
|
||||
|
||||
@@ -16,4 +16,6 @@ sudo apt install libsdbus-c++-dev libnm-dev
|
||||
|
||||
Pri Description
|
||||
-----------------
|
||||
9 Upload proteus patches.
|
||||
8 Migrate to Vite toolchain.
|
||||
5 Re-use plugin instances when rebuilding pedalboard.
|
||||
|
||||
Reference in New Issue
Block a user