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()
{
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;
for (const auto &dbFile : dbFiles)
{
@@ -298,7 +304,16 @@ std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
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();
std::string name = path.filename();
+14 -7
View File
@@ -142,25 +142,27 @@ uint32_t FileMetadataFeature::getFileMetadata(
json_variant jsonData;
std::string result;
if (fs::exists(path))
{
try
{
if (fs::exists(path))
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
result = jsonData.as_object()->at(std::string(key)).as_string();
}
catch (const std::exception &e)
{
return 0;
}
}
else
{
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
return 0;
}
}
catch (const std::exception &e)
{
Lv2Log::debug(SS("Permission denied for " << absolute_path << " key: " << key));
return 0;
}
size_t len = result.length() + 1; // +1 for null terminator
if (len > fileMetadataSize || fileMetadata == nullptr)
{
@@ -181,6 +183,7 @@ PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
}
Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
try {
std::filesystem::path path{SS(absolute_path << ".mdata")};
if (!fs::exists(path))
@@ -227,6 +230,10 @@ PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
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
}
}
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
+40 -14
View File
@@ -1690,7 +1690,8 @@ static void AddFilesToResult(
}
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
@@ -1733,6 +1734,8 @@ static void AddTracksToResult(
try
{
// Add directories first.
try
{
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
{
if (!IsValidUtf8(dir_entry.path().string()))
@@ -1742,10 +1745,21 @@ static void AddTracksToResult(
}
const auto &path = dir_entry.path();
auto name = path.filename().string();
try {
if (dir_entry.is_directory())
{
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)});
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();
std::sort(
@@ -1761,8 +1775,10 @@ static void AddTracksToResult(
});
// Add audio files.
auto audioFiles = AudioDirectoryInfo::Create(rootPath,
GetShadowIndexDirectory(audioRootDirectory,rootPath)
);
GetShadowIndexDirectory(audioRootDirectory, rootPath));
try
{
for (const auto &audioFile : audioFiles->GetFiles())
{
fs::path audioFilePath = rootPath / audioFile.fileName();
@@ -1780,7 +1796,14 @@ static void AddTracksToResult(
}
catch (const std::exception &error)
{
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string());
throw std::logic_error(
SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what()));
}
}
catch (const std::exception &error)
{
throw std::logic_error(SS("GetFileList failed. " << error.what() << "(" << rootPath.string() << ")"));
}
}
@@ -2074,7 +2097,6 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
return result;
}
bool Storage::IsValidArtworkFile(const std::filesystem::path &fullPath)
{
if (IsInAudioTracksDirectory(fullPath) && isArtworkFileName(fullPath.filename().string()))
@@ -2208,7 +2230,8 @@ std::string Storage::RenameFilePropertyFile(
}
std::filesystem::rename(oldPath, newPath);
if (fs::exists(oldPath.string()+".mdata")) {
if (fs::exists(oldPath.string() + ".mdata"))
{
// rename the metadata file as well.
std::filesystem::rename(oldPath.string() + ".mdata", newPath.string() + ".mdata");
}
@@ -2222,7 +2245,8 @@ fs::path MakeVersionedPath(const fs::path &path)
return path; // no need to version a non-existing file.
}
fs::path newPath = path;
auto stem = newPath.stem().string();;
auto stem = newPath.stem().string();
;
if (stem.ends_with(")"))
{
// remove the trailing (n) from the file name.
@@ -2246,9 +2270,12 @@ fs::path MakeVersionedPath(const fs::path &path)
{
// append (n) to the file name.
std::string newFileName;
if (version == 0) {
if (version == 0)
{
newFileName = SS(stem << newPath.extension().string());
} else {
}
else
{
// append (n) to the file name.
newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string());
}
@@ -2280,7 +2307,6 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath;
if (!this->IsValidSampleFileName(newPath))
{
throw std::runtime_error("Invalid file name.");
@@ -2299,11 +2325,11 @@ std::string Storage::CopyFilePropertyFile(
std::filesystem::create_hard_link(oldPath, newPath);
if (IsInAudioTracksDirectory(oldPath)
&& IsInAudioTracksDirectory(newPath))
if (IsInAudioTracksDirectory(oldPath) && IsInAudioTracksDirectory(newPath))
{
std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata");
if (fs::exists(metadataPath)) {
if (fs::exists(metadataPath))
{
// copy the metadata file as well.
std::filesystem::copy_file(
metadataPath,
+27 -22
View File
@@ -55,12 +55,10 @@ 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:
@@ -128,7 +126,6 @@ private:
std::vector<std::string> extensions;
};
class DownloadIntercept : public RequestHandler
{
PiPedalModel *model;
@@ -393,15 +390,14 @@ public:
auto lastModified = std::filesystem::last_write_time(path);
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,
GetShadowIndexDirectory(
this->model->GetPluginUploadDirectory(),
path));
}
virtual void get_response(
const uri &request_uri,
HttpRequest &req,
@@ -500,6 +496,8 @@ public:
throw PiPedalException("File not found.");
}
AudioDirectoryInfo::Ptr audioDirectory = CreateDirectoryInfo(path.parent_path());
try
{
auto files = audioDirectory->GetFiles();
for (const auto &file : files)
{
@@ -513,11 +511,21 @@ public:
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");
@@ -526,11 +534,10 @@ public:
!this->model->IsInUploadsDirectory(path) ||
HasDotDot(path))
{
std::shared_ptr<TemporaryFile> thumbnail;
// path for folder thumbnails.
path = request_uri.query("ffile");
std::shared_ptr<TemporaryFile> thumbnail;
if (!path.empty() && fs::exists(path) &&
this->model->IsInUploadsDirectory(path) &&
!HasDotDot(path))
@@ -544,7 +551,6 @@ public:
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(t.Path());
}
res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension()));
@@ -553,10 +559,8 @@ public:
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"));
@@ -564,21 +568,22 @@ public:
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();
thumbnailTemporaryFile = audioDirectory->GetThumbnail(path.filename(), width, height);
}
res.set(HttpField::content_type, thumbnail.GetMimeType());
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, path);
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail.Path())));
setLastModifiedFromFile(res, thumbnailTemporaryFile.Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnailTemporaryFile.Path())));
std::filesystem::path t = thumbnail.Path();
res.setBodyFile(t,thumbnail.DeleteFile());
thumbnail.Detach();
std::filesystem::path t = thumbnailTemporaryFile.Path();
res.setBodyFile(t, thumbnailTemporaryFile.DeleteFile());
thumbnailTemporaryFile.Detach();
}
catch (const std::exception &e)
{
-1
View File
@@ -105,7 +105,6 @@ add_custom_command(
public/var/current_pedalboard.json
public/var/config.json
public/favicon.ico
public/index.html
public/iso_codes.json
public/logo512.png
public/sample_lv2_plugins.json
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<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="color-scheme" content="dark light"/> <!-- uses media queries in web views. -->
<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 {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDoubleClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPressStart?: (currentTarget: HTMLButtonElement, e: React.PointerEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>) => boolean;
onLongPressMove?: (e: React.PointerEvent<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) {
// Ensure that the props are spread correctly
const { onClick, onLongPressStart, onLongPressMove:
const { onClick, onDoubleClick, onLongPressStart, onLongPressMove:
doLongPressMove,onLongPressEnd, longPressDelay,instantMouseLongPress, ...rest } = props;
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 [pointerDownPoint, setPointerDownPoint] = React.useState<Point>({ x: 0, y: 0 });
let [ longPressedElement, setLongPressedElement ] = React.useState<HTMLButtonElement | null>(null);
let [lastClick, setLastClick] = React.useState<number | null>(null);
function handleSuppressClick(e: MouseEvent) {
@@ -278,7 +280,25 @@ export default function DraggableButtonBase(props: DraggableButtonBaseProps) {
if (onClick) {
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) => {
if (pointerId !== null) {
+7 -7
View File
@@ -31,7 +31,7 @@ import Typography from '@mui/material/Typography';
import Input from '@mui/material/Input';
import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch';
import Utility, { nullCast } from './Utility';
import Utility from './Utility';
import MenuItem from '@mui/material/MenuItem';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialIcon from './svg/fx_dial.svg?react';
@@ -231,12 +231,12 @@ const PluginControl =
onInputFocus(event: SyntheticEvent): void {
//this.displayValueRef.current!.style.display = "none";
this.setState({ editFocused: true });
if (Utility.hasIMEKeyboard()) {
event.preventDefault();
event.stopPropagation();
this.inputRef.current?.blur();
this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
}
// if (Utility.hasIMEKeyboard()) {
// event.preventDefault();
// event.stopPropagation();
// this.inputRef.current?.blur();
// this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
// }
}
onInputKeyPress(e: any): void {
if (e.charCode === 13 && this.inputChanged) {
+3 -10
View File
@@ -713,24 +713,17 @@ export default function ToobPlayerControl(
bottom: 0
}}
>
{/**
*
sx={{
'& .MuiTouchRipple-root': {
},
'& .MuiTouchRipple-ripple': {
transform: 'scale(1.9) !important',
}
}}
*/}
<ButtonEx tooltip="Loop Settings" variant="dialogSecondary"
style={{
flex: "0 1 auto", maxWidth: 200,
textTransform: "none", padding: "4px 8px"
}}
disabled={audioFile === ""}
startIcon={(<RepeatIcon />)}
onClick={() => {
if (audioFile !== "") {
setShowLoopDialog(true);
}
}}
>
<Typography noWrap variant="caption">