@@ -29,7 +29,7 @@ jobs:
|
|||||||
sudo apt install libasound2-dev
|
sudo apt install libasound2-dev
|
||||||
sudo apt install libwebsocketpp-dev authbind
|
sudo apt install libwebsocketpp-dev authbind
|
||||||
sudo apt install libsdbus-c++-dev libsdbus-c++-bin
|
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
|
git submodule update --init --recursive
|
||||||
./react-config
|
./react-config
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
cmake_minimum_required(VERSION 3.16.0)
|
cmake_minimum_required(VERSION 3.16.0)
|
||||||
project(pipedal
|
project(pipedal
|
||||||
VERSION 1.2.36
|
VERSION 1.2.37
|
||||||
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
||||||
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
||||||
)
|
)
|
||||||
set (DISPLAY_VERSION "v1.2.36beta2")
|
set (DISPLAY_VERSION "v1.2.37beta4")
|
||||||
|
|
||||||
set (CMAKE_INSTALL_PREFIX "/usr/")
|
set (CMAKE_INSTALL_PREFIX "/usr/")
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
<img src='docs/GithubBanner.png' width="100%" /><br/>
|
<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).
|
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.
|
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.
|
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",
|
"socketServerAddress": "0.0.0.0:80",
|
||||||
|
|
||||||
/* Number of threads to use for servicing websockets */
|
/* Number of threads to use for servicing websockets */
|
||||||
"threads" : 10,
|
"threads" : 5,
|
||||||
|
|
||||||
"logHttpRequests": false,
|
"logHttpRequests": false,
|
||||||
/* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */
|
/* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */
|
||||||
"logLevel": 3,
|
"logLevel": 3,
|
||||||
|
|
||||||
/* Maximum filesize to allow when uploading */
|
/* 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.
|
/* Provide access point capture redirects on this gateway. -- provides automatic browser launching on Access Point access.
|
||||||
(not implemented)
|
(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 authbind
|
||||||
sudo apt install -y libavahi-client-dev
|
sudo apt install -y libavahi-client-dev
|
||||||
sudo apt install -y libnm-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
|
### Installing Sources
|
||||||
|
|||||||
+3
-3
@@ -13,17 +13,17 @@ page_icon: img/Install4.jpg
|
|||||||
|
|
||||||
Download the most recent Debian (.deb) package for your platform:
|
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)
|
- [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
|
Install the package by running
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
cd ~/Downloads
|
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.
|
Adjust accordingly if you have downloaded v1.1.31.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
# Release Notes
|
# 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
|
## Pipedal 1.2.36 Beta3
|
||||||
|
|
||||||
Fixes for the following issues:
|
Fixes for the following issues:
|
||||||
|
|||||||
+3
-3
@@ -4,17 +4,17 @@
|
|||||||
|
|
||||||
Download the most recent Debian (.deb) package for your platform:
|
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>
|
- <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
|
Install the package by running
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
cd ~/Downloads
|
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.
|
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%"/>
|
<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).
|
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.
|
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 xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||||
@prefix toobml: <http://two-play.com/plugins/toob-ml#> .
|
@prefix toobml: <http://two-play.com/plugins/toob-ml#> .
|
||||||
|
|
||||||
|
|
||||||
|
toobml:modelFile
|
||||||
|
a lv2:Parameter;
|
||||||
|
rdfs:label "Model";
|
||||||
|
rdfs:range atom:Path.
|
||||||
|
|
||||||
toobml:filterGroup
|
toobml:filterGroup
|
||||||
a param:ControlGroup ,
|
a param:ControlGroup ,
|
||||||
pg:InputGroup ;
|
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
|
TooB ML models use significantly less CPU, although TooB Neural Amp Modeller is significantly more flexible, and
|
||||||
has a huge number of downloadable models.
|
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
|
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.
|
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
|
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.
|
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,
|
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).
|
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:brand "TooB";
|
||||||
mod:label "TooB ML";
|
mod:label "TooB ML";
|
||||||
lv2:requiredFeature urid:map, work:schedule ;
|
lv2:requiredFeature urid:map, work:schedule ;
|
||||||
lv2:optionalFeature lv2:hardRTCapable;
|
lv2:optionalFeature state::freePath, state:makePath, state:mapPath ;
|
||||||
lv2:extensionData state:interface, work:interface;
|
lv2:extensionData state:interface, work:interface;
|
||||||
|
|
||||||
|
patch:readable
|
||||||
|
toobml:modelFile;
|
||||||
|
patch:writable
|
||||||
|
toobml:modelFile;
|
||||||
|
|
||||||
patch:readable
|
patch:readable
|
||||||
<http://two-play.com/plugins/toob#frequencyResponseVector>
|
<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 ;
|
units:unit units:db ;
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
# legagcy for loading of old instances.
|
||||||
a lv2:InputPort, lv2:ControlPort ;
|
a lv2:InputPort, lv2:ControlPort ;
|
||||||
|
lv2:portProperty epp:notOnGUI;
|
||||||
lv2:index 2 ;
|
lv2:index 2 ;
|
||||||
lv2:symbol "model" ;
|
lv2:symbol "model" ;
|
||||||
lv2:name "Model" ;
|
lv2:name "Model" ;
|
||||||
lv2:default 0.0 ;
|
lv2:default -1.0 ;
|
||||||
lv2:minimum 0.0 ;
|
lv2:minimum -1.0 ;
|
||||||
lv2:maximum 32.0 ;
|
lv2:maximum 32.0 ;
|
||||||
lv2:portProperty lv2:enumeration ;
|
lv2:portProperty lv2:enumeration ;
|
||||||
lv2:scalePoint
|
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>
|
<http://two-play.com/plugins/toob-ml#piPedalUI>
|
||||||
a pipedal_ui:ui ;
|
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
|
pipedal_ui:frequencyPlot
|
||||||
[
|
[
|
||||||
lv2:index 8 ;
|
lv2:index 8 ;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"socket_server_port": 80,
|
"socket_server_port": 8080,
|
||||||
"socket_server_address": "*",
|
"socket_server_address": "*",
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"max_upload_size": 1048576,
|
"max_upload_size": 536870912,
|
||||||
"fakeAndroid": false,
|
"fakeAndroid": false,
|
||||||
"ui_plugins": []
|
"ui_plugins": []
|
||||||
}
|
}
|
||||||
+69
-27
@@ -21,12 +21,12 @@ import React from 'react';
|
|||||||
import { ThemeProvider, createTheme, StyledEngineProvider, Theme } from '@mui/material/styles';
|
import { ThemeProvider, createTheme, StyledEngineProvider, Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
import AppThemed from "./AppThemed";
|
import AppThemed from "./AppThemed";
|
||||||
import {isDarkMode} from './DarkMode';
|
import { isDarkMode } from './DarkMode';
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
declare module '@mui/material/styles' {
|
||||||
interface Theme {
|
interface Theme {
|
||||||
mainBackground: React.CSSProperties['color'];
|
mainBackground: React.CSSProperties['color'];
|
||||||
toolbarColor: React.CSSProperties['color'];
|
toolbarColor: React.CSSProperties['color'];
|
||||||
}
|
}
|
||||||
interface ThemeOptions {
|
interface ThemeOptions {
|
||||||
mainBackground?: React.CSSProperties['color'];
|
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' {
|
declare module '@mui/styles/defaultTheme' {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
interface DefaultTheme extends Theme {}
|
interface DefaultTheme extends Theme { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -49,34 +54,71 @@ declare module '@mui/styles/defaultTheme' {
|
|||||||
const theme = createTheme(
|
const theme = createTheme(
|
||||||
isDarkMode() ?
|
isDarkMode() ?
|
||||||
{
|
{
|
||||||
palette: {
|
components: {
|
||||||
mode: 'dark',
|
MuiButton: {
|
||||||
primary: {
|
variants: [
|
||||||
main: '#A770E4'// #6750A4" // #5B5690 #60529A #5C5694
|
{
|
||||||
|
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",
|
||||||
mainBackground: "#222",
|
toolbarColor: '#222'
|
||||||
toolbarColor: '#FFFFFF'
|
}
|
||||||
}
|
|
||||||
:
|
:
|
||||||
{
|
{
|
||||||
palette: {
|
components: {
|
||||||
primary: {
|
MuiButton: {
|
||||||
main: "#6750A4" // #5B5690 #60529A #5C5694
|
variants: [
|
||||||
|
{
|
||||||
|
props: { variant: 'dialogPrimary' },
|
||||||
|
style: {
|
||||||
|
color: "rgb(0,0,0,0.87)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
props: { variant: 'dialogSecondary', },
|
||||||
|
style: {
|
||||||
|
color: "rgb(0,0,0,0.6)"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
secondary: {
|
palette: {
|
||||||
main: "#FF6060"
|
primary: {
|
||||||
}
|
main: "#6750A4" // #5B5690 #60529A #5C5694
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: "#FF6060"
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
mainBackground: "#FFFFFF",
|
mainBackground: "#FFFFFF",
|
||||||
toolbarColor: '#FFFFFF'
|
toolbarColor: '#FFFFFF'
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -97,7 +139,7 @@ const App = (class extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AppThemed/>
|
<AppThemed />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -879,7 +879,7 @@ render() {
|
|||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={this.handleCloseAlert} color="primary" autoFocus>
|
<Button variant="dialogPrimary" onClick={this.handleCloseAlert} color="primary" autoFocus>
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -546,10 +546,10 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
<Typography>Are you sure you want to delete bank '{this.getSelectedBankName()}'?</Typography>
|
<Typography>Are you sure you want to delete bank '{this.getSelectedBankName()}'?</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={()=> this.handleDeletePromptClose()} color="primary">
|
<Button variant="dialogSecondary" onClick={()=> this.handleDeletePromptClose()} color="primary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={()=> this.handleDeletePromptOk()} color="secondary" >
|
<Button variant="dialogPrimary" onClick={()=> this.handleDeletePromptOk()} color="secondary" >
|
||||||
DELETE
|
DELETE
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import createStyles from '@mui/styles/createStyles';
|
|||||||
import withStyles from '@mui/styles/withStyles';
|
import withStyles from '@mui/styles/withStyles';
|
||||||
import { UiFileProperty } from './Lv2Plugin';
|
import { UiFileProperty } from './Lv2Plugin';
|
||||||
import Typography from '@mui/material/Typography';
|
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 ButtonBase from '@mui/material/ButtonBase'
|
||||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
import {PedalboardItem} from './Pedalboard';
|
import {PedalboardItem} from './Pedalboard';
|
||||||
@@ -98,8 +98,18 @@ const FilePropertyControl =
|
|||||||
value: "",
|
value: "",
|
||||||
};
|
};
|
||||||
this.model = PiPedalModelFactory.getInstance();
|
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;
|
private propertyGetHandle?: ListenHandle;
|
||||||
|
|
||||||
monitorPropertyHandle?: ListenHandle;
|
monitorPropertyHandle?: ListenHandle;
|
||||||
@@ -119,6 +129,7 @@ const FilePropertyControl =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
unsubscribeToPatchProperty() {
|
unsubscribeToPatchProperty() {
|
||||||
if (this.monitorPropertyHandle !== undefined)
|
if (this.monitorPropertyHandle !== undefined)
|
||||||
@@ -126,15 +137,18 @@ const FilePropertyControl =
|
|||||||
this.model.cancelMonitorPatchProperty(this.monitorPropertyHandle);
|
this.model.cancelMonitorPatchProperty(this.monitorPropertyHandle);
|
||||||
this.monitorPropertyHandle = undefined;
|
this.monitorPropertyHandle = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
private mounted: boolean = false;
|
private mounted: boolean = false;
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.model.state.addOnChangedHandler(this.onStateChanged);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
this.subscribeToPatchProperty();
|
this.subscribeToPatchProperty();
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubscribeToPatchProperty();
|
this.unsubscribeToPatchProperty();
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
this.model.state.removeOnChangedHandler(this.onStateChanged);
|
||||||
}
|
}
|
||||||
componentDidUpdate(prevProps: Readonly<FilePropertyControlProps>, prevState: Readonly<FilePropertyControlState>, snapshot?: any): void {
|
componentDidUpdate(prevProps: Readonly<FilePropertyControlProps>, prevState: Readonly<FilePropertyControlState>, snapshot?: any): void {
|
||||||
if (prevProps.fileProperty.patchProperty !== this.props.fileProperty.patchProperty
|
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.
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Theme, createStyles } from '@mui/material/styles';
|
||||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
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 Button from '@mui/material/Button';
|
||||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||||
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
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 DialogActions from '@mui/material/DialogActions';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import OldDeleteIcon from './OldDeleteIcon';
|
import OldDeleteIcon from './OldDeleteIcon';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import { withStyles, WithStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
|
||||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||||
import { UiFileProperty } from './Lv2Plugin';
|
import { UiFileProperty } from './Lv2Plugin';
|
||||||
@@ -39,8 +49,20 @@ import Typography from '@mui/material/Typography';
|
|||||||
import DialogEx from './DialogEx';
|
import DialogEx from './DialogEx';
|
||||||
import UploadFileDialog from './UploadFileDialog';
|
import UploadFileDialog from './UploadFileDialog';
|
||||||
import OkCancelDialog from './OkCancelDialog';
|
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,
|
open: boolean,
|
||||||
fileProperty: UiFileProperty,
|
fileProperty: UiFileProperty,
|
||||||
selectedFile: string,
|
selectedFile: string,
|
||||||
@@ -51,16 +73,90 @@ export interface FilePropertyDialogProps {
|
|||||||
export interface FilePropertyDialogState {
|
export interface FilePropertyDialogState {
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
selectedFile: string;
|
selectedFile: string;
|
||||||
|
navDirectory: string;
|
||||||
hasSelection: boolean;
|
hasSelection: boolean;
|
||||||
files: string[];
|
canDelete: boolean;
|
||||||
|
fileEntries: FileEntry[];
|
||||||
columns: number;
|
columns: number;
|
||||||
columnWidth: number;
|
columnWidth: number;
|
||||||
openUploadFileDialog: boolean;
|
openUploadFileDialog: boolean;
|
||||||
openConfirmDeleteDialog: 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) {
|
constructor(props: FilePropertyDialogProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -71,29 +167,32 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
this.state = {
|
this.state = {
|
||||||
fullScreen: false,
|
fullScreen: false,
|
||||||
selectedFile: props.selectedFile,
|
selectedFile: props.selectedFile,
|
||||||
|
navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty),
|
||||||
hasSelection: false,
|
hasSelection: false,
|
||||||
|
canDelete: false,
|
||||||
columns: 0,
|
columns: 0,
|
||||||
columnWidth: 1,
|
columnWidth: 1,
|
||||||
files: [],
|
fileEntries: [],
|
||||||
openUploadFileDialog: false,
|
openUploadFileDialog: false,
|
||||||
openConfirmDeleteDialog: false
|
openConfirmDeleteDialog: false,
|
||||||
|
menuAnchorEl: null,
|
||||||
|
newFolderDialogOpen: false,
|
||||||
|
renameDialogOpen: false,
|
||||||
|
moveDialogOpen: false
|
||||||
};
|
};
|
||||||
this.requestScroll = true;
|
this.requestScroll = true;
|
||||||
this.requestFiles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollRef: HTMLDivElement | null = null;
|
private scrollRef: HTMLDivElement | null = null;
|
||||||
|
|
||||||
onScrollRef(element: HTMLDivElement | null) {
|
onScrollRef(element: HTMLDivElement | null) {
|
||||||
this.scrollRef = element;
|
this.scrollRef = element;
|
||||||
this.maybeScrollIntoView();
|
this.maybeScrollIntoView();
|
||||||
}
|
}
|
||||||
private maybeScrollIntoView()
|
private maybeScrollIntoView() {
|
||||||
{
|
if (this.scrollRef !== null && this.state.fileEntries.length !== 0 && this.mounted && this.requestScroll) {
|
||||||
if (this.scrollRef !== null && this.state.files.length !== 0 && this.mounted && this.requestScroll)
|
|
||||||
{
|
|
||||||
this.requestScroll = false;
|
this.requestScroll = false;
|
||||||
let options: ScrollIntoViewOptions = { block: "nearest"};
|
let options: ScrollIntoViewOptions = { block: "nearest" };
|
||||||
options.block = "nearest";
|
options.block = "nearest";
|
||||||
|
|
||||||
this.scrollRef.scrollIntoView(options);
|
this.scrollRef.scrollIntoView(options);
|
||||||
@@ -102,7 +201,7 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
private mounted: boolean = false;
|
private mounted: boolean = false;
|
||||||
private model: PiPedalModel;
|
private model: PiPedalModel;
|
||||||
|
|
||||||
private requestFiles() {
|
private requestFiles(navPath: string) {
|
||||||
if (!this.props.open) {
|
if (!this.props.open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,44 +209,44 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model.requestFileList(this.props.fileProperty)
|
this.model.requestFileList2(navPath, this.props.fileProperty)
|
||||||
.then((files) => {
|
.then((files) => {
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
files.splice(0,0,"");
|
// let insertionPoint = files.length;
|
||||||
this.setState({ files: files, hasSelection: this.isFileInList(files, this.state.selectedFile) });
|
// 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) => {
|
}).catch((error) => {
|
||||||
this.model.showAlert(error.toString())
|
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;
|
this.lastDivRef = div;
|
||||||
if (div) {
|
if (div) {
|
||||||
let width = div.offsetWidth;
|
let width = div.offsetWidth;
|
||||||
if (width === 0) return;
|
if (width === 0) return;
|
||||||
let columns = Math.floor((width-40)/280);
|
let columns = 1;
|
||||||
if(columns === 0)
|
let columnWidth = (width - 40) / columns;
|
||||||
{
|
if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) {
|
||||||
columns = 1;
|
this.setState({ columns: columns, columnWidth: columnWidth });
|
||||||
}
|
|
||||||
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 {
|
onWindowSizeChanged(width: number, height: number): void {
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
fullScreen: height < 200
|
fullScreen: height < 200
|
||||||
})
|
})
|
||||||
if (this.lastDivRef !== null)
|
if (this.lastDivRef !== null) {
|
||||||
{
|
|
||||||
this.onMeasureRef(this.lastDivRef);
|
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 {
|
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, snapshot?: any): void {
|
||||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||||
if (prevProps.open !== this.props.open)
|
if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) {
|
||||||
{
|
if (this.props.open) {
|
||||||
if (this.props.open)
|
let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty);
|
||||||
{
|
this.setState({
|
||||||
this.requestFiles()
|
selectedFile: this.props.selectedFile,
|
||||||
|
newFolderDialogOpen: false,
|
||||||
|
renameDialogOpen: false,
|
||||||
|
moveDialogOpen: false
|
||||||
|
});
|
||||||
|
this.requestFiles(navDirectory)
|
||||||
this.requestScroll = true;
|
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 {
|
private isDirectory(path: string): boolean {
|
||||||
let slashPos = path.lastIndexOf('/');
|
for (var fileEntry of this.state.fileEntries) {
|
||||||
if (slashPos < 0) {
|
if (fileEntry.filename === path) {
|
||||||
slashPos = 0;
|
return fileEntry.isDirectory;
|
||||||
} else {
|
}
|
||||||
++slashPos;
|
|
||||||
}
|
}
|
||||||
let extPos = path.lastIndexOf('.');
|
return false;
|
||||||
if (extPos < 0 || extPos < slashPos) {
|
|
||||||
extPos = path.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.substring(slashPos, extPos);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private isFileInList(files: FileEntry[], file: string) {
|
||||||
private isFileInList(files: string[], file: string) {
|
|
||||||
|
|
||||||
let hasSelection = false;
|
let hasSelection = false;
|
||||||
if (file === "") return true;
|
if (file === "") return true;
|
||||||
for (var listFile of files) {
|
for (var listFile of files) {
|
||||||
if (listFile === file) {
|
if (listFile.filename === file) {
|
||||||
hasSelection = true;
|
hasSelection = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -214,113 +304,262 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
|
|
||||||
onSelectValue(selectedFile: string) {
|
onSelectValue(selectedFile: string) {
|
||||||
this.requestScroll = true;
|
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) {
|
onDoubleClickValue(selectedFile: string) {
|
||||||
this.props.onOk(this.props.fileProperty,selectedFile);
|
this.openSelectedFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDelete() {
|
handleMenuOpen(event: React.MouseEvent<HTMLElement>) {
|
||||||
this.setState({openConfirmDeleteDialog: true});
|
this.setState({ menuAnchorEl: event.currentTarget });
|
||||||
|
|
||||||
}
|
}
|
||||||
handleConfirmDelete()
|
handleMenuClose() {
|
||||||
{
|
this.setState({ menuAnchorEl: null });
|
||||||
this.setState({openConfirmDeleteDialog: false});
|
}
|
||||||
if (this.state.hasSelection)
|
handleDelete() {
|
||||||
{
|
this.setState({ openConfirmDeleteDialog: true });
|
||||||
|
}
|
||||||
|
handleConfirmDelete() {
|
||||||
|
this.setState({ openConfirmDeleteDialog: false });
|
||||||
|
if (this.state.hasSelection) {
|
||||||
let selectedFile = this.state.selectedFile;
|
let selectedFile = this.state.selectedFile;
|
||||||
let position = -1;
|
let position = -1;
|
||||||
for (let i = 0; i < this.state.files.length;++i)
|
for (let i = 0; i < this.state.fileEntries.length; ++i) {
|
||||||
{
|
let file = this.state.fileEntries[i];
|
||||||
let file = this.state.files[i];
|
if (file.filename === selectedFile) {
|
||||||
if (file === selectedFile)
|
|
||||||
{
|
|
||||||
position = i;
|
position = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let newSelection = "";
|
let newSelection = "";
|
||||||
if (position >= 0 && position < this.state.files.length-1)
|
if (position >= 0 && position < this.state.fileEntries.length - 1) {
|
||||||
{
|
newSelection = this.state.fileEntries[position + 1].filename;
|
||||||
newSelection = this.state.files[position+1];
|
} else if (position === this.state.fileEntries.length - 1) {
|
||||||
} else if (position === this.state.files.length-1)
|
|
||||||
{
|
|
||||||
if (position !== 0) {
|
if (position !== 0) {
|
||||||
newSelection = this.state.files[position-1];
|
newSelection = this.state.fileEntries[position - 1].filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model.deleteUserFile(this.state.selectedFile)
|
this.model.deleteUserFile(this.state.selectedFile)
|
||||||
.then(
|
.then(
|
||||||
()=> {
|
() => {
|
||||||
this.setState({selectedFile: newSelection,hasSelection: newSelection !== ""});
|
this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" });
|
||||||
this.requestFiles();
|
this.requestFiles(this.state.navDirectory);
|
||||||
}
|
}
|
||||||
).catch(
|
).catch(
|
||||||
(e: any)=>
|
(e: any) => {
|
||||||
{
|
this.model.showAlert(e + "");
|
||||||
this.model.showAlert(e + "");
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private wantsScrollRef: boolean = true;
|
private wantsScrollRef: boolean = true;
|
||||||
|
|
||||||
mapKey: number = 0;
|
navigate(relativeDirectory: string) {
|
||||||
render() {
|
this.requestFiles(relativeDirectory);
|
||||||
this.mapKey = 0;
|
this.setState({ navDirectory: relativeDirectory });
|
||||||
let columnWidth = this.state.columnWidth;
|
|
||||||
|
|
||||||
|
}
|
||||||
|
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 &&
|
return this.props.open &&
|
||||||
(
|
(
|
||||||
<DialogEx onClose={() => this.props.onCancel()} open={this.props.open} tag="fileProperty" fullWidth maxWidth="xl" style={{ height: "90%" }}
|
<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" } }}
|
PaperProps={{ style: { minHeight: "90%", maxHeight: "90%", overflowY: "visible" } }}
|
||||||
>
|
>
|
||||||
<DialogTitle >
|
<DialogTitle >
|
||||||
<div>
|
<Toolbar style={{ padding: 0 }}>
|
||||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onCancel(); }} aria-label="back"
|
<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>
|
</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>
|
</div>
|
||||||
</DialogTitle>
|
</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 style={{ flex: "1 1 auto", height: "300", display: "flex", flexFlow: "row wrap", overflowX: "hidden", overflowY: "auto" }}>
|
||||||
<div
|
<div
|
||||||
ref={(element)=> this.onMeasureRef(element)}
|
ref={(element) => this.onMeasureRef(element)}
|
||||||
style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
|
style={{
|
||||||
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingTop: 16,paddingBottom: 16 }}>
|
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.columns !== 0) && // don't render until we have number of columns derived from layout.
|
||||||
this.state.files.map(
|
this.state.fileEntries.map(
|
||||||
(value: string, index: number) => {
|
(value: FileEntry, index: number) => {
|
||||||
let displayValue = value;
|
let isDefault = this.props.fileProperty.directory.startsWith("default");
|
||||||
if (displayValue === "")
|
let displayValue = value.filename;
|
||||||
{
|
if (displayValue === "") {
|
||||||
displayValue = "<none>";
|
displayValue = "<none>";
|
||||||
} else {
|
} 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)";
|
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;
|
let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined;
|
||||||
if (selected)
|
if (selected) {
|
||||||
{
|
scrollRef = (element) => { this.onScrollRef(element) };
|
||||||
scrollRef = (element)=> { this.onScrollRef(element)};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonBase key={this.mapKey++}
|
<ButtonBase key={value.filename}
|
||||||
|
|
||||||
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
|
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 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%" }}>
|
<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 }} />
|
{value.filename === "" ?
|
||||||
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}>{displayValue}</Typography>
|
(<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>
|
</div>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
);
|
);
|
||||||
@@ -333,25 +572,26 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||||
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
<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"
|
<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' />
|
<OldDeleteIcon fontSize='small' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
<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>
|
<div>Upload</div>
|
||||||
</Button>
|
</Button>
|
||||||
<div style={{ flex: "1 1 auto" }}> </div>
|
<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
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button style={{ flex: "0 0 auto" }}
|
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||||
onClick={() => { this.props.onOk(this.props.fileProperty, this.state.selectedFile); }}
|
onClick={() => { this.openSelectedFile(); }}
|
||||||
color="secondary"
|
|
||||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,22 +604,160 @@ export default class FilePropertyDialog extends ResizeResponsiveComponent<FilePr
|
|||||||
this.setState({ openUploadFileDialog: false });
|
this.setState({ openUploadFileDialog: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uploadPage={ "uploadUserFile?directory="+ encodeURIComponent(this.props.fileProperty.directory)}
|
uploadPage={
|
||||||
onUploaded={()=> { this.requestFiles();}}
|
"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}
|
fileProperty={this.props.fileProperty}
|
||||||
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
<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"
|
okButtonText="Delete"
|
||||||
onOk={()=>this.handleConfirmDelete()}
|
onOk={() => this.handleConfirmDelete()}
|
||||||
onClose={()=>{
|
onClose={() => {
|
||||||
this.setState({openConfirmDeleteDialog: false});
|
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>
|
</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>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => this.handleApply()} color="primary" disabled={
|
<Button variant="dialogPrimary" onClick={() => this.handleApply()} disabled={
|
||||||
(!this.state.alsaDevices) || !this.state.jackServerSettings.valid}>
|
(!this.state.alsaDevices) || !this.state.jackServerSettings.valid}>
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default class ListSelectDialog extends ResizeResponsiveComponent<ListSele
|
|||||||
}
|
}
|
||||||
</List>
|
</List>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={()=> this.props.onClose()} color="primary">
|
<Button variant="dialogSecondary" onClick={()=> this.props.onClose()} color="primary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -814,8 +814,8 @@ export const LoadPluginDialog =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: "relative", float: "right", flex: "0 1 200px", width: 200, display: "flex", alignItems: "center" }}>
|
<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 variant="dialogSecondary" onClick={this.handleCancel} style={{ width: 120 }} >Cancel</Button>
|
||||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ width: 180 }} >SELECT</Button>
|
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ width: 180 }} >SELECT</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -839,8 +839,8 @@ export const LoadPluginDialog =
|
|||||||
<div className={classes.bottom} style={{height: 64}}>
|
<div className={classes.bottom} style={{height: 64}}>
|
||||||
<div style={{ flex: "1 1 1px" }} />
|
<div style={{ flex: "1 1 1px" }} />
|
||||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||||
<Button onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
||||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export class UiFileProperty {
|
|||||||
this.directory = input.directory;
|
this.directory = input.directory;
|
||||||
this.index = input.index;
|
this.index = input.index;
|
||||||
this.portGroup = input.portGroup;
|
this.portGroup = input.portGroup;
|
||||||
|
this.resourceDirectory = input.resourceDirectory??"";
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
static deserialize_array(input: any): UiFileProperty[]
|
static deserialize_array(input: any): UiFileProperty[]
|
||||||
@@ -303,6 +304,7 @@ export class UiFileProperty {
|
|||||||
directory: string = "";
|
directory: string = "";
|
||||||
index: number = -1;
|
index: number = -1;
|
||||||
portGroup: string = "";
|
portGroup: string = "";
|
||||||
|
resourceDirectory: string = "";
|
||||||
|
|
||||||
};
|
};
|
||||||
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ export default class OkCancelDialog extends React.Component<OkCancelDialogProps,
|
|||||||
>{text}</Typography>
|
>{text}</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="secondary" >
|
<Button variant="dialogPrimary" onClick={handleOk} >
|
||||||
{okButtonText}
|
{okButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
+107
-46
@@ -39,6 +39,7 @@ import WifiChannel from './WifiChannel';
|
|||||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||||
import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode';
|
import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode';
|
||||||
|
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||||
|
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
@@ -52,6 +53,10 @@ export enum State {
|
|||||||
|
|
||||||
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||||
|
|
||||||
|
export interface FileEntry {
|
||||||
|
filename: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
|
export type PluginPresetsChangedHandler = (pluginUri: string) => void;
|
||||||
|
|
||||||
@@ -337,7 +342,7 @@ interface Vst3ControlChangedBody {
|
|||||||
|
|
||||||
export interface FavoritesList {
|
export interface FavoritesList {
|
||||||
[url: string]: boolean;
|
[url: string]: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export class PiPedalModel //implements PiPedalModel
|
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 {
|
getWebSocket(): PiPedalSocket {
|
||||||
if (this.webSocket === undefined) {
|
if (this.webSocket === undefined) {
|
||||||
throw new PiPedalStateError("Attempt to access web socket before it's connected.");
|
throw new PiPedalStateError("Attempt to access web socket before it's connected.");
|
||||||
@@ -653,6 +639,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSocketConnectionLost() {
|
onSocketConnectionLost() {
|
||||||
|
// remove all the events and subscriptions we have.
|
||||||
|
// yyy
|
||||||
|
this.vuSubscriptions = [];
|
||||||
|
this.monitorPatchPropertyListeners = [];
|
||||||
|
|
||||||
if (this.isAndroidHosted()) {
|
if (this.isAndroidHosted()) {
|
||||||
this.androidHost?.setDisconnected(true);
|
this.androidHost?.setDisconnected(true);
|
||||||
}
|
}
|
||||||
@@ -756,8 +747,8 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maxFileUploadSize: number = 100000000;
|
maxFileUploadSize: number = 512*1024*1024;
|
||||||
maxPresetUploadSize: number = 512 * 1024;
|
maxPresetUploadSize: number = 1024 * 1024;
|
||||||
debug: boolean = false;
|
debug: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
@@ -777,7 +768,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.androidHost = new FakeAndroidHost();
|
this.androidHost = new FakeAndroidHost();
|
||||||
}
|
}
|
||||||
this.debug = !!data.debug;
|
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 === "*") {
|
if ((!socket_server_address) || socket_server_address === "*") {
|
||||||
socket_server_address = window.location.hostname;
|
socket_server_address = window.location.hostname;
|
||||||
}
|
}
|
||||||
@@ -788,6 +779,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
this.socketServerUrl = socket_server;
|
this.socketServerUrl = socket_server;
|
||||||
this.varServerUrl = var_server_url;
|
this.varServerUrl = var_server_url;
|
||||||
|
this.maxFileUploadSize = parseInt(max_upload_size);
|
||||||
|
|
||||||
this.webSocket = new PiPedalSocket(
|
this.webSocket = new PiPedalSocket(
|
||||||
this.socketServerUrl,
|
this.socketServerUrl,
|
||||||
@@ -918,6 +910,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
// MUST not allow reconnect until at least one complete load has finished.
|
// MUST not allow reconnect until at least one complete load has finished.
|
||||||
this.webSocket.canReconnect = true;
|
this.webSocket.canReconnect = true;
|
||||||
}
|
}
|
||||||
|
this.setState(State.Ready);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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 {
|
onError(msg: string): void {
|
||||||
this.errorMessage.set(msg);
|
this.errorMessage.set(msg);
|
||||||
@@ -1630,14 +1609,21 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated.
|
||||||
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
||||||
return nullCast(this.webSocket)
|
return nullCast(this.webSocket)
|
||||||
.request<string[]>('requestFileList', piPedalFileProperty);
|
.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>
|
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) {
|
for (let i = 0; i < this.monitorPatchPropertyListeners.length; ++i) {
|
||||||
if (this.monitorPatchPropertyListeners[i].handle === listenHandle._handle) {
|
if (this.monitorPatchPropertyListeners[i].handle === listenHandle._handle) {
|
||||||
this.monitorPatchPropertyListeners.splice(i, 1);
|
this.monitorPatchPropertyListeners.splice(i, 1);
|
||||||
|
this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.webSocket?.send("cancelMonitorPatchProperty", listenHandle._handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2247,7 +2233,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
if (!ws) return;
|
if (!ws)
|
||||||
|
{
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
ws.request<void>(
|
ws.request<void>(
|
||||||
"setGovernorSettings",
|
"setGovernorSettings",
|
||||||
governor
|
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> {
|
setWifiConfigSettings(wifiConfigSettings: WifiConfigSettings): Promise<void> {
|
||||||
let result = new Promise<void>((resolve, reject) => {
|
let result = new Promise<void>((resolve, reject) => {
|
||||||
let oldSettings = this.wifiConfigSettings.get();
|
let oldSettings = this.wifiConfigSettings.get();
|
||||||
@@ -2362,7 +2420,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
//resolve();
|
//resolve();
|
||||||
});
|
});
|
||||||
// yyy
|
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -2555,8 +2612,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
if (this.getTheme() !== value)
|
if (this.getTheme() !== value)
|
||||||
{
|
{
|
||||||
setColorScheme(value);
|
setColorScheme(value);
|
||||||
this.reloadPage();
|
setTimeout(()=>{
|
||||||
}
|
this.reloadPage();
|
||||||
|
},
|
||||||
|
200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadRequested: boolean = false;
|
reloadRequested: boolean = false;
|
||||||
@@ -2564,10 +2624,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
reloadPage() {
|
reloadPage() {
|
||||||
this.reloadRequested = true;
|
this.reloadRequested = true;
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// 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;
|
let instance: PiPedalModel | undefined = undefined;
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ const PluginControlView =
|
|||||||
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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 });
|
this.setState({ showFileDialog: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</PluginInfoDialogContent>
|
</PluginInfoDialogContent>
|
||||||
<PluginInfoDialogActions>
|
<PluginInfoDialogActions>
|
||||||
<Button autoFocus onClick={handleClose} color="primary" style={{ width: "130px" }}>
|
<Button variant="dialogPrimary" autoFocus onClick={handleClose} style={{ width: "130px" }}>
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</PluginInfoDialogActions>
|
</PluginInfoDialogActions>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import DialogActions from '@mui/material/DialogActions';
|
|||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import { nullCast } from './Utility';
|
import { nullCast } from './Utility';
|
||||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||||
|
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
|
||||||
|
|
||||||
|
|
||||||
export interface RenameDialogProps {
|
export interface RenameDialogProps {
|
||||||
@@ -74,6 +75,12 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
|||||||
componentDidUpdate()
|
componentDidUpdate()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
checkForIllegalCharacters(filename: string) {
|
||||||
|
if (filename.indexOf('/') !== -1)
|
||||||
|
{
|
||||||
|
throw new Error("Illegal character: '/'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
@@ -86,6 +93,14 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
|||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
let text = nullCast(this.refText.current).value;
|
let text = nullCast(this.refText.current).value;
|
||||||
text = text.trim();
|
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;
|
if (text.length === 0) return;
|
||||||
onOk(text);
|
onOk(text);
|
||||||
}
|
}
|
||||||
@@ -116,10 +131,10 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="secondary" >
|
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||||
{acceptActionName}
|
{acceptActionName}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -107,10 +107,10 @@ function SelectChannelsDialog(props: SelectChannelsDialogProps) {
|
|||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</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
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -110,10 +110,10 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
|
|||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="primary" >
|
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ function SelectThemesDialog(props: SelectThemesDialogProps) {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary">
|
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk} color="primary" >
|
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -885,7 +885,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
onClose={()=> { this.setState({showThemeSelectDialog: false});} }
|
onClose={()=> { this.setState({showThemeSelectDialog: false});} }
|
||||||
onOk={(selectedTheme: ColorTheme) => {
|
onOk={(selectedTheme: ColorTheme) => {
|
||||||
this.model.setTheme(selectedTheme);
|
this.model.setTheme(selectedTheme);
|
||||||
this.setState({showThemeSelectDialog: false});
|
this.setState({showThemeSelectDialog: false});
|
||||||
}}
|
}}
|
||||||
defaultTheme={this.model.getTheme()}
|
defaultTheme={this.model.getTheme()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -208,7 +208,11 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
|||||||
{
|
{
|
||||||
throw new Error("Invalid file extension.");
|
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);
|
this.props.onUploaded(filename);
|
||||||
upload.status = FileUploadStatus.Uploaded;
|
upload.status = FileUploadStatus.Uploaded;
|
||||||
upload.statusMessage = "Uploaded.";
|
upload.statusMessage = "Uploaded.";
|
||||||
@@ -384,7 +388,8 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
|||||||
|
|
||||||
<DialogActions >
|
<DialogActions >
|
||||||
<Button
|
<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
|
Select Files
|
||||||
<input
|
<input
|
||||||
@@ -399,12 +404,12 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
|
|||||||
{
|
{
|
||||||
this.state.okEnabled ?
|
this.state.okEnabled ?
|
||||||
(
|
(
|
||||||
<Button onClick={() => this.handleClose()} color="primary">
|
<Button onClick={() => this.handleClose()} variant="dialogPrimary" >
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={() => this.handleCancel()} color="primary">
|
<Button onClick={() => this.handleCancel()} variant="dialogPrimary" >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -182,11 +182,11 @@ export default class UploadPresetDialog extends ResizeResponsiveComponent<Upload
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => this.handleClose()} color="primary">
|
<Button onClick={() => this.handleClose()} variant="dialogSecondary" >
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
component="label" color="secondary" style={{width: 120}}
|
component="label" variant="dialogPrimary" style={{width: 120}}
|
||||||
>
|
>
|
||||||
Select File
|
Select File
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -337,10 +337,10 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary" style={{ width: 120 }}>
|
<Button onClick={handleClose} variant="dialogSecondary" style={{ width: 120 }}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={()=> this.handleOk(false)} color="success" style={{ width: 120 }} >
|
<Button onClick={()=> this.handleOk(false)} variant="dialogPrimary" style={{ width: 120 }} >
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -356,13 +356,13 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })(
|
|||||||
</Typography>
|
</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={()=> this.setState({showWifiWarningDialog: false})} color="primary" style={{ width: 120 }}>
|
<Button onClick={()=> this.setState({showWifiWarningDialog: false})} variant="dialogSecondary" style={{ width: 120 }}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={()=> {
|
<Button onClick={()=> {
|
||||||
this.setState({showWifiWarningDialog: false});
|
this.setState({showWifiWarningDialog: false});
|
||||||
this.handleOk(true);
|
this.handleOk(true);
|
||||||
}} color="secondary" style={{ width: 120 }} >
|
}} variant="dialogPrimary" style={{ width: 120 }} >
|
||||||
PROCEED
|
PROCEED
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -506,10 +506,10 @@ const WifiDirectConfigDialog = withStyles(styles, { withTheme: true })(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose} color="primary" style={{ width: 120 }}>
|
<Button onClick={handleClose} variant="dialogSecondary" style={{ width: 120 }}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => this.handleOk()} color="primary" style={{ width: 120 }} >
|
<Button onClick={() => this.handleOk()} variant="dialogPrimary" style={{ width: 120 }} >
|
||||||
OK
|
OK
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
+8
-1
@@ -62,6 +62,7 @@ else()
|
|||||||
message(STATUS "LILV_0_INCLUDE_DIRS: ${LILV_0_INCLUDE_DIRS}")
|
message(STATUS "LILV_0_INCLUDE_DIRS: ${LILV_0_INCLUDE_DIRS}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
pkg_check_modules(ZIP "lilv-0")
|
||||||
|
|
||||||
# Use LV2 headers from the /usr/lib directory.
|
# Use LV2 headers from the /usr/lib directory.
|
||||||
#set (LV2DEV_INCLUDE_DIRS /usr/lib)
|
#set (LV2DEV_INCLUDE_DIRS /usr/lib)
|
||||||
@@ -131,6 +132,12 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PIPEDAL_SOURCES
|
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
|
atom_object.hpp atom_object.cpp
|
||||||
FileBrowserFiles.h
|
FileBrowserFiles.h
|
||||||
FileBrowserFilesFeature.hpp FileBrowserFilesFeature.cpp
|
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
|
pthread atomic stdc++fs asound avahi-common avahi-client systemd
|
||||||
${VST3_LIBRARIES}
|
${VST3_LIBRARIES}
|
||||||
${LILV_0_LIBRARIES}
|
${LILV_0_LIBRARIES}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ typedef struct {
|
|||||||
get_upload_path returns the supplied path unmodified.
|
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
|
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()`.
|
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.SetConfigRoot(configuration.GetDocRoot());
|
||||||
storage.SetDataRoot(configuration.GetLocalStoragePath());
|
storage.SetDataRoot(configuration.GetLocalStoragePath());
|
||||||
storage.Initialize();
|
storage.Initialize();
|
||||||
lv2Host.SetPluginStoragePath(storage.GetPluginAudioFileDirectory());
|
lv2Host.SetPluginStoragePath(storage.GetPluginUploadDirectory());
|
||||||
|
|
||||||
this->systemMidiBindings = storage.GetSystemMidiBindings();
|
this->systemMidiBindings = storage.GetSystemMidiBindings();
|
||||||
|
|
||||||
@@ -1151,6 +1151,7 @@ void PiPedalModel::RestartAudio()
|
|||||||
|
|
||||||
// do a complete reload.
|
// do a complete reload.
|
||||||
|
|
||||||
|
|
||||||
this->audioHost->SetPedalboard(nullptr);
|
this->audioHost->SetPedalboard(nullptr);
|
||||||
|
|
||||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
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.
|
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)
|
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
||||||
{
|
{
|
||||||
@@ -2046,9 +2068,23 @@ void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
|
|||||||
storage.DeleteSampleFile(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()
|
uint64_t PiPedalModel::CreateNewPreset()
|
||||||
@@ -2058,6 +2094,17 @@ uint64_t PiPedalModel::CreateNewPreset()
|
|||||||
return storage.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()
|
bool PiPedalModel::LoadCurrentPedalboard()
|
||||||
{
|
{
|
||||||
Lv2PedalboardErrorList errorMessages;
|
Lv2PedalboardErrorList errorMessages;
|
||||||
@@ -2066,6 +2113,7 @@ bool PiPedalModel::LoadCurrentPedalboard()
|
|||||||
|
|
||||||
// apply the error messages to the lv2Pedalboard.
|
// apply the error messages to the lv2Pedalboard.
|
||||||
// return true if the error messages have changed
|
// return true if the error messages have changed
|
||||||
|
CheckForResourceInitialization(this->pedalboard);
|
||||||
audioHost->SetPedalboard(lv2Pedalboard);
|
audioHost->SetPedalboard(lv2Pedalboard);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2087,3 +2135,7 @@ void PiPedalModel::OnNotifyLv2RealtimeError(int64_t instanceId, const std::strin
|
|||||||
}
|
}
|
||||||
delete[] t;
|
delete[] t;
|
||||||
}
|
}
|
||||||
|
std::filesystem::path PiPedalModel::GetPluginUploadDirectory() const
|
||||||
|
{
|
||||||
|
return storage.GetPluginUploadDirectory();
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include "Promise.hpp"
|
#include "Promise.hpp"
|
||||||
#include "AtomConverter.hpp"
|
#include "AtomConverter.hpp"
|
||||||
|
#include "FileEntry.hpp"
|
||||||
|
|
||||||
namespace pipedal
|
namespace pipedal
|
||||||
{
|
{
|
||||||
@@ -189,12 +190,13 @@ namespace pipedal
|
|||||||
|
|
||||||
PiPedalConfiguration configuration;
|
PiPedalConfiguration configuration;
|
||||||
|
|
||||||
|
void CheckForResourceInitialization(Pedalboard &pedalboard);
|
||||||
public:
|
public:
|
||||||
PiPedalModel();
|
PiPedalModel();
|
||||||
virtual ~PiPedalModel();
|
virtual ~PiPedalModel();
|
||||||
|
|
||||||
uint16_t GetWebPort() const { return webPort; }
|
uint16_t GetWebPort() const { return webPort; }
|
||||||
|
std::filesystem::path GetPluginUploadDirectory() const;
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
void SetOnboarding(bool value);
|
void SetOnboarding(bool value);
|
||||||
@@ -328,9 +330,14 @@ namespace pipedal
|
|||||||
void SetFavorites(const std::map<std::string, bool> &favorites);
|
void SetFavorites(const std::map<std::string, bool> &favorites);
|
||||||
|
|
||||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
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);
|
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();
|
uint64_t CreateNewPreset();
|
||||||
|
|
||||||
bool LoadCurrentPedalboard();
|
bool LoadCurrentPedalboard();
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
#include "SysExec.hpp"
|
#include "SysExec.hpp"
|
||||||
#include "PiPedalAlsa.hpp"
|
#include "PiPedalAlsa.hpp"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include "FileEntry.hpp"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
@@ -58,6 +59,33 @@ JSON_MAP_REFERENCE(GetPatchPropertyBody, propertyUri)
|
|||||||
JSON_MAP_END()
|
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 {
|
class Lv2StateChangedBody {
|
||||||
public:
|
public:
|
||||||
uint64_t instanceId_;
|
uint64_t instanceId_;
|
||||||
@@ -198,6 +226,21 @@ JSON_MAP_REFERENCE(MonitorResultBody, subscriptionHandle)
|
|||||||
JSON_MAP_REFERENCE(MonitorResultBody, value)
|
JSON_MAP_REFERENCE(MonitorResultBody, value)
|
||||||
JSON_MAP_END()
|
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
|
class MonitorPortBody
|
||||||
{
|
{
|
||||||
@@ -1417,6 +1460,13 @@ public:
|
|||||||
std::vector<std::string> list = this->model.GetFileList(fileProperty);
|
std::vector<std::string> list = this->model.GetFileList(fileProperty);
|
||||||
this->Reply(replyTo,"requestFileList",list);
|
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")
|
else if (message == "newPreset")
|
||||||
{
|
{
|
||||||
int64_t presetId = this->model.CreateNewPreset();
|
int64_t presetId = this->model.CreateNewPreset();
|
||||||
@@ -1430,6 +1480,29 @@ public:
|
|||||||
this->model.DeleteSampleFile(fileName);
|
this->model.DeleteSampleFile(fileName);
|
||||||
this->Reply(replyTo,"deleteUserFile",true);
|
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")
|
else if (message == "setOnboarding")
|
||||||
{
|
{
|
||||||
bool value;
|
bool value;
|
||||||
|
|||||||
+10
-1
@@ -201,7 +201,15 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st
|
|||||||
{
|
{
|
||||||
this->portGroup_ = portGroup.AsUri();
|
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);
|
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, patchProperty)
|
||||||
JSON_MAP_REFERENCE(UiFileProperty, fileTypes)
|
JSON_MAP_REFERENCE(UiFileProperty, fileTypes)
|
||||||
JSON_MAP_REFERENCE(UiFileProperty, portGroup)
|
JSON_MAP_REFERENCE(UiFileProperty, portGroup)
|
||||||
|
JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory)
|
||||||
JSON_MAP_END()
|
JSON_MAP_END()
|
||||||
|
|
||||||
JSON_MAP_BEGIN(UiFrequencyPlot)
|
JSON_MAP_BEGIN(UiFrequencyPlot)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#define PIPEDAL_UI__patchProperty PIPEDAL_UI_PREFIX "patchProperty"
|
#define PIPEDAL_UI__patchProperty PIPEDAL_UI_PREFIX "patchProperty"
|
||||||
#define PIPEDAL_UI__directory PIPEDAL_UI_PREFIX "directory"
|
#define PIPEDAL_UI__directory PIPEDAL_UI_PREFIX "directory"
|
||||||
#define PIPEDAL_UI__fileTypes PIPEDAL_UI_PREFIX "fileTypes"
|
#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__fileType PIPEDAL_UI_PREFIX "fileType"
|
||||||
#define PIPEDAL_UI__fileExtension PIPEDAL_UI_PREFIX "fileExtension"
|
#define PIPEDAL_UI__fileExtension PIPEDAL_UI_PREFIX "fileExtension"
|
||||||
@@ -112,6 +113,7 @@ namespace pipedal {
|
|||||||
std::vector<UiFileType> fileTypes_;
|
std::vector<UiFileType> fileTypes_;
|
||||||
std::string patchProperty_;
|
std::string patchProperty_;
|
||||||
std::string portGroup_;
|
std::string portGroup_;
|
||||||
|
std::string resourceDirectory_;
|
||||||
public:
|
public:
|
||||||
using ptr = std::shared_ptr<UiFileProperty>;
|
using ptr = std::shared_ptr<UiFileProperty>;
|
||||||
UiFileProperty() { }
|
UiFileProperty() { }
|
||||||
@@ -131,6 +133,8 @@ namespace pipedal {
|
|||||||
bool IsValidExtension(const std::string&extension) const;
|
bool IsValidExtension(const std::string&extension) const;
|
||||||
static bool IsDirectoryNameValid(const std::string&value);
|
static bool IsDirectoryNameValid(const std::string&value);
|
||||||
|
|
||||||
|
const std::string&resourceDirectory() const { return resourceDirectory_; }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
DECLARE_JSON_MAP(UiFileProperty);
|
DECLARE_JSON_MAP(UiFileProperty);
|
||||||
};
|
};
|
||||||
|
|||||||
+123
-15
@@ -32,7 +32,7 @@
|
|||||||
#include "JackConfiguration.hpp"
|
#include "JackConfiguration.hpp"
|
||||||
#include "lv2/urid/urid.h"
|
#include "lv2/urid/urid.h"
|
||||||
#include "lv2/ui/ui.h"
|
#include "lv2/ui/ui.h"
|
||||||
//#include "lv2.h"
|
// #include "lv2.h"
|
||||||
#include "lv2/atom/atom.h"
|
#include "lv2/atom/atom.h"
|
||||||
#include "lv2/time/time.h"
|
#include "lv2/time/time.h"
|
||||||
#include "lv2/state/state.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__bufferType = lilv_new_uri(pWorld, LV2_ATOM__bufferType);
|
||||||
atom__Path = lilv_new_uri(pWorld, LV2_ATOM__Path);
|
atom__Path = lilv_new_uri(pWorld, LV2_ATOM__Path);
|
||||||
presets__preset = lilv_new_uri(pWorld, LV2_PRESETS__Preset);
|
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");
|
rdfs__label = lilv_new_uri(pWorld, LILV_NS_RDFS "label");
|
||||||
|
|
||||||
lv2core__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
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__patchProperty = lilv_new_uri(pWorld, PIPEDAL_UI__patchProperty);
|
||||||
pipedalUI__directory = lilv_new_uri(pWorld, PIPEDAL_UI__directory);
|
pipedalUI__directory = lilv_new_uri(pWorld, PIPEDAL_UI__directory);
|
||||||
pipedalUI__fileTypes = lilv_new_uri(pWorld, PIPEDAL_UI__fileTypes);
|
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__fileProperty = lilv_new_uri(pWorld, PIPEDAL_UI__fileProperty);
|
||||||
pipedalUI__fileExtension = lilv_new_uri(pWorld, PIPEDAL_UI__fileExtension);
|
pipedalUI__fileExtension = lilv_new_uri(pWorld, PIPEDAL_UI__fileExtension);
|
||||||
pipedalUI__mimeType = lilv_new_uri(pWorld, PIPEDAL_UI__mimeType);
|
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__symbol = lilv_new_uri(pWorld, LV2_CORE__symbol);
|
||||||
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
|
lv2__port = lilv_new_uri(pWorld, LV2_CORE__port);
|
||||||
|
|
||||||
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
#define MOD_PREFIX "http://moddevices.com/ns/mod#"
|
||||||
mod__label = lilv_new_uri(pWorld,MOD_PREFIX "label");
|
mod__label = lilv_new_uri(pWorld, MOD_PREFIX "label");
|
||||||
mod__brand = lilv_new_uri(pWorld,MOD_PREFIX "brand");
|
mod__brand = lilv_new_uri(pWorld, MOD_PREFIX "brand");
|
||||||
// ui:portNotification
|
// ui:portNotification
|
||||||
// [
|
// [
|
||||||
// ui:portIndex 3;
|
// ui:portIndex 3;
|
||||||
@@ -660,16 +661,15 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
|||||||
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
|
AutoLilvNode name = (lilv_plugin_get_name(pPlugin));
|
||||||
this->name_ = nodeAsString(name);
|
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);
|
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);
|
this->label_ = nodeAsString(label);
|
||||||
if (label_.length() == 0)
|
if (label_.length() == 0)
|
||||||
{
|
{
|
||||||
this->label_ = this->name_;
|
this->label_ = this->name_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AutoLilvNode author_name = (lilv_plugin_get_author_name(pPlugin));
|
AutoLilvNode author_name = (lilv_plugin_get_author_name(pPlugin));
|
||||||
this->author_name_ = nodeAsString(author_name);
|
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)
|
if (type == pHost->urids->atom__Double)
|
||||||
{
|
{
|
||||||
(*pState->values)[symbol] = (float)(*static_cast<const double *>(value));
|
(*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);
|
(*pState->values)[symbol] = *static_cast<const float *>(value);
|
||||||
}
|
}
|
||||||
else if (type == pHost->urids->atom_Int)
|
else if (type == pHost->urids->atom_Int)
|
||||||
{
|
{
|
||||||
(*pState->values)[symbol] = *static_cast<const int32_t *>(value);
|
(*pState->values)[symbol] = *static_cast<const int32_t *>(value);
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
pState->failed = true;
|
pState->failed = true;
|
||||||
}
|
}
|
||||||
@@ -1289,7 +1291,6 @@ PluginPresets PluginHost::GetFactoryPluginPresets(const std::string &pluginUri)
|
|||||||
PresetCallbackState cbData{this, &controlValues, false};
|
PresetCallbackState cbData{this, &controlValues, false};
|
||||||
lilv_state_emit_port_values(state, PortValueCallback, (void *)&cbData);
|
lilv_state_emit_port_values(state, PortValueCallback, (void *)&cbData);
|
||||||
|
|
||||||
|
|
||||||
int numProperties = lilv_state_get_num_properties(state);
|
int numProperties = lilv_state_get_num_properties(state);
|
||||||
// can't handle std:state part of preset.
|
// can't handle std:state part of preset.
|
||||||
if (numProperties == 0)
|
if (numProperties == 0)
|
||||||
@@ -1298,11 +1299,12 @@ PluginPresets PluginHost::GetFactoryPluginPresets(const std::string &pluginUri)
|
|||||||
{
|
{
|
||||||
result.presets_.push_back(PluginPreset(result.nextInstanceId_++, strLabel, controlValues, Lv2PluginState()));
|
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)));
|
result.presets_.push_back(PluginPreset::MakeLilvPreset(result.nextInstanceId_++, strLabel, controlValues, lilv_node_as_uri(preset)));
|
||||||
}
|
}
|
||||||
lilv_state_free(state);
|
lilv_state_free(state);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1353,7 +1355,7 @@ Lv2PortGroup::Lv2PortGroup(PluginHost *lv2Host, const std::string &groupUri)
|
|||||||
name_ = nodeAsString(nameNode);
|
name_ = nodeAsString(nameNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Lv2PluginInfo::isSplit() const
|
bool Lv2PluginInfo::isSplit() const
|
||||||
{
|
{
|
||||||
return uri_ == SPLIT_PEDALBOARD_ITEM_URI;
|
return uri_ == SPLIT_PEDALBOARD_ITEM_URI;
|
||||||
}
|
}
|
||||||
@@ -1362,6 +1364,112 @@ std::shared_ptr<HostWorkerThread> PluginHost::GetHostWorkerThread()
|
|||||||
return pHostWorkerThread;
|
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)
|
// void PiPedalHostLogError(const std::string &error)
|
||||||
// {
|
// {
|
||||||
// Lv2Log::error("%s",error.c_str());
|
// Lv2Log::error("%s",error.c_str());
|
||||||
|
|||||||
+8
-1
@@ -30,6 +30,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "IHost.hpp"
|
#include "IHost.hpp"
|
||||||
|
#include <set>
|
||||||
|
|
||||||
//#include "lv2.h"
|
//#include "lv2.h"
|
||||||
#include "Units.hpp"
|
#include "Units.hpp"
|
||||||
@@ -685,7 +686,7 @@ namespace pipedal
|
|||||||
AutoLilvNode pipedalUI__patchProperty;
|
AutoLilvNode pipedalUI__patchProperty;
|
||||||
|
|
||||||
AutoLilvNode pipedalUI__fileProperty;
|
AutoLilvNode pipedalUI__fileProperty;
|
||||||
|
AutoLilvNode pipedalUI__resourceDirectory;
|
||||||
AutoLilvNode pipedalUI__fileTypes;
|
AutoLilvNode pipedalUI__fileTypes;
|
||||||
AutoLilvNode pipedalUI__fileExtension;
|
AutoLilvNode pipedalUI__fileExtension;
|
||||||
AutoLilvNode pipedalUI__mimeType;
|
AutoLilvNode pipedalUI__mimeType;
|
||||||
@@ -811,8 +812,14 @@ namespace pipedal
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
virtual MapFeature &GetMapFeature() { return this->mapFeature; }
|
virtual MapFeature &GetMapFeature() { return this->mapFeature; }
|
||||||
|
void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
std::set<std::string> pluginsThatHaveBeenCheckedForResources;
|
||||||
|
|
||||||
|
void CheckForResourceInitization(const std::string pluginUri);
|
||||||
|
|
||||||
virtual LV2_URID_Map *GetLv2UridMap()
|
virtual LV2_URID_Map *GetLv2UridMap()
|
||||||
{
|
{
|
||||||
return this->mapFeature.GetMap();
|
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
|
// 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
|
// 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";
|
#define USER_SETTINGS_FILENAME "userSettings.json";
|
||||||
|
|
||||||
Storage::Storage()
|
Storage::Storage()
|
||||||
|
: locale("en_US.UTF-8")
|
||||||
{
|
{
|
||||||
SetConfigRoot("~/var/Config");
|
SetConfigRoot("~/var/Config");
|
||||||
SetDataRoot("~/var/PiPedal");
|
SetDataRoot("~/var/PiPedal");
|
||||||
@@ -265,7 +266,7 @@ std::filesystem::path Storage::GetPluginPresetsDirectory() const
|
|||||||
{
|
{
|
||||||
return this->dataRoot / "plugin_presets";
|
return this->dataRoot / "plugin_presets";
|
||||||
}
|
}
|
||||||
std::filesystem::path Storage::GetPluginAudioFileDirectory() const
|
std::filesystem::path Storage::GetPluginUploadDirectory() const
|
||||||
{
|
{
|
||||||
return this->dataRoot / "audio_uploads";
|
return this->dataRoot / "audio_uploads";
|
||||||
}
|
}
|
||||||
@@ -1467,11 +1468,6 @@ std::vector<MidiBinding> Storage::GetSystemMidiBindings()
|
|||||||
return result;
|
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)
|
static bool containsDirectorySeparator(const std::string &value)
|
||||||
{
|
{
|
||||||
if (value.find("/") != std::string::npos)
|
if (value.find("/") != std::string::npos)
|
||||||
@@ -1483,19 +1479,12 @@ static bool containsDirectorySeparator(const std::string &value)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void ThrowPermissionDeniedError()
|
static void ThrowPermissionDeniedError()
|
||||||
{
|
{
|
||||||
throw std::logic_error("Permission denied.");
|
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)
|
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 has a user-accessible directory, push the entire file path.
|
||||||
if (fileProperty.directory().size() != 0)
|
if (fileProperty.directory().size() != 0)
|
||||||
{
|
{
|
||||||
std::filesystem::path audioFileDirectory = this->GetPluginAudioFileDirectory() / fileProperty.directory();
|
std::filesystem::path audioFileDirectory = this->GetPluginUploadDirectory() / fileProperty.directory();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory))
|
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
|
// 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;
|
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())
|
if (!fileName.is_absolute())
|
||||||
{
|
{
|
||||||
return false;
|
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();
|
std::filesystem::path parentDirectory = fileName.parent_path();
|
||||||
while (true)
|
while (true)
|
||||||
@@ -1557,9 +1639,6 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::string name = parentDirectory.filename().string();
|
std::string name = parentDirectory.filename().string();
|
||||||
if (name == ".." || name == ".")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (parentDirectory == audioFilePath)
|
if (parentDirectory == audioFilePath)
|
||||||
return true;
|
return true;
|
||||||
parentDirectory = parentDirectory.parent_path();
|
parentDirectory = parentDirectory.parent_path();
|
||||||
@@ -1571,7 +1650,7 @@ bool Storage::IsValidSampleFile(const std::filesystem::path &fileName)
|
|||||||
}
|
}
|
||||||
void Storage::DeleteSampleFile(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.");
|
throw std::logic_error("Permission denied.");
|
||||||
}
|
}
|
||||||
@@ -1581,7 +1660,16 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
|
|||||||
}
|
}
|
||||||
try
|
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 &)
|
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)
|
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};
|
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.");
|
throw std::logic_error(SS("Invalid upload path: " << result));
|
||||||
}
|
|
||||||
std::filesystem::path result = this->GetPluginAudioFileDirectory() / directory / filename;
|
|
||||||
if (!this->IsValidSampleFile(result))
|
|
||||||
{
|
|
||||||
throw std::logic_error("Permission denied.");
|
|
||||||
}
|
}
|
||||||
return 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;
|
std::filesystem::path path;
|
||||||
if (directory.length() != 0)
|
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 << "."));
|
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)
|
} catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
Lv2Log::error(SS("Upload failed. " << e.what()));
|
Lv2Log::error(SS("Upload failed. " << e.what()));
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return path.string();
|
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()
|
const PluginPresetIndex &Storage::GetPluginPresetIndex()
|
||||||
{
|
{
|
||||||
return pluginPresetIndex;
|
return pluginPresetIndex;
|
||||||
|
|||||||
+16
-4
@@ -28,7 +28,10 @@
|
|||||||
#include "JackServerSettings.hpp"
|
#include "JackServerSettings.hpp"
|
||||||
#include "WifiConfigSettings.hpp"
|
#include "WifiConfigSettings.hpp"
|
||||||
#include "WifiDirectConfigSettings.hpp"
|
#include "WifiDirectConfigSettings.hpp"
|
||||||
|
#include "FileEntry.hpp"
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <locale>
|
||||||
|
#include "FilePropertyDirectoryTree.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace pipedal {
|
namespace pipedal {
|
||||||
@@ -64,6 +67,7 @@ struct PluginPresetValues {
|
|||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
private:
|
private:
|
||||||
|
std::locale locale;
|
||||||
std::filesystem::path dataRoot;
|
std::filesystem::path dataRoot;
|
||||||
std::filesystem::path configRoot;
|
std::filesystem::path configRoot;
|
||||||
BankIndex bankIndex;
|
BankIndex bankIndex;
|
||||||
@@ -71,6 +75,7 @@ private:
|
|||||||
PluginPresetIndex pluginPresetIndex;
|
PluginPresetIndex pluginPresetIndex;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const;
|
||||||
|
|
||||||
void MaybeCopyDefaultPresets();
|
void MaybeCopyDefaultPresets();
|
||||||
static std::string SafeEncodeName(const std::string& name);
|
static std::string SafeEncodeName(const std::string& name);
|
||||||
@@ -107,9 +112,9 @@ public:
|
|||||||
void SetDataRoot(const std::filesystem::path& path);
|
void SetDataRoot(const std::filesystem::path& path);
|
||||||
void SetConfigRoot(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; }
|
const BankIndex & GetBanks() const { return bankIndex; }
|
||||||
|
|
||||||
@@ -149,6 +154,7 @@ public:
|
|||||||
int64_t DeleteBank(int64_t bankId);
|
int64_t DeleteBank(int64_t bankId);
|
||||||
|
|
||||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||||
|
std::vector<FileEntry> GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
|
||||||
|
|
||||||
|
|
||||||
void SetJackChannelSelection(const JackChannelSelection&channelSelection);
|
void SetJackChannelSelection(const JackChannelSelection&channelSelection);
|
||||||
@@ -176,7 +182,7 @@ private:
|
|||||||
void SavePluginPresetIndex();
|
void SavePluginPresetIndex();
|
||||||
|
|
||||||
std::filesystem::path GetPluginPresetPath(const std::string &pluginUri) const;
|
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);
|
std::filesystem::path MakeUserFilePath(const std::string &directory, const std::string&filename);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -212,7 +218,13 @@ public:
|
|||||||
void SetSystemMidiBindings(const std::vector<MidiBinding>&bindings);
|
void SetSystemMidiBindings(const std::vector<MidiBinding>&bindings);
|
||||||
std::vector<MidiBinding> GetSystemMidiBindings();
|
std::vector<MidiBinding> GetSystemMidiBindings();
|
||||||
void DeleteSampleFile(const std::filesystem::path &fileName);
|
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"
|
#include "pch.h"
|
||||||
|
|
||||||
@@ -28,29 +46,484 @@
|
|||||||
#include <websocketpp/server.hpp>
|
#include <websocketpp/server.hpp>
|
||||||
|
|
||||||
#include "WebServerLog.hpp"
|
#include "WebServerLog.hpp"
|
||||||
|
#include "TemporaryFile.hpp"
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
const bool ENABLE_KEEP_ALIVE = true;
|
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>
|
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;
|
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:
|
public:
|
||||||
typedef CustomPpConfig type;
|
typedef CustomPpConfig type;
|
||||||
typedef websocketpp::config::asio base;
|
typedef websocketpp::config::asio base;
|
||||||
|
|
||||||
|
static size_t max_http_body_size; // websocketpp::config::asio::max_http_body_size;
|
||||||
static const size_t max_http_body_size = 100000000; //websocketpp::config::asio::max_http_body_size;
|
|
||||||
typedef pipedal_elog elog_type;
|
typedef pipedal_elog elog_type;
|
||||||
typedef pipedal_alog alog_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::concurrency_type concurrency_type;
|
||||||
typedef type::alog_type alog_type;
|
typedef type::alog_type alog_type;
|
||||||
typedef type::elog_type elog_type;
|
typedef type::elog_type elog_type;
|
||||||
@@ -62,10 +535,9 @@ public:
|
|||||||
|
|
||||||
typedef websocketpp::transport::asio::endpoint<transport_config>
|
typedef websocketpp::transport::asio::endpoint<transport_config>
|
||||||
transport_type;
|
transport_type;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
size_t CustomPpConfig::max_http_body_size = MAX_READ_SIZE;
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
pipedal::last_modified(const std::filesystem::path &path)
|
pipedal::last_modified(const std::filesystem::path &path)
|
||||||
@@ -86,7 +558,7 @@ pipedal::last_modified(const std::filesystem::path &path)
|
|||||||
static std::string getHostName()
|
static std::string getHostName()
|
||||||
{
|
{
|
||||||
char buff[512];
|
char buff[512];
|
||||||
if (gethostname(buff,sizeof(buff)) == 0)
|
if (gethostname(buff, sizeof(buff)) == 0)
|
||||||
{
|
{
|
||||||
buff[511] = '\0';
|
buff[511] = '\0';
|
||||||
return buff;
|
return buff;
|
||||||
@@ -98,7 +570,7 @@ static std::string getIpv4Address(const std::string interface)
|
|||||||
{
|
{
|
||||||
int fd = -1;
|
int fd = -1;
|
||||||
struct ifreq ifr;
|
struct ifreq ifr;
|
||||||
memset(&ifr,0,sizeof(ifr));
|
memset(&ifr, 0, sizeof(ifr));
|
||||||
|
|
||||||
fd = socket(AF_INET, SOCK_DGRAM, 0);
|
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;
|
ifr.ifr_addr.sa_family = AF_INET;
|
||||||
|
|
||||||
/* I want an IP address attached to "eth0" */
|
/* 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);
|
int result = ioctl(fd, SIOCGIFADDR, &ifr);
|
||||||
if (result == -1) return "";
|
if (result == -1)
|
||||||
|
return "";
|
||||||
|
|
||||||
close(fd);
|
close(fd);
|
||||||
char *name = inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr);
|
char *name = inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr);
|
||||||
if (name == nullptr) return "";
|
if (name == nullptr)
|
||||||
|
return "";
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
static std::map<std::string, std::string> extensionsToMimeType = {
|
static std::map<std::string, std::string> extensionsToMimeType = {
|
||||||
@@ -175,6 +648,7 @@ namespace pipedal
|
|||||||
int port = -1;
|
int port = -1;
|
||||||
std::filesystem::path rootPath;
|
std::filesystem::path rootPath;
|
||||||
int threads = 1;
|
int threads = 1;
|
||||||
|
size_t maxUploadSize = 512 * 1024 * 1024;
|
||||||
|
|
||||||
std::thread *pBgThread = nullptr;
|
std::thread *pBgThread = nullptr;
|
||||||
std::recursive_mutex io_mutex;
|
std::recursive_mutex io_mutex;
|
||||||
@@ -187,15 +661,22 @@ namespace pipedal
|
|||||||
class HttpRequestImpl : public HttpRequest
|
class HttpRequestImpl : public HttpRequest
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
const server::connection_type::request_type &m_request;
|
server::connection_type::request_type &m_request;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
HttpRequestImpl(const server::connection_type::request_type &request)
|
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 &method() const { return m_request.get_method(); }
|
||||||
virtual const std::string &get(const std::string &key) const { return m_request.get_header(key); }
|
virtual const std::string &get(const std::string &key) const { return m_request.get_header(key); }
|
||||||
virtual bool keepAlive() const
|
virtual bool keepAlive() const
|
||||||
@@ -241,8 +722,9 @@ namespace pipedal
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
// IWriteCallback
|
// IWriteCallback
|
||||||
virtual void close() {
|
virtual void close()
|
||||||
webSocket->close(websocketpp::close::status::normal, "");
|
{
|
||||||
|
webSocket->close(websocketpp::close::status::normal, "");
|
||||||
webSocket = nullptr;
|
webSocket = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +761,6 @@ namespace pipedal
|
|||||||
|
|
||||||
Lv2Log::info(SS("WebSocketSession opened. (" << fromAddress << ")"));
|
Lv2Log::info(SS("WebSocketSession opened. (" << fromAddress << ")"));
|
||||||
|
|
||||||
|
|
||||||
auto pFactory = pServer->GetSocketFactory(requestUri);
|
auto pFactory = pServer->GetSocketFactory(requestUri);
|
||||||
if (!pFactory)
|
if (!pFactory)
|
||||||
{
|
{
|
||||||
@@ -289,9 +770,9 @@ namespace pipedal
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
using websocketpp::lib::bind;
|
using std::bind;
|
||||||
using websocketpp::lib::placeholders::_1;
|
using std::placeholders::_1;
|
||||||
using websocketpp::lib::placeholders::_2;
|
using std::placeholders::_2;
|
||||||
|
|
||||||
webSocket->set_message_handler(bind(&WebSocketSession::on_message, this, _1, _2));
|
webSocket->set_message_handler(bind(&WebSocketSession::on_message, this, _1, _2));
|
||||||
webSocket->set_close_handler(bind(&WebSocketSession::on_close, this, _1));
|
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)
|
void on_session_closed(WebSocketSession::ptr &session, connection_hdl hConnection)
|
||||||
{
|
{
|
||||||
m_sessions.erase(session);
|
m_sessions.erase(session);
|
||||||
session = nullptr; // probably delete here.
|
session = nullptr; // probably delete here.
|
||||||
m_connections.erase(hConnection);
|
m_connections.erase(hConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotFound(server::connection_type &connection, const std::string &filename)
|
void NotFound(server::connection_type &connection, const std::string &filename)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// 404 error
|
// 404 error
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
|
|
||||||
ss << "<!doctype html><html><head>"
|
ss << "<!doctype html><html><head>"
|
||||||
<< "<title>Error 404 (Resource not found)</title><body>"
|
<< "<title>Error 404 (Resource not found)</title><body>"
|
||||||
<< "<h1>Error 404</h1>"
|
<< "<h1>Error 404</h1>"
|
||||||
<< "<p>The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.</p>"
|
<< "<p>The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.</p>"
|
||||||
<< "</body></head></html>";
|
<< "</body></head></html>";
|
||||||
|
|
||||||
std::string body = ss.str();
|
std::string body = ss.str();
|
||||||
connection.set_body(body);
|
connection.set_body(body);
|
||||||
@@ -346,23 +828,25 @@ namespace pipedal
|
|||||||
ssLen << body.length();
|
ssLen << body.length();
|
||||||
connection.replace_header(HttpField::content_length, ssLen.str());
|
connection.replace_header(HttpField::content_length, ssLen.str());
|
||||||
connection.set_status(websocketpp::http::status_code::not_found);
|
connection.set_status(websocketpp::http::status_code::not_found);
|
||||||
} catch (const std::exception&)
|
}
|
||||||
|
catch (const std::exception &)
|
||||||
{
|
{
|
||||||
// ignored. Things weren't going well anyway.
|
// ignored. Things weren't going well anyway.
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
void ServerError(server::connection_type &connection, const std::string &error)
|
void ServerError(server::connection_type &connection, const std::string &error)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
// 404 error
|
// 404 error
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
|
|
||||||
ss << "<!doctype html><html><head>"
|
ss << "<!doctype html><html><head>"
|
||||||
<< "<title>Error 500 (Server error)</title><body>"
|
<< "<title>Error 500 (Server error)</title><body>"
|
||||||
<< "<h1>Error 500</h1>"
|
<< "<h1>Error 500</h1>"
|
||||||
<< "<p>" << HtmlHelper::HtmlEncode(error) << "</p>"
|
<< "<p>" << HtmlHelper::HtmlEncode(error) << "</p>"
|
||||||
<< "</body></head></html>";
|
<< "</body></head></html>";
|
||||||
std::string body = ss.str();
|
std::string body = ss.str();
|
||||||
connection.set_body(body);
|
connection.set_body(body);
|
||||||
std::stringstream ssLen;
|
std::stringstream ssLen;
|
||||||
@@ -370,7 +854,8 @@ namespace pipedal
|
|||||||
connection.replace_header(HttpField::content_length, ssLen.str());
|
connection.replace_header(HttpField::content_length, ssLen.str());
|
||||||
|
|
||||||
connection.set_status(websocketpp::http::status_code::internal_server_error);
|
connection.set_status(websocketpp::http::status_code::internal_server_error);
|
||||||
} catch (const std::exception&)
|
}
|
||||||
|
catch (const std::exception &)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -457,17 +942,18 @@ namespace pipedal
|
|||||||
HttpResponseImpl res((*con));
|
HttpResponseImpl res((*con));
|
||||||
|
|
||||||
uri requestUri;
|
uri requestUri;
|
||||||
try {
|
try
|
||||||
requestUri.set(con->get_uri()->str().c_str());
|
|
||||||
} catch (const std::exception &e)
|
|
||||||
{
|
{
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string fromAddress = SS(con->get_remote_endpoint());
|
std::string fromAddress = SS(con->get_remote_endpoint());
|
||||||
|
|
||||||
|
|
||||||
if (req.method() == HttpVerb::options)
|
if (req.method() == HttpVerb::options)
|
||||||
{
|
{
|
||||||
res.set(HttpField::access_control_allow_origin, origin);
|
res.set(HttpField::access_control_allow_origin, origin);
|
||||||
@@ -486,14 +972,13 @@ namespace pipedal
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
if (req.method() == HttpVerb::head)
|
if (req.method() == HttpVerb::head)
|
||||||
{
|
{
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
||||||
res.set(HttpField::access_control_allow_origin, origin);
|
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());
|
res.keepAlive(req.keepAlive());
|
||||||
if (ec == std::errc::no_such_file_or_directory)
|
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::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
||||||
res.set(HttpField::access_control_allow_origin, origin);
|
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());
|
res.keepAlive(req.keepAlive());
|
||||||
|
|
||||||
if (ec == std::errc::no_such_file_or_directory)
|
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::date, HtmlHelper::timeToHttpDate(time(nullptr)));
|
||||||
res.set(HttpField::access_control_allow_origin, origin);
|
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)
|
if (ec == std::errc::no_such_file_or_directory)
|
||||||
{
|
{
|
||||||
@@ -562,7 +1046,7 @@ namespace pipedal
|
|||||||
}
|
}
|
||||||
catch (std::exception &e)
|
catch (std::exception &e)
|
||||||
{
|
{
|
||||||
ServerError(*con, SS("Unexpected error. " << e.what()));
|
ServerError(*con, SS("Unexpected error. " << e.what()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,13 +1112,14 @@ namespace pipedal
|
|||||||
{
|
{
|
||||||
m_connections.insert(hdl);
|
m_connections.insert(hdl);
|
||||||
|
|
||||||
|
try
|
||||||
try {
|
{
|
||||||
server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl);
|
server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl);
|
||||||
WebSocketSession::ptr socketSession = std::make_shared<WebSocketSession>(this, webSocket);
|
WebSocketSession::ptr socketSession = std::make_shared<WebSocketSession>(this, webSocket);
|
||||||
socketSession->Open();
|
socketSession->Open();
|
||||||
m_sessions.insert(socketSession);
|
m_sessions.insert(socketSession);
|
||||||
} catch (const std::exception&e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
Lv2Log::error("Failed to open session: %s", e.what());
|
Lv2Log::error("Failed to open session: %s", e.what());
|
||||||
}
|
}
|
||||||
@@ -666,9 +1151,9 @@ namespace pipedal
|
|||||||
m_endpoint.init_asio(&ioc);
|
m_endpoint.init_asio(&ioc);
|
||||||
|
|
||||||
// Bind the handlers we are using
|
// Bind the handlers we are using
|
||||||
using websocketpp::lib::bind;
|
using std::bind;
|
||||||
using websocketpp::lib::placeholders::_1;
|
using std::placeholders::_1;
|
||||||
using websocketpp::lib::placeholders::_2;
|
using std::placeholders::_2;
|
||||||
|
|
||||||
m_endpoint.set_open_handler(bind(&WebServerImpl::on_open, this, _1));
|
m_endpoint.set_open_handler(bind(&WebServerImpl::on_open, this, _1));
|
||||||
m_endpoint.set_close_handler(bind(&WebServerImpl::on_close, this, _1));
|
m_endpoint.set_close_handler(bind(&WebServerImpl::on_close, this, _1));
|
||||||
@@ -695,7 +1180,7 @@ namespace pipedal
|
|||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << port;
|
ss << port;
|
||||||
// m_endpoint.listen(this->address, ss.str());
|
// 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();
|
m_endpoint.start_accept();
|
||||||
|
|
||||||
// Start IOC service threads.
|
// Start IOC service threads.
|
||||||
@@ -703,7 +1188,7 @@ namespace pipedal
|
|||||||
v.reserve(threads - 1);
|
v.reserve(threads - 1);
|
||||||
for (auto i = threads - 1; i > 0; --i)
|
for (auto i = threads - 1; i > 0; --i)
|
||||||
v.emplace_back(
|
v.emplace_back(
|
||||||
[&ioc,i]
|
[&ioc, i]
|
||||||
{
|
{
|
||||||
SetThreadName(SS("web_" << i));
|
SetThreadName(SS("web_" << i));
|
||||||
ioc.run();
|
ioc.run();
|
||||||
@@ -789,13 +1274,7 @@ namespace pipedal
|
|||||||
this->pBgThread = new std::thread(ThreadProc, this);
|
this->pBgThread = new std::thread(ThreadProc, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebServerImpl(const std::string &address, int port, const char *rootPath, int threads)
|
WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize);
|
||||||
: address(address),
|
|
||||||
rootPath(rootPath),
|
|
||||||
port(port),
|
|
||||||
threads(threads)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} // namespace pipedal
|
} // namespace pipedal
|
||||||
|
|
||||||
@@ -811,8 +1290,21 @@ std::shared_ptr<ISocketFactory> WebServerImpl::GetSocketFactory(const uri &reque
|
|||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
WebServerImpl::WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize)
|
||||||
std::shared_ptr<WebServer> pipedal::WebServer::create(const boost::asio::ip::address &address, int port, const char *rootPath, int threads)
|
: 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 {
|
class HttpRequest {
|
||||||
public:
|
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 &method() const = 0;
|
||||||
virtual const std::string&get(const std::string&key) const = 0;
|
virtual const std::string&get(const std::string&key) const = 0;
|
||||||
virtual bool keepAlive() const = 0;
|
virtual bool keepAlive() const = 0;
|
||||||
@@ -161,14 +164,14 @@ public:
|
|||||||
|
|
||||||
virtual void head_response(
|
virtual void head_response(
|
||||||
const uri&request_uri,
|
const uri&request_uri,
|
||||||
const HttpRequest &req,
|
HttpRequest &req,
|
||||||
HttpResponse &res,
|
HttpResponse &res,
|
||||||
std::error_code &ec) = 0;
|
std::error_code &ec) = 0;
|
||||||
|
|
||||||
virtual void head_response(
|
virtual void head_response(
|
||||||
const std::string&fromAddress,
|
const std::string&fromAddress,
|
||||||
const uri&request_uri,
|
const uri&request_uri,
|
||||||
const HttpRequest &req,
|
HttpRequest &req,
|
||||||
HttpResponse &res,
|
HttpResponse &res,
|
||||||
std::error_code &ec) {
|
std::error_code &ec) {
|
||||||
head_response(request_uri,req,res,ec);
|
head_response(request_uri,req,res,ec);
|
||||||
@@ -178,14 +181,14 @@ public:
|
|||||||
|
|
||||||
virtual void get_response(
|
virtual void get_response(
|
||||||
const uri&request_uri,
|
const uri&request_uri,
|
||||||
const HttpRequest &req,
|
HttpRequest &req,
|
||||||
HttpResponse &res,
|
HttpResponse &res,
|
||||||
std::error_code &ec) = 0;
|
std::error_code &ec) = 0;
|
||||||
|
|
||||||
virtual void get_response(
|
virtual void get_response(
|
||||||
const std::string&fromAddress,
|
const std::string&fromAddress,
|
||||||
const uri&request_uri,
|
const uri&request_uri,
|
||||||
const HttpRequest &req,
|
HttpRequest &req,
|
||||||
HttpResponse &res,
|
HttpResponse &res,
|
||||||
std::error_code &ec)
|
std::error_code &ec)
|
||||||
{
|
{
|
||||||
@@ -195,7 +198,7 @@ public:
|
|||||||
|
|
||||||
virtual void post_response(
|
virtual void post_response(
|
||||||
const uri&request_uri,
|
const uri&request_uri,
|
||||||
const HttpRequest &req,
|
HttpRequest &req,
|
||||||
HttpResponse &res,
|
HttpResponse &res,
|
||||||
std::error_code &ec)
|
std::error_code &ec)
|
||||||
{
|
{
|
||||||
@@ -204,7 +207,7 @@ public:
|
|||||||
virtual void post_response(
|
virtual void post_response(
|
||||||
const std::string&fromAddress,
|
const std::string&fromAddress,
|
||||||
const uri&request_uri,
|
const uri&request_uri,
|
||||||
const HttpRequest &req,
|
HttpRequest &req,
|
||||||
HttpResponse &res,
|
HttpResponse &res,
|
||||||
std::error_code &ec)
|
std::error_code &ec)
|
||||||
{
|
{
|
||||||
@@ -233,7 +236,8 @@ public:
|
|||||||
const boost::asio::ip::address &address,
|
const boost::asio::ip::address &address,
|
||||||
int port,
|
int port,
|
||||||
const char *rootPath,
|
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 const threads = 3;
|
||||||
|
|
||||||
auto server = WebServer::create(
|
auto server = WebServer::create(
|
||||||
address, port, doc_root.c_str(), threads);
|
address, port, doc_root.c_str(), threads,512*1024*1024);
|
||||||
server->RunInBackground();
|
server->RunInBackground();
|
||||||
sleep(5);
|
sleep(5);
|
||||||
server->ShutDown(1000);
|
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 "Lv2Log.hpp"
|
||||||
#include "ServiceConfiguration.hpp"
|
#include "ServiceConfiguration.hpp"
|
||||||
#include "AvahiService.hpp"
|
#include "AvahiService.hpp"
|
||||||
|
#include "WebServerConfig.hpp"
|
||||||
|
|
||||||
#include "PiPedalSocket.hpp"
|
#include "PiPedalSocket.hpp"
|
||||||
#include "PluginHost.hpp"
|
#include "PluginHost.hpp"
|
||||||
@@ -38,7 +39,6 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include "HtmlHelper.hpp"
|
#include "HtmlHelper.hpp"
|
||||||
#include "Ipv6Helpers.hpp"
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -47,11 +47,10 @@
|
|||||||
|
|
||||||
#include <systemd/sd-daemon.h>
|
#include <systemd/sd-daemon.h>
|
||||||
|
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
|
||||||
#define PRESET_EXTENSION ".piPreset"
|
|
||||||
#define BANK_EXTENSION ".piBank"
|
|
||||||
#define PLUGIN_PRESETS_EXTENSION ".piPluginPresets"
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __ARM_ARCH_ISA_A64
|
#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()
|
static bool isJackServiceRunning()
|
||||||
{
|
{
|
||||||
@@ -659,7 +214,7 @@ int main(int argc, char *argv[])
|
|||||||
auto const threads = std::max<int>(1, configuration.GetThreads());
|
auto const threads = std::max<int>(1, configuration.GetThreads());
|
||||||
|
|
||||||
server = WebServer::create(
|
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);
|
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);
|
server->AddSocketFactory(pipedalSocketFactory);
|
||||||
|
|
||||||
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, configuration.GetMaxUploadSize())};
|
ConfigureWebServer(*server,model,port,configuration.GetMaxUploadSize());
|
||||||
server->AddRequestHandler(interceptConfig);
|
|
||||||
|
|
||||||
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
|
|
||||||
server->AddRequestHandler(downloadIntercept);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
server->RunInBackground(-1);
|
server->RunInBackground(-1);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
- make sure lv2-dev shows up in packaging. Make sure dhcpcd shows up in packaging
|
- Zip file upload in a subdirectory.
|
||||||
- Toob nam input VU is not working.
|
- Turning off wifi-direct sould re-enable NetworkManager.
|
||||||
- x TooB NAM: dbVu telltale is zero0.
|
- ToobML save/restore state.
|
||||||
- x TooB NAM: bass [] mid trebble.
|
|
||||||
- X check FLAC decode.
|
|
||||||
- make app use the same theme settings as the website.
|
- make app use the same theme settings as the website.
|
||||||
- BUG: gcs when we have an animated output control
|
- BUG: gcs when we have an animated output control
|
||||||
- Shutdown/reboot midi bindings
|
- 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?
|
- 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
|
Pri Description
|
||||||
-----------------
|
-----------------
|
||||||
|
9 Upload proteus patches.
|
||||||
|
8 Migrate to Vite toolchain.
|
||||||
5 Re-use plugin instances when rebuilding pedalboard.
|
5 Re-use plugin instances when rebuilding pedalboard.
|
||||||
|
|||||||
Reference in New Issue
Block a user