1525 lines
55 KiB
C++
1525 lines
55 KiB
C++
// 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 "PresetBundle.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"
|
|
#include "ofstream_synced.hpp"
|
|
#include "UpdaterSecurity.hpp"
|
|
#include "TemporaryFile.hpp"
|
|
#include "PresetBundle.hpp"
|
|
#include "json.hpp"
|
|
#include "HotspotManager.hpp"
|
|
#include "MimeTypes.hpp"
|
|
#include "AudioFileMetadataReader.hpp"
|
|
#include "AudioFiles.hpp"
|
|
#include "util.hpp"
|
|
#include "HtmlHelper.hpp"
|
|
#include "WebServerMod.hpp"
|
|
|
|
#define OLD_PRESET_EXTENSION ".piPreset"
|
|
#define PRESET_EXTENSION ".piPreset"
|
|
#define OLD_BANK_EXTENSION ".piBank"
|
|
#define BANK_EXTENSION ".piBank"
|
|
#define PLUGIN_PRESETS_EXTENSION ".piPluginPresets"
|
|
|
|
static const std::filesystem::path WEB_TEMP_DIR{ "/var/pipedal/web_temp" };
|
|
|
|
static const std::string PLUGIN_PRESETS_MIME_TYPE = "application/vnd.pipedal.pluginPresets";
|
|
static const std::string PRESET_MIME_TYPE = "application/vnd.pipedal.preset";
|
|
static const std::string BANK_MIME_TYPE = "application/vnd.pipedal.bank";
|
|
|
|
static const std::string CACHE_CONTROL_INDEFINITELY = "max-age=31536000,public,immutable"; // 1 year
|
|
static const std::string CACHE_CONTROL_SHORT = "max-age=300,public"; // 5 minutes
|
|
|
|
using namespace pipedal;
|
|
using namespace boost::system;
|
|
namespace fs = std::filesystem;
|
|
|
|
class UserUploadResponse
|
|
{
|
|
public:
|
|
std::string errorMessage_;
|
|
std::string path_;
|
|
DECLARE_JSON_MAP(UserUploadResponse);
|
|
};
|
|
JSON_MAP_BEGIN(UserUploadResponse)
|
|
JSON_MAP_REFERENCE(UserUploadResponse, errorMessage)
|
|
JSON_MAP_REFERENCE(UserUploadResponse, path)
|
|
JSON_MAP_END()
|
|
|
|
static std::string GetMimeType(const std::filesystem::path& path)
|
|
{
|
|
std::string extension = path.extension();
|
|
const MimeTypes& mimeTypes = MimeTypes::instance();
|
|
auto result = mimeTypes.MimeTypeFromExtension(extension);
|
|
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;
|
|
}
|
|
static bool IsSafeThumbnailPath(const fs::path path)
|
|
{
|
|
if (!HtmlHelper::IsSafeFileName(path)) {
|
|
return false;
|
|
}
|
|
if ((!path.string().starts_with("/var/pipedal/tone3000_thumbnails/")) &&
|
|
(!path.string().starts_with("/var/pipedal/audio_uploads/tone3000_thumbnails/"))
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
auto extension = path.extension();
|
|
if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" && extension != ".webm")
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
int32_t ConvertThumbnailSize(const std::string& param)
|
|
{
|
|
if (param.empty())
|
|
{
|
|
return 0;
|
|
}
|
|
return static_cast<int32_t>(std::stoi(param));
|
|
}
|
|
|
|
static bool IsZipFile(const std::filesystem::path& path)
|
|
{
|
|
std::ifstream f(path);
|
|
if (!f.is_open())
|
|
return false;
|
|
|
|
char c[4];
|
|
memset(c, 0, sizeof(c));
|
|
|
|
f >> c[0] >> c[1] >> c[2] >> c[3];
|
|
|
|
// official file header according to PKware documetnation.
|
|
return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04;
|
|
}
|
|
|
|
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;
|
|
};
|
|
|
|
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))
|
|
{
|
|
return {};
|
|
}
|
|
|
|
for (const auto& entry : fs::directory_iterator(thumbnailDirectory))
|
|
{
|
|
if (!entry.is_regular_file())
|
|
{
|
|
continue;
|
|
}
|
|
const auto& path = entry.path();
|
|
if (path.stem() == stem)
|
|
{
|
|
return path;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
namespace pipedal::implementation {
|
|
|
|
void UpgradeBank(PiPedalModel& model,
|
|
const std::filesystem::path& existingBankFilePath,
|
|
const std::filesystem::path& piBankFilePath,
|
|
const std::vector<std::string>& presetsToUpgrade
|
|
) {
|
|
BankFile sourceBankFile;
|
|
|
|
|
|
if (piBankFilePath.empty())
|
|
{
|
|
throw std::runtime_error("Unexpected.");
|
|
}
|
|
if (IsZipFile(piBankFilePath))
|
|
{
|
|
auto presetReader = PresetBundleReader::LoadPresetsFile(model, piBankFilePath);
|
|
presetReader->ExtractMediaFiles();
|
|
|
|
std::stringstream ss(presetReader->GetPresetJson());
|
|
json_reader reader(ss);
|
|
reader.read(&sourceBankFile);
|
|
}
|
|
else
|
|
{
|
|
std::ifstream f{ piBankFilePath };
|
|
if (!f.is_open()) {
|
|
throw std::runtime_error(SS("Unable to open file " << piBankFilePath));
|
|
}
|
|
// legacy json format, no zip, no media files.
|
|
json_reader reader(f);
|
|
reader.read(&sourceBankFile);
|
|
}
|
|
|
|
BankFile existingBankFile;
|
|
{
|
|
std::ifstream ifs{ existingBankFilePath };
|
|
if (!ifs.is_open()) {
|
|
return;
|
|
}
|
|
json_reader reader{ ifs };
|
|
reader.read(&existingBankFile);
|
|
}
|
|
|
|
for (const std::string& presetToUpgrade : presetsToUpgrade)
|
|
{
|
|
BankFileEntry* existingEntry = existingBankFile.getPresetByName(presetToUpgrade);
|
|
if (existingEntry) {
|
|
BankFileEntry* newEntry = sourceBankFile.getPresetByName(presetToUpgrade);
|
|
if (newEntry)
|
|
{
|
|
existingEntry->preset(newEntry->preset());
|
|
}
|
|
}
|
|
}
|
|
{
|
|
std::ofstream ofs{ existingBankFilePath };
|
|
json_writer writer{ ofs };
|
|
writer.write(existingBankFile);
|
|
}
|
|
|
|
|
|
}
|
|
int64_t ImportBankFile(PiPedalModel& model, const std::filesystem::path& filePath, uint64_t uploadAfter = -1)
|
|
{
|
|
BankFile bankFile;
|
|
|
|
|
|
if (filePath.empty())
|
|
{
|
|
throw std::runtime_error("Unexpected.");
|
|
}
|
|
if (IsZipFile(filePath))
|
|
{
|
|
auto presetReader = PresetBundleReader::LoadPresetsFile(model, filePath);
|
|
presetReader->ExtractMediaFiles();
|
|
|
|
std::stringstream ss(presetReader->GetPresetJson());
|
|
json_reader reader(ss);
|
|
reader.read(&bankFile);
|
|
}
|
|
else
|
|
{
|
|
std::ifstream f{ filePath };
|
|
if (!f.is_open()) {
|
|
throw std::runtime_error(SS("Unable to open file " << filePath));
|
|
}
|
|
// legacy json format, no zip, no media files.
|
|
json_reader reader(f);
|
|
reader.read(&bankFile);
|
|
}
|
|
|
|
uint64_t instanceId = model.UploadBank(bankFile, uploadAfter);
|
|
|
|
return instanceId;
|
|
}
|
|
}
|
|
|
|
using namespace pipedal::implementation;
|
|
|
|
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 == "t3k_uploadAsset")
|
|
{
|
|
return true;
|
|
}
|
|
if (segment == "t3k_response.html")
|
|
{
|
|
return true;
|
|
}
|
|
if (segment == "tone3000_thumbnail")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (segment == "downloadMediaFile")
|
|
{
|
|
return true;
|
|
}
|
|
if (segment == "displayMediaFile")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
else if (segment == "AudioMetadata")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "Thumbnail")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "PluginPresets")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "PluginPreset")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "PluginBanks")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "PluginBank")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "GetPluginInfo")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "GetPluginPresets")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "GetPreset")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "GetBank")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "NextAudioFile")
|
|
{
|
|
return true;
|
|
}
|
|
else if (segment == "PreviousAudioFile")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static 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->GetPluginHost().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 == "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 == "")
|
|
{
|
|
throw PiPedalException("Invalid request.");
|
|
}
|
|
fs::path path = GetTone3000ThumbnailFile(this->model->Tone3000ThumbnailDirectory(), id);
|
|
if (!fs::exists(path))
|
|
{
|
|
path = GetTone3000ThumbnailFile(this->model->OldTone3000ThumbnailDirectory(), id);
|
|
if (!fs::exists(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
}
|
|
auto mimeType = GetMimeType(path);
|
|
if (mimeType.empty())
|
|
{
|
|
throw PiPedalException("Can't download files of this type.");
|
|
}
|
|
if (!mimeType.starts_with("image/"))
|
|
{
|
|
throw PiPedalException("Can't download files of this type.");
|
|
}
|
|
res.set(HttpField::content_type, mimeType);
|
|
size_t contentLength = std::filesystem::file_size(path);
|
|
res.setContentLength(contentLength);
|
|
return;
|
|
}
|
|
|
|
if (segment == "downloadMediaFile" || segment == "displayMediaFile")
|
|
{
|
|
fs::path path = request_uri.query("path");
|
|
|
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
auto mimeType = GetMimeType(path);
|
|
if (mimeType.empty())
|
|
{
|
|
if (isInfoFile(path))
|
|
{
|
|
mimeType = "text/plain";
|
|
}
|
|
else
|
|
{
|
|
throw PiPedalException("Can't download files of this type.");
|
|
}
|
|
}
|
|
res.set(HttpField::content_type, mimeType);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
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;
|
|
}
|
|
|
|
if (segment == "downloadPluginPresets")
|
|
{
|
|
std::string name;
|
|
std::string content;
|
|
GetPluginPresets(request_uri, &name, &content);
|
|
|
|
TemporaryFile tmpFile{ WEB_TEMP_DIR };
|
|
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content);
|
|
presetbundleWriter->WriteToFile(tmpFile.Path());
|
|
size_t contentLength = std::filesystem::file_size(tmpFile.Path());
|
|
|
|
res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE);
|
|
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);
|
|
|
|
TemporaryFile tmpFile{ WEB_TEMP_DIR };
|
|
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
|
|
presetbundleWriter->WriteToFile(tmpFile.Path());
|
|
size_t contentLength = std::filesystem::file_size(tmpFile.Path());
|
|
|
|
res.set(HttpField::content_type, PRESET_MIME_TYPE);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
res.setContentLength(contentLength);
|
|
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
|
return;
|
|
}
|
|
if (segment == "downloadBank")
|
|
{
|
|
std::string name;
|
|
std::string content;
|
|
GetBank(request_uri, &name, &content);
|
|
|
|
TemporaryFile tmpFile{ WEB_TEMP_DIR };
|
|
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
|
|
presetbundleWriter->WriteToFile(tmpFile.Path());
|
|
size_t contentLength = std::filesystem::file_size(tmpFile.Path());
|
|
|
|
res.set(HttpField::content_type, BANK_MIME_TYPE);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
|
res.setContentLength(contentLength);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void setLastModifiedFromFile(HttpResponse& res, const std::filesystem::path& path)
|
|
{
|
|
auto lastModified = std::filesystem::last_write_time(path);
|
|
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
|
|
}
|
|
AudioDirectoryInfo::Ptr CreateDirectoryInfo(const fs::path& path)
|
|
{
|
|
return AudioDirectoryInfo::Create(path,
|
|
GetShadowIndexDirectory(
|
|
this->model->GetPluginUploadDirectory(),
|
|
path));
|
|
}
|
|
|
|
bool isInfoFile(const fs::path& path)
|
|
{
|
|
auto extension = path.extension();
|
|
if (extension == ".pdf")
|
|
{
|
|
return true;
|
|
}
|
|
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,
|
|
HttpResponse& res,
|
|
std::error_code& ec) override
|
|
{
|
|
try
|
|
{
|
|
std::string segment = request_uri.segment(1);
|
|
|
|
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))
|
|
{
|
|
path = GetTone3000ThumbnailFile(this->model->OldTone3000ThumbnailDirectory(), id);
|
|
if (!fs::exists(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
}
|
|
auto mimeType = GetMimeType(path);
|
|
if (mimeType.empty())
|
|
{
|
|
throw PiPedalException("Can't download files of this type.");
|
|
}
|
|
res.set(HttpField::content_type, mimeType);
|
|
size_t contentLength = std::filesystem::file_size(path);
|
|
res.setContentLength(contentLength);
|
|
res.setBodyFile(path, false);
|
|
return;
|
|
}
|
|
else if (segment == "downloadMediaFile" || segment == "displayMediaFile")
|
|
{
|
|
fs::path path = request_uri.query("path");
|
|
|
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
auto mimeType = GetMimeType(path);
|
|
if (mimeType.empty())
|
|
{
|
|
if (isInfoFile(path))
|
|
{
|
|
mimeType = "text/plain";
|
|
}
|
|
else
|
|
{
|
|
throw PiPedalException("Can't download files of this type.");
|
|
}
|
|
}
|
|
res.set(HttpField::content_type, mimeType);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
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);
|
|
return;
|
|
}
|
|
else if (segment == "downloadPluginPresets")
|
|
{
|
|
std::string name;
|
|
std::string content;
|
|
GetPluginPresets(request_uri, &name, &content);
|
|
|
|
std::shared_ptr<TemporaryFile> tmpFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
|
|
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content);
|
|
presetbundleWriter->WriteToFile(tmpFile->Path());
|
|
size_t contentLength = std::filesystem::file_size(tmpFile->Path());
|
|
|
|
res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
res.setContentLength(contentLength);
|
|
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
|
res.setBodyFile(tmpFile);
|
|
}
|
|
else if (segment == "downloadPreset")
|
|
{
|
|
std::string name;
|
|
std::string content;
|
|
GetPreset(request_uri, &name, &content);
|
|
|
|
std::shared_ptr<TemporaryFile> tmpFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
|
|
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
|
|
presetbundleWriter->WriteToFile(tmpFile->Path());
|
|
size_t contentLength = std::filesystem::file_size(tmpFile->Path());
|
|
|
|
res.set(HttpField::content_type, PRESET_MIME_TYPE);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
res.setContentLength(contentLength);
|
|
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
|
res.setBodyFile(tmpFile);
|
|
}
|
|
else if (segment == "downloadBank")
|
|
{
|
|
std::string name;
|
|
std::string content;
|
|
GetBank(request_uri, &name, &content);
|
|
|
|
std::shared_ptr<TemporaryFile> tmpFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
|
|
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
|
|
presetbundleWriter->WriteToFile(tmpFile->Path());
|
|
size_t contentLength = std::filesystem::file_size(tmpFile->Path());
|
|
|
|
res.set(HttpField::content_type, BANK_MIME_TYPE);
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
res.setContentLength(contentLength);
|
|
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
|
res.setBodyFile(tmpFile);
|
|
}
|
|
else if (segment == "AudioMetadata")
|
|
{
|
|
|
|
res.set(HttpField::content_type, "application/json");
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
fs::path path = model->GetStorage().FromAbstractPathString(request_uri.query("path"));
|
|
|
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path());
|
|
try
|
|
{
|
|
auto files = audioDirectory->GetFiles();
|
|
for (const auto& file : files)
|
|
{
|
|
std::string fileNameOnly = path.filename();
|
|
if (file.fileName() == fileNameOnly)
|
|
{
|
|
std::stringstream ss;
|
|
json_writer writer(ss);
|
|
writer.write(file);
|
|
res.setBody(ss.str());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Lv2Log::error("Error getting audio directory info: %s - (%s)", path.c_str(), e.what());
|
|
throw e;
|
|
}
|
|
// If we get here, the file was not found in the directory.
|
|
throw PiPedalException("File not found in directory.");
|
|
}
|
|
else if (segment == "Thumbnail")
|
|
{
|
|
ThumbnailTemporaryFile thumbnailTemporaryFile;
|
|
try
|
|
{
|
|
std::shared_ptr<TemporaryFile> thumbnail;
|
|
try
|
|
{
|
|
fs::path path = request_uri.query("path");
|
|
if (path.empty() ||
|
|
!fs::exists(path) ||
|
|
!this->model->IsInUploadsDirectory(path) ||
|
|
HasDotDot(path))
|
|
{
|
|
std::shared_ptr<TemporaryFile> thumbnail;
|
|
// path for folder thumbnails.
|
|
path = request_uri.query("ffile");
|
|
|
|
if (!path.empty() && fs::exists(path) &&
|
|
this->model->IsInUploadsDirectory(path) &&
|
|
!HasDotDot(path))
|
|
{
|
|
// path is a folder.
|
|
thumbnail = std::make_shared<TemporaryFile>();
|
|
thumbnail->SetNonDeletedPath(path);
|
|
}
|
|
else
|
|
{
|
|
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
|
|
thumbnail = std::make_shared<TemporaryFile>();
|
|
thumbnail->SetNonDeletedPath(t.Path());
|
|
}
|
|
|
|
res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension()));
|
|
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified.
|
|
setLastModifiedFromFile(res, thumbnail->Path());
|
|
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
|
|
res.setBodyFile(thumbnail);
|
|
return;
|
|
}
|
|
|
|
int32_t width = ConvertThumbnailSize(request_uri.query("w"));
|
|
int32_t height = ConvertThumbnailSize(request_uri.query("h"));
|
|
|
|
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(
|
|
path.parent_path());
|
|
audioDirectory->GetFiles(); // ensure that the .index file is up to date.
|
|
|
|
thumbnailTemporaryFile = audioDirectory->GetThumbnail(path.filename(), width, height);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
fs::path defaultThumbnail = model->GetWebRoot() / "img/missing_thumbnail.jpg";
|
|
thumbnailTemporaryFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
|
|
}
|
|
res.set(HttpField::content_type, thumbnailTemporaryFile.GetMimeType());
|
|
|
|
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp.
|
|
setLastModifiedFromFile(res, thumbnailTemporaryFile.Path());
|
|
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnailTemporaryFile.Path())));
|
|
|
|
std::filesystem::path t = thumbnailTemporaryFile.Path();
|
|
res.setBodyFile(t, thumbnailTemporaryFile.DeleteFile());
|
|
thumbnailTemporaryFile.Detach();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Lv2Log::error("Error getting thumbnail: %s - (%s)", request_uri.str().c_str(), e.what());
|
|
throw e;
|
|
}
|
|
}
|
|
else if (segment == "NextAudioFile")
|
|
{
|
|
res.set(HttpField::content_type, "application/json");
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
fs::path path = request_uri.query("path");
|
|
|
|
try
|
|
{
|
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
auto directoryPath = path.parent_path();
|
|
auto directoryInfo = CreateDirectoryInfo(directoryPath);
|
|
std::string result = directoryInfo->GetNextAudioFile(path.filename());
|
|
if (!result.empty())
|
|
{
|
|
result = (directoryPath / result).string();
|
|
}
|
|
|
|
// json-encode the result.
|
|
std::stringstream ss;
|
|
json_writer writer(ss);
|
|
writer.write(result);
|
|
|
|
res.setBody(ss.str());
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
res.setBody("\"\"");
|
|
}
|
|
}
|
|
else if (segment == "PreviousAudioFile")
|
|
{
|
|
res.set(HttpField::content_type, "application/json");
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
fs::path path = request_uri.query("path");
|
|
|
|
try
|
|
{
|
|
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
|
{
|
|
throw PiPedalException("File not found.");
|
|
}
|
|
auto directoryPath = path.parent_path();
|
|
auto directoryInfo = CreateDirectoryInfo(directoryPath);
|
|
std::string result = directoryInfo->GetPreviousAudioFile(path.filename());
|
|
if (!result.empty())
|
|
{
|
|
result = (directoryPath / result).string();
|
|
}
|
|
|
|
// json-encode the result.
|
|
std::stringstream ss;
|
|
json_writer writer(ss);
|
|
writer.write(result);
|
|
|
|
res.setBody(ss.str());
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
res.setBody("\"\"");
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|
|
}
|
|
static bool HasSingleRootDirectory(ZipFileReader& zipFile)
|
|
{
|
|
bool hasDirectory = false;
|
|
std::string previousDirectory;
|
|
for (auto& file : zipFile.GetFiles())
|
|
{
|
|
auto pos = file.find('/');
|
|
if (pos == std::string::npos)
|
|
{
|
|
// a file in the root.
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
std::string currentRootDirectory = file.substr(0, pos);
|
|
if (hasDirectory)
|
|
{
|
|
if (currentRootDirectory != previousDirectory)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hasDirectory = true;
|
|
previousDirectory = currentRootDirectory;
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
HttpResponse& res,
|
|
std::error_code& ec) override
|
|
{
|
|
try
|
|
{
|
|
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) && !IsSafeThumbnailPath(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;
|
|
fs::path filePath = req.get_body_temporary_file();
|
|
if (filePath.empty())
|
|
{
|
|
throw std::runtime_error("Unexpected.");
|
|
}
|
|
if (IsZipFile(filePath))
|
|
{
|
|
auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model), filePath);
|
|
presetReader->ExtractMediaFiles();
|
|
|
|
std::stringstream ss(presetReader->GetPluginPresetsJson());
|
|
json_reader reader(ss);
|
|
reader.read(&presets);
|
|
}
|
|
else
|
|
{
|
|
json_reader reader(req.get_body_input_stream());
|
|
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")
|
|
{
|
|
|
|
BankFile bankFile;
|
|
|
|
fs::path filePath = req.get_body_temporary_file();
|
|
if (filePath.empty())
|
|
{
|
|
throw std::runtime_error("Unexpected.");
|
|
}
|
|
if (IsZipFile(filePath))
|
|
{
|
|
auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model), filePath);
|
|
presetReader->ExtractMediaFiles();
|
|
|
|
std::stringstream ss(presetReader->GetPresetJson());
|
|
json_reader reader(ss);
|
|
reader.read(&bankFile);
|
|
}
|
|
else
|
|
{
|
|
// legacy json format, no zip, no media files.
|
|
json_reader reader(req.get_body_input_stream());
|
|
reader.read(&bankFile);
|
|
}
|
|
|
|
uint64_t uploadAfter = -1;
|
|
std::string strUploadAfter = request_uri.query("uploadAfter");
|
|
if (strUploadAfter.length() != 0)
|
|
{
|
|
uploadAfter = std::stol(strUploadAfter);
|
|
}
|
|
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")
|
|
{
|
|
|
|
fs::path filePath = req.get_body_temporary_file();
|
|
|
|
uint64_t uploadAfter = -1;
|
|
std::string strUploadAfter = request_uri.query("uploadAfter");
|
|
if (strUploadAfter.length() != 0)
|
|
{
|
|
uploadAfter = std::stol(strUploadAfter);
|
|
}
|
|
|
|
auto instanceId = ImportBankFile(*(this->model), filePath, 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")
|
|
{
|
|
UserUploadResponse result;
|
|
try
|
|
{
|
|
|
|
res.set(HttpField::content_type, "application/json");
|
|
res.set(HttpField::cache_control, "no-cache");
|
|
std::string instanceIdString = request_uri.query("id");
|
|
std::string directory = request_uri.query("directory");
|
|
std::string filename = request_uri.query("filename");
|
|
std::string patchProperty = request_uri.query("property");
|
|
|
|
int64_t instanceId;
|
|
{
|
|
std::istringstream ss{ instanceIdString };
|
|
ss >> instanceId;
|
|
if (!ss)
|
|
{
|
|
throw PiPedalException("Invalid instanceID");
|
|
}
|
|
}
|
|
|
|
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");
|
|
|
|
fs::path outputFileName = std::filesystem::path(directory) / filename;
|
|
|
|
if (filename.ends_with(".zip"))
|
|
{
|
|
ExtensionChecker extensionChecker{ request_uri.query("ext") };
|
|
namespace fs = std::filesystem;
|
|
|
|
try
|
|
{
|
|
auto zipFile = ZipFileReader::Create(req.get_body_temporary_file());
|
|
std::vector<std::string> files = zipFile->GetFiles();
|
|
bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile);
|
|
if (!hasSingleRootDirectory)
|
|
{
|
|
directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).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);
|
|
}
|
|
}
|
|
}
|
|
// 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 = fs::path(directory) / returnPath;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Lv2Log::error(SS("Unzip failed. " << e.what()));
|
|
throw;
|
|
}
|
|
FileSystemSync();
|
|
}
|
|
else
|
|
{
|
|
outputFileName = this->model->UploadUserFile(directory, instanceId, patchProperty, filename, req.get_body_input_stream(), req.content_length());
|
|
}
|
|
|
|
if (outputFileName.is_relative())
|
|
{
|
|
outputFileName = this->model->GetPluginUploadDirectory() / outputFileName;
|
|
}
|
|
result.path_ = outputFileName.string();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
result.errorMessage_ = e.what();
|
|
}
|
|
|
|
std::stringstream ss;
|
|
json_writer writer(ss);
|
|
writer.write(result);
|
|
std::string response = ss.str();
|
|
|
|
res.setContentLength(response.length());
|
|
res.setBody(response);
|
|
}
|
|
else
|
|
{
|
|
throw PiPedalException("Not found");
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Lv2Log::error(SS("Error uploading file: " << e.what()));
|
|
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 StripPortNumber(const std::string& fromAddress)
|
|
{
|
|
std::string address = fromAddress;
|
|
|
|
if (address.size() == 0)
|
|
return fromAddress;
|
|
|
|
char lastChar = address[address.size() - 1];
|
|
size_t pos = address.find_last_of(':');
|
|
|
|
// if ipv6, make sure we found an actual port address.
|
|
size_t posBracket = address.find_last_of(']');
|
|
if (posBracket != std::string::npos && pos != std::string::npos)
|
|
{
|
|
if (posBracket > pos)
|
|
{
|
|
pos = std::string::npos;
|
|
}
|
|
}
|
|
if (pos != std::string::npos)
|
|
{
|
|
address = address.substr(0, pos);
|
|
}
|
|
return address;
|
|
}
|
|
|
|
/* 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:
|
|
PiPedalModel& model;
|
|
uint64_t maxUploadSize;
|
|
int portNumber;
|
|
|
|
public:
|
|
InterceptConfig(PiPedalModel& model, int portNumber, uint64_t maxUploadSize)
|
|
: RequestHandler("/var/config.json"),
|
|
model(model),
|
|
maxUploadSize(maxUploadSize),
|
|
portNumber(portNumber)
|
|
{
|
|
}
|
|
std::string GetConfig(const std::string& fromAddress)
|
|
{
|
|
std::string webSocketAddress = GetNonLinkLocalAddress(StripPortNumber(fromAddress));
|
|
Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber));
|
|
|
|
std::stringstream s;
|
|
|
|
s << "{ \"socket_server_port\": " << portNumber
|
|
<< ", \"socket_server_address\": \"" << webSocketAddress
|
|
<< "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize
|
|
<< ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false")
|
|
<< ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false")
|
|
<< ", \"tone3000_A2_models\": " << (model.Configuration().GetTone3000A2Models() ? " true" : "false")
|
|
<< " }";
|
|
|
|
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(model,port, maxUploadSize) };
|
|
server.AddRequestHandler(interceptConfig);
|
|
|
|
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
|
server.AddRequestHandler(downloadIntercept);
|
|
|
|
std::shared_ptr<ModWebIntercept> modWebIntercept = ModWebIntercept::Create(&model);
|
|
server.AddRequestHandler(modWebIntercept);
|
|
}
|