Handle permission denied errors.

This commit is contained in:
Robin E. R. Davies
2025-06-24 13:59:57 -04:00
parent 30938d967b
commit 98ee16bb82
10 changed files with 258 additions and 329 deletions
+17 -2
View File
@@ -207,7 +207,13 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::QueryTracks()
std::vector<AudioFileMetadata> AudioDirectoryInfoImpl::GetFiles() std::vector<AudioFileMetadata> AudioDirectoryInfoImpl::GetFiles()
{ {
OpenAudioDb(); OpenAudioDb();
std::vector<DbFileInfo> dbFiles = UpdateDbFiles(); std::vector<DbFileInfo> dbFiles;
try {
dbFiles = UpdateDbFiles();
} catch (const std::exception &e) {
Lv2Log::error("Error updating audio file info: %s - %s",
path.string().c_str(), e.what());
}
std::vector<AudioFileMetadata> metadataResults; std::vector<AudioFileMetadata> metadataResults;
for (const auto &dbFile : dbFiles) for (const auto &dbFile : dbFiles)
{ {
@@ -298,7 +304,16 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
for (auto dirEntry : fs::directory_iterator(path)) for (auto dirEntry : fs::directory_iterator(path))
{ {
if (!dirEntry.is_directory()) bool isDirectory;
try {
isDirectory = dirEntry.is_directory();
} catch (
const std::filesystem::filesystem_error &e) {
Lv2Log::error("Error accessing directory entry: %s - %s", dirEntry.path().string().c_str(), e.what());
continue; // skip this entry if we cannot access it.
}
if (!isDirectory)
{ {
auto path = dirEntry.path(); auto path = dirEntry.path();
std::string name = path.filename(); std::string name = path.filename();
+51 -44
View File
@@ -142,25 +142,27 @@ uint32_t FileMetadataFeature::getFileMetadata(
json_variant jsonData; json_variant jsonData;
std::string result; std::string result;
if (fs::exists(path)) try
{ {
try if (fs::exists(path))
{ {
std::ifstream f(path); std::ifstream f(path);
json_reader reader(f); json_reader reader(f);
reader.read(&jsonData); reader.read(&jsonData);
result = jsonData.as_object()->at(std::string(key)).as_string(); result = jsonData.as_object()->at(std::string(key)).as_string();
} }
catch (const std::exception &e) else
{ {
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
return 0; return 0;
} }
} }
else catch (const std::exception &e)
{ {
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key)); Lv2Log::debug(SS("Permission denied for " << absolute_path << " key: " << key));
return 0; return 0;
} }
size_t len = result.length() + 1; // +1 for null terminator size_t len = result.length() + 1; // +1 for null terminator
if (len > fileMetadataSize || fileMetadata == nullptr) if (len > fileMetadataSize || fileMetadata == nullptr)
{ {
@@ -168,7 +170,7 @@ uint32_t FileMetadataFeature::getFileMetadata(
return len; return len;
} }
memcpy(fileMetadata, result.c_str(), len); memcpy(fileMetadata, result.c_str(), len);
Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result)); Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result));
return len; return len;
} }
PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata( PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
@@ -181,52 +183,57 @@ PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
} }
Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key)); Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
std::filesystem::path path{SS(absolute_path << ".mdata")}; try {
std::filesystem::path path{SS(absolute_path << ".mdata")};
if (!fs::exists(path)) if (!fs::exists(path))
{ {
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist
} }
json_variant jsonData; json_variant jsonData;
try try
{ {
std::ifstream f(path); std::ifstream f(path);
json_reader reader(f); json_reader reader(f);
reader.read(&jsonData); reader.read(&jsonData);
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata
} }
auto obj = jsonData.as_object(); auto obj = jsonData.as_object();
auto it = obj->find(std::string(key)); auto it = obj->find(std::string(key));
if (it == obj->end()) if (it == obj->end())
{ {
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found
} }
obj->erase(it); obj->erase(it);
if (obj->begin() == obj->end()) if (obj->begin() == obj->end())
{ {
// If the object is empty, delete the metadata file // If the object is empty, delete the metadata file
fs::remove(path); fs::remove(path);
return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully
} }
std::ofstream f(path); std::ofstream f(path);
if (!f.is_open()) if (!f.is_open())
{ {
Lv2Log::error(SS("Failed to write metadata file " << path)); Lv2Log::error(SS("Failed to write metadata file " << path));
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
}
json_writer writer(f);
writer.write(jsonData);
return PIPEDAL_FILE_METADATA_SUCCESS;
} catch (const std::exception &e) {
Lv2Log::error(SS("Exception while deleting metadata: " << e.what()));
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
} }
json_writer writer(f);
writer.write(jsonData);
return PIPEDAL_FILE_METADATA_SUCCESS;
} }
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata( PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
+76 -50
View File
@@ -1690,7 +1690,8 @@ static void AddFilesToResult(
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); throw std::logic_error(
SS("GetFileList failed. " << rootPath.string() << " - " << error.what()));
} }
// sort lexicographically // sort lexicographically
@@ -1723,7 +1724,7 @@ static void AddTracksToResult(
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions( std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
modDirectoryInfo ? modDirectoryInfo->modType : ""); modDirectoryInfo ? modDirectoryInfo->modType : "");
if (validExtensions.size() == 0) if (validExtensions.size() == 0)
{ {
const auto &audioExtensions = MimeTypes::instance().AudioExtensions(); const auto &audioExtensions = MimeTypes::instance().AudioExtensions();
validExtensions.insert(audioExtensions.begin(), audioExtensions.end()); validExtensions.insert(audioExtensions.begin(), audioExtensions.end());
@@ -1733,54 +1734,76 @@ static void AddTracksToResult(
try try
{ {
// Add directories first. // Add directories first.
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath)) try
{ {
if (!IsValidUtf8(dir_entry.path().string())) for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
{ {
Lv2Log::warning("Invalid UTF-8 name in directory: " + dir_entry.path().string()); if (!IsValidUtf8(dir_entry.path().string()))
continue; // skip invalid UTF-8 names. {
} Lv2Log::warning("Invalid UTF-8 name in directory: " + dir_entry.path().string());
const auto &path = dir_entry.path(); continue; // skip invalid UTF-8 names.
auto name = path.filename().string(); }
if (dir_entry.is_directory()) const auto &path = dir_entry.path();
{ auto name = path.filename().string();
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)}); try {
if (dir_entry.is_directory())
{
resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()});
}
} catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what()));
}
} }
} }
catch (const std::exception &error)
{
throw std::logic_error(
SS("AddTracksToResult failed to enumerate directories. " << rootPath.string() << " - " << error.what()));
}
auto collator = Locale::GetInstance()->GetCollator(); auto collator = Locale::GetInstance()->GetCollator();
std::sort( std::sort(
resultFiles.begin(), resultFiles.begin(),
resultFiles.end(), resultFiles.end(),
[collator](const FileEntry &l, const FileEntry &r) [collator](const FileEntry &l, const FileEntry &r)
{
if (l.isDirectory_ != r.isDirectory_)
{ {
return l.isDirectory_ > r.isDirectory_; if (l.isDirectory_ != r.isDirectory_)
} {
return collator->Compare(l.displayName_ ,r.displayName_) < 0; return l.isDirectory_ > r.isDirectory_;
}); }
return collator->Compare(l.displayName_, r.displayName_) < 0;
});
// Add audio files. // Add audio files.
auto audioFiles = AudioDirectoryInfo::Create(rootPath, auto audioFiles = AudioDirectoryInfo::Create(rootPath,
GetShadowIndexDirectory(audioRootDirectory,rootPath) GetShadowIndexDirectory(audioRootDirectory, rootPath));
);
for (const auto &audioFile : audioFiles->GetFiles()) try
{ {
fs::path audioFilePath = rootPath / audioFile.fileName(); for (const auto &audioFile : audioFiles->GetFiles())
std::string extension = UiFileProperty::GetFileExtension(audioFilePath);
if (validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*"))
{ {
resultFiles.push_back( fs::path audioFilePath = rootPath / audioFile.fileName();
FileEntry( std::string extension = UiFileProperty::GetFileExtension(audioFilePath);
audioFilePath, if (validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*"))
audioFile.title(), {
false, resultFiles.push_back(
std::make_shared<AudioFileMetadata>(audioFile))); FileEntry(
audioFilePath,
audioFile.title(),
false,
std::make_shared<AudioFileMetadata>(audioFile)));
}
} }
} }
catch (const std::exception &error)
{
throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
}
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); throw std::logic_error(SS("GetFileList failed. " << error.what() << "(" << rootPath.string() << ")"));
} }
} }
@@ -1869,7 +1892,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
if (IsInAudioTracksDirectory(relativePath)) if (IsInAudioTracksDirectory(relativePath))
{ {
AddTracksToResult(this->GetPluginUploadDirectory(),result, rootModDirectory, fileProperty, relativePath); AddTracksToResult(this->GetPluginUploadDirectory(), result, rootModDirectory, fileProperty, relativePath);
} }
else else
{ {
@@ -2046,7 +2069,7 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
else else
{ {
std::filesystem::remove(fileName); std::filesystem::remove(fileName);
if (IsInAudioTracksDirectory(fileName)) if (IsInAudioTracksDirectory(fileName))
{ {
// remove the metadata file as well. // remove the metadata file as well.
std::filesystem::remove(fileName.string() + ".mdata"); std::filesystem::remove(fileName.string() + ".mdata");
@@ -2074,13 +2097,12 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
return result; return result;
} }
bool Storage::IsValidArtworkFile(const std::filesystem::path &fullPath)
bool Storage::IsValidArtworkFile(const std::filesystem::path& fullPath)
{ {
if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string())) if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string()))
{ {
// allow artwork files. // allow artwork files.
return true; return true;
} }
return false; return false;
} }
@@ -2208,9 +2230,10 @@ std::string Storage::RenameFilePropertyFile(
} }
std::filesystem::rename(oldPath, newPath); std::filesystem::rename(oldPath, newPath);
if (fs::exists(oldPath.string()+".mdata")) { if (fs::exists(oldPath.string() + ".mdata"))
{
// rename the metadata file as well. // rename the metadata file as well.
std::filesystem::rename(oldPath.string()+".mdata", newPath.string()+".mdata"); std::filesystem::rename(oldPath.string() + ".mdata", newPath.string() + ".mdata");
} }
return newPath; return newPath;
} }
@@ -2222,12 +2245,13 @@ fs::path MakeVersionedPath(const fs::path &path)
return path; // no need to version a non-existing file. return path; // no need to version a non-existing file.
} }
fs::path newPath = path; fs::path newPath = path;
auto stem = newPath.stem().string();; auto stem = newPath.stem().string();
;
if (stem.ends_with(")")) if (stem.ends_with(")"))
{ {
// remove the trailing (n) from the file name. // remove the trailing (n) from the file name.
size_t pos = stem.find_last_of('('); size_t pos = stem.find_last_of('(');
std::string stemVersion = stem.substr(pos+1, stem.length()-1-(pos+1)); std::string stemVersion = stem.substr(pos + 1, stem.length() - 1 - (pos + 1));
// check if it is a number. // check if it is a number.
if (stemVersion.find_first_not_of("0123456789") == std::string::npos) if (stemVersion.find_first_not_of("0123456789") == std::string::npos)
{ {
@@ -2237,7 +2261,7 @@ fs::path MakeVersionedPath(const fs::path &path)
{ {
// remove trailing space. // remove trailing space.
stem = stem.substr(0, stem.length() - 1); stem = stem.substr(0, stem.length() - 1);
} }
} }
} }
@@ -2246,12 +2270,15 @@ fs::path MakeVersionedPath(const fs::path &path)
{ {
// append (n) to the file name. // append (n) to the file name.
std::string newFileName; std::string newFileName;
if (version == 0) { if (version == 0)
{
newFileName = SS(stem << newPath.extension().string()); newFileName = SS(stem << newPath.extension().string());
} else { }
else
{
// append (n) to the file name. // append (n) to the file name.
newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string()); newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string());
} }
newPath = newPath.parent_path() / newFileName; newPath = newPath.parent_path() / newFileName;
++version; ++version;
} }
@@ -2280,7 +2307,6 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath; std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath;
if (!this->IsValidSampleFileName(newPath)) if (!this->IsValidSampleFileName(newPath))
{ {
throw std::runtime_error("Invalid file name."); throw std::runtime_error("Invalid file name.");
@@ -2299,14 +2325,14 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::create_hard_link(oldPath, newPath); std::filesystem::create_hard_link(oldPath, newPath);
if (IsInAudioTracksDirectory(oldPath) if (IsInAudioTracksDirectory(oldPath) && IsInAudioTracksDirectory(newPath))
&& IsInAudioTracksDirectory(newPath))
{ {
std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata"); std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata");
if (fs::exists(metadataPath)) { if (fs::exists(metadataPath))
{
// copy the metadata file as well. // copy the metadata file as well.
std::filesystem::copy_file( std::filesystem::copy_file(
metadataPath, metadataPath,
SS(newPath.string() << ".mdata"), SS(newPath.string() << ".mdata"),
fs::copy_options::overwrite_existing); fs::copy_options::overwrite_existing);
} }
+81 -76
View File
@@ -52,15 +52,13 @@ static const std::string PLUGIN_PRESETS_MIME_TYPE = "application/vnd.pipedal.plu
static const std::string PRESET_MIME_TYPE = "application/vnd.pipedal.preset"; 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 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_INDEFINITELY = "max-age=31536000,public,immutable"; // 1 year
static const std::string CACHE_CONTROL_SHORT = "max-age=300,public"; // 5 minutes static const std::string CACHE_CONTROL_SHORT = "max-age=300,public"; // 5 minutes
using namespace pipedal; using namespace pipedal;
using namespace boost::system; using namespace boost::system;
namespace fs = std::filesystem; namespace fs = std::filesystem;
class UserUploadResponse class UserUploadResponse
{ {
public: public:
@@ -87,7 +85,7 @@ int32_t ConvertThumbnailSize(const std::string &param)
{ {
return 0; return 0;
} }
return static_cast<int32_t > (std::stoi(param)); return static_cast<int32_t>(std::stoi(param));
} }
static bool IsZipFile(const std::filesystem::path &path) static bool IsZipFile(const std::filesystem::path &path)
@@ -128,7 +126,6 @@ private:
std::vector<std::string> extensions; std::vector<std::string> extensions;
}; };
class DownloadIntercept : public RequestHandler class DownloadIntercept : public RequestHandler
{ {
PiPedalModel *model; PiPedalModel *model;
@@ -392,16 +389,15 @@ public:
{ {
auto lastModified = std::filesystem::last_write_time(path); auto lastModified = std::filesystem::last_write_time(path);
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified)); res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
} }
AudioDirectoryInfo::Ptr CreateDirectoryInfo(const fs::path &path) { AudioDirectoryInfo::Ptr CreateDirectoryInfo(const fs::path &path)
{
return AudioDirectoryInfo::Create(path, return AudioDirectoryInfo::Create(path,
GetShadowIndexDirectory( GetShadowIndexDirectory(
this->model->GetPluginUploadDirectory(), this->model->GetPluginUploadDirectory(),
path)); path));
} }
virtual void get_response( virtual void get_response(
const uri &request_uri, const uri &request_uri,
HttpRequest &req, HttpRequest &req,
@@ -500,89 +496,98 @@ public:
throw PiPedalException("File not found."); throw PiPedalException("File not found.");
} }
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path()); AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path());
auto files = audioDirectory->GetFiles(); try
for (const auto &file : files)
{ {
std::string fileNameOnly = path.filename(); auto files = audioDirectory->GetFiles();
if (file.fileName() == fileNameOnly) for (const auto &file : files)
{ {
std::stringstream ss; std::string fileNameOnly = path.filename();
json_writer writer(ss); if (file.fileName() == fileNameOnly)
writer.write(file); {
res.setBody(ss.str()); std::stringstream ss;
return; 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. // If we get here, the file was not found in the directory.
throw PiPedalException("File not found in directory."); throw PiPedalException("File not found in directory.");
} }
else if (segment == "Thumbnail") else if (segment == "Thumbnail")
{ {
ThumbnailTemporaryFile thumbnailTemporaryFile;
try try
{ {
fs::path path = request_uri.query("path"); std::shared_ptr<TemporaryFile> thumbnail;
if (path.empty() || try
!fs::exists(path) ||
!this->model->IsInUploadsDirectory(path) ||
HasDotDot(path))
{ {
// path for folder thumbnails. fs::path path = request_uri.query("path");
path = request_uri.query("ffile"); if (path.empty() ||
!fs::exists(path) ||
std::shared_ptr<TemporaryFile> thumbnail; !this->model->IsInUploadsDirectory(path) ||
HasDotDot(path))
if (!path.empty() && fs::exists(path) &&
this->model->IsInUploadsDirectory(path) &&
!HasDotDot(path))
{ {
// path is a folder. std::shared_ptr<TemporaryFile> thumbnail;
thumbnail = std::make_shared<TemporaryFile>(); // path for folder thumbnails.
thumbnail->SetNonDeletedPath(path); path = request_uri.query("ffile");
}
else if (!path.empty() && fs::exists(path) &&
{ this->model->IsInUploadsDirectory(path) &&
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile(); !HasDotDot(path))
thumbnail = std::make_shared<TemporaryFile>(); {
thumbnail->SetNonDeletedPath(t.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;
} }
res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension())); int32_t width = ConvertThumbnailSize(request_uri.query("w"));
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified. int32_t height = ConvertThumbnailSize(request_uri.query("h"));
setLastModifiedFromFile(res,thumbnail->Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
res.setBodyFile(thumbnail);
return;
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)
{
int32_t width = ConvertThumbnailSize(request_uri.query("w")); fs::path defaultThumbnail = model->GetWebRoot() / "img/missing_thumbnail.jpg";
int32_t height = ConvertThumbnailSize(request_uri.query("h")); thumbnailTemporaryFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(
path.parent_path());
audioDirectory->GetFiles(); // ensure that the .index file is up to date.
ThumbnailTemporaryFile thumbnail;
try {
thumbnail = audioDirectory->GetThumbnail(path.filename(), width, height);
} catch (const std::exception &e) {
thumbnail = audioDirectory->DefaultThumbnailTemporaryFile();
} }
res.set(HttpField::content_type, thumbnail.GetMimeType()); res.set(HttpField::content_type, thumbnailTemporaryFile.GetMimeType());
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp. res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted with time-stamp.
setLastModifiedFromFile(res, path); setLastModifiedFromFile(res, thumbnailTemporaryFile.Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail.Path()))); res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnailTemporaryFile.Path())));
std::filesystem::path t = thumbnail.Path(); std::filesystem::path t = thumbnailTemporaryFile.Path();
res.setBodyFile(t,thumbnail.DeleteFile()); res.setBodyFile(t, thumbnailTemporaryFile.DeleteFile());
thumbnail.Detach(); thumbnailTemporaryFile.Detach();
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::error("Error getting thumbnail: %s - (%s)", request_uri.str().c_str(), e.what()); Lv2Log::error("Error getting thumbnail: %s - (%s)", request_uri.str().c_str(), e.what());
throw e; throw e;
} }
} }
@@ -605,11 +610,11 @@ public:
{ {
result = (directoryPath / result).string(); result = (directoryPath / result).string();
} }
// json-encode the result. // json-encode the result.
std::stringstream ss; std::stringstream ss;
json_writer writer(ss); json_writer writer(ss);
writer.write(result); writer.write(result);
res.setBody(ss.str()); res.setBody(ss.str());
} }
@@ -637,11 +642,11 @@ public:
{ {
result = (directoryPath / result).string(); result = (directoryPath / result).string();
} }
// json-encode the result. // json-encode the result.
std::stringstream ss; std::stringstream ss;
json_writer writer(ss); json_writer writer(ss);
writer.write(result); writer.write(result);
res.setBody(ss.str()); res.setBody(ss.str());
} }
-1
View File
@@ -105,7 +105,6 @@ add_custom_command(
public/var/current_pedalboard.json public/var/current_pedalboard.json
public/var/config.json public/var/config.json
public/favicon.ico public/favicon.ico
public/index.html
public/iso_codes.json public/iso_codes.json
public/logo512.png public/logo512.png
public/sample_lv2_plugins.json public/sample_lv2_plugins.json
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-visual" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light"/> <!-- uses media queries in web views. --> <meta name="color-scheme" content="dark light"/> <!-- uses media queries in web views. -->
<meta name="description" content="PiPedal Guitar Stomp Box for Raspberry Pi" /> <meta name="description" content="PiPedal Guitar Stomp Box for Raspberry Pi" />
-136
View File
@@ -1,136 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light"/> <!-- uses media queries in web views. -->
<meta name="description" content="PiPedal Guitar Pedals" />
<!--
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
-->
<link rel="stylesheet" href="/css/roboto.css" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PiPedal</title>
<style>
BODY {
background: #D0D0D0;
}
</style>
<style id="bgStyle">
BODY {
background: #333; overscroll-behavior: "none"
}
</style>
<script>
const androidHosted = !!(window.AndroidHost);
var colorScheme = localStorage.getItem("colorScheme");
if (androidHosted)
{
var hostColorScheme = window.AndroidHost.getThemePreference();
switch (hostColorScheme)
{
case 0:
colorScheme = "Light";
break;
case 1:
colorScheme = "Dark";
break;
case 2:
{
// use the host's interpretation of the current system night mode.
colorScheme = window.AndroidHost.isDarkTheme() ? "Dark": "Light";
}
break;
}
}
if (!colorScheme)
{
colorScheme = "Dark";
}
var darkMode = false;
var useSystem = false;
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
switch (colorScheme) {
case null:
default:
darkMode = false;
break;
case "Light":
darkMode = false;
break;
case "Dark":
darkMode = true;
break;
case "System":
useSystem = true;
break;
}
if (useSystem) {
darkMode = prefersDark;
}
if (!darkMode) {
let bgStyle = document.getElementById("bgStyle");
if (bgStyle) {
// disable the style block.
bgStyle.setAttribute('media', "max-width: 1px");
}
}
function removeHashOnLoad() {
if (window.location.hash) {
// Store the hash value (without the '#' symbol)
var hash = window.location.hash.substring(1);
// Replace the current URL without the hash
var newUrl = window.location.href.replace(window.location.hash, '');
// Use HTML5 history API to change the URL without reloading the page
history.replaceState(null, document.title, newUrl);
}
}
// Run the function when the window loads
window.addEventListener('load', removeHashOnLoad);
</script>
</head>
<body style="overscroll-behavior: none; overflow: hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" style="height: 100%;position: absolute;left: px;right: 0px;top:0px;bottom: 0px"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+21 -1
View File
@@ -26,6 +26,7 @@ import ButtonBase, { ButtonBaseProps } from "@mui/material/ButtonBase";
export interface DraggableButtonBaseProps extends ButtonBaseProps { export interface DraggableButtonBaseProps extends ButtonBaseProps {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDoubleClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean; onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean;
onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void; onLongPressMove?: (e: React.PointerEvent<HTMLButtonElement>) => void;
onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>| React.MouseEvent<HTMLButtonElement>) => void; onLongPressEnd?: (e: React.PointerEvent<HTMLButtonElement>| React.MouseEvent<HTMLButtonElement>) => void;
@@ -59,7 +60,7 @@ function screenToClient(e: React.PointerEvent): Point {
export default function DraggableButtonBase(props: DraggableButtonBaseProps) { export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
// Ensure that the props are spread correctly // Ensure that the props are spread correctly
const { onClick, onLongPressStart, onLongPressMove: const { onClick, onDoubleClick, onLongPressStart, onLongPressMove:
doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props; doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props;
let [hTimeout, setHTimeout] = React.useState<number | null>(null); let [hTimeout, setHTimeout] = React.useState<number | null>(null);
@@ -68,6 +69,7 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
let [clickSuppressed, setClickSuppressed] = React.useState<boolean>(false); let [clickSuppressed, setClickSuppressed] = React.useState<boolean>(false);
let [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 }); let [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 });
let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null); let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null);
let [lastClick, setLastClick] = React.useState<number | null>(null);
function handleSuppressClick(e: MouseEvent) { function handleSuppressClick(e: MouseEvent) {
@@ -278,8 +280,26 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
if (onClick) { if (onClick) {
onClick(e); onClick(e);
} }
// check to see whether this is a double-click
let time = Date.now();
if (lastClick && time-lastClick < 500) {
if (onDoubleClick) {
onDoubleClick(e);
}
setLastClick(null);
} else {
setLastClick(time);
}
} }
}} }}
onDoubleClick={(e) => {
// chrome doesnt' do double-click for touch events
// so we need to re-implement it on the onclick handler.
e.stopPropagation();
e.preventDefault();
return false;
}}
onPointerCancelCapture={(e) => { onPointerCancelCapture={(e) => {
if (pointerId !== null) { if (pointerId !== null) {
e.currentTarget.releasePointerCapture(e.pointerId); e.currentTarget.releasePointerCapture(e.pointerId);
+7 -7
View File
@@ -31,7 +31,7 @@ import Typography from '@mui/material/Typography';
import Input from '@mui/material/Input'; import Input from '@mui/material/Input';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import Utility, { nullCast } from './Utility'; import Utility from './Utility';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialIcon from './svg/fx_dial.svg?react'; import DialIcon from './svg/fx_dial.svg?react';
@@ -231,12 +231,12 @@ const PluginControl =
onInputFocus(event: SyntheticEvent): void { onInputFocus(event: SyntheticEvent): void {
//this.displayValueRef.current!.style.display = "none"; //this.displayValueRef.current!.style.display = "none";
this.setState({ editFocused: true }); this.setState({ editFocused: true });
if (Utility.hasIMEKeyboard()) { // if (Utility.hasIMEKeyboard()) {
event.preventDefault(); // event.preventDefault();
event.stopPropagation(); // event.stopPropagation();
this.inputRef.current?.blur(); // this.inputRef.current?.blur();
this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value) // this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
} // }
} }
onInputKeyPress(e: any): void { onInputKeyPress(e: any): void {
if (e.charCode === 13 && this.inputChanged) { if (e.charCode === 13 && this.inputChanged) {
+4 -11
View File
@@ -713,24 +713,17 @@ export default function ToobPlayerControl(
bottom: 0 bottom: 0
}} }}
> >
{/**
*
sx={{
'& .MuiTouchRipple-root': {
},
'& .MuiTouchRipple-ripple': {
transform: 'scale(1.9) !important',
}
}}
*/}
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary" <ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
style={{ style={{
flex: "0 1 auto", maxWidth: 200, flex: "0 1 auto", maxWidth: 200,
textTransform: "none", padding: "4px 8px" textTransform: "none", padding: "4px 8px"
}} }}
disabled={audioFile === ""}
startIcon={(<RepeatIcon />)} startIcon={(<RepeatIcon />)}
onClick={() => { onClick={() => {
setShowLoopDialog(true); if (audioFile !== "") {
setShowLoopDialog(true);
}
}} }}
> >
<Typography noWrap variant="caption"> <Typography noWrap variant="caption">