@@ -29,7 +29,7 @@ jobs:
|
||||
sudo apt install libasound2-dev
|
||||
sudo apt install libwebsocketpp-dev authbind
|
||||
sudo apt install libsdbus-c++-dev libsdbus-c++-bin
|
||||
sudo apt install libavahi-client-dev
|
||||
sudo apt install libavahi-client-dev libzip-dev
|
||||
git submodule update --init --recursive
|
||||
./react-config
|
||||
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.16.0)
|
||||
project(pipedal
|
||||
VERSION 1.2.36
|
||||
VERSION 1.2.37
|
||||
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
||||
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
||||
)
|
||||
set (DISPLAY_VERSION "v1.2.36beta2")
|
||||
set (DISPLAY_VERSION "v1.2.37beta4")
|
||||
|
||||
set (CMAKE_INSTALL_PREFIX "/usr/")
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
<img src='docs/GithubBanner.png' width="100%" /><br/>
|
||||
|
||||
Download: <a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.36 Beta3</a>
|
||||
Download: <a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.36 Beta4</a>
|
||||
|
||||
Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
|
||||
|
||||
|
||||
> NEW version 1.2.36 Beta3 release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
||||
> NEW version 1.2.36 Beta4 release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
||||
|
||||
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
|
||||
PiPedal running on a Raspberry Pi 4 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats.
|
||||
|
||||
+2
-2
@@ -14,14 +14,14 @@
|
||||
"socketServerAddress": "0.0.0.0:80",
|
||||
|
||||
/* Number of threads to use for servicing websockets */
|
||||
"threads" : 10,
|
||||
"threads" : 5,
|
||||
|
||||
"logHttpRequests": false,
|
||||
/* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */
|
||||
"logLevel": 3,
|
||||
|
||||
/* Maximum filesize to allow when uploading */
|
||||
"maxUploadSize": 1048576,
|
||||
"maxUploadSize": 536870912,
|
||||
|
||||
/* Provide access point capture redirects on this gateway. -- provides automatic browser launching on Access Point access.
|
||||
(not implemented)
|
||||
|
||||
@@ -27,7 +27,7 @@ Run the following commands to install dependent libraries required by the PiPeda
|
||||
sudo apt install -y authbind
|
||||
sudo apt install -y libavahi-client-dev
|
||||
sudo apt install -y libnm-dev
|
||||
sudo apt install -y libsdbus-c++-dev
|
||||
sudo apt install -y libsdbus-c++-dev libzip-dev
|
||||
|
||||
|
||||
### Installing Sources
|
||||
|
||||
+3
-3
@@ -13,17 +13,17 @@ page_icon: img/Install4.jpg
|
||||
|
||||
Download the most recent Debian (.deb) package for your platform:
|
||||
|
||||
- [Raspberry Pi OS bookworm (64-bit) v1.2.36 Beta3](https://github.com/rerdavies/pipedal/releases/download/)
|
||||
- [Raspberry Pi OS bookworm (64-bit) v1.2.37 Beta4](https://github.com/rerdavies/pipedal/releases/download/)
|
||||
- [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb)
|
||||
|
||||
Version 1.2.36 has not yet been tested Ubuntu and Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31.
|
||||
Version 1.2.37 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31.
|
||||
|
||||
Install the package by running
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
cd ~/Downloads
|
||||
sudo dpkg -i pipedal_1.2.36_arm64.deb
|
||||
sudo dpkg -i pipedal_1.2.37_arm64.deb
|
||||
```
|
||||
Adjust accordingly if you have downloaded v1.1.31.
|
||||
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
# Release Notes
|
||||
## Pipedal 1.2.37 Beta4
|
||||
|
||||
You can now upload GuitarML Proteus models from the GuitarML project into TooB ML.
|
||||
|
||||
- Download protues models from https://guitarml.com/tonelibrary/tonelib-pro.html
|
||||
- Load the TooB ML plugin.
|
||||
- click on the Model control.
|
||||
- click on the Upload button in the lower left corner of the Model selection dialog.
|
||||
- Upload the .json model file or .zip model collection that you downloaded from The GuitarML Tone Library.
|
||||
|
||||
Features:
|
||||
- ToobML allows uploading of models.
|
||||
- TooBML support for large models (e.g. GuitarML Proteus models)
|
||||
- Support for sub-directories when uploading files.
|
||||
- Support for uploading .zip file bundles.
|
||||
|
||||
Fixes for the following issues:
|
||||
- Dialog button colors follow Android UI conventions.
|
||||
- automatic provisioning of resource files in .LV2 plugins.
|
||||
- The web server supports uploading of large files (limited by default to 512MB).
|
||||
|
||||
|
||||
## Pipedal 1.2.36 Beta3
|
||||
|
||||
Fixes for the following issues:
|
||||
|
||||
+3
-3
@@ -4,17 +4,17 @@
|
||||
|
||||
Download the most recent Debian (.deb) package for your platform:
|
||||
|
||||
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.36/pipedal_1.2.36_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.36 Beta3</a>
|
||||
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.37/pipedal_1.2.37_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.37 Beta4</a>
|
||||
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb">Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31</a>
|
||||
|
||||
v1.2.36 Beta3 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS.
|
||||
v1.2.37 Beta4 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS.
|
||||
|
||||
Install the package by running
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
cd ~/Downloads
|
||||
sudo apt-get install ./pipedal_1.2.36_arm64.deb
|
||||
sudo apt-get install ./pipedal_1.2.37_arm64.deb
|
||||
```
|
||||
|
||||
Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation.
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
|
||||
|
||||
<img src="GithubBanner.png" width="100%"/>
|
||||
<a href="Installing.html"><i>v1.2.36</i></a>
|
||||
<a href="Installing.html"><i>v1.2.37</i></a>
|
||||
|
||||
|
||||
|
||||
To download PiPedal, click [here](download.md).
|
||||
|
||||
> NEW version 1.2.36 Beta3 release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
||||
> NEW version 1.2.37 Beta4 release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
||||
|
||||
|
||||
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,12 @@
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix toobml: <http://two-play.com/plugins/toob-ml#> .
|
||||
|
||||
|
||||
toobml:modelFile
|
||||
a lv2:Parameter;
|
||||
rdfs:label "Model";
|
||||
rdfs:range atom:Path.
|
||||
|
||||
toobml:filterGroup
|
||||
a param:ControlGroup ,
|
||||
pg:InputGroup ;
|
||||
@@ -66,7 +72,25 @@ Generally, TooB ML uses smaller and more efficient neural networks than does Too
|
||||
TooB ML models use significantly less CPU, although TooB Neural Amp Modeller is significantly more flexible, and
|
||||
has a huge number of downloadable models.
|
||||
|
||||
The BluesJR model has an additional Gain control the is used by the BluesJR model to simulate the effect of Blues Junior's gain control.
|
||||
TooB ML models are compatible with models from the GuitarML project (). You can download new models from the GuitarML project
|
||||
at
|
||||
|
||||
https://guitarml.com/tonelibrary/tonelib-pro.html
|
||||
|
||||
To use GuitarML models, download them locally using your browser, and then upload them to TooB ML using the following steps:
|
||||
|
||||
- Load Toob ML in PiPedal.
|
||||
|
||||
- Click on the "Models" control.
|
||||
|
||||
- Click on the _Upload_ button in the lower left corner of the selection dialog.
|
||||
|
||||
- Select the file that you downloaded localy.
|
||||
|
||||
TooB ML can upload indiviual .json files, or collections of models in .zip files downloaded from the GuitarML site.
|
||||
|
||||
Some models have an additional Gain control that can be used to simulate the effect of a Gain control on the amplifier
|
||||
being simulated. The gain control will be enabled if the model you have selected has a gain input.
|
||||
|
||||
For convenience, tone controls are provided for rough shaping of tone before the input signal is fed to the neural net models. The tone
|
||||
section can be completely bypassed by setting all tone controls to exactly 0.5.
|
||||
@@ -85,7 +109,6 @@ on the edge of breakup. It does not work particularly well for models of heavily
|
||||
compressor. Notionally, Sag Feedback models the effect of transformer sag on intermediate transformer stages (which causes them to clip at a lower level than
|
||||
they ordinarily would); and Sag Out models the effect of transformer sag on the final power stage of a guitar amplifier.
|
||||
|
||||
|
||||
The TooB ML Amplifier is based heavily on code ported from from the RTNeural project (https://github.com/jatinchowdhury18/RTNeural), by Jatin Chowdhury,
|
||||
and uses model files from the GuitarML Neural Pi project (https://github.com/GuitarML/NeuralPi).
|
||||
|
||||
@@ -94,9 +117,14 @@ and uses model files from the GuitarML Neural Pi project (https://github.com/Gui
|
||||
mod:brand "TooB";
|
||||
mod:label "TooB ML";
|
||||
lv2:requiredFeature urid:map, work:schedule ;
|
||||
lv2:optionalFeature lv2:hardRTCapable;
|
||||
lv2:optionalFeature state::freePath, state:makePath, state:mapPath ;
|
||||
lv2:extensionData state:interface, work:interface;
|
||||
|
||||
patch:readable
|
||||
toobml:modelFile;
|
||||
patch:writable
|
||||
toobml:modelFile;
|
||||
|
||||
patch:readable
|
||||
<http://two-play.com/plugins/toob#frequencyResponseVector>
|
||||
;
|
||||
@@ -128,12 +156,14 @@ and uses model files from the GuitarML Neural Pi project (https://github.com/Gui
|
||||
units:unit units:db ;
|
||||
],
|
||||
[
|
||||
# legagcy for loading of old instances.
|
||||
a lv2:InputPort, lv2:ControlPort ;
|
||||
lv2:portProperty epp:notOnGUI;
|
||||
lv2:index 2 ;
|
||||
lv2:symbol "model" ;
|
||||
lv2:name "Model" ;
|
||||
lv2:default 0.0 ;
|
||||
lv2:minimum 0.0 ;
|
||||
lv2:default -1.0 ;
|
||||
lv2:minimum -1.0 ;
|
||||
lv2:maximum 32.0 ;
|
||||
lv2:portProperty lv2:enumeration ;
|
||||
lv2:scalePoint
|
||||
@@ -462,6 +492,31 @@ and uses model files from the GuitarML Neural Pi project (https://github.com/Gui
|
||||
|
||||
<http://two-play.com/plugins/toob-ml#piPedalUI>
|
||||
a pipedal_ui:ui ;
|
||||
pipedal_ui:fileProperties
|
||||
[
|
||||
a pipedal_ui:fileProperty;
|
||||
rdfs:label "Model" ;
|
||||
pipedal_ui:directory "ToobMlModels";
|
||||
lv2:index 2 ;
|
||||
pipedal_ui:patchProperty toobml:modelFile ;
|
||||
pipedal_ui:resourceDirectory "models/tones";
|
||||
|
||||
pipedal_ui:fileTypes
|
||||
[
|
||||
a pipedal_ui:fileType;
|
||||
rdfs:label ".json file";
|
||||
pipedal_ui:fileExtension ".json";
|
||||
pipedal_ui:mimeType "application/json";
|
||||
],[
|
||||
a pipedal_ui:fileType;
|
||||
rdfs:label ".ml file";
|
||||
pipedal_ui:fileExtension ".zip";
|
||||
pipedal_ui:mimeType "application/zip";
|
||||
|
||||
]
|
||||
;
|
||||
];
|
||||
|
||||
pipedal_ui:frequencyPlot
|
||||
[
|
||||
lv2:index 8 ;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"socket_server_port": 80,
|
||||
"socket_server_port": 8080,
|
||||
"socket_server_address": "*",
|
||||
"debug": true,
|
||||
"max_upload_size": 1048576,
|
||||
"max_upload_size": 536870912,
|
||||
"fakeAndroid": false,
|
||||
"ui_plugins": []
|
||||
}
|
||||
+69
-27
@@ -21,12 +21,12 @@ import React from 'react';
|
||||
import { ThemeProvider, createTheme, StyledEngineProvider, Theme } from '@mui/material/styles';
|
||||
|
||||
import AppThemed from "./AppThemed";
|
||||
import {isDarkMode} from './DarkMode';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme {
|
||||
mainBackground: React.CSSProperties['color'];
|
||||
toolbarColor: React.CSSProperties['color'];
|
||||
mainBackground: React.CSSProperties['color'];
|
||||
toolbarColor: React.CSSProperties['color'];
|
||||
}
|
||||
interface ThemeOptions {
|
||||
mainBackground?: React.CSSProperties['color'];
|
||||
@@ -35,12 +35,17 @@ declare module '@mui/material/styles' {
|
||||
|
||||
}
|
||||
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
dialogPrimary: true;
|
||||
dialogSecondary: true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
declare module '@mui/styles/defaultTheme' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface DefaultTheme extends Theme {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface DefaultTheme extends Theme { }
|
||||
}
|
||||
|
||||
|
||||
@@ -49,34 +54,71 @@ declare module '@mui/styles/defaultTheme' {
|
||||
const theme = createTheme(
|
||||
isDarkMode() ?
|
||||
{
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#A770E4'// #6750A4" // #5B5690 #60529A #5C5694
|
||||
components: {
|
||||
MuiButton: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: 'dialogPrimary' },
|
||||
style: {
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
props: { variant: 'dialogSecondary', },
|
||||
style: {
|
||||
color: "rgb(255,255,255,0.7)"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
main: "#FF6060"
|
||||
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#A770E4'// #6750A4" // #5B5690 #60529A #5C5694
|
||||
},
|
||||
secondary: {
|
||||
main: "#FF6060"
|
||||
},
|
||||
},
|
||||
},
|
||||
mainBackground: "#222",
|
||||
toolbarColor: '#FFFFFF'
|
||||
}
|
||||
mainBackground: "#222",
|
||||
toolbarColor: '#222'
|
||||
}
|
||||
:
|
||||
{
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#6750A4" // #5B5690 #60529A #5C5694
|
||||
components: {
|
||||
MuiButton: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: 'dialogPrimary' },
|
||||
style: {
|
||||
color: "rgb(0,0,0,0.87)"
|
||||
}
|
||||
},
|
||||
{
|
||||
props: { variant: 'dialogSecondary', },
|
||||
style: {
|
||||
color: "rgb(0,0,0,0.6)"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
main: "#FF6060"
|
||||
}
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#6750A4" // #5B5690 #60529A #5C5694
|
||||
},
|
||||
secondary: {
|
||||
main: "#FF6060"
|
||||
}
|
||||
|
||||
},
|
||||
mainBackground: "#FFFFFF",
|
||||
toolbarColor: '#FFFFFF'
|
||||
},
|
||||
mainBackground: "#FFFFFF",
|
||||
toolbarColor: '#FFFFFF'
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -97,7 +139,7 @@ const App = (class extends React.Component {
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AppThemed/>
|
||||
<AppThemed />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
|
||||
@@ -879,7 +879,7 @@ render() {
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleCloseAlert} color="primary" autoFocus>
|
||||
<Button variant="dialogPrimary" onClick={this.handleCloseAlert} color="primary" autoFocus>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -546,10 +546,10 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
||||
<Typography>Are you sure you want to delete bank '{this.getSelectedBankName()}'?</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={()=> this.handleDeletePromptClose()} color="primary">
|
||||
<Button variant="dialogSecondary" onClick={()=> this.handleDeletePromptClose()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={()=> this.handleDeletePromptOk()} color="secondary" >
|
||||
<Button variant="dialogPrimary" onClick={()=> this.handleDeletePromptOk()} color="secondary" >
|
||||
DELETE
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import createStyles from '@mui/styles/createStyles';
|
||||
import withStyles from '@mui/styles/withStyles';
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, ListenHandle} from './PiPedalModel';
|
||||
import { PiPedalModel, PiPedalModelFactory, ListenHandle,State} from './PiPedalModel';
|
||||
import ButtonBase from '@mui/material/ButtonBase'
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import {PedalboardItem} from './Pedalboard';
|
||||
@@ -98,8 +98,18 @@ const FilePropertyControl =
|
||||
value: "",
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.onStateChanged = this.onStateChanged.bind(this);
|
||||
}
|
||||
|
||||
|
||||
onStateChanged(state: State) {
|
||||
if (this.mounted)
|
||||
{
|
||||
if (state === State.Ready) {
|
||||
this.subscribeToPatchProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
private propertyGetHandle?: ListenHandle;
|
||||
|
||||
monitorPropertyHandle?: ListenHandle;
|
||||
@@ -119,6 +129,7 @@ const FilePropertyControl =
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
unsubscribeToPatchProperty() {
|
||||
if (this.monitorPropertyHandle !== undefined)
|
||||
@@ -126,15 +137,18 @@ const FilePropertyControl =
|
||||
this.model.cancelMonitorPatchProperty(this.monitorPropertyHandle);
|
||||
this.monitorPropertyHandle = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
private mounted: boolean = false;
|
||||
componentDidMount() {
|
||||
this.model.state.addOnChangedHandler(this.onStateChanged);
|
||||
this.mounted = true;
|
||||
this.subscribeToPatchProperty();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.unsubscribeToPatchProperty();
|
||||
this.mounted = false;
|
||||
this.model.state.removeOnChangedHandler(this.onStateChanged);
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyControlProps>, prevState: Readonly<FilePropertyControlState>, snapshot?: any): void {
|
||||
if (prevProps.fileProperty.patchProperty !== this.props.fileProperty.patchProperty
|
||||
|
||||
+515
-137
@@ -18,19 +18,29 @@
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
|
||||
import { Theme, createStyles } from '@mui/material/styles';
|
||||
import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder';
|
||||
import RenameDialog from './RenameDialog';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MoreIcon from '@mui/icons-material/MoreVert';
|
||||
import { PiPedalModel, PiPedalModelFactory, FileEntry } from './PiPedalModel';
|
||||
import Link from '@mui/material/Link';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Button from '@mui/material/Button';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import OldDeleteIcon from './OldDeleteIcon';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { withStyles, WithStyles } from '@mui/styles';
|
||||
|
||||
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
@@ -39,8 +49,20 @@ import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import UploadFileDialog from './UploadFileDialog';
|
||||
import OkCancelDialog from './OkCancelDialog';
|
||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
|
||||
|
||||
export interface FilePropertyDialogProps {
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
secondaryText: {
|
||||
color: theme.palette.text.secondary
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export interface FilePropertyDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean,
|
||||
fileProperty: UiFileProperty,
|
||||
selectedFile: string,
|
||||
@@ -51,16 +73,90 @@ export interface FilePropertyDialogProps {
|
||||
export interface FilePropertyDialogState {
|
||||
fullScreen: boolean;
|
||||
selectedFile: string;
|
||||
navDirectory: string;
|
||||
hasSelection: boolean;
|
||||
files: string[];
|
||||
canDelete: boolean;
|
||||
fileEntries: FileEntry[];
|
||||
columns: number;
|
||||
columnWidth: number;
|
||||
openUploadFileDialog: boolean;
|
||||
openConfirmDeleteDialog: boolean;
|
||||
menuAnchorEl: null | HTMLElement;
|
||||
newFolderDialogOpen: boolean;
|
||||
renameDialogOpen: boolean;
|
||||
moveDialogOpen: boolean;
|
||||
};
|
||||
|
||||
export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
|
||||
function pathExtension(path: string)
|
||||
{
|
||||
let dotPos = path.lastIndexOf('.');
|
||||
if (dotPos === -1) return "";
|
||||
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos !== -1)
|
||||
{
|
||||
if (dotPos <= slashPos+1) return "";
|
||||
}
|
||||
return path.substring(dotPos); // include the '.'.
|
||||
|
||||
}
|
||||
function pathConcat(left: string, right: string) {
|
||||
if (left === "") return right;
|
||||
if (right === "") return left;
|
||||
if (left.endsWith('/')) {
|
||||
left = left.substring(0, left.length - 1);
|
||||
}
|
||||
if (right.startsWith("/")) {
|
||||
right = right.substring(1);
|
||||
}
|
||||
return left + "/" + right;
|
||||
}
|
||||
export function pathFileNameOnly(path: string): string {
|
||||
if (path === "..") return path;
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos < 0) {
|
||||
slashPos = 0;
|
||||
} else {
|
||||
++slashPos;
|
||||
}
|
||||
let extPos = path.lastIndexOf('.');
|
||||
if (extPos < 0 || extPos < slashPos) {
|
||||
extPos = path.length;
|
||||
}
|
||||
|
||||
return path.substring(slashPos, extPos);
|
||||
}
|
||||
|
||||
export function pathFileName(path: string): string {
|
||||
if (path === "..") return path;
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos < 0) {
|
||||
slashPos = 0;
|
||||
} else {
|
||||
++slashPos;
|
||||
}
|
||||
return path.substring(slashPos);
|
||||
}
|
||||
|
||||
|
||||
export default withStyles(styles, { withTheme: true })(
|
||||
class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
|
||||
|
||||
getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string {
|
||||
|
||||
// would be easier if we had the data root (/var/pipedal/audio_downloads, but could be different when we're debugging. :-/
|
||||
let nPos = selectedFile.indexOf("/" + fileProperty.directory + "/");
|
||||
if (nPos === -1) return "";
|
||||
let relativePath = selectedFile.substring(nPos + fileProperty.directory.length + 2);
|
||||
let segments = relativePath.split('/');
|
||||
|
||||
let result = "";
|
||||
for (let i = 0; i < segments.length - 1; ++i) {
|
||||
if (result !== "") result += '/';
|
||||
result += segments[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(props: FilePropertyDialogProps) {
|
||||
super(props);
|
||||
@@ -71,29 +167,32 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
this.state = {
|
||||
fullScreen: false,
|
||||
selectedFile: props.selectedFile,
|
||||
navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty),
|
||||
hasSelection: false,
|
||||
canDelete: false,
|
||||
columns: 0,
|
||||
columnWidth: 1,
|
||||
files: [],
|
||||
fileEntries: [],
|
||||
openUploadFileDialog: false,
|
||||
openConfirmDeleteDialog: false
|
||||
openConfirmDeleteDialog: false,
|
||||
menuAnchorEl: null,
|
||||
newFolderDialogOpen: false,
|
||||
renameDialogOpen: false,
|
||||
moveDialogOpen: false
|
||||
};
|
||||
this.requestScroll = true;
|
||||
this.requestFiles();
|
||||
}
|
||||
|
||||
|
||||
private scrollRef: HTMLDivElement | null = null;
|
||||
|
||||
onScrollRef(element: HTMLDivElement | null) {
|
||||
this.scrollRef = element;
|
||||
this.maybeScrollIntoView();
|
||||
}
|
||||
private maybeScrollIntoView()
|
||||
{
|
||||
if (this.scrollRef !== null && this.state.files.length !== 0 && this.mounted && this.requestScroll)
|
||||
{
|
||||
private maybeScrollIntoView() {
|
||||
if (this.scrollRef !== null && this.state.fileEntries.length !== 0 && this.mounted && this.requestScroll) {
|
||||
this.requestScroll = false;
|
||||
let options: ScrollIntoViewOptions = { block: "nearest"};
|
||||
let options: ScrollIntoViewOptions = { block: "nearest" };
|
||||
options.block = "nearest";
|
||||
|
||||
this.scrollRef.scrollIntoView(options);
|
||||
@@ -102,7 +201,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
private mounted: boolean = false;
|
||||
private model: PiPedalModel;
|
||||
|
||||
private requestFiles() {
|
||||
private requestFiles(navPath: string) {
|
||||
if (!this.props.open) {
|
||||
return;
|
||||
}
|
||||
@@ -110,44 +209,44 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.requestFileList(this.props.fileProperty)
|
||||
this.model.requestFileList2(navPath, this.props.fileProperty)
|
||||
.then((files) => {
|
||||
if (this.mounted) {
|
||||
files.splice(0,0,"");
|
||||
this.setState({ files: files, hasSelection: this.isFileInList(files, this.state.selectedFile) });
|
||||
// let insertionPoint = files.length;
|
||||
// for (let i = 0; i < files.length; ++i) {
|
||||
// if (!files[i].isDirectory) {
|
||||
// insertionPoint = i;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
files.splice(0, 0, { filename: "", isDirectory: false });
|
||||
this.setState({ fileEntries: files, hasSelection: this.isFileInList(files, this.state.selectedFile), navDirectory: navPath });
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.model.showAlert(error.toString())
|
||||
});
|
||||
}
|
||||
|
||||
private lastDivRef : HTMLDivElement | null = null;
|
||||
private lastDivRef: HTMLDivElement | null = null;
|
||||
|
||||
onMeasureRef(div: HTMLDivElement | null)
|
||||
{
|
||||
onMeasureRef(div: HTMLDivElement | null) {
|
||||
this.lastDivRef = div;
|
||||
if (div) {
|
||||
let width = div.offsetWidth;
|
||||
if (width === 0) return;
|
||||
let columns = Math.floor((width-40)/280);
|
||||
if(columns === 0)
|
||||
{
|
||||
columns = 1;
|
||||
}
|
||||
let columnWidth = (width-40)/columns;
|
||||
if (columns !== this.state.columns || columnWidth !== this.state.columnWidth)
|
||||
{
|
||||
this.setState({columns: columns, columnWidth: columnWidth});
|
||||
let columns = 1;
|
||||
let columnWidth = (width - 40) / columns;
|
||||
if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) {
|
||||
this.setState({ columns: columns, columnWidth: columnWidth });
|
||||
}
|
||||
}
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
|
||||
this.setState({
|
||||
fullScreen: height < 200
|
||||
this.setState({
|
||||
fullScreen: height < 200
|
||||
})
|
||||
if (this.lastDivRef !== null)
|
||||
{
|
||||
if (this.lastDivRef !== null) {
|
||||
this.onMeasureRef(this.lastDivRef);
|
||||
}
|
||||
|
||||
@@ -166,45 +265,36 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, snapshot?: any): void {
|
||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
if (prevProps.open !== this.props.open)
|
||||
{
|
||||
if (this.props.open)
|
||||
{
|
||||
this.requestFiles()
|
||||
if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) {
|
||||
if (this.props.open) {
|
||||
let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty);
|
||||
this.setState({
|
||||
selectedFile: this.props.selectedFile,
|
||||
newFolderDialogOpen: false,
|
||||
renameDialogOpen: false,
|
||||
moveDialogOpen: false
|
||||
});
|
||||
this.requestFiles(navDirectory)
|
||||
this.requestScroll = true;
|
||||
if (this.state.selectedFile !== this.props.selectedFile)
|
||||
{
|
||||
this.setState({selectedFile: this.props.selectedFile});
|
||||
}
|
||||
}
|
||||
} else if (prevProps.fileProperty !== this.props.fileProperty) {
|
||||
this.requestFiles()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fileNameOnly(path: string): string {
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos < 0) {
|
||||
slashPos = 0;
|
||||
} else {
|
||||
++slashPos;
|
||||
private isDirectory(path: string): boolean {
|
||||
for (var fileEntry of this.state.fileEntries) {
|
||||
if (fileEntry.filename === path) {
|
||||
return fileEntry.isDirectory;
|
||||
}
|
||||
}
|
||||
let extPos = path.lastIndexOf('.');
|
||||
if (extPos < 0 || extPos < slashPos) {
|
||||
extPos = path.length;
|
||||
}
|
||||
|
||||
return path.substring(slashPos, extPos);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isFileInList(files: string[], file: string) {
|
||||
private isFileInList(files: FileEntry[], file: string) {
|
||||
|
||||
let hasSelection = false;
|
||||
if (file === "") return true;
|
||||
for (var listFile of files) {
|
||||
if (listFile === file) {
|
||||
if (listFile.filename === file) {
|
||||
hasSelection = true;
|
||||
break;
|
||||
}
|
||||
@@ -214,113 +304,262 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
|
||||
onSelectValue(selectedFile: string) {
|
||||
this.requestScroll = true;
|
||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.files, selectedFile) })
|
||||
this.setState({ selectedFile: selectedFile, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) })
|
||||
}
|
||||
onDoubleClickValue(selectedFile: string) {
|
||||
this.props.onOk(this.props.fileProperty,selectedFile);
|
||||
this.openSelectedFile();
|
||||
}
|
||||
|
||||
handleDelete() {
|
||||
this.setState({openConfirmDeleteDialog: true});
|
||||
handleMenuOpen(event: React.MouseEvent<HTMLElement>) {
|
||||
this.setState({ menuAnchorEl: event.currentTarget });
|
||||
|
||||
}
|
||||
handleConfirmDelete()
|
||||
{
|
||||
this.setState({openConfirmDeleteDialog: false});
|
||||
if (this.state.hasSelection)
|
||||
{
|
||||
handleMenuClose() {
|
||||
this.setState({ menuAnchorEl: null });
|
||||
}
|
||||
handleDelete() {
|
||||
this.setState({ openConfirmDeleteDialog: true });
|
||||
}
|
||||
handleConfirmDelete() {
|
||||
this.setState({ openConfirmDeleteDialog: false });
|
||||
if (this.state.hasSelection) {
|
||||
let selectedFile = this.state.selectedFile;
|
||||
let position = -1;
|
||||
for (let i = 0; i < this.state.files.length;++i)
|
||||
{
|
||||
let file = this.state.files[i];
|
||||
if (file === selectedFile)
|
||||
{
|
||||
for (let i = 0; i < this.state.fileEntries.length; ++i) {
|
||||
let file = this.state.fileEntries[i];
|
||||
if (file.filename === selectedFile) {
|
||||
position = i;
|
||||
}
|
||||
}
|
||||
let newSelection = "";
|
||||
if (position >= 0 && position < this.state.files.length-1)
|
||||
{
|
||||
newSelection = this.state.files[position+1];
|
||||
} else if (position === this.state.files.length-1)
|
||||
{
|
||||
if (position >= 0 && position < this.state.fileEntries.length - 1) {
|
||||
newSelection = this.state.fileEntries[position + 1].filename;
|
||||
} else if (position === this.state.fileEntries.length - 1) {
|
||||
if (position !== 0) {
|
||||
newSelection = this.state.files[position-1];
|
||||
newSelection = this.state.fileEntries[position - 1].filename;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.model.deleteUserFile(this.state.selectedFile)
|
||||
.then(
|
||||
()=> {
|
||||
this.setState({selectedFile: newSelection,hasSelection: newSelection !== ""});
|
||||
this.requestFiles();
|
||||
}
|
||||
).catch(
|
||||
(e: any)=>
|
||||
{
|
||||
this.model.showAlert(e + "");
|
||||
}
|
||||
);
|
||||
.then(
|
||||
() => {
|
||||
this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" });
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
}
|
||||
).catch(
|
||||
(e: any) => {
|
||||
this.model.showAlert(e + "");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private wantsScrollRef: boolean = true;
|
||||
|
||||
mapKey: number = 0;
|
||||
render() {
|
||||
this.mapKey = 0;
|
||||
let columnWidth = this.state.columnWidth;
|
||||
navigate(relativeDirectory: string) {
|
||||
this.requestFiles(relativeDirectory);
|
||||
this.setState({ navDirectory: relativeDirectory });
|
||||
|
||||
}
|
||||
renderBreadcrumbs() {
|
||||
if (this.state.navDirectory === "") {
|
||||
return (<div style={{ height: 0 }} />);
|
||||
}
|
||||
let breadcrumbs: React.ReactElement[] = [(
|
||||
<Link underline="hover"
|
||||
color="inherit"
|
||||
key="1" onClick={() => { this.navigate(""); }}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />
|
||||
Home
|
||||
</Link>
|
||||
)
|
||||
];
|
||||
let directories = this.state.navDirectory.split("/");
|
||||
let target = "";
|
||||
for (let i = 0; i < directories.length - 1; ++i) {
|
||||
target = pathConcat(target, directories[i]);
|
||||
let myTarget = target;
|
||||
breadcrumbs.push((
|
||||
<Link underline="hover" key={(i + 1).toString()}
|
||||
color="inherit" onClick={() => { this.navigate(myTarget) }}>
|
||||
{directories[i]}
|
||||
</Link>
|
||||
));
|
||||
}
|
||||
{
|
||||
let lastdirectory = directories[directories.length - 1];
|
||||
breadcrumbs.push((
|
||||
<Typography noWrap key={directories.length.toString()} color="text.secondary"> {lastdirectory}</Typography>
|
||||
));
|
||||
|
||||
}
|
||||
return (
|
||||
<Breadcrumbs separator='/' aria-label="breadcrumb" style={{ marginLeft: 24 }}>
|
||||
{breadcrumbs}
|
||||
</Breadcrumbs>
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultPath(): string {
|
||||
try {
|
||||
let storage = window.localStorage;
|
||||
let result = storage.getItem("fpDefaultPath");
|
||||
if (result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
} catch (e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return this.state.navDirectory;
|
||||
}
|
||||
setDefaultPath(path: string) {
|
||||
try {
|
||||
let storage = window.localStorage;
|
||||
storage.setItem("fpDefaultPath",path);
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
hasSelectedFileOrFolder(): boolean {
|
||||
return this.state.hasSelection && this.state.selectedFile !== "";
|
||||
}
|
||||
getFileExtensionList(uiFileProperty: UiFileProperty): string {
|
||||
let result = "";
|
||||
for (var fileType of uiFileProperty.fileTypes)
|
||||
{
|
||||
if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip")
|
||||
{
|
||||
if (result !== "") result = result + ",";
|
||||
result += fileType.fileExtension;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let columnWidth = this.state.columnWidth;
|
||||
return this.props.open &&
|
||||
(
|
||||
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="fileProperty" fullWidth maxWidth="xl" style={{ height: "90%" }}
|
||||
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle >
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onCancel(); }} aria-label="back"
|
||||
<Toolbar style={{ padding: 0 }}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="back"
|
||||
style={{ opacity: 0.6 }}
|
||||
onClick={() => { this.props.onCancel(); }}
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" >{this.props.fileProperty.label}</Typography>
|
||||
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
{this.props.fileProperty.label}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
aria-label="new folder"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
style={{ opacity: 0.6, marginRight: 8 }}
|
||||
onClick={(ev) => { this.onNewFolder(); }}
|
||||
>
|
||||
<CreateNewFolderIcon />
|
||||
</IconButton>
|
||||
|
||||
|
||||
<IconButton
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
style={{ opacity: 0.6 }}
|
||||
onClick={(ev) => { this.handleMenuOpen(ev); }}
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={this.state.menuAnchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={this.state.menuAnchorEl !== null}
|
||||
onClose={() => { this.handleMenuClose(); }}
|
||||
>
|
||||
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
|
||||
{this.hasSelectedFileOrFolder() && (<Divider />)}
|
||||
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose();this.onMove(); }}>Move</MenuItem>)}
|
||||
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose();this.onRename(); }}>Rename</MenuItem>)}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
<div style={{ flex: "0 0 auto", marginLeft: "auto", marginRight: "auto" }}>
|
||||
{this.renderBreadcrumbs()}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: "rgba(0,0,0,0.2" }}> </div>
|
||||
<div style={{ flex: "0 0 auto", height: "1px", background: isDarkMode() ? "rgba(255,255,255,0.1" : "rgba(0,0,0,0.2" }}> </div>
|
||||
<div style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row wrap", overflowX: "hidden", overflowY: "auto" }}>
|
||||
<div
|
||||
ref={(element)=> this.onMeasureRef(element)}
|
||||
style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
|
||||
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingTop: 16,paddingBottom: 16 }}>
|
||||
<div
|
||||
ref={(element) => this.onMeasureRef(element)}
|
||||
style={{
|
||||
flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
|
||||
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingTop: 16, paddingBottom: 16
|
||||
}}>
|
||||
{
|
||||
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
|
||||
this.state.files.map(
|
||||
(value: string, index: number) => {
|
||||
let displayValue = value;
|
||||
if (displayValue === "")
|
||||
{
|
||||
this.state.fileEntries.map(
|
||||
(value: FileEntry, index: number) => {
|
||||
let isDefault = this.props.fileProperty.directory.startsWith("default");
|
||||
let displayValue = value.filename;
|
||||
if (displayValue === "") {
|
||||
displayValue = "<none>";
|
||||
} else {
|
||||
displayValue = this.fileNameOnly(value);
|
||||
if (isDefault)
|
||||
{
|
||||
displayValue = pathFileName(displayValue);
|
||||
} else {
|
||||
displayValue = pathFileNameOnly(displayValue);
|
||||
}
|
||||
}
|
||||
let selected = value === this.state.selectedFile;
|
||||
let selected = value.filename === this.state.selectedFile;
|
||||
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
|
||||
if (isDarkMode()) {
|
||||
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
|
||||
}
|
||||
let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined;
|
||||
if (selected)
|
||||
{
|
||||
scrollRef = (element)=> { this.onScrollRef(element)};
|
||||
if (selected) {
|
||||
scrollRef = (element) => { this.onScrollRef(element) };
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonBase key={this.mapKey++}
|
||||
|
||||
<ButtonBase key={value.filename}
|
||||
|
||||
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
|
||||
onClick={() => this.onSelectValue(value)} onDoubleClick={()=> {this.onDoubleClickValue(value);}}
|
||||
onClick={() => this.onSelectValue(value.filename)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }}
|
||||
>
|
||||
<div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{displayValue}</Typography>
|
||||
{value.filename === "" ?
|
||||
(<InsertDriveFileOutlinedIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />)
|
||||
: (
|
||||
value.isDirectory ? (
|
||||
<FolderIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
) : (
|
||||
<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
))}
|
||||
<Typography noWrap className={classes.secondaryText} variant="body2" style={{ flex: "1 1 auto", textAlign: "left" }}>{displayValue}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
@@ -333,25 +572,26 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
||||
onClick={()=> this.handleDelete()} >
|
||||
disabled={!this.state.hasSelection || this.state.selectedFile === ""}
|
||||
onClick={() => this.handleDelete()} >
|
||||
<OldDeleteIcon fontSize='small' />
|
||||
</IconButton>
|
||||
|
||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
||||
onClick={()=> { this.setState({openUploadFileDialog: true})}}
|
||||
onClick={() => { this.setState({ openUploadFileDialog: true }) }}
|
||||
>
|
||||
<div>Upload</div>
|
||||
</Button>
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
|
||||
<Button onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
||||
<Button variant="dialogSecondary" onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={{ flex: "0 0 auto" }}
|
||||
onClick={() => { this.props.onOk(this.props.fileProperty, this.state.selectedFile); }}
|
||||
color="secondary"
|
||||
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||
onClick={() => { this.openSelectedFile(); }}
|
||||
|
||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
||||
>
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
@@ -364,22 +604,160 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
||||
this.setState({ openUploadFileDialog: false });
|
||||
}
|
||||
}
|
||||
uploadPage={ "uploadUserFile?directory="+ encodeURIComponent(this.props.fileProperty.directory)}
|
||||
onUploaded={()=> { this.requestFiles();}}
|
||||
uploadPage={
|
||||
"uploadUserFile?directory=" + encodeURIComponent(
|
||||
pathConcat(this.props.fileProperty.directory,this.state.navDirectory)
|
||||
)
|
||||
+ "&ext="
|
||||
+ encodeURIComponent(this.getFileExtensionList(this.props.fileProperty))
|
||||
}
|
||||
onUploaded={() => { this.requestFiles(this.state.navDirectory); }}
|
||||
fileProperty={this.props.fileProperty}
|
||||
|
||||
|
||||
/>
|
||||
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
||||
text="Are you sure you want to delete the selected file?"
|
||||
text={
|
||||
(this.isDirectory(this.state.selectedFile))
|
||||
? "Are you sure you want to delete the selected directory and all its contents?"
|
||||
: "Are you sure you want to delete the selected file?"}
|
||||
okButtonText="Delete"
|
||||
onOk={()=>this.handleConfirmDelete()}
|
||||
onClose={()=>{
|
||||
this.setState({openConfirmDeleteDialog: false});
|
||||
onOk={() => this.handleConfirmDelete()}
|
||||
onClose={() => {
|
||||
this.setState({ openConfirmDeleteDialog: false });
|
||||
}}
|
||||
|
||||
/>
|
||||
{
|
||||
this.state.newFolderDialogOpen && (
|
||||
<RenameDialog open={this.state.newFolderDialogOpen} defaultName=""
|
||||
onOk={(newName) => { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }}
|
||||
onClose={() => { this.setState({ newFolderDialogOpen: false }); }}
|
||||
acceptActionName="OK"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.renameDialogOpen && (
|
||||
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
|
||||
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
|
||||
onClose={() => { this.setState({ renameDialogOpen: false }); }}
|
||||
acceptActionName="OK"
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.moveDialogOpen && (
|
||||
(
|
||||
<FilePropertyDirectorySelectDialog
|
||||
open={this.state.moveDialogOpen}
|
||||
uiFileProperty={this.props.fileProperty}
|
||||
defaultPath={this.getDefaultPath()}
|
||||
excludeDirectory={
|
||||
this.isDirectory(this.state.selectedFile)
|
||||
? pathConcat(this.state.navDirectory,pathFileName(this.state.selectedFile)): ""}
|
||||
onClose={() => {this.setState({moveDialogOpen: false});}}
|
||||
onOk={
|
||||
(path) => {
|
||||
this.setState({moveDialogOpen: false});
|
||||
this.setDefaultPath(path);
|
||||
this.onExecuteMove(path);
|
||||
}
|
||||
}
|
||||
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
openSelectedFile(): void {
|
||||
if (this.isDirectory(this.state.selectedFile)) {
|
||||
let directoryName = pathFileNameOnly(this.state.selectedFile);
|
||||
let navDirectory = pathConcat(this.state.navDirectory, directoryName);
|
||||
this.requestFiles(navDirectory);
|
||||
this.setState({ navDirectory: navDirectory });
|
||||
} else {
|
||||
this.props.onOk(this.props.fileProperty, this.state.selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
private renameDefaultName(): string {
|
||||
let name = this.state.selectedFile;
|
||||
if (name === "") return "";
|
||||
if (this.isDirectory(name))
|
||||
{
|
||||
return pathFileName(name);
|
||||
} else {
|
||||
return pathFileNameOnly(name);
|
||||
}
|
||||
}
|
||||
private onMove(): void {
|
||||
this.setState({ moveDialogOpen: true });
|
||||
}
|
||||
private onExecuteMove(newDirectory: string)
|
||||
{
|
||||
let fileName = pathFileName(this.state.selectedFile);
|
||||
let oldFilePath = pathConcat(this.state.navDirectory,fileName);
|
||||
let newFilePath = pathConcat(newDirectory,fileName);
|
||||
|
||||
this.model.renameFilePropertyFile(oldFilePath,newFilePath,this.props.fileProperty)
|
||||
.then(()=>{
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
})
|
||||
.catch((e)=>{
|
||||
this.model.showAlert(e.toString());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private onRename(): void {
|
||||
this.setState({ renameDialogOpen: true });
|
||||
}
|
||||
private onExecuteRename(newName: string) {
|
||||
let newPath: string = "";
|
||||
let oldPath: string = "";
|
||||
if (this.isDirectory(this.state.selectedFile))
|
||||
{
|
||||
let oldName = pathFileName(this.state.selectedFile);
|
||||
if (oldName === newName) return;
|
||||
oldPath = pathConcat(this.state.navDirectory,oldName);
|
||||
newPath = pathConcat(this.state.navDirectory,newName);;
|
||||
} else {
|
||||
let oldName = pathFileNameOnly(this.state.selectedFile);
|
||||
if (oldName === newName) return;
|
||||
let extension = pathExtension(this.state.selectedFile);
|
||||
oldPath = pathConcat(this.state.navDirectory,oldName+extension);
|
||||
newPath = pathConcat(this.state.navDirectory,newName + extension);
|
||||
}
|
||||
this.model.renameFilePropertyFile(oldPath,newPath,this.props.fileProperty)
|
||||
.then((newPath)=>{
|
||||
this.setState({selectedFile: newPath});
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
this.requestScroll = true
|
||||
|
||||
})
|
||||
.catch((e) =>{
|
||||
this.model.showAlert(e.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private onNewFolder(): void {
|
||||
this.setState({ newFolderDialogOpen: true });
|
||||
}
|
||||
|
||||
private onExecuteNewFolder(newName: string) {
|
||||
this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory,newName), this.props.fileProperty)
|
||||
.then((newPath) => {
|
||||
this.setState({selectedFile: newPath});
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
this.requestScroll = true
|
||||
})
|
||||
.catch((e) => {
|
||||
this.model.showAlert(e.toString());
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,347 @@
|
||||
// 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.
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Divider from '@mui/material/Divider';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
|
||||
function pathConcat(l: string, r: string): string
|
||||
{
|
||||
if (l.length === 0) return r;
|
||||
if (r.length === 0) return l;
|
||||
return l +'/' + r;
|
||||
}
|
||||
|
||||
class DirectoryTree {
|
||||
name: string = "";
|
||||
path: string = "";
|
||||
expanded: boolean = false;
|
||||
children: DirectoryTree[] = [];
|
||||
|
||||
constructor(tree: FilePropertyDirectoryTree, parentTree: (DirectoryTree | null) = null) {
|
||||
this.name = tree.directoryName;
|
||||
if (parentTree) {
|
||||
this.path = pathConcat(parentTree.path,tree.directoryName);
|
||||
} else {
|
||||
this.name = "Home";
|
||||
this.path = tree.directoryName;
|
||||
this.expanded = true;
|
||||
}
|
||||
for (var treeChild of tree.children) {
|
||||
this.children.push(new DirectoryTree(treeChild, this));
|
||||
}
|
||||
}
|
||||
canExpand(): boolean {
|
||||
return this.children.length !== 0;
|
||||
}
|
||||
expand(path: string): void {
|
||||
this.expanded = true;
|
||||
|
||||
let currentNode: DirectoryTree = this;
|
||||
for (var segment of path.split('/')) {
|
||||
let found = false;
|
||||
for (var child of currentNode.children) {
|
||||
if (child.name === segment) {
|
||||
found = true;
|
||||
if (child.children.length !== 0) {
|
||||
child.expanded = true;
|
||||
}
|
||||
currentNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static remove_(directoryTree: DirectoryTree, path: string): boolean
|
||||
{
|
||||
for (let i = 0; i < directoryTree.children.length; ++i)
|
||||
{
|
||||
let child = directoryTree.children[i];
|
||||
if (child.path === path)
|
||||
{
|
||||
directoryTree.children.splice(i,1)
|
||||
return true;
|
||||
}
|
||||
if (DirectoryTree.remove_(child,path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
remove(path: string): boolean
|
||||
{
|
||||
return DirectoryTree.remove_(this,path);
|
||||
}
|
||||
find(path: string): DirectoryTree | null {
|
||||
let currentNode: DirectoryTree = this;
|
||||
for (var segment of path.split('/')) {
|
||||
let found = false;
|
||||
for (var child of currentNode.children) {
|
||||
if (child.name === segment) {
|
||||
found = true;
|
||||
currentNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return currentNode;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface FilePropertyDirectorySelectDialogProps {
|
||||
open: boolean,
|
||||
uiFileProperty: UiFileProperty;
|
||||
defaultPath: string,
|
||||
excludeDirectory: string,
|
||||
onOk: (path: string) => void,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface FilePropertyDirectorySelectDialogState {
|
||||
fullScreen: boolean;
|
||||
selectedPath: string;
|
||||
directoryTree?: DirectoryTree;
|
||||
hasSelection: boolean;
|
||||
directoryTreeInvalidatecount: number;
|
||||
|
||||
};
|
||||
|
||||
export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveComponent<FilePropertyDirectorySelectDialogProps, FilePropertyDirectorySelectDialogState> {
|
||||
|
||||
private model: PiPedalModel;
|
||||
private refSelected: React.Ref<HTMLElement>;
|
||||
|
||||
constructor(props: FilePropertyDirectorySelectDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
fullScreen: false,
|
||||
selectedPath: props.defaultPath,
|
||||
directoryTree: undefined,
|
||||
hasSelection: false,
|
||||
directoryTreeInvalidatecount: 0
|
||||
|
||||
};
|
||||
this.refSelected = React.createRef<HTMLElement>();
|
||||
|
||||
this.requestDirectoryTree();
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
private requestScroll = true;
|
||||
requestDirectoryTree() {
|
||||
if (!this.props.open) return;
|
||||
|
||||
this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty)
|
||||
.then((filePropertyDirectoryTree) => {
|
||||
let myTree = new DirectoryTree(filePropertyDirectoryTree);
|
||||
if (this.props.excludeDirectory)
|
||||
{
|
||||
myTree.remove(this.props.excludeDirectory);
|
||||
}
|
||||
myTree.expand(this.state.selectedPath);
|
||||
|
||||
let hasSelection = myTree.find(this.state.selectedPath) != null;
|
||||
this.setState({ directoryTree: myTree, hasSelection: hasSelection });
|
||||
this.requestScroll = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.model.showAlert(e.toString());
|
||||
this.setState({ hasSelection: false });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyDirectorySelectDialogProps>, prevState: Readonly<FilePropertyDirectorySelectDialogState>, snapshot: any): void {
|
||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
|
||||
if (prevProps.open !== this.props.open) {
|
||||
if (this.props.open) {
|
||||
this.setState({ selectedPath: this.props.defaultPath, hasSelection: false });
|
||||
this.requestDirectoryTree();
|
||||
this.requestScroll = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onOK() {
|
||||
if (this.state.hasSelection) {
|
||||
this.props.onOk(this.state.selectedPath);
|
||||
}
|
||||
}
|
||||
onClose() {
|
||||
this.props.onClose();
|
||||
}
|
||||
private onToggleExpand(directoryTree: DirectoryTree)
|
||||
{
|
||||
directoryTree.expanded = !directoryTree.expanded;
|
||||
this.setState({directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount+1});
|
||||
}
|
||||
private onTreeClick(directoryTree: DirectoryTree) {
|
||||
if (!this.state.directoryTree) return;
|
||||
this.state.directoryTree.expand(directoryTree.path);
|
||||
this.setState({ selectedPath: directoryTree.path, hasSelection: true, directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 });
|
||||
}
|
||||
private renderTree_(directoryTree: DirectoryTree) {
|
||||
let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined;
|
||||
let selected = directoryTree.path === this.state.selectedPath;
|
||||
if (selected) {
|
||||
ref = (element) => {
|
||||
if (this.requestScroll) {
|
||||
this.requestScroll = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
|
||||
if (isDarkMode()) {
|
||||
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
|
||||
}
|
||||
let showToggle = directoryTree.canExpand() && directoryTree.path.length !== 0;
|
||||
return (
|
||||
<div key={directoryTree.path} style={{flexGrow: 1}}>
|
||||
<div style={{ width: "100%", display: "flex", flexFlow: "row", flexWrap: "nowrap", justifyContent: "flex-start", alignItems: "center" }}>
|
||||
<IconButton onClick={()=>{ this.onToggleExpand(directoryTree); }}
|
||||
style={{opacity: showToggle ? 1: 0}}
|
||||
disabled={!showToggle}
|
||||
>
|
||||
{
|
||||
directoryTree.expanded &&
|
||||
(
|
||||
<ArrowDropDownIcon />
|
||||
)
|
||||
}
|
||||
{
|
||||
!directoryTree.expanded &&
|
||||
(
|
||||
<ArrowRightIcon/>
|
||||
)
|
||||
}
|
||||
</IconButton>
|
||||
<ButtonBase ref={ref} style={{ flexGrow: 1, flexShrink: 1, position: "relative" }} onClick={() => { this.onTreeClick(directoryTree); }}>
|
||||
<div style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ width: "100%", display: "flex", flexFlow: "row", flexWrap: "nowrap", justifyContent: "flex-start", alignItems: "center", paddingLeft: 8 }}>
|
||||
{
|
||||
directoryTree.path === "" ? (
|
||||
<HomeIcon fontSize="small" />
|
||||
) : (
|
||||
<FolderIcon />
|
||||
)
|
||||
}
|
||||
|
||||
<Typography noWrap style={{ marginLeft: 8, marginTop: 8, marginBottom: 8, textAlign: "left", flexGrow: 1, flexShrink: 1 }} variant="body2">{directoryTree.name === "" ? "Home" : directoryTree.name}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
{directoryTree.expanded
|
||||
&& directoryTree.canExpand()
|
||||
&& (
|
||||
<div style={{ marginLeft: 32 }}>
|
||||
{
|
||||
directoryTree.children.map(
|
||||
(child, index) => {
|
||||
return this.renderTree_(child);
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTree() {
|
||||
if (!this.state.directoryTree) { return undefined; }
|
||||
return this.renderTree_(this.state.directoryTree);
|
||||
}
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="fpDirectorySelect" open={this.props.open} fullWidth onClose={() => { this.onClose(); }} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{ userSelect: "none" }}
|
||||
PaperProps={{ style: { minHeight: "80%", maxHeight: "80%", minWidth: "75%", maxWidth: "75%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="body1">Select folder</Typography>
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent style={{marginLeft: 0, paddingLeft: 0}} >
|
||||
<div style={{ display: "flex", flexGrow: 1, flexShrink: 1, overflowY: "auto" }} >
|
||||
{
|
||||
this.renderTree()
|
||||
}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={() => { this.onClose(); }} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={() => { this.onOK(); }} color="secondary" disabled={!this.state.hasSelection} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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.
|
||||
|
||||
|
||||
export default class FilePropertyDirectoryTree {
|
||||
deserialize(input: any) : FilePropertyDirectoryTree {
|
||||
this.directoryName = input.directoryName;
|
||||
this.children = FilePropertyDirectoryTree.deserialize_array(input.children);
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): FilePropertyDirectoryTree[] {
|
||||
let result: FilePropertyDirectoryTree[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new FilePropertyDirectoryTree().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
directoryName: string = "";
|
||||
children: FilePropertyDirectoryTree[] = [];
|
||||
}
|
||||
@@ -362,10 +362,10 @@ const JackServerSettingsDialog = withStyles(styles)(
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => this.handleApply()} color="primary" disabled={
|
||||
<Button variant="dialogPrimary" onClick={() => this.handleApply()} disabled={
|
||||
(!this.state.alsaDevices) || !this.state.jackServerSettings.valid}>
|
||||
OK
|
||||
</Button>
|
||||
|
||||
@@ -106,7 +106,7 @@ export default class ListSelectDialog extends ResizeResponsiveComponent<ListSele
|
||||
}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={()=> this.props.onClose()} color="primary">
|
||||
<Button variant="dialogSecondary" onClick={()=> this.props.onClose()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -814,8 +814,8 @@ export const LoadPluginDialog =
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ position: "relative", float: "right", flex: "0 1 200px", width: 200, display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ width: 180 }} >SELECT</Button>
|
||||
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ width: 120 }} >Cancel</Button>
|
||||
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ width: 180 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
@@ -839,8 +839,8 @@ export const LoadPluginDialog =
|
||||
<div className={classes.bottom} style={{height: 64}}>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
||||
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
||||
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
@@ -250,6 +250,7 @@ export class UiFileProperty {
|
||||
this.directory = input.directory;
|
||||
this.index = input.index;
|
||||
this.portGroup = input.portGroup;
|
||||
this.resourceDirectory = input.resourceDirectory??"";
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): UiFileProperty[]
|
||||
@@ -303,6 +304,7 @@ export class UiFileProperty {
|
||||
directory: string = "";
|
||||
index: number = -1;
|
||||
portGroup: string = "";
|
||||
resourceDirectory: string = "";
|
||||
|
||||
};
|
||||
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
|
||||
@@ -70,10 +70,10 @@ export default class OkCancelDialog extends React.Component<OkCancelDialogProps,
|
||||
>{text}</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="secondary" >
|
||||
<Button variant="dialogPrimary" onClick={handleOk} >
|
||||
{okButtonText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
+107
-46
@@ -39,6 +39,7 @@ import WifiChannel from './WifiChannel';
|
||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||
import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode';
|
||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||
|
||||
|
||||
export enum State {
|
||||
@@ -52,6 +53,10 @@ export enum State {
|
||||
|
||||
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||
|
||||
export interface FileEntry {
|
||||
filename: string;
|
||||
isDirectory: boolean;
|
||||
};
|
||||
|
||||
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
|
||||
|
||||
@@ -337,7 +342,7 @@ interface Vst3ControlChangedBody {
|
||||
|
||||
export interface FavoritesList {
|
||||
[url: string]: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class PiPedalModel //implements PiPedalModel
|
||||
@@ -626,25 +631,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
|
||||
|
||||
requestPluginClasses(): Promise<boolean> {
|
||||
const myRequest = new Request(this.varRequest('plugin_classes.json'));
|
||||
return fetch(myRequest)
|
||||
.then(
|
||||
response => response.json()
|
||||
)
|
||||
.then(data => {
|
||||
let plugins = new PluginClass().deserialize(data);
|
||||
|
||||
this.validatePlugins(plugins);
|
||||
this.plugin_classes.set(plugins);
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setError("Can't contact server.\n\n" + error);
|
||||
return false;
|
||||
});
|
||||
|
||||
}
|
||||
getWebSocket(): PiPedalSocket {
|
||||
if (this.webSocket === undefined) {
|
||||
throw new PiPedalStateError("Attempt to access web socket before it's connected.");
|
||||
@@ -653,6 +639,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
onSocketConnectionLost() {
|
||||
// remove all the events and subscriptions we have.
|
||||
// yyy
|
||||
this.vuSubscriptions = [];
|
||||
this.monitorPatchPropertyListeners = [];
|
||||
|
||||
if (this.isAndroidHosted()) {
|
||||
this.androidHost?.setDisconnected(true);
|
||||
}
|
||||
@@ -756,8 +747,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
}
|
||||
|
||||
maxFileUploadSize: number = 100000000;
|
||||
maxPresetUploadSize: number = 512 * 1024;
|
||||
maxFileUploadSize: number = 512*1024*1024;
|
||||
maxPresetUploadSize: number = 1024 * 1024;
|
||||
debug: boolean = false;
|
||||
|
||||
|
||||
@@ -777,7 +768,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.androidHost = new FakeAndroidHost();
|
||||
}
|
||||
this.debug = !!data.debug;
|
||||
let { socket_server_port, socket_server_address } = data;
|
||||
let { socket_server_port, socket_server_address,max_upload_size } = data;
|
||||
if ((!socket_server_address) || socket_server_address === "*") {
|
||||
socket_server_address = window.location.hostname;
|
||||
}
|
||||
@@ -788,6 +779,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
this.socketServerUrl = socket_server;
|
||||
this.varServerUrl = var_server_url;
|
||||
this.maxFileUploadSize = parseInt(max_upload_size);
|
||||
|
||||
this.webSocket = new PiPedalSocket(
|
||||
this.socketServerUrl,
|
||||
@@ -918,6 +910,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
// MUST not allow reconnect until at least one complete load has finished.
|
||||
this.webSocket.canReconnect = true;
|
||||
}
|
||||
this.setState(State.Ready);
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -929,20 +922,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
;
|
||||
}
|
||||
|
||||
requestCurrentPedalboard(): Promise<void> {
|
||||
const myRequest = new Request(this.varRequest('current_pedalboard.json'));
|
||||
return fetch(myRequest)
|
||||
.then(
|
||||
(response) => {
|
||||
return response.json();
|
||||
}
|
||||
)
|
||||
.then(data => {
|
||||
let pedalboard = new Pedalboard().deserialize(data);
|
||||
pedalboard.ensurePedalboardIds();
|
||||
this.pedalboard.set(pedalboard);
|
||||
});
|
||||
}
|
||||
|
||||
onError(msg: string): void {
|
||||
this.errorMessage.set(msg);
|
||||
@@ -1630,14 +1609,21 @@ export class PiPedalModel //implements PiPedalModel
|
||||
});
|
||||
}
|
||||
|
||||
// deprecated.
|
||||
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
||||
return nullCast(this.webSocket)
|
||||
.request<string[]>('requestFileList', piPedalFileProperty);
|
||||
}
|
||||
|
||||
requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
|
||||
return nullCast(this.webSocket)
|
||||
.request<FileEntry[]>('requestFileList2',
|
||||
{relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty}
|
||||
);
|
||||
}
|
||||
|
||||
deleteUserFile(fileName: string) : Promise<boolean>
|
||||
{
|
||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName)
|
||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName);
|
||||
}
|
||||
|
||||
|
||||
@@ -2053,10 +2039,10 @@ export class PiPedalModel //implements PiPedalModel
|
||||
for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) {
|
||||
if (this.monitorPatchPropertyListeners[i].handle === listenHandle._handle) {
|
||||
this.monitorPatchPropertyListeners.splice(i, 1);
|
||||
this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle);
|
||||
}
|
||||
|
||||
|
||||
@@ -2247,7 +2233,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
let ws = this.webSocket;
|
||||
if (!ws) return;
|
||||
if (!ws)
|
||||
{
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
ws.request<void>(
|
||||
"setGovernorSettings",
|
||||
governor
|
||||
@@ -2260,6 +2250,74 @@ export class PiPedalModel //implements PiPedalModel
|
||||
});
|
||||
});
|
||||
}
|
||||
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
||||
{
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
|
||||
let ws = this.webSocket;
|
||||
if (!ws) {
|
||||
resolve("");
|
||||
return;
|
||||
}
|
||||
ws.request<string>(
|
||||
"createNewSampleDirectory",
|
||||
{
|
||||
relativePath: relativePath,
|
||||
uiFileProperty: uiFileProperty
|
||||
}
|
||||
)
|
||||
.then((newPath) => {
|
||||
resolve(newPath);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty) : Promise<FilePropertyDirectoryTree> {
|
||||
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
||||
let ws = this.webSocket;
|
||||
if (!ws) {
|
||||
resolve(new FilePropertyDirectoryTree());
|
||||
return;
|
||||
}
|
||||
ws.request<FilePropertyDirectoryTree>(
|
||||
"getFilePropertyDirectoryTree",
|
||||
uiFileProperty
|
||||
).then((result)=> {
|
||||
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
||||
{
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
|
||||
let ws = this.webSocket;
|
||||
if (!ws) {
|
||||
resolve("");
|
||||
return;
|
||||
}
|
||||
ws.request<string>(
|
||||
"renameFilePropertyFile",
|
||||
{
|
||||
oldRelativePath: oldRelativePath,
|
||||
newRelativePath: newRelativePath,
|
||||
uiFileProperty: uiFileProperty
|
||||
}
|
||||
)
|
||||
.then((newPath) => {
|
||||
resolve(newPath);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
|
||||
let result = new Promise<void>((resolve, reject) => {
|
||||
let oldSettings = this.wifiConfigSettings.get();
|
||||
@@ -2362,7 +2420,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
.catch((err) => {
|
||||
//resolve();
|
||||
});
|
||||
// yyy
|
||||
resolve();
|
||||
|
||||
});
|
||||
@@ -2555,8 +2612,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
if (this.getTheme() !== value)
|
||||
{
|
||||
setColorScheme(value);
|
||||
this.reloadPage();
|
||||
}
|
||||
setTimeout(()=>{
|
||||
this.reloadPage();
|
||||
},
|
||||
200);
|
||||
}
|
||||
}
|
||||
|
||||
reloadRequested: boolean = false;
|
||||
@@ -2564,10 +2624,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
reloadPage() {
|
||||
this.reloadRequested = true;
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
window.location.reload();
|
||||
let url = window.location.href.split('#')[0];
|
||||
window.location.href = url;
|
||||
//window.location.reload();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
let instance: PiPedalModel | undefined = undefined;
|
||||
|
||||
@@ -663,7 +663,7 @@ const PluginControlView =
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.showAlert("setPatchProperty failed: " + error);
|
||||
this.model.showAlert("Unable to complete the operation. Audio is not running." + error);
|
||||
});
|
||||
this.setState({ showFileDialog: false });
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => {
|
||||
</div>
|
||||
</PluginInfoDialogContent>
|
||||
<PluginInfoDialogActions>
|
||||
<Button autoFocus onClick={handleClose} color="primary" style={{ width: "130px" }}>
|
||||
<Button variant="dialogPrimary" autoFocus onClick={handleClose} style={{ width: "130px" }}>
|
||||
OK
|
||||
</Button>
|
||||
</PluginInfoDialogActions>
|
||||
|
||||
@@ -25,6 +25,7 @@ import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { nullCast } from './Utility';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
|
||||
|
||||
|
||||
export interface RenameDialogProps {
|
||||
@@ -74,6 +75,12 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
||||
componentDidUpdate()
|
||||
{
|
||||
}
|
||||
checkForIllegalCharacters(filename: string) {
|
||||
if (filename.indexOf('/') !== -1)
|
||||
{
|
||||
throw new Error("Illegal character: '/'");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
@@ -86,6 +93,14 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
||||
const handleOk = () => {
|
||||
let text = nullCast(this.refText.current).value;
|
||||
text = text.trim();
|
||||
try {
|
||||
this.checkForIllegalCharacters(text);
|
||||
} catch (e:any)
|
||||
{
|
||||
let model:PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
model.showAlert(e.toString());
|
||||
return;
|
||||
}
|
||||
if (text.length === 0) return;
|
||||
onOk(text);
|
||||
}
|
||||
@@ -116,10 +131,10 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="secondary" >
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
{acceptActionName}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -107,10 +107,10 @@ function SelectChannelsDialog(props: SelectChannelsDialogProps) {
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary" disabled={currentSelection.length === 0 || currentSelection.length > 2} >
|
||||
<Button onClick={handleOk} variant="dialogPrimary" disabled={currentSelection.length === 0 || currentSelection.length > 2} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -110,10 +110,10 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary" >
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -79,10 +79,10 @@ function SelectThemesDialog(props: SelectThemesDialogProps) {
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary" >
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -885,7 +885,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
onClose={()=> { this.setState({showThemeSelectDialog: false});} }
|
||||
onOk={(selectedTheme: ColorTheme) => {
|
||||
this.model.setTheme(selectedTheme);
|
||||
this.setState({showThemeSelectDialog: false});
|
||||
this.setState({showThemeSelectDialog: false});
|
||||
}}
|
||||
defaultTheme={this.model.getTheme()}
|
||||
/>
|
||||
|
||||
@@ -208,7 +208,11 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
||||
{
|
||||
throw new Error("Invalid file extension.");
|
||||
}
|
||||
let filename = await this.model.uploadFile(this.props.uploadPage, this.uploadList[i].file, "application/octet-stream", upload.abortController);
|
||||
let filename = await this.model.uploadFile(
|
||||
this.props.uploadPage,
|
||||
this.uploadList[i].file,
|
||||
"application/octet-stream",
|
||||
upload.abortController);
|
||||
this.props.onUploaded(filename);
|
||||
upload.status = FileUploadStatus.Uploaded;
|
||||
upload.statusMessage = "Uploaded.";
|
||||
@@ -384,7 +388,8 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
||||
|
||||
<DialogActions >
|
||||
<Button
|
||||
component="label" color="primary" style={{ whiteSpace: "nowrap", textOverflow: "ellipsis", marginLeft: 16, width: 120, flex: "0 0 auto" }}
|
||||
component="label" variant="dialogPrimary" style={{ whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis", marginLeft: 16, width: 120, flex: "0 0 auto" }}
|
||||
>
|
||||
Select Files
|
||||
<input
|
||||
@@ -399,12 +404,12 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
||||
{
|
||||
this.state.okEnabled ?
|
||||
(
|
||||
<Button onClick={() => this.handleClose()} color="primary">
|
||||
<Button onClick={() => this.handleClose()} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<Button onClick={() => this.handleCancel()} color="primary">
|
||||
<Button onClick={() => this.handleCancel()} variant="dialogPrimary" >
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -182,11 +182,11 @@ export default class UploadPresetDialog extends ResizeResponsiveComponent<Upload
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.handleClose()} color="primary">
|
||||
<Button onClick={() => this.handleClose()} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
component="label" color="secondary" style={{width: 120}}
|
||||
component="label" variant="dialogPrimary" style={{width: 120}}
|
||||
>
|
||||
Select File
|
||||
<input
|
||||
|
||||
@@ -337,10 +337,10 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary" style={{ width: 120 }}>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" style={{ width: 120 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={()=> this.handleOk(false)} color="success" style={{ width: 120 }} >
|
||||
<Button onClick={()=> this.handleOk(false)} variant="dialogPrimary" style={{ width: 120 }} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -356,13 +356,13 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })(
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={()=> this.setState({showWifiWarningDialog: false})} color="primary" style={{ width: 120 }}>
|
||||
<Button onClick={()=> this.setState({showWifiWarningDialog: false})} variant="dialogSecondary" style={{ width: 120 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={()=> {
|
||||
this.setState({showWifiWarningDialog: false});
|
||||
this.handleOk(true);
|
||||
}} color="secondary" style={{ width: 120 }} >
|
||||
}} variant="dialogPrimary" style={{ width: 120 }} >
|
||||
PROCEED
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -506,10 +506,10 @@ const WifiDirectConfigDialog = withStyles(styles, { withTheme: true })(
|
||||
)}
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary" style={{ width: 120 }}>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" style={{ width: 120 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => this.handleOk()} color="primary" style={{ width: 120 }} >
|
||||
<Button onClick={() => this.handleOk()} variant="dialogPrimary" style={{ width: 120 }} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
+8
-1
@@ -62,6 +62,7 @@ else()
|
||||
message(STATUS "LILV_0_INCLUDE_DIRS: ${LILV_0_INCLUDE_DIRS}")
|
||||
endif()
|
||||
|
||||
pkg_check_modules(ZIP "lilv-0")
|
||||
|
||||
# Use LV2 headers from the /usr/lib directory.
|
||||
#set (LV2DEV_INCLUDE_DIRS /usr/lib)
|
||||
@@ -131,6 +132,12 @@ else()
|
||||
endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
WebServerConfig.cpp WebServerConfig.hpp
|
||||
Finally.hpp
|
||||
ZipFile.cpp ZipFile.hpp
|
||||
TemporaryFile.cpp TemporaryFile.hpp
|
||||
FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp
|
||||
FileEntry.cpp FileEntry.hpp
|
||||
atom_object.hpp atom_object.cpp
|
||||
FileBrowserFiles.h
|
||||
FileBrowserFilesFeature.hpp FileBrowserFilesFeature.cpp
|
||||
@@ -227,7 +234,7 @@ set (PIPEDAL_INCLUDES
|
||||
.
|
||||
)
|
||||
|
||||
set(PIPEDAL_LIBS libpipedald
|
||||
set(PIPEDAL_LIBS libpipedald zip
|
||||
pthread atomic stdc++fs asound avahi-common avahi-client systemd
|
||||
${VST3_LIBRARIES}
|
||||
${LILV_0_LIBRARIES}
|
||||
|
||||
@@ -111,7 +111,7 @@ typedef struct {
|
||||
get_upload_path returns the supplied path unmodified.
|
||||
`
|
||||
If the filename is a child of rootResourceDirectory, creates a corresponding link to the file in the file browser
|
||||
directory and returns the name of that link.
|
||||
directory and returns the path of that link.
|
||||
|
||||
The caller must free memory for the returned value with `LV2_FileBrowser_Files.free_path()`.
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 "FileEntry.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
JSON_MAP_BEGIN(FileEntry)
|
||||
JSON_MAP_REFERENCE(FileEntry,filename)
|
||||
JSON_MAP_REFERENCE(FileEntry,isDirectory)
|
||||
|
||||
JSON_MAP_END()
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class FileEntry {
|
||||
public:
|
||||
FileEntry() { }
|
||||
FileEntry(std::string filename,bool isDirectory)
|
||||
:filename_(filename), isDirectory_(isDirectory)
|
||||
{
|
||||
|
||||
}
|
||||
std::string filename_;
|
||||
bool isDirectory_ = false;
|
||||
|
||||
DECLARE_JSON_MAP(FileEntry);
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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 "FilePropertyDirectoryTree.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
FilePropertyDirectoryTree::FilePropertyDirectoryTree()
|
||||
{
|
||||
}
|
||||
FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName)
|
||||
: directoryName_(directoryName)
|
||||
{
|
||||
}
|
||||
|
||||
JSON_MAP_BEGIN(FilePropertyDirectoryTree)
|
||||
JSON_MAP_REFERENCE(FilePropertyDirectoryTree, directoryName)
|
||||
JSON_MAP_REFERENCE(FilePropertyDirectoryTree, children)
|
||||
JSON_MAP_END()
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#pragma once
|
||||
#include "json.hpp"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
namespace pipedal {
|
||||
class FilePropertyDirectoryTree {
|
||||
public:
|
||||
using ptr = std::unique_ptr<FilePropertyDirectoryTree>;
|
||||
|
||||
FilePropertyDirectoryTree();
|
||||
FilePropertyDirectoryTree(const std::string&directoryName);
|
||||
std::string directoryName_;
|
||||
std::vector<std::unique_ptr<FilePropertyDirectoryTree>> children_;
|
||||
|
||||
DECLARE_JSON_MAP(FilePropertyDirectoryTree);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#pragma once
|
||||
#include <functional>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Finally {
|
||||
public:
|
||||
Finally(std::function<void(void)> &&fn)
|
||||
:fn (std::move(fn))
|
||||
{
|
||||
|
||||
}
|
||||
~Finally() {
|
||||
fn();
|
||||
}
|
||||
private:
|
||||
std::function<void(void)> fn;
|
||||
};
|
||||
|
||||
}
|
||||
+55
-3
@@ -138,7 +138,7 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration)
|
||||
storage.SetConfigRoot(configuration.GetDocRoot());
|
||||
storage.SetDataRoot(configuration.GetLocalStoragePath());
|
||||
storage.Initialize();
|
||||
lv2Host.SetPluginStoragePath(storage.GetPluginAudioFileDirectory());
|
||||
lv2Host.SetPluginStoragePath(storage.GetPluginUploadDirectory());
|
||||
|
||||
this->systemMidiBindings = storage.GetSystemMidiBindings();
|
||||
|
||||
@@ -1151,6 +1151,7 @@ void PiPedalModel::RestartAudio()
|
||||
|
||||
// do a complete reload.
|
||||
|
||||
|
||||
this->audioHost->SetPedalboard(nullptr);
|
||||
|
||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
||||
@@ -2039,6 +2040,27 @@ std::vector<std::string> PiPedalModel::GetFileList(const UiFileProperty &filePro
|
||||
return std::vector<std::string>(); // don't disclose to users what the problem is.
|
||||
}
|
||||
}
|
||||
std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty)
|
||||
{
|
||||
try
|
||||
{
|
||||
return this->storage.GetFileList2(relativePath,fileProperty);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::warning("GetFileList() failed: (%s)", e.what());
|
||||
return std::vector<FileEntry>(); // don't disclose to users what the problem is.
|
||||
}
|
||||
}
|
||||
|
||||
std::string PiPedalModel::RenameFilePropertyFile(
|
||||
const std::string&oldRelativePath,
|
||||
const std::string&newRelativePath,
|
||||
const UiFileProperty&uiFileProperty)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
return storage.RenameFilePropertyFile(oldRelativePath,newRelativePath,uiFileProperty);
|
||||
}
|
||||
|
||||
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||
{
|
||||
@@ -2046,9 +2068,23 @@ void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||
storage.DeleteSampleFile(fileName);
|
||||
}
|
||||
|
||||
std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, const std::string &fileBody)
|
||||
std::string PiPedalModel::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty)
|
||||
{
|
||||
return storage.UploadUserFile(directory, patchProperty, filename, fileBody);
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
return storage.CreateNewSampleDirectory(relativePath, uiFileProperty);
|
||||
|
||||
}
|
||||
FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||
return storage.GetFilePropertydirectoryTree(uiFileProperty);
|
||||
|
||||
}
|
||||
|
||||
|
||||
std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream&stream, size_t contentLength)
|
||||
{
|
||||
return storage.UploadUserFile(directory, patchProperty, filename, stream,contentLength);
|
||||
}
|
||||
|
||||
uint64_t PiPedalModel::CreateNewPreset()
|
||||
@@ -2058,6 +2094,17 @@ uint64_t PiPedalModel::CreateNewPreset()
|
||||
return storage.CreateNewPreset();
|
||||
}
|
||||
|
||||
void PiPedalModel::CheckForResourceInitialization(Pedalboard &pedalboard)
|
||||
{
|
||||
for (auto item: pedalboard.GetAllPlugins())
|
||||
{
|
||||
if (!item->isSplit())
|
||||
{
|
||||
lv2Host.CheckForResourceInitialization(item->uri(),storage.GetPluginUploadDirectory());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
bool PiPedalModel::LoadCurrentPedalboard()
|
||||
{
|
||||
Lv2PedalboardErrorList errorMessages;
|
||||
@@ -2066,6 +2113,7 @@ bool PiPedalModel::LoadCurrentPedalboard()
|
||||
|
||||
// apply the error messages to the lv2Pedalboard.
|
||||
// return true if the error messages have changed
|
||||
CheckForResourceInitialization(this->pedalboard);
|
||||
audioHost->SetPedalboard(lv2Pedalboard);
|
||||
return true;
|
||||
}
|
||||
@@ -2087,3 +2135,7 @@ void PiPedalModel::OnNotifyLv2RealtimeError(int64_t instanceId, const std::strin
|
||||
}
|
||||
delete[] t;
|
||||
}
|
||||
std::filesystem::path PiPedalModel::GetPluginUploadDirectory() const
|
||||
{
|
||||
return storage.GetPluginUploadDirectory();
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
#include <thread>
|
||||
#include "Promise.hpp"
|
||||
#include "AtomConverter.hpp"
|
||||
#include "FileEntry.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
@@ -189,12 +190,13 @@ namespace pipedal
|
||||
|
||||
PiPedalConfiguration configuration;
|
||||
|
||||
|
||||
void CheckForResourceInitialization(Pedalboard &pedalboard);
|
||||
public:
|
||||
PiPedalModel();
|
||||
virtual ~PiPedalModel();
|
||||
|
||||
uint16_t GetWebPort() const { return webPort; }
|
||||
std::filesystem::path GetPluginUploadDirectory() const;
|
||||
void Close();
|
||||
|
||||
void SetOnboarding(bool value);
|
||||
@@ -328,9 +330,14 @@ namespace pipedal
|
||||
void SetFavorites(const std::map<std::string, bool> &favorites);
|
||||
|
||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
||||
|
||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
||||
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody);
|
||||
std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty);
|
||||
std::string RenameFilePropertyFile(const std::string&oldRelativePath,const std::string&newRelativePath,const UiFileProperty&uiFileProperty);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty);
|
||||
|
||||
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,std::istream&inputStream,size_t streamLength);
|
||||
uint64_t CreateNewPreset();
|
||||
|
||||
bool LoadCurrentPedalboard();
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "SysExec.hpp"
|
||||
#include "PiPedalAlsa.hpp"
|
||||
#include <filesystem>
|
||||
#include "FileEntry.hpp"
|
||||
|
||||
using namespace std;
|
||||
using namespace pipedal;
|
||||
@@ -58,6 +59,33 @@ JSON_MAP_REFERENCE(GetPatchPropertyBody, propertyUri)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
class CreateNewSampleDirectoryArgs {
|
||||
public:
|
||||
std::string relativePath_;
|
||||
UiFileProperty uiFileProperty_;
|
||||
DECLARE_JSON_MAP(CreateNewSampleDirectoryArgs);
|
||||
};
|
||||
|
||||
JSON_MAP_BEGIN(CreateNewSampleDirectoryArgs)
|
||||
JSON_MAP_REFERENCE(CreateNewSampleDirectoryArgs, relativePath)
|
||||
JSON_MAP_REFERENCE(CreateNewSampleDirectoryArgs, uiFileProperty)
|
||||
JSON_MAP_END()
|
||||
|
||||
class RenameSampleFileArgs {
|
||||
public:
|
||||
std::string oldRelativePath_;
|
||||
std::string newRelativePath_;
|
||||
UiFileProperty uiFileProperty_;
|
||||
DECLARE_JSON_MAP(RenameSampleFileArgs);
|
||||
};
|
||||
|
||||
JSON_MAP_BEGIN(RenameSampleFileArgs)
|
||||
JSON_MAP_REFERENCE(RenameSampleFileArgs, oldRelativePath)
|
||||
JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath)
|
||||
JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
class Lv2StateChangedBody {
|
||||
public:
|
||||
uint64_t instanceId_;
|
||||
@@ -198,6 +226,21 @@ JSON_MAP_REFERENCE(MonitorResultBody, subscriptionHandle)
|
||||
JSON_MAP_REFERENCE(MonitorResultBody, value)
|
||||
JSON_MAP_END()
|
||||
|
||||
class FileRequestArgs
|
||||
{
|
||||
public:
|
||||
std::string relativePath_;
|
||||
UiFileProperty fileProperty_;
|
||||
|
||||
DECLARE_JSON_MAP(FileRequestArgs);
|
||||
};
|
||||
JSON_MAP_BEGIN(FileRequestArgs)
|
||||
JSON_MAP_REFERENCE(FileRequestArgs, relativePath)
|
||||
JSON_MAP_REFERENCE(FileRequestArgs, fileProperty)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
|
||||
|
||||
class MonitorPortBody
|
||||
{
|
||||
@@ -1417,6 +1460,13 @@ public:
|
||||
std::vector<std::string> list = this->model.GetFileList(fileProperty);
|
||||
this->Reply(replyTo,"requestFileList",list);
|
||||
}
|
||||
else if (message == "requestFileList2")
|
||||
{
|
||||
FileRequestArgs requestArgs;
|
||||
pReader->read(&requestArgs);
|
||||
std::vector<FileEntry> list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_);
|
||||
this->Reply(replyTo,"requestFileList2",list);
|
||||
}
|
||||
else if (message == "newPreset")
|
||||
{
|
||||
int64_t presetId = this->model.CreateNewPreset();
|
||||
@@ -1430,6 +1480,29 @@ public:
|
||||
this->model.DeleteSampleFile(fileName);
|
||||
this->Reply(replyTo,"deleteUserFile",true);
|
||||
}
|
||||
else if (message == "createNewSampleDirectory")
|
||||
{
|
||||
CreateNewSampleDirectoryArgs args;
|
||||
pReader->read(&args);
|
||||
|
||||
std::string newFileName = this->model.CreateNewSampleDirectory(args.relativePath_,args.uiFileProperty_);
|
||||
this->Reply(replyTo,"createNewSampleDirectory",newFileName);
|
||||
}
|
||||
else if (message == "renameFilePropertyFile")
|
||||
{
|
||||
RenameSampleFileArgs args;
|
||||
pReader->read(&args);
|
||||
|
||||
std::string newFileName = this->model.RenameFilePropertyFile(args.oldRelativePath_,args.newRelativePath_,args.uiFileProperty_);
|
||||
this->Reply(replyTo,"renameFilePropertyFile",newFileName);
|
||||
} else if (message == "getFilePropertyDirectoryTree"){
|
||||
UiFileProperty uiFileProperty;
|
||||
pReader->read(&uiFileProperty);
|
||||
FilePropertyDirectoryTree::ptr result = model.GetFilePropertydirectoryTree(uiFileProperty);
|
||||
this->Reply(replyTo,"GetFilePropertydirectoryTree",result);
|
||||
|
||||
}
|
||||
|
||||
else if (message == "setOnboarding")
|
||||
{
|
||||
bool value;
|
||||
|
||||
+10
-1
@@ -201,7 +201,15 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st
|
||||
{
|
||||
this->portGroup_ = portGroup.AsUri();
|
||||
}
|
||||
|
||||
AutoLilvNode resourceDirectory = lilv_world_get(pWorld,node,pHost->lilvUris->pipedalUI__resourceDirectory,nullptr);
|
||||
if (resourceDirectory)
|
||||
{
|
||||
this->resourceDirectory_ = resourceDirectory.AsString();
|
||||
}
|
||||
if (this->resourceDirectory_.empty())
|
||||
{
|
||||
this->resourceDirectory_ = "default";
|
||||
}
|
||||
this->fileTypes_ = UiFileType::GetArray(pHost, node, pHost->lilvUris->pipedalUI__fileTypes);
|
||||
}
|
||||
|
||||
@@ -460,6 +468,7 @@ JSON_MAP_REFERENCE(UiFileProperty, directory)
|
||||
JSON_MAP_REFERENCE(UiFileProperty, patchProperty)
|
||||
JSON_MAP_REFERENCE(UiFileProperty, fileTypes)
|
||||
JSON_MAP_REFERENCE(UiFileProperty, portGroup)
|
||||
JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(UiFrequencyPlot)
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#define PIPEDAL_UI__patchProperty PIPEDAL_UI_PREFIX "patchProperty"
|
||||
#define PIPEDAL_UI__directory PIPEDAL_UI_PREFIX "directory"
|
||||
#define PIPEDAL_UI__fileTypes PIPEDAL_UI_PREFIX "fileTypes"
|
||||
#define PIPEDAL_UI__resourceDirectory PIPEDAL_UI_PREFIX "resourceDirectory"
|
||||
|
||||
#define PIPEDAL_UI__fileType PIPEDAL_UI_PREFIX "fileType"
|
||||
#define PIPEDAL_UI__fileExtension PIPEDAL_UI_PREFIX "fileExtension"
|
||||
@@ -112,6 +113,7 @@ namespace pipedal {
|
||||
std::vector<UiFileType> fileTypes_;
|
||||
std::string patchProperty_;
|
||||
std::string portGroup_;
|
||||
std::string resourceDirectory_;
|
||||
public:
|
||||
using ptr = std::shared_ptr<UiFileProperty>;
|
||||
UiFileProperty() { }
|
||||
@@ -131,6 +133,8 @@ namespace pipedal {
|
||||
bool IsValidExtension(const std::string&extension) const;
|
||||
static bool IsDirectoryNameValid(const std::string&value);
|
||||
|
||||
const std::string&resourceDirectory() const { return resourceDirectory_; }
|
||||
|
||||
public:
|
||||
DECLARE_JSON_MAP(UiFileProperty);
|
||||
};
|
||||
|
||||
+123
-15
@@ -32,7 +32,7 @@
|
||||
#include "JackConfiguration.hpp"
|
||||
#include "lv2/urid/urid.h"
|
||||
#include "lv2/ui/ui.h"
|
||||
//#include "lv2.h"
|
||||
// #include "lv2.h"
|
||||
#include "lv2/atom/atom.h"
|
||||
#include "lv2/time/time.h"
|
||||
#include "lv2/state/state.h"
|
||||
@@ -127,7 +127,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
atom__bufferType = lilv_new_uri(pWorld, LV2_ATOM__bufferType);
|
||||
atom__Path = lilv_new_uri(pWorld, LV2_ATOM__Path);
|
||||
presets__preset = lilv_new_uri(pWorld, LV2_PRESETS__Preset);
|
||||
state__state = lilv_new_uri(pWorld,LV2_STATE__state);
|
||||
state__state = lilv_new_uri(pWorld, LV2_STATE__state);
|
||||
rdfs__label = lilv_new_uri(pWorld, LILV_NS_RDFS "label");
|
||||
|
||||
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
||||
@@ -140,6 +140,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
pipedalUI__patchProperty = lilv_new_uri(pWorld, PIPEDAL_UI__patchProperty);
|
||||
pipedalUI__directory = lilv_new_uri(pWorld, PIPEDAL_UI__directory);
|
||||
pipedalUI__fileTypes = lilv_new_uri(pWorld, PIPEDAL_UI__fileTypes);
|
||||
pipedalUI__resourceDirectory = lilv_new_uri(pWorld, PIPEDAL_UI__resourceDirectory);
|
||||
pipedalUI__fileProperty = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperty);
|
||||
pipedalUI__fileExtension = lilv_new_uri(pWorld, PIPEDAL_UI__fileExtension);
|
||||
pipedalUI__mimeType = lilv_new_uri(pWorld, PIPEDAL_UI__mimeType);
|
||||
@@ -163,9 +164,9 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
|
||||
lv2__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
||||
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
|
||||
|
||||
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
||||
mod__label = lilv_new_uri(pWorld,MOD_PREFIX "label");
|
||||
mod__brand = lilv_new_uri(pWorld,MOD_PREFIX "brand");
|
||||
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
||||
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
|
||||
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
|
||||
// ui:portNotification
|
||||
// [
|
||||
// ui:portIndex 3;
|
||||
@@ -660,16 +661,15 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
||||
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
|
||||
this->name_ = nodeAsString(name);
|
||||
|
||||
AutoLilvNode brand = lilv_world_get(pWorld,plugUri,lv2Host->lilvUris->mod__brand, nullptr);
|
||||
AutoLilvNode brand = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__brand, nullptr);
|
||||
this->brand_ = nodeAsString(brand);
|
||||
|
||||
AutoLilvNode label = lilv_world_get(pWorld,plugUri,lv2Host->lilvUris->mod__label, nullptr);
|
||||
AutoLilvNode label = lilv_world_get(pWorld, plugUri, lv2Host->lilvUris->mod__label, nullptr);
|
||||
this->label_ = nodeAsString(label);
|
||||
if (label_.length() == 0)
|
||||
{
|
||||
this->label_ = this->name_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AutoLilvNode author_name = (lilv_plugin_get_author_name(pPlugin));
|
||||
this->author_name_ = nodeAsString(author_name);
|
||||
@@ -1242,14 +1242,16 @@ void PluginHost::PortValueCallback(const char *symbol, void *user_data, const vo
|
||||
if (type == pHost->urids->atom__Double)
|
||||
{
|
||||
(*pState->values)[symbol] = (float)(*static_cast<const double *>(value));
|
||||
} else if (type == pHost->urids->atom__Float)
|
||||
}
|
||||
else if (type == pHost->urids->atom__Float)
|
||||
{
|
||||
(*pState->values)[symbol] = *static_cast<const float *>(value);
|
||||
}
|
||||
else if (type == pHost->urids->atom_Int)
|
||||
{
|
||||
(*pState->values)[symbol] = *static_cast<const int32_t *>(value);
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
pState->failed = true;
|
||||
}
|
||||
@@ -1289,7 +1291,6 @@ PluginPresets PluginHost::GetFactoryPluginPresets(const std::string &pluginUri)
|
||||
PresetCallbackState cbData{this, &controlValues, false};
|
||||
lilv_state_emit_port_values(state, PortValueCallback, (void *)&cbData);
|
||||
|
||||
|
||||
int numProperties = lilv_state_get_num_properties(state);
|
||||
// can't handle std:state part of preset.
|
||||
if (numProperties == 0)
|
||||
@@ -1298,11 +1299,12 @@ PluginPresets PluginHost::GetFactoryPluginPresets(const std::string &pluginUri)
|
||||
{
|
||||
result.presets_.push_back(PluginPreset(result.nextInstanceId_++, strLabel, controlValues, Lv2PluginState()));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
result.presets_.push_back(PluginPreset::MakeLilvPreset(result.nextInstanceId_++, strLabel, controlValues, lilv_node_as_uri(preset)));
|
||||
}
|
||||
lilv_state_free(state);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1353,7 +1355,7 @@ Lv2PortGroup::Lv2PortGroup(PluginHost *lv2Host, const std::string &groupUri)
|
||||
name_ = nodeAsString(nameNode);
|
||||
}
|
||||
|
||||
bool Lv2PluginInfo::isSplit() const
|
||||
bool Lv2PluginInfo::isSplit() const
|
||||
{
|
||||
return uri_ == SPLIT_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
@@ -1362,6 +1364,112 @@ std::shared_ptr<HostWorkerThread> PluginHost::GetHostWorkerThread()
|
||||
return pHostWorkerThread;
|
||||
}
|
||||
|
||||
class ResourceInfo {
|
||||
public:
|
||||
ResourceInfo(const std::string&filePropertyDirectory,const std::string&resourceDirectory)
|
||||
:filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {}
|
||||
|
||||
std::string filePropertyDirectory;
|
||||
std::string resourceDirectory;
|
||||
bool operator==(const ResourceInfo&other) const {
|
||||
return this->filePropertyDirectory == other.filePropertyDirectory && this->resourceDirectory == other.resourceDirectory;
|
||||
|
||||
}
|
||||
bool operator<(const ResourceInfo&other) const {
|
||||
if (this->filePropertyDirectory < other.filePropertyDirectory)
|
||||
return true;
|
||||
if (this->filePropertyDirectory > other.filePropertyDirectory)
|
||||
return false;
|
||||
return this->resourceDirectory < other.resourceDirectory;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path&targetDirectory)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
try {
|
||||
if (!fs::exists(targetDirectory)) return false;
|
||||
for (auto&directoryEntry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
fs::path thisPath = directoryEntry.path();
|
||||
if (directoryEntry.is_directory())
|
||||
{
|
||||
auto name = thisPath.filename();
|
||||
fs::path childSource = sourceDirectory / name;
|
||||
fs::path childTarget = targetDirectory / name;
|
||||
if (anyTargetFilesExist(childSource,childTarget))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
fs::path targetPath = targetDirectory / thisPath.filename();
|
||||
if (fs::exists(targetPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const std::exception&) {
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void createTargetLinks(const std::filesystem::path &sourceDirectory, const std::filesystem::path &targetDirectory)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::create_directories(targetDirectory);
|
||||
|
||||
for (auto &dirEntry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
fs::path childSource = dirEntry.path();
|
||||
fs::path childTarget = targetDirectory / childSource.filename();
|
||||
if (dirEntry.is_directory()) {
|
||||
createTargetLinks(childSource,childTarget);
|
||||
} else {
|
||||
fs::create_symlink(childSource,childTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,const std::filesystem::path&pluginUploadDirectory)
|
||||
{
|
||||
|
||||
auto plugin = GetPluginInfo(pluginUri);
|
||||
if (plugin)
|
||||
{
|
||||
std::filesystem::path bundlePath = plugin->bundle_path();
|
||||
if (!plugin->piPedalUI())
|
||||
return;
|
||||
const auto& fileProperties = plugin->piPedalUI()->fileProperties();
|
||||
if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri))
|
||||
{
|
||||
pluginsThatHaveBeenCheckedForResources.insert(pluginUri);
|
||||
|
||||
// eliminate duplicates.
|
||||
std::set<ResourceInfo> resourceInfoSet;
|
||||
for (const auto&fileProperty: fileProperties)
|
||||
{
|
||||
if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty())
|
||||
{
|
||||
resourceInfoSet.insert(ResourceInfo(fileProperty->directory(),fileProperty->resourceDirectory()));
|
||||
}
|
||||
}
|
||||
for (const ResourceInfo&resourceInfo: resourceInfoSet)
|
||||
{
|
||||
std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory;
|
||||
std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory;
|
||||
if (!anyTargetFilesExist(sourcePath,targetPath))
|
||||
{
|
||||
createTargetLinks(sourcePath,targetPath);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// void PiPedalHostLogError(const std::string &error)
|
||||
// {
|
||||
// Lv2Log::error("%s",error.c_str());
|
||||
|
||||
+8
-1
@@ -30,6 +30,7 @@
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include "IHost.hpp"
|
||||
#include <set>
|
||||
|
||||
//#include "lv2.h"
|
||||
#include "Units.hpp"
|
||||
@@ -685,7 +686,7 @@ namespace pipedal
|
||||
AutoLilvNode pipedalUI__patchProperty;
|
||||
|
||||
AutoLilvNode pipedalUI__fileProperty;
|
||||
|
||||
AutoLilvNode pipedalUI__resourceDirectory;
|
||||
AutoLilvNode pipedalUI__fileTypes;
|
||||
AutoLilvNode pipedalUI__fileExtension;
|
||||
AutoLilvNode pipedalUI__mimeType;
|
||||
@@ -811,8 +812,14 @@ namespace pipedal
|
||||
|
||||
public:
|
||||
virtual MapFeature &GetMapFeature() { return this->mapFeature; }
|
||||
void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory);
|
||||
|
||||
private:
|
||||
|
||||
std::set<std::string> pluginsThatHaveBeenCheckedForResources;
|
||||
|
||||
void CheckForResourceInitization(const std::string pluginUri);
|
||||
|
||||
virtual LV2_URID_Map *GetLv2UridMap()
|
||||
{
|
||||
return this->mapFeature.GetMap();
|
||||
|
||||
+232
-35
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022-2023 Robin Davies
|
||||
// Copyright (c) 2022-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
|
||||
@@ -40,6 +40,7 @@ const char *BANKS_FILENAME = "index.banks";
|
||||
#define USER_SETTINGS_FILENAME "userSettings.json";
|
||||
|
||||
Storage::Storage()
|
||||
: locale("en_US.UTF-8")
|
||||
{
|
||||
SetConfigRoot("~/var/Config");
|
||||
SetDataRoot("~/var/PiPedal");
|
||||
@@ -265,7 +266,7 @@ std::filesystem::path Storage::GetPluginPresetsDirectory() const
|
||||
{
|
||||
return this->dataRoot / "plugin_presets";
|
||||
}
|
||||
std::filesystem::path Storage::GetPluginAudioFileDirectory() const
|
||||
std::filesystem::path Storage::GetPluginUploadDirectory() const
|
||||
{
|
||||
return this->dataRoot / "audio_uploads";
|
||||
}
|
||||
@@ -1467,11 +1468,6 @@ std::vector<MidiBinding> Storage::GetSystemMidiBindings()
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool containsDotDot(const std::string &value)
|
||||
{
|
||||
std::size_t offset = value.find("..");
|
||||
return offset != std::string::npos;
|
||||
}
|
||||
static bool containsDirectorySeparator(const std::string &value)
|
||||
{
|
||||
if (value.find("/") != std::string::npos)
|
||||
@@ -1483,19 +1479,12 @@ static bool containsDirectorySeparator(const std::string &value)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static void ThrowPermissionDeniedError()
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
}
|
||||
|
||||
class LexicographicCompare
|
||||
{
|
||||
public:
|
||||
bool operator()(const std::string &left, const std::string &right)
|
||||
{
|
||||
return std::lexicographical_compare(left.begin(), left.end(), right.begin(), right.end());
|
||||
}
|
||||
} lexicographicCompare;
|
||||
|
||||
std::vector<std::string> Storage::GetFileList(const UiFileProperty &fileProperty)
|
||||
{
|
||||
@@ -1509,7 +1498,7 @@ std::vector<std::string> Storage::GetFileList(const UiFileProperty &fileProperty
|
||||
// if fileProperty has a user-accessible directory, push the entire file path.
|
||||
if (fileProperty.directory().size() != 0)
|
||||
{
|
||||
std::filesystem::path audioFileDirectory = this->GetPluginAudioFileDirectory() / fileProperty.directory();
|
||||
std::filesystem::path audioFileDirectory = this->GetPluginUploadDirectory() / fileProperty.directory();
|
||||
try
|
||||
{
|
||||
for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory))
|
||||
@@ -1537,17 +1526,110 @@ std::vector<std::string> Storage::GetFileList(const UiFileProperty &fileProperty
|
||||
|
||||
// sort lexicographically
|
||||
|
||||
std::sort(result.begin(), result.end(), lexicographicCompare);
|
||||
std::sort(result.begin(), result.end(), [this](const std::string&left,const std::string&right) {
|
||||
return this->locale(left,right) < 0;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
||||
static bool ensureNoDotDot(const std::filesystem::path&path)
|
||||
{
|
||||
for (auto segment_: path)
|
||||
{
|
||||
std::string segment = segment_.string();
|
||||
if (segment.starts_with("."))
|
||||
{
|
||||
// the linux rule: any path that consists of all '.'s.
|
||||
bool valid = false;
|
||||
for (auto c: segment)
|
||||
{
|
||||
if (c != '.')
|
||||
{
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
std::vector<FileEntry> Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty)
|
||||
{
|
||||
if (!ensureNoDotDot(relativePath))
|
||||
{
|
||||
ThrowPermissionDeniedError();
|
||||
}
|
||||
if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory()))
|
||||
{
|
||||
ThrowPermissionDeniedError();
|
||||
}
|
||||
|
||||
std::vector<FileEntry> result;
|
||||
|
||||
// if fileProperty has a user-accessible directory, push the entire file path.
|
||||
if (fileProperty.directory().size() != 0)
|
||||
{
|
||||
std::filesystem::path audioFileDirectory = this->GetPluginUploadDirectory() / fileProperty.directory() / relativePath;
|
||||
try
|
||||
{
|
||||
for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory))
|
||||
{
|
||||
if (dir_entry.is_regular_file())
|
||||
{
|
||||
auto &path = dir_entry.path();
|
||||
auto name = path.filename().string();
|
||||
if (name.length() > 0 && name[0] != '.') // don't show hidden files.
|
||||
{
|
||||
if (fileProperty.IsValidExtension(path.extension().string()))
|
||||
{
|
||||
// a relative path!
|
||||
result.push_back(FileEntry(path,false));
|
||||
}
|
||||
}
|
||||
} else if (dir_entry.is_directory()) {
|
||||
result.push_back(FileEntry(dir_entry.path(),true));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string());
|
||||
}
|
||||
}
|
||||
|
||||
// sort lexicographically
|
||||
|
||||
std::sort(result.begin(), result.end(),[this](const FileEntry&l, const FileEntry&r) {
|
||||
if (l.isDirectory_ != r.isDirectory_)
|
||||
{
|
||||
return l.isDirectory_ > r.isDirectory_;
|
||||
}
|
||||
return this->locale(l.filename_,r.filename_);
|
||||
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool Storage::IsValidSampleFileName(const std::filesystem::path &fileName)
|
||||
{
|
||||
if (!fileName.is_absolute())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::filesystem::path audioFilePath = this->GetPluginAudioFileDirectory();
|
||||
if (!ensureNoDotDot(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path audioFilePath = this->GetPluginUploadDirectory();
|
||||
|
||||
std::filesystem::path parentDirectory = fileName.parent_path();
|
||||
while (true)
|
||||
@@ -1557,9 +1639,6 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
||||
return false;
|
||||
}
|
||||
std::string name = parentDirectory.filename().string();
|
||||
if (name == ".." || name == ".")
|
||||
return false;
|
||||
|
||||
if (parentDirectory == audioFilePath)
|
||||
return true;
|
||||
parentDirectory = parentDirectory.parent_path();
|
||||
@@ -1571,7 +1650,7 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
||||
}
|
||||
void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||
{
|
||||
if (!IsValidSampleFile(fileName))
|
||||
if (!IsValidSampleFileName(fileName))
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
}
|
||||
@@ -1581,7 +1660,16 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||
}
|
||||
try
|
||||
{
|
||||
std::filesystem::remove(fileName);
|
||||
if (std::filesystem::is_directory(fileName))
|
||||
{
|
||||
if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience)
|
||||
{
|
||||
std::filesystem::remove_all(fileName);
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::filesystem::remove(fileName);
|
||||
}
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
@@ -1590,24 +1678,20 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||
}
|
||||
std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, const std::string &filename)
|
||||
{
|
||||
if (!UiFileProperty::IsDirectoryNameValid(directory))
|
||||
if (!ensureNoDotDot(directory))
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
throw std::logic_error(SS("Invalide filename: " << filename));
|
||||
}
|
||||
std::filesystem::path filePath{filename};
|
||||
|
||||
if (filePath.has_parent_path())
|
||||
std::filesystem::path result = this->GetPluginUploadDirectory() / directory / filename;
|
||||
if (!this->IsValidSampleFileName(result))
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
}
|
||||
std::filesystem::path result = this->GetPluginAudioFileDirectory() / directory / filename;
|
||||
if (!this->IsValidSampleFile(result))
|
||||
{
|
||||
throw std::logic_error("Permission denied.");
|
||||
throw std::logic_error(SS("Invalid upload path: " << result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::string Storage::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, const std::string &fileBody)
|
||||
std::string Storage::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream&stream, size_t contentLength)
|
||||
{
|
||||
std::filesystem::path path;
|
||||
if (directory.length() != 0)
|
||||
@@ -1632,15 +1716,128 @@ std::string Storage::UploadUserFile(const std::string &directory, const std::str
|
||||
{
|
||||
throw std::logic_error(SS("Can't create file " << path << "."));
|
||||
}
|
||||
f.write(fileBody.c_str(), fileBody.length());
|
||||
std::vector<uint8_t> buffer;
|
||||
size_t BUFFER_SIZE = 64*1024;
|
||||
buffer.resize(BUFFER_SIZE);
|
||||
char *pBuffer= (char*)&(buffer[0]);
|
||||
while (contentLength != 0)
|
||||
{
|
||||
size_t thisTime = std::min(BUFFER_SIZE,contentLength);
|
||||
stream.read(pBuffer,(std::streamsize)thisTime);
|
||||
if (!stream) {
|
||||
throw std::runtime_error("Unable to read body input stream.");
|
||||
}
|
||||
f.write(pBuffer,(std::streamsize)thisTime);
|
||||
if (!f) {
|
||||
throw std::runtime_error("Failed to write to upload file.");
|
||||
}
|
||||
contentLength -= thisTime;
|
||||
}
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Upload failed. " << e.what()));
|
||||
std::filesystem::remove(path);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return path.string();
|
||||
}
|
||||
|
||||
std::string Storage::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty)
|
||||
{
|
||||
if (uiFileProperty.directory().empty())
|
||||
{
|
||||
throw std::runtime_error("Invalid UI File Property.");
|
||||
}
|
||||
std::filesystem::path path = this->GetPluginUploadDirectory() / uiFileProperty.directory() / relativePath;
|
||||
if (!this->IsValidSampleFileName(path))
|
||||
{
|
||||
throw std::runtime_error("Invalid file name.");
|
||||
}
|
||||
if (std::filesystem::exists(path))
|
||||
{
|
||||
throw std::runtime_error("A directory with that name already exists.");
|
||||
}
|
||||
std::filesystem::create_directories(path);
|
||||
return path;
|
||||
|
||||
}
|
||||
std::string Storage::RenameFilePropertyFile(
|
||||
const std::string&oldRelativePath,
|
||||
const std::string&newRelativePath,
|
||||
const UiFileProperty&uiFileProperty)
|
||||
{
|
||||
if (uiFileProperty.directory().empty())
|
||||
{
|
||||
throw std::runtime_error("Invalid UI File Property.");
|
||||
}
|
||||
std::filesystem::path oldPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / oldRelativePath;
|
||||
if (!this->IsValidSampleFileName(oldPath))
|
||||
{
|
||||
throw std::runtime_error("Invalid file name.");
|
||||
}
|
||||
if (!std::filesystem::exists(oldPath))
|
||||
{
|
||||
throw std::runtime_error("Original path does not exist.");
|
||||
}
|
||||
|
||||
std::filesystem::path newPath = this->GetPluginUploadDirectory() / uiFileProperty.directory() / newRelativePath;
|
||||
if (!this->IsValidSampleFileName(newPath))
|
||||
{
|
||||
throw std::runtime_error("Invalid file name.");
|
||||
}
|
||||
if (std::filesystem::exists(newPath))
|
||||
{
|
||||
if (std::filesystem::is_directory(newPath))
|
||||
{
|
||||
throw std::runtime_error("A directory with that name already exists.");
|
||||
} else {
|
||||
throw std::runtime_error("A file with that name already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::rename(oldPath,newPath);
|
||||
return newPath;
|
||||
|
||||
}
|
||||
|
||||
void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const
|
||||
{
|
||||
for (auto child: std::filesystem::directory_iterator(directory))
|
||||
{
|
||||
if (child.is_directory())
|
||||
{
|
||||
const auto& childPath = child.path();
|
||||
FilePropertyDirectoryTree::ptr childTree = std::make_unique<FilePropertyDirectoryTree>(childPath.filename());
|
||||
FillSampleDirectoryTree(childTree.get(),childPath);
|
||||
node->children_.push_back(std::move(childTree));
|
||||
}
|
||||
}
|
||||
std::sort(node->children_.begin(),node->children_.end(),
|
||||
[this](const FilePropertyDirectoryTree::ptr&left,const FilePropertyDirectoryTree::ptr&right)
|
||||
{
|
||||
return this->locale(left->directoryName_,right->directoryName_);
|
||||
});
|
||||
}
|
||||
FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const
|
||||
{
|
||||
FilePropertyDirectoryTree::ptr result = std::make_unique<FilePropertyDirectoryTree>("");
|
||||
if (uiFileProperty.directory().empty())
|
||||
{
|
||||
throw std::runtime_error("Invalid uiFileProperty");
|
||||
}
|
||||
if (!ensureNoDotDot(uiFileProperty.directory()))
|
||||
{
|
||||
throw std::runtime_error("Invalid uiFileProperty");
|
||||
}
|
||||
std::filesystem::path rootDirectory = this->GetPluginUploadDirectory() / uiFileProperty.directory();
|
||||
|
||||
FillSampleDirectoryTree(result.get(),rootDirectory);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
const PluginPresetIndex &Storage::GetPluginPresetIndex()
|
||||
{
|
||||
return pluginPresetIndex;
|
||||
|
||||
+16
-4
@@ -28,7 +28,10 @@
|
||||
#include "JackServerSettings.hpp"
|
||||
#include "WifiConfigSettings.hpp"
|
||||
#include "WifiDirectConfigSettings.hpp"
|
||||
#include "FileEntry.hpp"
|
||||
#include <map>
|
||||
#include <locale>
|
||||
#include "FilePropertyDirectoryTree.hpp"
|
||||
|
||||
|
||||
namespace pipedal {
|
||||
@@ -64,6 +67,7 @@ struct PluginPresetValues {
|
||||
|
||||
class Storage {
|
||||
private:
|
||||
std::locale locale;
|
||||
std::filesystem::path dataRoot;
|
||||
std::filesystem::path configRoot;
|
||||
BankIndex bankIndex;
|
||||
@@ -71,6 +75,7 @@ private:
|
||||
PluginPresetIndex pluginPresetIndex;
|
||||
|
||||
private:
|
||||
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
|
||||
|
||||
void MaybeCopyDefaultPresets();
|
||||
static std::string SafeEncodeName(const std::string& name);
|
||||
@@ -107,9 +112,9 @@ public:
|
||||
void SetDataRoot(const std::filesystem::path& path);
|
||||
void SetConfigRoot(const std::filesystem::path& path);
|
||||
|
||||
std::filesystem::path GetPluginAudioFileDirectory() const;
|
||||
std::filesystem::path GetPluginUploadDirectory() const;
|
||||
|
||||
std::vector<std::string> GetPedalboards();
|
||||
//std::vector<std::string> GetPedalboards();
|
||||
|
||||
const BankIndex & GetBanks() const { return bankIndex; }
|
||||
|
||||
@@ -149,6 +154,7 @@ public:
|
||||
int64_t DeleteBank(int64_t bankId);
|
||||
|
||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||
std::vector<FileEntry> GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
|
||||
|
||||
|
||||
void SetJackChannelSelection(const JackChannelSelection&channelSelection);
|
||||
@@ -176,7 +182,7 @@ private:
|
||||
void SavePluginPresetIndex();
|
||||
|
||||
std::filesystem::path GetPluginPresetPath(const std::string &pluginUri) const;
|
||||
bool IsValidSampleFile(const std::filesystem::path&fileName);
|
||||
bool IsValidSampleFileName(const std::filesystem::path&fileName);
|
||||
std::filesystem::path MakeUserFilePath(const std::string &directory, const std::string&filename);
|
||||
|
||||
public:
|
||||
@@ -212,7 +218,13 @@ public:
|
||||
void SetSystemMidiBindings(const std::vector<MidiBinding>&bindings);
|
||||
std::vector<MidiBinding> GetSystemMidiBindings();
|
||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
||||
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,const std::string&fileBody);
|
||||
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,std::istream&stream, size_t contentLength);
|
||||
std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty);
|
||||
std::string RenameFilePropertyFile(
|
||||
const std::string&oldRelativePath,
|
||||
const std::string&newRelativePath,
|
||||
const UiFileProperty&uiFileProperty);
|
||||
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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 "TemporaryFile.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
|
||||
TemporaryFile::TemporaryFile(const std::filesystem::path&directory)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
fs::create_directories(directory);
|
||||
|
||||
// Generate a unique filename
|
||||
std::string filename;
|
||||
do {
|
||||
std::string random_string(8, '\0');
|
||||
const char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
srand(static_cast<unsigned int>(time(nullptr)));
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
random_string[i] = alphanum[rand() % (sizeof(alphanum) - 1)];
|
||||
}
|
||||
|
||||
filename = directory / ("temp_" + random_string + ".tmp");
|
||||
} while (fs::exists(filename));
|
||||
|
||||
// Create the file
|
||||
std::ofstream file(filename);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to create temporary file");
|
||||
}
|
||||
file.close();
|
||||
|
||||
this->path = filename;
|
||||
|
||||
}
|
||||
TemporaryFile::~TemporaryFile()
|
||||
{
|
||||
if (!path.empty())
|
||||
{
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
namespace pipedal {
|
||||
class TemporaryFile {
|
||||
public:
|
||||
TemporaryFile(const TemporaryFile&) = delete;
|
||||
TemporaryFile&operator=(const TemporaryFile&) = delete;
|
||||
|
||||
TemporaryFile(const std::filesystem::path&parentDirectory);
|
||||
~TemporaryFile();
|
||||
const std::filesystem::path&Path()const { return path;}
|
||||
private:
|
||||
std::filesystem::path path;
|
||||
};
|
||||
}
|
||||
+557
-65
@@ -1,3 +1,21 @@
|
||||
// Copyright (c) 2022-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 "pch.h"
|
||||
|
||||
@@ -28,29 +46,484 @@
|
||||
#include <websocketpp/server.hpp>
|
||||
|
||||
#include "WebServerLog.hpp"
|
||||
#include "TemporaryFile.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace std;
|
||||
|
||||
const bool ENABLE_KEEP_ALIVE = true;
|
||||
const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
|
||||
|
||||
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
|
||||
|
||||
const size_t MAX_READ_SIZE = 1 * 1024 * 204;
|
||||
const size_t MAX_READ_SIZE = 512 * 1024 * 1024;
|
||||
;
|
||||
using namespace boost;
|
||||
|
||||
class CustomPpConfig: public websocketpp::config::asio {
|
||||
class request_with_file_upload : public websocketpp::http::parser::parser
|
||||
{
|
||||
public:
|
||||
using super = websocketpp::http::parser::parser;
|
||||
|
||||
typedef request_with_file_upload type;
|
||||
typedef std::shared_ptr<type> ptr;
|
||||
|
||||
request_with_file_upload()
|
||||
: m_buf(std::make_shared<std::string>()), m_ready(false) {}
|
||||
|
||||
size_t consume(char const *buf, size_t len, std::error_code &ec);
|
||||
|
||||
/// Returns whether or not the request is ready for reading.
|
||||
bool ready() const
|
||||
{
|
||||
return m_ready;
|
||||
}
|
||||
std::istream &get_body_input_stream();
|
||||
|
||||
const std::filesystem::path &get_body_input_file();
|
||||
size_t content_length() const { return m_content_length; }
|
||||
|
||||
/// Returns the full raw request (including the body)
|
||||
std::string raw() const;
|
||||
|
||||
/// Returns the raw request headers only (similar to an HTTP HEAD request)
|
||||
std::string raw_head() const;
|
||||
|
||||
/// Set the HTTP method.
|
||||
/**
|
||||
* Must be a valid HTTP token
|
||||
*
|
||||
* @since 0.9.0 added return value and removed exception
|
||||
*
|
||||
* @param [in] method The value to set the method to.
|
||||
* @return A status code describing the outcome of the operation.
|
||||
*/
|
||||
std::error_code set_method(std::string const &method);
|
||||
|
||||
/// Return the request method
|
||||
std::string const &get_method() const
|
||||
{
|
||||
return m_method;
|
||||
}
|
||||
|
||||
/// Set the HTTP uri.
|
||||
/**
|
||||
* Must be a valid HTTP uri
|
||||
*
|
||||
* @since 0.9.0 Return value added
|
||||
*
|
||||
* @param uri The URI to set
|
||||
* @return A status code describing the outcome of the operation.
|
||||
*/
|
||||
std::error_code set_uri(std::string const &uri);
|
||||
|
||||
/// Return the requested URI
|
||||
std::string const &get_uri() const
|
||||
{
|
||||
return m_uri;
|
||||
}
|
||||
/// Helper function for message::consume. Process request line
|
||||
/**
|
||||
* @since 0.9.0 (ec parameter added, exceptions removed)
|
||||
*
|
||||
* @param [in] begin An iterator to the beginning of the sequence.
|
||||
* @param [in] end An iterator to the end of the sequence.
|
||||
* @return A status code describing the outcome of the operation.
|
||||
*/
|
||||
|
||||
bool prepare_body(std::error_code &ec);
|
||||
|
||||
std::error_code process(std::string::iterator begin, std::string::iterator end);
|
||||
size_t process_body(char const *buf, size_t len,
|
||||
std::error_code &ec);
|
||||
std::shared_ptr<std::string> m_buf;
|
||||
std::string m_method;
|
||||
std::string m_uri;
|
||||
size_t m_content_length;
|
||||
bool m_ready;
|
||||
bool m_uploading_to_file = false;
|
||||
size_t m_max_in_memory_upload = 0; // saver to have one code path. And any input body is going to be an upload anyway.
|
||||
std::shared_ptr<TemporaryFile> m_temporaryFile;
|
||||
std::ofstream m_outputStream;
|
||||
std::ifstream m_inputStream;
|
||||
std::stringstream m_stringInputStream;
|
||||
bool m_outputOpen = false;
|
||||
};
|
||||
const std::filesystem::path &request_with_file_upload::get_body_input_file()
|
||||
{
|
||||
if (!this->m_temporaryFile)
|
||||
throw std::runtime_error("Request does not have a body.");
|
||||
return this->m_temporaryFile->Path();
|
||||
}
|
||||
std::istream &request_with_file_upload::get_body_input_stream()
|
||||
{
|
||||
if (!m_outputOpen)
|
||||
{
|
||||
m_outputOpen = true;
|
||||
if (m_uploading_to_file)
|
||||
{
|
||||
m_inputStream.open(this->m_temporaryFile->Path(), std::ios_base::in | std::ios_base::binary);
|
||||
return m_inputStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto body = super::get_body();
|
||||
m_stringInputStream.write(body.c_str(), body.length());
|
||||
m_stringInputStream.flush();
|
||||
m_stringInputStream.seekg(0);
|
||||
return m_stringInputStream;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_uploading_to_file)
|
||||
{
|
||||
return m_inputStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_stringInputStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool request_with_file_upload::prepare_body(std::error_code &ec)
|
||||
{
|
||||
using namespace websocketpp::http;
|
||||
bool result = super::prepare_body(ec);
|
||||
m_content_length = 0;
|
||||
if (!result)
|
||||
return result;
|
||||
m_content_length = m_body_bytes_needed;
|
||||
if (m_body_bytes_needed > m_max_in_memory_upload)
|
||||
{
|
||||
m_uploading_to_file = true;
|
||||
try
|
||||
{
|
||||
this->m_temporaryFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
|
||||
m_outputStream.open(this->m_temporaryFile->Path(), std::ios_base::trunc | std::ios_base::out | std::ios_base::binary);
|
||||
if (!m_outputStream)
|
||||
{
|
||||
throw std::runtime_error(SS("Unable to open file " << this->m_temporaryFile->Path()));
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Unabe to create upload file. " << e.what()));
|
||||
ec = error::make_error_code(error::istream_bad);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline size_t request_with_file_upload::process_body(char const *buf, size_t len,
|
||||
std::error_code &ec)
|
||||
{
|
||||
using namespace websocketpp::http;
|
||||
using namespace websocketpp::http::parser;
|
||||
|
||||
if (!this->m_uploading_to_file)
|
||||
{
|
||||
return super::process_body(buf, len, ec);
|
||||
}
|
||||
if (m_body_encoding == body_encoding::plain)
|
||||
{
|
||||
size_t processed = (std::min)(m_body_bytes_needed, len);
|
||||
try
|
||||
{
|
||||
m_outputStream.write(buf, processed);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Can't write to web temporary file " << m_temporaryFile->Path() << ". " << e.what()));
|
||||
ec = error::make_error_code(error::istream_bad);
|
||||
return 0;
|
||||
}
|
||||
m_body_bytes_needed -= processed;
|
||||
ec = std::error_code();
|
||||
return processed;
|
||||
}
|
||||
else if (m_body_encoding == body_encoding::chunked)
|
||||
{
|
||||
ec = error::make_error_code(error::unsupported_transfer_encoding);
|
||||
return 0;
|
||||
// TODO: support for chunked transfers?
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = error::make_error_code(error::unknown_transfer_encoding);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
inline size_t request_with_file_upload::consume(char const *buf, size_t len, std::error_code &ec)
|
||||
{
|
||||
using namespace websocketpp::http;
|
||||
|
||||
size_t bytes_processed = 0;
|
||||
|
||||
if (m_ready)
|
||||
{
|
||||
// the request is already complete. End immediately without reading.
|
||||
ec = std::error_code();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (m_body_bytes_needed > 0)
|
||||
{
|
||||
// The headers are complete, but we are still expecting more body
|
||||
// bytes. Process body bytes.
|
||||
bytes_processed = process_body(buf, len, ec);
|
||||
if (ec)
|
||||
{
|
||||
return bytes_processed;
|
||||
}
|
||||
|
||||
// if we have ready all the expected body bytes set the ready flag
|
||||
if (body_ready())
|
||||
{
|
||||
m_outputStream.close();
|
||||
m_ready = true;
|
||||
}
|
||||
return bytes_processed;
|
||||
}
|
||||
|
||||
// at this point we have an incomplete request still waiting for headers
|
||||
|
||||
// copy new candidate bytes into our local buffer. This buffer may have
|
||||
// leftover bytes from previous calls. Not all of these bytes are
|
||||
// necessarily header bytes (they might be body or even data after this
|
||||
// request entirely for a keepalive request)
|
||||
m_buf->append(buf, len);
|
||||
|
||||
// Search for delimiter in buf. If found read until then. If not read all
|
||||
std::string::iterator begin = m_buf->begin();
|
||||
std::string::iterator end;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
// search for line delimiter in our local buffer
|
||||
end = std::search(
|
||||
begin,
|
||||
m_buf->end(),
|
||||
header_delimiter,
|
||||
header_delimiter + sizeof(header_delimiter) - 1);
|
||||
|
||||
if (end == m_buf->end())
|
||||
{
|
||||
// we didn't find the delimiter
|
||||
|
||||
// check that the confirmed header bytes plus the outstanding
|
||||
// candidate bytes do not put us over the header size limit.
|
||||
if (m_header_bytes + (end - begin) > max_header_size)
|
||||
{
|
||||
ec = error::make_error_code(error::request_header_fields_too_large);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We are out of bytes but not over any limits yet. Discard the
|
||||
// processed bytes and copy the remaining unprecessed bytes to the
|
||||
// beginning of the buffer in prep for another call to consume.
|
||||
|
||||
// If there are no processed bytes in the buffer right now don't
|
||||
// copy the unprocessed ones over themselves.
|
||||
if (begin != m_buf->begin())
|
||||
{
|
||||
std::copy(begin, end, m_buf->begin());
|
||||
m_buf->resize(static_cast<std::string::size_type>(end - begin));
|
||||
}
|
||||
|
||||
ec = std::error_code();
|
||||
return len;
|
||||
}
|
||||
|
||||
// at this point we have found a delimiter and the range [begin,end)
|
||||
// represents a line to be processed
|
||||
|
||||
// update count of header bytes read so far
|
||||
m_header_bytes += (end - begin + sizeof(header_delimiter));
|
||||
|
||||
if (m_header_bytes > max_header_size)
|
||||
{
|
||||
// This read exceeded max header size
|
||||
ec = error::make_error_code(error::request_header_fields_too_large);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (end - begin == 0)
|
||||
{
|
||||
// we got a blank line, which indicates the end of the headers
|
||||
|
||||
// If we never got a valid method or are missing a host header then
|
||||
// this request is invalid.
|
||||
if (m_method.empty() || get_header("Host").empty())
|
||||
{
|
||||
ec = error::make_error_code(error::incomplete_request);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// any bytes left over in the local buffer are bytes we didn't use.
|
||||
// When we report how many bytes we consumed we need to subtract
|
||||
// these so the caller knows that they need to be processed by some
|
||||
// other logic.
|
||||
bytes_processed = (len - static_cast<std::string::size_type>(m_buf->end() - end) + sizeof(header_delimiter) - 1);
|
||||
|
||||
// frees memory used temporarily during request parsing
|
||||
m_buf.reset();
|
||||
|
||||
// if this was not an upgrade request and has a content length
|
||||
// continue capturing content-length bytes and expose them as a
|
||||
// request body.
|
||||
|
||||
bool need_more = prepare_body(ec);
|
||||
if (ec)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (need_more)
|
||||
{
|
||||
bytes_processed += process_body(buf + bytes_processed, len - bytes_processed, ec);
|
||||
if (ec)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (body_ready())
|
||||
{
|
||||
m_ready = true;
|
||||
}
|
||||
ec = std::error_code();
|
||||
return bytes_processed;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ready = true;
|
||||
|
||||
// return number of bytes processed (starting bytes - bytes left)
|
||||
ec = std::error_code();
|
||||
return bytes_processed;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// we got a line with content
|
||||
if (m_method.empty())
|
||||
{
|
||||
// if we haven't found a method yet process this line as a first line
|
||||
ec = this->process(begin, end);
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is a second (or later) line, process as a header
|
||||
ec = this->process_header(begin, end);
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// if we got here it means there is another header line to read.
|
||||
// advance our cursor to the first character after the most recent
|
||||
// delimiter found.
|
||||
begin = end + (sizeof(header_delimiter) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string request_with_file_upload::raw() const
|
||||
{
|
||||
// TODO: validation. Make sure all required fields have been set?
|
||||
std::stringstream ret;
|
||||
|
||||
ret << m_method << " " << m_uri << " " << get_version() << "\r\n";
|
||||
ret << raw_headers() << "\r\n"
|
||||
<< m_body;
|
||||
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
inline std::string request_with_file_upload::raw_head() const
|
||||
{
|
||||
// TODO: validation. Make sure all required fields have been set?
|
||||
std::stringstream ret;
|
||||
|
||||
ret << m_method << " " << m_uri << " " << get_version() << "\r\n";
|
||||
ret << raw_headers() << "\r\n";
|
||||
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
inline std::error_code request_with_file_upload::set_method(std::string const &method)
|
||||
{
|
||||
using namespace websocketpp::http;
|
||||
|
||||
if (std::find_if(method.begin(), method.end(), is_not_token_char) != method.end())
|
||||
{
|
||||
return error::make_error_code(error::invalid_format);
|
||||
}
|
||||
|
||||
m_method = method;
|
||||
return std::error_code();
|
||||
}
|
||||
|
||||
inline std::error_code request_with_file_upload::set_uri(std::string const &uri)
|
||||
{
|
||||
// TODO: validation?
|
||||
m_uri = uri;
|
||||
|
||||
return std::error_code();
|
||||
}
|
||||
|
||||
inline std::error_code request_with_file_upload::process(std::string::iterator begin, std::string::iterator
|
||||
end)
|
||||
{
|
||||
using namespace websocketpp::http;
|
||||
std::error_code ec;
|
||||
|
||||
std::string::iterator cursor_start = begin;
|
||||
std::string::iterator cursor_end = std::find(begin, end, ' ');
|
||||
|
||||
if (cursor_end == end)
|
||||
{
|
||||
return error::make_error_code(error::incomplete_request);
|
||||
}
|
||||
|
||||
ec = set_method(std::string(cursor_start, cursor_end));
|
||||
if (ec)
|
||||
{
|
||||
return ec;
|
||||
}
|
||||
|
||||
cursor_start = cursor_end + 1;
|
||||
cursor_end = std::find(cursor_start, end, ' ');
|
||||
|
||||
if (cursor_end == end)
|
||||
{
|
||||
return error::make_error_code(error::incomplete_request);
|
||||
}
|
||||
|
||||
ec = set_uri(std::string(cursor_start, cursor_end));
|
||||
if (ec)
|
||||
{
|
||||
return ec;
|
||||
}
|
||||
|
||||
return set_version(std::string(cursor_end + 1, end));
|
||||
}
|
||||
|
||||
class CustomPpConfig : public websocketpp::config::asio
|
||||
{
|
||||
public:
|
||||
typedef CustomPpConfig type;
|
||||
typedef websocketpp::config::asio base;
|
||||
|
||||
|
||||
static const size_t max_http_body_size = 100000000; //websocketpp::config::asio::max_http_body_size;
|
||||
static size_t max_http_body_size; // websocketpp::config::asio::max_http_body_size;
|
||||
typedef pipedal_elog elog_type;
|
||||
typedef pipedal_alog alog_type;
|
||||
|
||||
typedef request_with_file_upload request_type;
|
||||
|
||||
struct transport_config : public base::transport_config {
|
||||
struct transport_config : public base::transport_config
|
||||
{
|
||||
typedef type::concurrency_type concurrency_type;
|
||||
typedef type::alog_type alog_type;
|
||||
typedef type::elog_type elog_type;
|
||||
@@ -62,10 +535,9 @@ public:
|
||||
|
||||
typedef websocketpp::transport::asio::endpoint<transport_config>
|
||||
transport_type;
|
||||
|
||||
};
|
||||
|
||||
|
||||
size_t CustomPpConfig::max_http_body_size = MAX_READ_SIZE;
|
||||
|
||||
std::string
|
||||
pipedal::last_modified(const std::filesystem::path &path)
|
||||
@@ -86,7 +558,7 @@ pipedal::last_modified(const std::filesystem::path &path)
|
||||
static std::string getHostName()
|
||||
{
|
||||
char buff[512];
|
||||
if (gethostname(buff,sizeof(buff)) == 0)
|
||||
if (gethostname(buff, sizeof(buff)) == 0)
|
||||
{
|
||||
buff[511] = '\0';
|
||||
return buff;
|
||||
@@ -98,7 +570,7 @@ static std::string getIpv4Address(const std::string interface)
|
||||
{
|
||||
int fd = -1;
|
||||
struct ifreq ifr;
|
||||
memset(&ifr,0,sizeof(ifr));
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
|
||||
fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
|
||||
@@ -106,15 +578,16 @@ static std::string getIpv4Address(const std::string interface)
|
||||
ifr.ifr_addr.sa_family = AF_INET;
|
||||
|
||||
/* I want an IP address attached to "eth0" */
|
||||
strncpy(ifr.ifr_name, interface.c_str(), IFNAMSIZ-1);
|
||||
strncpy(ifr.ifr_name, interface.c_str(), IFNAMSIZ - 1);
|
||||
|
||||
int result = ioctl(fd, SIOCGIFADDR, &ifr);
|
||||
if (result == -1) return "";
|
||||
|
||||
if (result == -1)
|
||||
return "";
|
||||
|
||||
close(fd);
|
||||
char *name = inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr);
|
||||
if (name == nullptr) return "";
|
||||
if (name == nullptr)
|
||||
return "";
|
||||
return name;
|
||||
}
|
||||
static std::map<std::string, std::string> extensionsToMimeType = {
|
||||
@@ -175,6 +648,7 @@ namespace pipedal
|
||||
int port = -1;
|
||||
std::filesystem::path rootPath;
|
||||
int threads = 1;
|
||||
size_t maxUploadSize = 512 * 1024 * 1024;
|
||||
|
||||
std::thread *pBgThread = nullptr;
|
||||
std::recursive_mutex io_mutex;
|
||||
@@ -187,15 +661,22 @@ namespace pipedal
|
||||
class HttpRequestImpl : public HttpRequest
|
||||
{
|
||||
private:
|
||||
const server::connection_type::request_type &m_request;
|
||||
server::connection_type::request_type &m_request;
|
||||
|
||||
public:
|
||||
HttpRequestImpl(const server::connection_type::request_type &request)
|
||||
: m_request(request)
|
||||
: m_request(const_cast<server::connection_type::request_type &>(request)) // so we can get access to get_body_input_stream
|
||||
{
|
||||
}
|
||||
|
||||
virtual const std::string &body() const { return m_request.get_body(); }
|
||||
virtual std::istream &get_body_input_stream() override { return m_request.get_body_input_stream(); }
|
||||
virtual const std::filesystem::path &get_body_temporary_file() override
|
||||
{
|
||||
return m_request.get_body_input_file();
|
||||
}
|
||||
|
||||
virtual size_t content_length() const { return m_request.content_length(); }
|
||||
|
||||
virtual const std::string &method() const { return m_request.get_method(); }
|
||||
virtual const std::string &get(const std::string &key) const { return m_request.get_header(key); }
|
||||
virtual bool keepAlive() const
|
||||
@@ -241,8 +722,9 @@ namespace pipedal
|
||||
|
||||
private:
|
||||
// IWriteCallback
|
||||
virtual void close() {
|
||||
webSocket->close(websocketpp::close::status::normal, "");
|
||||
virtual void close()
|
||||
{
|
||||
webSocket->close(websocketpp::close::status::normal, "");
|
||||
webSocket = nullptr;
|
||||
}
|
||||
|
||||
@@ -279,7 +761,6 @@ namespace pipedal
|
||||
|
||||
Lv2Log::info(SS("WebSocketSession opened. (" << fromAddress << ")"));
|
||||
|
||||
|
||||
auto pFactory = pServer->GetSocketFactory(requestUri);
|
||||
if (!pFactory)
|
||||
{
|
||||
@@ -289,9 +770,9 @@ namespace pipedal
|
||||
}
|
||||
else
|
||||
{
|
||||
using websocketpp::lib::bind;
|
||||
using websocketpp::lib::placeholders::_1;
|
||||
using websocketpp::lib::placeholders::_2;
|
||||
using std::bind;
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
|
||||
webSocket->set_message_handler(bind(&WebSocketSession::on_message, this, _1, _2));
|
||||
webSocket->set_close_handler(bind(&WebSocketSession::on_close, this, _1));
|
||||
@@ -323,22 +804,23 @@ namespace pipedal
|
||||
|
||||
void on_session_closed(WebSocketSession::ptr &session, connection_hdl hConnection)
|
||||
{
|
||||
m_sessions.erase(session);
|
||||
m_sessions.erase(session);
|
||||
session = nullptr; // probably delete here.
|
||||
m_connections.erase(hConnection);
|
||||
}
|
||||
|
||||
void NotFound(server::connection_type &connection, const std::string &filename)
|
||||
{
|
||||
try {
|
||||
try
|
||||
{
|
||||
// 404 error
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "<!doctype html><html><head>"
|
||||
<< "<title>Error 404 (Resource not found)</title><body>"
|
||||
<< "<h1>Error 404</h1>"
|
||||
<< "<p>The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.</p>"
|
||||
<< "</body></head></html>";
|
||||
<< "<title>Error 404 (Resource not found)</title><body>"
|
||||
<< "<h1>Error 404</h1>"
|
||||
<< "<p>The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.</p>"
|
||||
<< "</body></head></html>";
|
||||
|
||||
std::string body = ss.str();
|
||||
connection.set_body(body);
|
||||
@@ -346,23 +828,25 @@ namespace pipedal
|
||||
ssLen << body.length();
|
||||
connection.replace_header(HttpField::content_length, ssLen.str());
|
||||
connection.set_status(websocketpp::http::status_code::not_found);
|
||||
} catch (const std::exception&)
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
// ignored. Things weren't going well anyway.
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
void ServerError(server::connection_type &connection, const std::string &error)
|
||||
{
|
||||
try {
|
||||
try
|
||||
{
|
||||
// 404 error
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "<!doctype html><html><head>"
|
||||
<< "<title>Error 500 (Server error)</title><body>"
|
||||
<< "<h1>Error 500</h1>"
|
||||
<< "<p>" << HtmlHelper::HtmlEncode(error) << "</p>"
|
||||
<< "</body></head></html>";
|
||||
<< "<title>Error 500 (Server error)</title><body>"
|
||||
<< "<h1>Error 500</h1>"
|
||||
<< "<p>" << HtmlHelper::HtmlEncode(error) << "</p>"
|
||||
<< "</body></head></html>";
|
||||
std::string body = ss.str();
|
||||
connection.set_body(body);
|
||||
std::stringstream ssLen;
|
||||
@@ -370,7 +854,8 @@ namespace pipedal
|
||||
connection.replace_header(HttpField::content_length, ssLen.str());
|
||||
|
||||
connection.set_status(websocketpp::http::status_code::internal_server_error);
|
||||
} catch (const std::exception&)
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
}
|
||||
return;
|
||||
@@ -457,17 +942,18 @@ namespace pipedal
|
||||
HttpResponseImpl res((*con));
|
||||
|
||||
uri requestUri;
|
||||
try {
|
||||
requestUri.set(con->get_uri()->str().c_str());
|
||||
} catch (const std::exception &e)
|
||||
try
|
||||
{
|
||||
ServerError(*con, SS("Unexpected error. " << e.what()));
|
||||
requestUri.set(con->get_uri()->str().c_str());
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
ServerError(*con, SS("Unexpected error. " << e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string fromAddress = SS(con->get_remote_endpoint());
|
||||
|
||||
|
||||
if (req.method() == HttpVerb::options)
|
||||
{
|
||||
res.set(HttpField::access_control_allow_origin, origin);
|
||||
@@ -486,14 +972,13 @@ namespace pipedal
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
if (req.method() == HttpVerb::head)
|
||||
{
|
||||
std::error_code ec;
|
||||
res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
||||
res.set(HttpField::access_control_allow_origin, origin);
|
||||
|
||||
requestHandler->head_response(fromAddress,requestUri, req, res, ec);
|
||||
requestHandler->head_response(fromAddress, requestUri, req, res, ec);
|
||||
res.keepAlive(req.keepAlive());
|
||||
if (ec == std::errc::no_such_file_or_directory)
|
||||
{
|
||||
@@ -515,8 +1000,7 @@ namespace pipedal
|
||||
res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
||||
res.set(HttpField::access_control_allow_origin, origin);
|
||||
|
||||
|
||||
requestHandler->get_response(fromAddress,requestUri, req, res, ec);
|
||||
requestHandler->get_response(fromAddress, requestUri, req, res, ec);
|
||||
res.keepAlive(req.keepAlive());
|
||||
|
||||
if (ec == std::errc::no_such_file_or_directory)
|
||||
@@ -540,7 +1024,7 @@ namespace pipedal
|
||||
res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
||||
res.set(HttpField::access_control_allow_origin, origin);
|
||||
|
||||
requestHandler->post_response(fromAddress,requestUri, req, res, ec);
|
||||
requestHandler->post_response(fromAddress, requestUri, req, res, ec);
|
||||
|
||||
if (ec == std::errc::no_such_file_or_directory)
|
||||
{
|
||||
@@ -562,7 +1046,7 @@ namespace pipedal
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
ServerError(*con, SS("Unexpected error. " << e.what()));
|
||||
ServerError(*con, SS("Unexpected error. " << e.what()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -628,13 +1112,14 @@ namespace pipedal
|
||||
{
|
||||
m_connections.insert(hdl);
|
||||
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl);
|
||||
WebSocketSession::ptr socketSession = std::make_shared<WebSocketSession>(this, webSocket);
|
||||
socketSession->Open();
|
||||
m_sessions.insert(socketSession);
|
||||
} catch (const std::exception&e)
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error("Failed to open session: %s", e.what());
|
||||
}
|
||||
@@ -666,9 +1151,9 @@ namespace pipedal
|
||||
m_endpoint.init_asio(&ioc);
|
||||
|
||||
// Bind the handlers we are using
|
||||
using websocketpp::lib::bind;
|
||||
using websocketpp::lib::placeholders::_1;
|
||||
using websocketpp::lib::placeholders::_2;
|
||||
using std::bind;
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
|
||||
m_endpoint.set_open_handler(bind(&WebServerImpl::on_open, this, _1));
|
||||
m_endpoint.set_close_handler(bind(&WebServerImpl::on_close, this, _1));
|
||||
@@ -695,7 +1180,7 @@ namespace pipedal
|
||||
std::stringstream ss;
|
||||
ss << port;
|
||||
// m_endpoint.listen(this->address, ss.str());
|
||||
m_endpoint.listen(tcp::v6(),(uint16_t)port);
|
||||
m_endpoint.listen(tcp::v6(), (uint16_t)port);
|
||||
m_endpoint.start_accept();
|
||||
|
||||
// Start IOC service threads.
|
||||
@@ -703,7 +1188,7 @@ namespace pipedal
|
||||
v.reserve(threads - 1);
|
||||
for (auto i = threads - 1; i > 0; --i)
|
||||
v.emplace_back(
|
||||
[&ioc,i]
|
||||
[&ioc, i]
|
||||
{
|
||||
SetThreadName(SS("web_" << i));
|
||||
ioc.run();
|
||||
@@ -789,13 +1274,7 @@ namespace pipedal
|
||||
this->pBgThread = new std::thread(ThreadProc, this);
|
||||
}
|
||||
|
||||
WebServerImpl(const std::string &address, int port, const char *rootPath, int threads)
|
||||
: address(address),
|
||||
rootPath(rootPath),
|
||||
port(port),
|
||||
threads(threads)
|
||||
{
|
||||
}
|
||||
WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize);
|
||||
};
|
||||
} // namespace pipedal
|
||||
|
||||
@@ -811,8 +1290,21 @@ std::shared_ptr<ISocketFactory> WebServerImpl::GetSocketFactory(const uri &reque
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<WebServer> pipedal::WebServer::create(const boost::asio::ip::address &address, int port, const char *rootPath, int threads)
|
||||
WebServerImpl::WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize)
|
||||
: address(address),
|
||||
rootPath(rootPath),
|
||||
port(port),
|
||||
threads(threads),
|
||||
maxUploadSize(maxUploadSize)
|
||||
{
|
||||
return std::shared_ptr<WebServer>(new WebServerImpl(address.to_string(), port, rootPath, threads));
|
||||
::CustomPpConfig::max_http_body_size = maxUploadSize;
|
||||
}
|
||||
|
||||
std::shared_ptr<WebServer> pipedal::WebServer::create(
|
||||
const boost::asio::ip::address &address,
|
||||
int port,
|
||||
const char *rootPath, int threads,
|
||||
size_t maxUploadSize)
|
||||
{
|
||||
return std::shared_ptr<WebServer>(new WebServerImpl(address.to_string(), port, rootPath, threads, maxUploadSize));
|
||||
}
|
||||
|
||||
+12
-8
@@ -26,7 +26,10 @@ namespace pipedal {
|
||||
|
||||
class HttpRequest {
|
||||
public:
|
||||
virtual const std::string&body() const = 0;
|
||||
//virtual const std::string&body() const = 0;
|
||||
virtual std::istream &get_body_input_stream() = 0;
|
||||
virtual const std::filesystem::path& get_body_temporary_file() = 0;
|
||||
virtual size_t content_length() const = 0;
|
||||
virtual const std::string &method() const = 0;
|
||||
virtual const std::string&get(const std::string&key) const = 0;
|
||||
virtual bool keepAlive() const = 0;
|
||||
@@ -161,14 +164,14 @@ public:
|
||||
|
||||
virtual void head_response(
|
||||
const uri&request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) = 0;
|
||||
|
||||
virtual void head_response(
|
||||
const std::string&fromAddress,
|
||||
const uri&request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) {
|
||||
head_response(request_uri,req,res,ec);
|
||||
@@ -178,14 +181,14 @@ public:
|
||||
|
||||
virtual void get_response(
|
||||
const uri&request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) = 0;
|
||||
|
||||
virtual void get_response(
|
||||
const std::string&fromAddress,
|
||||
const uri&request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
@@ -195,7 +198,7 @@ public:
|
||||
|
||||
virtual void post_response(
|
||||
const uri&request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
@@ -204,7 +207,7 @@ public:
|
||||
virtual void post_response(
|
||||
const std::string&fromAddress,
|
||||
const uri&request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
@@ -233,7 +236,8 @@ public:
|
||||
const boost::asio::ip::address &address,
|
||||
int port,
|
||||
const char *rootPath,
|
||||
int threads);
|
||||
int threads,
|
||||
size_t maxUploadsize);
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "WebServerConfig.hpp"
|
||||
#include "WebServer.hpp"
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <filesystem>
|
||||
#include "PiPedalConfiguration.hpp"
|
||||
#include "PiPedalModel.hpp"
|
||||
#include "Banks.hpp"
|
||||
#include "Ipv6Helpers.hpp"
|
||||
#include <memory>
|
||||
#include "ZipFile.hpp"
|
||||
#include "PiPedalUI.hpp"
|
||||
|
||||
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
#define BANK_EXTENSION ".piBank"
|
||||
#define PLUGIN_PRESETS_EXTENSION ".piPluginPresets"
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace boost::system;
|
||||
|
||||
static std::vector<std::string> split(const std::string& s, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::string token;
|
||||
std::istringstream tokenStream(s);
|
||||
while (std::getline(tokenStream, token, delimiter)) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
class ExtensionChecker {
|
||||
public:
|
||||
ExtensionChecker(const std::string&extensionList)
|
||||
:extensions(split(extensionList,','))
|
||||
{
|
||||
}
|
||||
bool IsValidExtension(const std::string&extension)
|
||||
{
|
||||
if (extensions.size() == 0)
|
||||
return true;
|
||||
for (const auto&ext: extensions)
|
||||
{
|
||||
if (ext == extension)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
std::vector<std::string> extensions;
|
||||
};
|
||||
|
||||
class DownloadIntercept : public RequestHandler
|
||||
{
|
||||
PiPedalModel *model;
|
||||
|
||||
public:
|
||||
DownloadIntercept(PiPedalModel *model)
|
||||
: RequestHandler("/var"),
|
||||
model(model)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool wants(const std::string &method, const uri &request_uri) const
|
||||
{
|
||||
if (request_uri.segment_count() != 2 || request_uri.segment(0) != "var")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
return true;
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetContentDispositionHeader(const std::string &name, const std::string &extension)
|
||||
{
|
||||
std::string fileName = name.substr(0, 64) + extension;
|
||||
std::stringstream s;
|
||||
|
||||
s << "attachment; filename*=" << HtmlHelper::Rfc5987EncodeFileName(fileName) << "; filename=\"" << HtmlHelper::SafeFileName(fileName) << "\"";
|
||||
std::string result = s.str();
|
||||
return result;
|
||||
}
|
||||
|
||||
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string pluginUri = request_uri.query("id");
|
||||
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
|
||||
*pName = plugin->name();
|
||||
|
||||
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(pluginPresets);
|
||||
*pContent = s.str();
|
||||
}
|
||||
void GetPreset(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
auto pedalboard = model->GetPreset(instanceId);
|
||||
|
||||
// a certain elegance to using same file format for banks and presets.
|
||||
BankFile file;
|
||||
file.name(pedalboard.name());
|
||||
int64_t newInstanceId = file.addPreset(pedalboard);
|
||||
file.selectedPreset(newInstanceId);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(file);
|
||||
*pContent = s.str();
|
||||
*pName = pedalboard.name();
|
||||
}
|
||||
void GetBank(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
BankFile bank;
|
||||
model->GetBank(instanceId, &bank);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s, true); // do what we can to reduce the file size.
|
||||
writer.write(bank);
|
||||
*pContent = s.str();
|
||||
*pName = bank.name();
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setContentLength(content.length());
|
||||
return;
|
||||
}
|
||||
throw PiPedalException("Not found.");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
static std::string GetFirstFolderOrFile(const std::vector<std::string>&fileNames)
|
||||
{
|
||||
for (const auto &fileName: fileNames)
|
||||
{
|
||||
size_t nPos = fileName.find('/');
|
||||
if (nPos != std::string::npos) {
|
||||
return fileName.substr(0,nPos);
|
||||
}
|
||||
}
|
||||
if (fileNames.size() == 0)
|
||||
{
|
||||
return 0;
|
||||
} else {
|
||||
return fileNames[0];
|
||||
}
|
||||
}
|
||||
virtual void post_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
PluginPresets presets;
|
||||
reader.read(&presets);
|
||||
model->UploadPluginPresets(presets);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << -1;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadPreset(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
json_reader reader(req.get_body_input_stream());
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadBank(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
const std::string instanceId = request_uri.query("id");
|
||||
const std::string directory = request_uri.query("directory");
|
||||
const std::string filename = request_uri.query("filename");
|
||||
const std::string patchProperty = request_uri.query("property");
|
||||
|
||||
|
||||
if (patchProperty.length() == 0 && directory.length() == 0)
|
||||
{
|
||||
// yyy no throwing!
|
||||
throw PiPedalException("Malformed request.");
|
||||
|
||||
}
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
std::string outputFileName = std::filesystem::path(directory) / filename;
|
||||
|
||||
if (filename.ends_with(".zip"))
|
||||
{
|
||||
ExtensionChecker extensionChecker { request_uri.query("ext") };
|
||||
namespace fs = std::filesystem;
|
||||
try {
|
||||
auto zipFile = ZipFile::Create(req.get_body_temporary_file());
|
||||
std::vector<std::string> files = zipFile->GetFiles();
|
||||
for (const auto&inputFile : files)
|
||||
{
|
||||
if (!inputFile.ends_with("/")) // don't process directory entries.
|
||||
{
|
||||
fs::path inputPath { inputFile};
|
||||
std::string extension = inputPath.extension();
|
||||
if (extensionChecker.IsValidExtension(extension))
|
||||
{
|
||||
auto si = zipFile->GetFileInputStream(inputFile);
|
||||
std::string path = this->model->UploadUserFile(directory,patchProperty,inputFile, si,zipFile->GetFileSize(inputFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
// set outputPath to the file or folder we would like focus to go to.
|
||||
// almost always a single folder in the root.
|
||||
std::string returnPath = GetFirstFolderOrFile(files);
|
||||
outputFileName = this->model->GetPluginUploadDirectory() / directory / returnPath;
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error(SS("Unzip failed. " << e.what()));
|
||||
throw;
|
||||
}
|
||||
|
||||
} else {
|
||||
outputFileName = this->model->UploadUserFile(directory,patchProperty,filename,req.get_body_input_stream(), req.content_length());
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(outputFileName);
|
||||
std::string response = ss.str();
|
||||
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* When hosting a react app, replace /var/config.json with
|
||||
data that will connect the react app with our socket server.
|
||||
*/
|
||||
|
||||
class InterceptConfig : public RequestHandler
|
||||
{
|
||||
private:
|
||||
uint64_t maxUploadSize;
|
||||
int portNumber;
|
||||
|
||||
public:
|
||||
InterceptConfig(int portNumber, uint64_t maxUploadSize)
|
||||
: RequestHandler("/var/config.json"),
|
||||
maxUploadSize(maxUploadSize),
|
||||
portNumber(portNumber)
|
||||
{
|
||||
}
|
||||
std::string GetConfig(const std::string &fromAddress)
|
||||
{
|
||||
#define LINK_LOCAL_WEB_SOCKET 1
|
||||
#if LINK_LOCAL_WEB_SOCKET
|
||||
std::string webSocketAddress = GetLinkLocalAddress(fromAddress);
|
||||
Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber));
|
||||
#else
|
||||
std::string webSocketAddress = "*";
|
||||
#endif
|
||||
|
||||
std::stringstream s;
|
||||
|
||||
s << "{ \"socket_server_port\": " << portNumber
|
||||
<< ", \"socket_server_address\": \"" << webSocketAddress <<
|
||||
"\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << " }";
|
||||
|
||||
return s.str();
|
||||
}
|
||||
virtual ~InterceptConfig() {}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
// intercepted. See the other overload.
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
return;
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
// intercepted. see the other overload.
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec) override
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
};
|
||||
|
||||
void pipedal::ConfigureWebServer(
|
||||
WebServer&server,
|
||||
PiPedalModel&model,
|
||||
int port,
|
||||
size_t maxUploadSize)
|
||||
{
|
||||
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, maxUploadSize)};
|
||||
server.AddRequestHandler(interceptConfig);
|
||||
|
||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
||||
server.AddRequestHandler(downloadIntercept);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace pipedal {
|
||||
class WebServer;
|
||||
class PiPedalModel;
|
||||
void ConfigureWebServer(WebServer&server,PiPedalModel&model,int port,size_t maxUploadSize);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ TEST_CASE("WebServer shutdown", "[webServerShutdown][Build][Dev]")
|
||||
auto const threads = 3;
|
||||
|
||||
auto server = WebServer::create(
|
||||
address, port, doc_root.c_str(), threads);
|
||||
address, port, doc_root.c_str(), threads,512*1024*1024);
|
||||
server->RunInBackground();
|
||||
sleep(5);
|
||||
server->ShutDown(1000);
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// 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 "ZipFile.hpp"
|
||||
#include "zip.h"
|
||||
#include <stdexcept>
|
||||
#include <map>
|
||||
#include "ss.hpp"
|
||||
#include "Finally.hpp"
|
||||
#include <cstring>
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
ZipFile::ZipFile()
|
||||
{
|
||||
}
|
||||
ZipFile::~ZipFile()
|
||||
{
|
||||
}
|
||||
|
||||
class ZipFileImpl : public ZipFile
|
||||
{
|
||||
public:
|
||||
ZipFileImpl(const std::filesystem::path &path)
|
||||
: path(path)
|
||||
{
|
||||
int errorOp = 0;
|
||||
zipFile = zip_open(path.c_str(), ZIP_RDONLY, &errorOp);
|
||||
if (zipFile == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Can't open zip file.");
|
||||
}
|
||||
}
|
||||
virtual ~ZipFileImpl();
|
||||
virtual std::vector<std::string> GetFiles() override;
|
||||
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path &path) override;
|
||||
virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) override;
|
||||
virtual size_t GetFileSize(const std::string&filename) override;
|
||||
|
||||
private:
|
||||
std::map<std::string, zip_int64_t> nameMap; // avoid o(2) extraction operations.
|
||||
const std::filesystem::path path;
|
||||
zip_t *zipFile = nullptr;
|
||||
};
|
||||
|
||||
ZipFile::ptr ZipFile::Create(const std::filesystem::path &path)
|
||||
{
|
||||
return std::shared_ptr<ZipFile>(new ZipFileImpl(path));
|
||||
}
|
||||
ZipFileImpl::~ZipFileImpl()
|
||||
{
|
||||
if (zipFile)
|
||||
{
|
||||
zip_close(zipFile);
|
||||
zipFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ZipFileImpl::GetFiles()
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
zip_int64_t nEntries = zip_get_num_entries(zipFile, 0);
|
||||
for (zip_int64_t i = 0; i < nEntries; ++i)
|
||||
{
|
||||
const char *name = zip_get_name(zipFile, i, ZIP_FL_ENC_STRICT);
|
||||
if (name)
|
||||
{
|
||||
result.push_back(name);
|
||||
nameMap[name] = i;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ZipFileImpl::ExtractTo(const std::string &zipName, const std::filesystem::path &path)
|
||||
{
|
||||
auto fi = nameMap.find(zipName);
|
||||
|
||||
if (fi == nameMap.end())
|
||||
{
|
||||
// must call GetFiles() firest.
|
||||
throw std::runtime_error("Zip content file not found.");
|
||||
}
|
||||
zip_int64_t fileIndex = fi->second;
|
||||
|
||||
zip_file_t *fIn = zip_fopen_index(this->zipFile, fileIndex, 0);
|
||||
if (fIn == nullptr)
|
||||
{
|
||||
zip_error_t *error = zip_get_error(this->zipFile);
|
||||
const char *strError = zip_error_strerror(error);
|
||||
|
||||
throw std::runtime_error(SS("Failed to read zip content file. " << strError));
|
||||
}
|
||||
Finally t{[fIn]() mutable
|
||||
{
|
||||
zip_fclose(fIn);
|
||||
}};
|
||||
|
||||
std::ofstream fo{path, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary};
|
||||
|
||||
constexpr int BUFFER_SIZE = 64 * 1024;
|
||||
std::vector<char> vBuff(BUFFER_SIZE);
|
||||
char *pBuff = (char *)&(vBuff[0]);
|
||||
while (true)
|
||||
{
|
||||
zip_int64_t nRead = zip_fread(fIn, pBuff, BUFFER_SIZE);
|
||||
if (nRead = 0)
|
||||
break;
|
||||
if (nRead == -1)
|
||||
{
|
||||
zip_error_t *error = zip_file_get_error(fIn);
|
||||
const char *strError = zip_error_strerror(error);
|
||||
throw std::runtime_error(SS("Error reading zip content file." << strError));
|
||||
}
|
||||
fo.write(pBuff, (std::streamsize)nRead);
|
||||
if (!fo)
|
||||
{
|
||||
throw std::runtime_error(SS("Unable to write to " << path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zip_file_input_stream_buf::zip_file_input_stream_buf(zip_file_t *file, size_t buffer_size)
|
||||
: file(file), buffer(buffer_size + putback_size)
|
||||
{
|
||||
char *end = buffer.data() + buffer.size();
|
||||
setg(end, end, end);
|
||||
}
|
||||
zip_file_input_stream_buf::~zip_file_input_stream_buf()
|
||||
{
|
||||
zip_fclose(file);
|
||||
}
|
||||
|
||||
std::streamsize zip_file_input_stream_buf::xsgetn(char *s, std::streamsize n)
|
||||
{
|
||||
std::streamsize num_copied = 0;
|
||||
while (n > 0)
|
||||
{
|
||||
if (gptr() >= egptr())
|
||||
{
|
||||
if (underflow() == traits_type::eof())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::streamsize chunk = std::min(n, egptr() - gptr());
|
||||
std::memcpy(s, gptr(), chunk);
|
||||
s += chunk;
|
||||
n -= chunk;
|
||||
num_copied += chunk;
|
||||
gbump(chunk);
|
||||
}
|
||||
return num_copied;
|
||||
}
|
||||
zip_file_input_stream_buf::int_type zip_file_input_stream_buf::underflow()
|
||||
{
|
||||
if (gptr() < egptr())
|
||||
{
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
char *base = buffer.data();
|
||||
char *start = base;
|
||||
|
||||
if (eback() != base)
|
||||
{
|
||||
std::memmove(base, egptr() - putback_size, putback_size);
|
||||
start += putback_size;
|
||||
}
|
||||
|
||||
zip_int64_t n = zip_fread(file, start, (zip_uint64_t)(buffer.size() - (start - base)));
|
||||
if (n <= 0)
|
||||
{
|
||||
return traits_type::eof();
|
||||
}
|
||||
|
||||
setg(base, start, start + n);
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
zip_file_input_stream ZipFileImpl::GetFileInputStream(const std::string& filename, size_t bufferSize)
|
||||
{
|
||||
zip_file_t *f = zip_fopen(zipFile,filename.c_str(),0);
|
||||
if (!f) {
|
||||
throw std::runtime_error(SS("Failed to open zip content file " << filename));
|
||||
}
|
||||
return zip_file_input_stream(f,bufferSize);
|
||||
}
|
||||
size_t ZipFileImpl::GetFileSize(const std::string&filename)
|
||||
{
|
||||
zip_stat_t stat;
|
||||
if (zip_stat(zipFile,filename.c_str(),0,&stat) < 0)
|
||||
{
|
||||
throw std::runtime_error("File not found.");
|
||||
}
|
||||
if ((stat.valid & ZIP_STAT_SIZE) == 0)
|
||||
{
|
||||
throw std::runtime_error("Failed to get file size.");
|
||||
}
|
||||
return stat.size;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <zip.h>
|
||||
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class zip_file_input_stream_buf : public std::streambuf {
|
||||
private:
|
||||
zip_file_t* file;
|
||||
std::vector<char> buffer;
|
||||
static const size_t putback_size = 8;
|
||||
public:
|
||||
zip_file_input_stream_buf(zip_file_t* file, size_t buffer_size = 16384);
|
||||
~zip_file_input_stream_buf();
|
||||
protected:
|
||||
virtual int_type underflow() override;
|
||||
virtual std::streamsize xsgetn(char* s, std::streamsize n) override;
|
||||
};
|
||||
class zip_file_input_stream : public std::istream {
|
||||
private:
|
||||
zip_file_input_stream_buf buf;
|
||||
public:
|
||||
zip_file_input_stream(zip_file_t *file, size_t buff_size = 16384)
|
||||
: std::istream(nullptr), buf(file,buff_size)
|
||||
{
|
||||
rdbuf(&buf);
|
||||
|
||||
}
|
||||
};
|
||||
class ZipFile {
|
||||
protected:
|
||||
ZipFile();
|
||||
|
||||
public:
|
||||
using ptr = std::shared_ptr<ZipFile>;
|
||||
static ptr Create(const std::filesystem::path &path);
|
||||
ZipFile(const ZipFile&) = delete;
|
||||
ZipFile&operator=(const ZipFile&) = delete;
|
||||
virtual ~ZipFile();
|
||||
|
||||
virtual std::vector<std::string> GetFiles() = 0;
|
||||
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) = 0;
|
||||
virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) = 0;
|
||||
virtual size_t GetFileSize(const std::string&filename) = 0;
|
||||
|
||||
};
|
||||
}
|
||||
+5
-455
@@ -26,6 +26,7 @@
|
||||
#include "Lv2Log.hpp"
|
||||
#include "ServiceConfiguration.hpp"
|
||||
#include "AvahiService.hpp"
|
||||
#include "WebServerConfig.hpp"
|
||||
|
||||
#include "PiPedalSocket.hpp"
|
||||
#include "PluginHost.hpp"
|
||||
@@ -38,7 +39,6 @@
|
||||
#include <sys/stat.h>
|
||||
#include <boost/asio.hpp>
|
||||
#include "HtmlHelper.hpp"
|
||||
#include "Ipv6Helpers.hpp"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
@@ -47,11 +47,10 @@
|
||||
|
||||
#include <systemd/sd-daemon.h>
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
#define BANK_EXTENSION ".piBank"
|
||||
#define PLUGIN_PRESETS_EXTENSION ".piPluginPresets"
|
||||
|
||||
|
||||
|
||||
#ifdef __ARM_ARCH_ISA_A64
|
||||
@@ -94,450 +93,6 @@ void throwSystemError(int error)
|
||||
{
|
||||
|
||||
}
|
||||
using namespace boost::system;
|
||||
|
||||
class DownloadIntercept : public RequestHandler
|
||||
{
|
||||
PiPedalModel *model;
|
||||
|
||||
public:
|
||||
DownloadIntercept(PiPedalModel *model)
|
||||
: RequestHandler("/var"),
|
||||
model(model)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool wants(const std::string &method, const uri &request_uri) const
|
||||
{
|
||||
if (request_uri.segment_count() != 2 || request_uri.segment(0) != "var")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
if (strInstanceId != "")
|
||||
return true;
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
return true;
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetContentDispositionHeader(const std::string &name, const std::string &extension)
|
||||
{
|
||||
std::string fileName = name.substr(0, 64) + extension;
|
||||
std::stringstream s;
|
||||
|
||||
s << "attachment; filename*=" << HtmlHelper::Rfc5987EncodeFileName(fileName) << "; filename=\"" << HtmlHelper::SafeFileName(fileName) << "\"";
|
||||
std::string result = s.str();
|
||||
return result;
|
||||
}
|
||||
|
||||
void GetPluginPresets(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string pluginUri = request_uri.query("id");
|
||||
auto plugin = model->GetLv2Host().GetPluginInfo(pluginUri);
|
||||
*pName = plugin->name();
|
||||
|
||||
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(pluginPresets);
|
||||
*pContent = s.str();
|
||||
}
|
||||
void GetPreset(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
auto pedalboard = model->GetPreset(instanceId);
|
||||
|
||||
// a certain elegance to using same file format for banks and presets.
|
||||
BankFile file;
|
||||
file.name(pedalboard.name());
|
||||
int64_t newInstanceId = file.addPreset(pedalboard);
|
||||
file.selectedPreset(newInstanceId);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s,false);
|
||||
writer.write(file);
|
||||
*pContent = s.str();
|
||||
*pName = pedalboard.name();
|
||||
}
|
||||
void GetBank(const uri &request_uri, std::string *pName, std::string *pContent)
|
||||
{
|
||||
std::string strInstanceId = request_uri.query("id");
|
||||
int64_t instanceId = std::stol(strInstanceId);
|
||||
BankFile bank;
|
||||
model->GetBank(instanceId, &bank);
|
||||
|
||||
std::stringstream s;
|
||||
json_writer writer(s, true); // do what we can to reduce the file size.
|
||||
writer.write(bank);
|
||||
*pContent = s.str();
|
||||
*pName = bank.name();
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
return;
|
||||
}
|
||||
if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setContentLength(content.length());
|
||||
return;
|
||||
}
|
||||
throw PiPedalException("Not found.");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const uri &request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "downloadPluginPresets")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPluginPresets(request_uri, &name, &content);
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PLUGIN_PRESETS_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadPreset")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetPreset(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else if (segment == "downloadBank")
|
||||
{
|
||||
std::string name;
|
||||
std::string content;
|
||||
GetBank(request_uri, &name, &content);
|
||||
|
||||
res.set(HttpField::content_type, "application/octet-stream");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(content.length());
|
||||
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
|
||||
res.setBody(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
virtual void post_response(
|
||||
const uri &request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::string segment = request_uri.segment(1);
|
||||
|
||||
if (segment == "uploadPluginPresets")
|
||||
{
|
||||
const std::string &presetBody = req.body();
|
||||
std::stringstream s(presetBody);
|
||||
json_reader reader(s);
|
||||
PluginPresets presets;
|
||||
reader.read(&presets);
|
||||
model->UploadPluginPresets(presets);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << -1;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadPreset")
|
||||
{
|
||||
const std::string &presetBody = req.body();
|
||||
std::stringstream s(presetBody);
|
||||
json_reader reader(s);
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadPreset(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
}
|
||||
else if (segment == "uploadBank")
|
||||
{
|
||||
const std::string &presetBody = req.body();
|
||||
std::istringstream s(presetBody);
|
||||
json_reader reader(s);
|
||||
|
||||
uint64_t uploadAfter = -1;
|
||||
std::string strUploadAfter = request_uri.query("uploadAfter");
|
||||
if (strUploadAfter.length() != 0)
|
||||
{
|
||||
uploadAfter = std::stol(strUploadAfter);
|
||||
}
|
||||
|
||||
BankFile bankFile;
|
||||
reader.read(&bankFile);
|
||||
|
||||
uint64_t instanceId = model->UploadBank(bankFile, uploadAfter);
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
std::stringstream sResult;
|
||||
sResult << instanceId;
|
||||
std::string result = sResult.str();
|
||||
res.setContentLength(result.length());
|
||||
|
||||
res.setBody(result);
|
||||
} else if (segment == "uploadUserFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
const std::string &fileBody = req.body();
|
||||
const std::string &directory = request_uri.query("directory");
|
||||
const std::string &filename = request_uri.query("filename");
|
||||
const std::string &patchProperty = request_uri.query("property");
|
||||
|
||||
|
||||
if (patchProperty.length() == 0 && directory.length() == 0)
|
||||
{
|
||||
throw PiPedalException("Malformed request.");
|
||||
|
||||
}
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
|
||||
std::string outputFileName = std::filesystem::path(directory) / filename;
|
||||
|
||||
std::string path = this->model->UploadUserFile(directory,patchProperty,filename,fileBody);
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(outputFileName);
|
||||
std::string response = ss.str();
|
||||
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PiPedalException("Not found");
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
if (strcmp(e.what(), "Not found") == 0)
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = boost::system::errc::make_error_code(boost::system::errc::invalid_argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* When hosting a react app, replace /var/config.json with
|
||||
data that will connect the react app with our socket server.
|
||||
*/
|
||||
|
||||
class InterceptConfig : public RequestHandler
|
||||
{
|
||||
private:
|
||||
uint64_t maxUploadSize;
|
||||
int portNumber;
|
||||
|
||||
public:
|
||||
InterceptConfig(int portNumber, uint64_t maxUploadSize)
|
||||
: RequestHandler("/var/config.json"),
|
||||
maxUploadSize(maxUploadSize),
|
||||
portNumber(portNumber)
|
||||
{
|
||||
}
|
||||
std::string GetConfig(const std::string &fromAddress)
|
||||
{
|
||||
#define LINK_LOCAL_WEB_SOCKET 1
|
||||
#if LINK_LOCAL_WEB_SOCKET
|
||||
std::string webSocketAddress = GetLinkLocalAddress(fromAddress);
|
||||
Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber));
|
||||
#else
|
||||
std::string webSocketAddress = "*";
|
||||
#endif
|
||||
|
||||
std::stringstream s;
|
||||
|
||||
s << "{ \"socket_server_port\": " << portNumber
|
||||
<< ", \"socket_server_address\": \"" << webSocketAddress << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << " }";
|
||||
|
||||
return s.str();
|
||||
}
|
||||
virtual ~InterceptConfig() {}
|
||||
|
||||
virtual void head_response(
|
||||
const uri &request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
// intercepted. See the other overload.
|
||||
}
|
||||
|
||||
virtual void head_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
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,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
// intercepted. see the other overload.
|
||||
}
|
||||
|
||||
virtual void get_response(
|
||||
const std::string &fromAddress,
|
||||
const uri &request_uri,
|
||||
const HttpRequest &req,
|
||||
HttpResponse &res,
|
||||
std::error_code &ec)
|
||||
{
|
||||
std::string response = GetConfig(fromAddress);
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
res.setContentLength(response.length());
|
||||
res.setBody(response);
|
||||
}
|
||||
};
|
||||
|
||||
static bool isJackServiceRunning()
|
||||
{
|
||||
@@ -659,7 +214,7 @@ int main(int argc, char *argv[])
|
||||
auto const threads = std::max<int>(1, configuration.GetThreads());
|
||||
|
||||
server = WebServer::create(
|
||||
address, port, web_root.c_str(), threads);
|
||||
address, port, web_root.c_str(), threads,configuration.GetMaxUploadSize());
|
||||
|
||||
Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads);
|
||||
|
||||
@@ -794,12 +349,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
server->AddSocketFactory(pipedalSocketFactory);
|
||||
|
||||
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, configuration.GetMaxUploadSize())};
|
||||
server->AddRequestHandler(interceptConfig);
|
||||
|
||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
||||
server->AddRequestHandler(downloadIntercept);
|
||||
|
||||
ConfigureWebServer(*server,model,port,configuration.GetMaxUploadSize());
|
||||
{
|
||||
server->RunInBackground(-1);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
- make sure lv2-dev shows up in packaging. Make sure dhcpcd shows up in packaging
|
||||
- Toob nam input VU is not working.
|
||||
- x TooB NAM: dbVu telltale is zero0.
|
||||
- x TooB NAM: bass [] mid trebble.
|
||||
- X check FLAC decode.
|
||||
- Zip file upload in a subdirectory.
|
||||
- Turning off wifi-direct sould re-enable NetworkManager.
|
||||
- ToobML save/restore state.
|
||||
|
||||
|
||||
- make app use the same theme settings as the website.
|
||||
- BUG: gcs when we have an animated output control
|
||||
- Shutdown/reboot midi bindings
|
||||
@@ -10,12 +10,12 @@
|
||||
- do we want to take a second crack at establishin multi-p2p connections with the driver_param?
|
||||
|
||||
|
||||
sudo apt install libsdbus-c++-dev libnm-dev
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Pri Description
|
||||
-----------------
|
||||
9 Upload proteus patches.
|
||||
8 Migrate to Vite toolchain.
|
||||
5 Re-use plugin instances when rebuilding pedalboard.
|
||||
|
||||
Reference in New Issue
Block a user