From 09f73e26d63e30e9d102069979e51fbe14ce789d Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 15 Feb 2025 12:29:53 -0500 Subject: [PATCH] Improvements to file uploads. UI improvements for future looper and audio capture plugins. --- .vscode/launch.json | 1 + .vscode/settings.json | 23 +- PiPedalCommon/src/include/util.hpp | 79 +++-- PiPedalCommon/src/util.cpp | 53 ++++ docs/index.md | 6 +- react/MiniSpinner.tsx | 69 +++++ react/src/AboutDialog.tsx | 4 +- react/src/BankDialog.tsx | 8 +- react/src/FilePropertyDialog.tsx | 13 +- .../src/FilePropertyDirectorySelectDialog.tsx | 6 +- react/src/LoadPluginDialog.tsx | 3 +- react/src/ModFileTypes.tsx | 4 +- react/src/PiPedalModel.tsx | 4 +- react/src/PluginControl.tsx | 135 ++++++--- react/src/PluginControlView.tsx | 1 + react/src/PluginPresetsDialog.tsx | 3 +- react/src/PresetDialog.tsx | 3 +- react/src/SettingsDialog.tsx | 3 +- react/src/UploadFileDialog.tsx | 42 ++- src/CMakeLists.txt | 9 +- src/FileBrowserFiles.h | 138 --------- src/FileBrowserFilesFeature.cpp | 80 ++++- src/FileBrowserFilesFeature.hpp | 18 +- src/Ipv6Helpers.cpp | 20 +- src/MimeTypes.cpp | 204 +++++++------ src/MimeTypes.hpp | 31 +- src/ModFileTypes.cpp | 121 ++++---- src/ModFileTypes.hpp | 40 ++- src/ModFileTypesTest.cpp | 162 +++++++++++ src/PiPedalModel.cpp | 49 +++- src/PiPedalModel.hpp | 6 +- src/PiPedalSocket.cpp | 31 +- src/PiPedalUI.cpp | 275 +++++++++++++++++- src/PiPedalUI.hpp | 33 ++- src/PluginHost.cpp | 189 ++++++------ src/PluginHost.hpp | 9 +- src/Storage.cpp | 183 +++++++----- src/Storage.hpp | 7 +- src/WebServerConfig.cpp | 16 +- src/lv2ext/pipedal.lv2/ext/fileBrowser.h | 257 ++++++++++++++++ todo.txt | 2 + 41 files changed, 1714 insertions(+), 626 deletions(-) create mode 100644 react/MiniSpinner.tsx delete mode 100644 src/FileBrowserFiles.h create mode 100644 src/ModFileTypesTest.cpp create mode 100644 src/lv2ext/pipedal.lv2/ext/fileBrowser.h diff --git a/.vscode/launch.json b/.vscode/launch.json index 7c3ac1c..4b516f5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -142,6 +142,7 @@ //"[pipedal_alsa_test]" // "[wifi_channels_test]" // "[locale]" + "[modFileTypes]" ], "stopAtEntry": false, diff --git a/.vscode/settings.json b/.vscode/settings.json index 66b150e..8e97e94 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -105,15 +105,34 @@ "text_encoding": "cpp" }, "cSpell.words": [ + "Alsa", + "flac", "Guitarix", "hostapd", + "Hotspot", + "invada", + "jmap", + "Lilv", + "midiclip", + "midisong", + "mlmodel", + "Pedalboard", "pipedal", "pipedal_0", "pipedalconfig", "pipedald", "pipedalshutdownd", "pipedaltest", - "ToobAmp" + "pipedalui", + "rdfs", + "ToobAmp", + "urids", + "Writables", + "xruns" ], - "C_Cpp.default.compilerPath": "/usr/bin/g++-13" + "C_Cpp.default.compilerPath": "/usr/bin/g++-13", + "cSpell.ignoreWords": [ + "nammodel" + ], + "cSpell.enabled": false } \ No newline at end of file diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 405b6e2..b4168cb 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -20,67 +20,102 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -#pragma once +#pragma once #include #include #include #include #include +#include -namespace pipedal { +namespace pipedal +{ - inline bool endsWith(const std::string& str, const std::string& suffix) + inline bool endsWith(const std::string &str, const std::string &suffix) { - return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix); + return str.size() >= suffix.size() && 0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix); } void SetThreadName(const std::string &name); std::u32string ToUtf32(const std::string &s); - inline bool isPathSeperator(char c) { return c == '/' || c == std::filesystem::path::preferred_separator;} + inline bool isPathSeparator(char c) { return c == '/' || c == std::filesystem::path::preferred_separator; } std::string GetHostName(); std::vector split(const std::string &value, char delimiter); - inline void ReadBarrier() { + inline void ReadBarrier() + { std::atomic_thread_fence(std::memory_order::acquire); } - inline void WriteBarrier() { + inline void WriteBarrier() + { std::atomic_thread_fence(std::memory_order::release); - } template - inline bool contains(const std::vector&vector,const T&value) + inline bool contains(const std::vector &vector, const T &value) { for (auto i = vector.begin(); i != vector.end(); ++i) { - if ((*i) == value) { + if ((*i) == value) + { return true; } } return false; } - inline bool contains(const std::vector&vector,const char*value) + inline bool contains(const std::vector &vector, const char *value) { - return contains(vector,std::string(value)); + return contains(vector, std::string(value)); } - class NoCopy { + class NoCopy + { public: - NoCopy() { } - NoCopy(const NoCopy&) = delete; - NoCopy&operator=(const NoCopy&) = delete; + NoCopy() {} + NoCopy(const NoCopy &) = delete; + NoCopy &operator=(const NoCopy &) = delete; ~NoCopy() {} }; - class NoMove { + class NoMove + { public: - NoMove() { } - NoMove(NoMove&&) = delete; - NoMove&operator=(NoMove&&) = delete; - ~NoMove() { } - + NoMove() {} + NoMove(NoMove &&) = delete; + NoMove &operator=(NoMove &&) = delete; + ~NoMove() {} }; + bool IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath); + + template + std::set Intersect(const std::set &left, const std::set &right) + { + std::set result; + for (const T &v : left) + { + if (right.contains(v)) + { + result.insert(v); + } + } + return result; + } + template + bool IsSubset(const std::set &subset, const std::set &set) + { + std::set result; + for (const T &v : subset) + { + if (!set.contains(v)) return false; + } + return true; + } + + // C locale to lower. Only does 'A'-'Z'. + std::string ToLower(const std::string&value); + + std::filesystem::path MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath); } diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index e01c179..47cd458 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -21,6 +21,7 @@ * SOFTWARE. */ #include "util.hpp" +#include "ss.hpp" #include #include @@ -136,3 +137,55 @@ std::vector pipedal::split(const std::string &value, char delimiter } return result; } + + +bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath) +{ + auto iter = path.begin(); + + for (auto i = rootPath.begin(); i != rootPath.end(); ++i) + { + if (iter == path.end() || *iter != *i) + { + return false; + } + ++iter; + } + return true; +} + + +std::string pipedal::ToLower(const std::string&value) +{ + std::string result; + result.resize(value.length()); + for (size_t i = 0; i < value.length(); ++i) + { + result[i] = (char)tolower(value[i]); + } + return result; +} + + +std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath) +{ + if (path.is_relative()) return path; + + auto iter = path.begin(); + + for (auto i = parentPath.begin(); i != parentPath.end(); ++i) + { + if (iter == path.end() || *iter != *i) { + // not a child directory of parent directory. + throw std::runtime_error("Not a child directory."); + } + ++iter; + } + std::filesystem::path remander; + while (iter != path.end()) + { + remander /= *iter++; + } + return remander; + +} diff --git a/docs/index.md b/docs/index.md index d6a143b..938b3db 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,4 @@ - - v1.3.69   @@ -8,7 +6,9 @@ To download PiPedal, click [here](download.md). To view PiPedal documentation, click [here](Documentation.md). -#### Important notice: PiPedal has a submitted a request to support sharing of Pipedal presets at [PatchStorage.com](https:///patchstorage.com). We urgently need your support! Please vote for PiPedal [here](https://patchstorage.com/requests/pipedal/). +#### Important notice: [PatchStorage.com](https:///patchstorage.com) has added support for PiPedal. We urgently need your support! I you could please upload patches [here](https://patchstorage.com/platform/pipedal/) I would +be enormously grateful. Note that media and model files are included in patch files, so you can share patches that +include model files or audio samples. If you make use of plugins that are not included in the PiPedal distribution, please include a link to the plugin in the patch description. #### NEW version 1.3.69 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. diff --git a/react/MiniSpinner.tsx b/react/MiniSpinner.tsx new file mode 100644 index 0000000..74bd742 --- /dev/null +++ b/react/MiniSpinner.tsx @@ -0,0 +1,69 @@ +/* + * MIT License + * + * Copyright (c) 2024 Robin E. R. 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 * as React from 'react'; +import { Theme } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import createStyles from '@mui/styles/createStyles'; +import withStyles from '@mui/styles/withStyles'; + + +const styles = (theme: Theme) => createStyles({ + frame: { + position: "relative", + margin: "12px" + } +}); + + +export interface MiniSpinnerProps extends WithStyles { + theme: Theme +} + +export interface MiniSpinnerProps { + size?: number; +}; + + +const MiniSpinner = withStyles(styles)((props: MiniSpinnerProps) => { + const { classes } = props; + + let size: number = 24; + if (props.size) + { + size = props.size; + } + let thickness = 3; + let r = size-thickness/2; + return ( +
+ width={size} height={size} + + +
+ ); +}); diff --git a/react/src/AboutDialog.tsx b/react/src/AboutDialog.tsx index eb95eef..5a81b9f 100644 --- a/react/src/AboutDialog.tsx +++ b/react/src/AboutDialog.tsx @@ -12,7 +12,9 @@ import JackHostStatus from './JackHostStatus'; import { PiPedalError } from './PiPedalError'; import DialogEx from './DialogEx'; -import { Theme, createStyles } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; + +import { Theme } from '@mui/material/styles'; import { withStyles, WithStyles } from '@mui/styles'; import Slide, { SlideProps } from '@mui/material/Slide'; diff --git a/react/src/BankDialog.tsx b/react/src/BankDialog.tsx index e22fd4c..197250a 100644 --- a/react/src/BankDialog.tsx +++ b/react/src/BankDialog.tsx @@ -27,7 +27,9 @@ import ButtonBase from "@mui/material/ButtonBase"; import Slide, { SlideProps } from '@mui/material/Slide'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; -import { Theme, createStyles } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; + +import { Theme } from '@mui/material/styles'; import { WithStyles, withStyles } from "@mui/styles"; import DraggableGrid, { ScrollDirection } from './DraggableGrid'; import Fade from '@mui/material/Fade'; @@ -304,8 +306,8 @@ const BankDialog = withStyles(styles, { withTheme: true })( >
-
- +
+
diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index cd4bb90..3f46507 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -19,7 +19,9 @@ import React from 'react'; -import { Theme, createStyles } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; + +import { Theme } from '@mui/material/styles'; import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; import RenameDialog from './RenameDialog'; import Divider from '@mui/material/Divider'; @@ -32,7 +34,7 @@ import Button from '@mui/material/Button'; import FileUploadIcon from '@mui/icons-material/FileUpload'; import AudioFileIcon from '@mui/icons-material/AudioFile'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; +import FolderIcon from '@mui/icons-material/Folder'; import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import DialogActions from '@mui/material/DialogActions'; @@ -92,6 +94,7 @@ function isAudioFile(filename: string) { export interface FilePropertyDialogProps extends WithStyles { open: boolean, fileProperty: UiFileProperty, + instanceId: number, selectedFile: string, onOk: (fileProperty: UiFileProperty, selectedItem: string) => void, onCancel: () => void @@ -557,7 +560,7 @@ export default withStyles(styles, { withTheme: true })( } if (fileEntry.isDirectory) { return ( - + ); } if (isAudioFile(fileEntry.pathname)) { @@ -750,6 +753,8 @@ export default withStyles(styles, { withTheme: true })( } uploadPage={ "uploadUserFile?directory=" + encodeURIComponent(this.state.uploadDirectory) + + "&id=" + this.props.instanceId.toString() + + "&property=" + encodeURIComponent(this.props.fileProperty.patchProperty) + "&ext=" + encodeURIComponent(this.getFileExtensionList(this.props.fileProperty)) } @@ -794,8 +799,10 @@ export default withStyles(styles, { withTheme: true })( ( void, onClose: () => void }; @@ -181,7 +183,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC requestDirectoryTree() { if (!this.props.open) return; - this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty) + this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty,this.props.selectedFile) .then((filePropertyDirectoryTree) => { let myTree = new DirectoryTree(filePropertyDirectoryTree); if (this.props.excludeDirectory.length !== 0) @@ -327,7 +329,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC PaperProps={{ style: { minHeight: "80%", maxHeight: "80%", minWidth: "75%", maxWidth: "75%", overflowY: "visible" } }} > - Select folder + {this.props.dialogTitle} diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx index 4429f1f..5f56ff1 100644 --- a/react/src/LoadPluginDialog.tsx +++ b/react/src/LoadPluginDialog.tsx @@ -43,7 +43,8 @@ import { FixedSizeGrid } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import StarBorderIcon from '@mui/icons-material/StarBorder'; import StarIcon from '@mui/icons-material/Star'; -import { createStyles, Theme } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; +import { Theme } from '@mui/material/styles'; import { WithStyles, withStyles } from '@mui/styles'; import FilterListIcon from '@mui/icons-material/FilterList'; import FilterListOffIcon from '@mui/icons-material/FilterListOff'; diff --git a/react/src/ModFileTypes.tsx b/react/src/ModFileTypes.tsx index 2624106..a03960f 100644 --- a/react/src/ModFileTypes.tsx +++ b/react/src/ModFileTypes.tsx @@ -38,8 +38,8 @@ export const modDirectories: ModDirectory[] = [ {modType: "ir", pipedalPath: "ReverbImpulseFiles", displayName: "Impulse Responses", defaultFileExtensions: ["audio/*"]}, // Impulse Responses {modType: "midiclip", pipedalPath: "shared/midiClips",displayName: "MIDI Clips", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension {modType: "midisong", pipedalPath: "shared/midiSongs", displayName: "MIDI Songs", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Songs, meant to be used as full-performance/song or backtrack - {modType: "sf2", pipedalPath: "shared/sf2",displayName: "Sound Fonts", defaultFileExtensions: ["sf2", "sf3"]}, // SF2 Instruments, must have sf2 or sf3 file extension - {modType: "sfz", pipedalPath: "shared/sfz",displayName: "Sfz Files", defaultFileExtensions: ["sfz"]}, // SFZ Instruments, must have sfz file extension + {modType: "sf2", pipedalPath: "shared/sf2",displayName: "Sound Fonts", defaultFileExtensions: [".sf2", ".sf3"]}, // SF2 Instruments, must have sf2 or sf3 file extension + {modType: "sfz", pipedalPath: "shared/sfz",displayName: "Sfz Files", defaultFileExtensions: [".sfz"]}, // SFZ Instruments, must have sfz file extension // extensions observed in the field. {modType: "audio", pipedalPath: "shared/audio", displayName: "Audio", defaultFileExtensions: ["audio/*"]}, // all audio files (Ratatoille) {modType: "nammodel", pipedalPath: "NeuralAmpModels", displayName: "Neural Amp Models", defaultFileExtensions: [".nam"]}, // Ratatoille, Mike's NAM. diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 83a0fb8..b648e2b 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -2610,7 +2610,7 @@ export class PiPedalModel //implements PiPedalModel }); } - getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty): Promise { + getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty,selectedPath: string): Promise { return new Promise((resolve, reject) => { let ws = this.webSocket; if (!ws) { @@ -2619,7 +2619,7 @@ export class PiPedalModel //implements PiPedalModel } ws.request( "getFilePropertyDirectoryTree", - uiFileProperty + {fileProperty: uiFileProperty, selectedPath: selectedPath} ).then((result) => { resolve(new FilePropertyDirectoryTree().deserialize(result)); }).catch((e) => { diff --git a/react/src/PluginControl.tsx b/react/src/PluginControl.tsx index 79a577a..69c3240 100644 --- a/react/src/PluginControl.tsx +++ b/react/src/PluginControl.tsx @@ -73,7 +73,7 @@ export const pluginControlStyles = (theme: Theme) => createStyles({ }, titleSection: { - flex: "0 0 auto", alignSelf:"stretch", marginBottom: 8, marginLeft: 0, marginRight: 0 + flex: "0 0 auto", alignSelf: "stretch", marginBottom: 8, marginLeft: 0, marginRight: 0 }, displayValue: { position: "absolute", @@ -87,10 +87,10 @@ export const pluginControlStyles = (theme: Theme) => createStyles({ // zIndex: -1, }, midSection: { - flex: "1 1 1", display: "flex",flexFlow: "column nowrap",alignContent: "center",justifyContent: "center" + flex: "1 1 1", display: "flex", flexFlow: "column nowrap", alignContent: "center", justifyContent: "center" }, editSection: { - flex: "0 0 0", display: "flex", flexFlow: "column nowrap", justifyContent: "center",position: "relative", width: 60, height: 28,minHeight: 28 + flex: "0 0 0", display: "flex", flexFlow: "column nowrap", justifyContent: "center", position: "relative", width: 60, height: 28, minHeight: 28 } }); @@ -500,14 +500,12 @@ const PluginControl = } handleTriggerMouseDown() { let uiControl = this.props.uiControl; - if (uiControl) - { + if (uiControl) { let value = uiControl.max_value; - if (uiControl.max_value === uiControl.default_value) - { + if (uiControl.max_value === uiControl.default_value) { value = uiControl.min_value; } - this.model.sendPedalboardControlTrigger(this.props.instanceId,uiControl.symbol,value); + this.model.sendPedalboardControlTrigger(this.props.instanceId, uiControl.symbol, value); } } handleTriggerMouseUp() { @@ -650,13 +648,39 @@ const PluginControl = } let range: number; if (uiControl.is_logarithmic) { - range = Math.log(value / uiControl.min_value) / Math.log(uiControl.max_value / uiControl.min_value); + let minValue = uiControl.min_value; + if (minValue === 0) // LSP plugins do this. + { + minValue = 0.0001; + } + range = Math.log(value / minValue) / Math.log(uiControl.max_value / minValue); + if (!isFinite(range)) { + if (range < 0) + { + range = 0; + } else { + range = 1.0; + } + } else if (isNaN(range)) { + range = 0; + } if (uiControl.range_steps > 1) { range = Math.round(range * (uiControl.range_steps - 1)) / (uiControl.range_steps - 1); } } else { range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value); + if (!isFinite(range)) { + if (range < 0) + { + range = 0; + } else { + range = 1.0; + } + } else if (isNaN(range)) { + range = 0; + } } + if (range > 1) range = 1; if (range < 0) range = 0; return range; @@ -676,7 +700,17 @@ const PluginControl = value = uiControl.min_value; } else { if (uiControl.is_logarithmic) { - value = uiControl.min_value * Math.pow(uiControl.max_value / uiControl.min_value, range); + let minValue = uiControl.min_value; + if (minValue === 0) // LSP controls. + { + minValue = 0.0001; + } + value = minValue * Math.pow(uiControl.max_value / minValue, range); + if (!isFinite(value)) { + value = uiControl.max_value; + } else if (isNaN(value)) { + value = uiControl.min_value; + } } else { value = range * (uiControl.max_value - uiControl.min_value) + uiControl.min_value; } @@ -731,23 +765,23 @@ const PluginControl = } let item_width: number | undefined = isSelect ? 160 : 80; - if (isTrigger) - { + if (isTrigger) { item_width = undefined; } return ( -
+ > {/* TITLE SECTION */}
+ }}> - - {isTrigger ? ( - + + ) : ( + + ) + ) - > - {control.name} - - ) : ((isSelect || isAbSwitch || isOnOffSwitch) ? ( this.makeSelect(control, value) ) : ( diff --git a/react/src/PluginControlView.tsx b/react/src/PluginControlView.tsx index 0fc2f44..0eb74ab 100644 --- a/react/src/PluginControlView.tsx +++ b/react/src/PluginControlView.tsx @@ -767,6 +767,7 @@ const PluginControlView = { this.setState({ showFileDialog: false }); diff --git a/react/src/PluginPresetsDialog.tsx b/react/src/PluginPresetsDialog.tsx index 0ab5dac..4dbfb1d 100644 --- a/react/src/PluginPresetsDialog.tsx +++ b/react/src/PluginPresetsDialog.tsx @@ -43,7 +43,8 @@ import EditIcon from '@mui/icons-material//Edit'; import RenameDialog from './RenameDialog'; import Slide, {SlideProps} from '@mui/material/Slide'; -import { createStyles, Theme } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; +import { Theme } from '@mui/material/styles'; import { WithStyles, withStyles} from '@mui/styles'; import {ReactComponent as PluginPresetIcon} from "./svg/ic_pluginpreset2.svg"; diff --git a/react/src/PresetDialog.tsx b/react/src/PresetDialog.tsx index 5d5f9c6..f9f3531 100644 --- a/react/src/PresetDialog.tsx +++ b/react/src/PresetDialog.tsx @@ -43,7 +43,8 @@ import EditIcon from '@mui/icons-material//Edit'; import RenameDialog from './RenameDialog'; import Slide, {SlideProps} from '@mui/material/Slide'; -import { createStyles, Theme } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; +import { Theme } from '@mui/material/styles'; import { WithStyles, withStyles} from '@mui/styles'; import { ReactComponent as DownloadIcon} from './svg/file_download_black_24dp.svg'; import { ReactComponent as UploadIcon} from './svg/file_upload_black_24dp.svg'; diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index a08b7b5..cdde593 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -49,7 +49,8 @@ import SystemMidiBindingsDialog from './SystemMidiBindingsDialog'; import SelectThemeDialog from './SelectThemeDialog'; import Slide, { SlideProps } from '@mui/material/Slide'; -import { createStyles, Theme } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; +import { Theme } from '@mui/material/styles'; import { WithStyles, withStyles } from '@mui/styles'; import { canScaleWindow, getWindowScaleOptions, getWindowScaleText, setWindowScale, getWindowScale } from './WindowScale'; import OptionsDialog from './OptionsDialog'; diff --git a/react/src/UploadFileDialog.tsx b/react/src/UploadFileDialog.tsx index 7a55e3f..4582f65 100644 --- a/react/src/UploadFileDialog.tsx +++ b/react/src/UploadFileDialog.tsx @@ -284,23 +284,39 @@ export default class UploadFileDialog extends ResizeResponsiveComponent
); + return (); case FileUploadStatus.Uploaded: - return (
); + return ( + ); case FileUploadStatus.Uploading: - return (); + + return (); + default: - return (); - } + return () + } + + } + getIcon(status: FileUploadStatus) { + let size = 16; + + //let top = 0; + + return ( +
+ { + this.innerGetIcon(status) + } +
+ ); } makeBrowserAcceptList() { let result = UiFileType.MergeMimeTypes(this.props.fileProperty.fileTypes); @@ -368,9 +384,13 @@ export default class UploadFileDialog extends ResizeResponsiveComponent -
+
{this.getIcon(upload.status)} - {upload.name} + {upload.name}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0c2155c..a867dea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -190,6 +190,7 @@ set (PIPEDAL_SOURCES CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp SchedulerPriority.hpp SchedulerPriority.cpp ModFileTypes.cpp ModFileTypes.hpp + MimeTypes.cpp MimeTypes.hpp PatchPropertyWriter.hpp PresetBundle.cpp PresetBundle.hpp RealtimeMidiEventType.hpp @@ -208,9 +209,8 @@ set (PIPEDAL_SOURCES FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp FileEntry.cpp FileEntry.hpp atom_object.hpp atom_object.cpp - FileBrowserFiles.h + lv2ext/pipedal.lv2/ext/fileBrowser.h FileBrowserFilesFeature.hpp FileBrowserFilesFeature.cpp - MimeTypes.cpp MimeTypes.hpp inverting_mutex.hpp DbDezipper.hpp DbDezipper.cpp WebServerLog.hpp @@ -372,8 +372,10 @@ set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL true) -add_executable(pipedaltest testMain.cpp +add_executable(pipedaltest +testMain.cpp + ModFileTypesTest.cpp jsonTest.cpp UpdaterTest.cpp @@ -692,6 +694,7 @@ add_executable(pipedalconfig alsaCheck.hpp BootConfig.cpp BootConfig.hpp ModFileTypes.cpp ModFileTypes.hpp + MimeTypes.cpp MimeTypes.hpp PiPedalConfiguration.hpp PiPedalConfiguration.cpp JackServerSettings.hpp JackServerSettings.cpp SystemConfigFile.hpp SystemConfigFile.cpp diff --git a/src/FileBrowserFiles.h b/src/FileBrowserFiles.h deleted file mode 100644 index 50687d4..0000000 --- a/src/FileBrowserFiles.h +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2023 Robin E. R. Davies - * All rights reserved. - - * 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. - */ - -#ifndef FILEBROWSERFILES_H -#define FILEBROWSERFILES_H - -#define LV2_FILEBROWSER_URI "http://two-play.com/ns/ext/fileBrowser" -#define LV2_FILEBROWSER_PREFIX LV2_FILEBROWSER_URI "#" -#define LV2_FILEBROWSER__files LV2_FILEBROWSER_PREFIX "files" ///< http://two-play.com/ns/ext/fileBrowser#files > - -/** - @defgroup filedialog LV2_FileBrowser_Files - @ingroup lv2 - - A feature for LV2 plugins to provide sample files (or other selectable files) within the plugin's resource bundle to host file selection dialogs. - - Browser directories may contain actual files that typically have been uploaded by users; but may also contain symlinks to files - in other locations. This feature allows plugins to publish files within the plugin's resource bundle to browser directories. For - each published file consists of a symlink in the browser directory that links to a corresponding file in the plugins resource bundle. - - The LV2_FileBrowser_Files.publish_resource_files() method allows plugins to publish entire directories. publish_resource_files() - implements a versioning system, which tracks whether sample files have been deleted by the user. If the symlink for sample files that were installed - by a previous version of the plugin have been deleted, the sample files from the current version are not reinstalled. See LV2_FileBrowser_Files.publish_resource_files. - not re-installed. - - See: for more details. - - @{ -*/ - - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - LV2_FileBrowser_Status_Success = 0, - LV2_FileBrowser_Status_Err_Filesystem = 1, - LV2_FileBrowser_Status_Err_InvalidParameter = 1, -} LV2_FileBrowser_Status; - -typedef void* - LV2_FileBrowser_Files_Handle; ///< Opaque handle for pipedal:uploadPath feature - - -/** - Feature data for filedialog:files (@ref LV2_FILEBROWSER_file). -*/ -typedef struct { - /** - Opaque host data. - */ - LV2_FileBrowser_Files_Handle handle; - - LV2_FileBrowser_Status (*publish_resource_files)(LV2_FileBrowser_Files_Handle handle,uint32_t version,const char*resourcePath, const char*uploadDirectory); - - /** - Return a path in which upload files will be stored for this plugin. - @param handle MUST be the `handle` member of this struct. - @param path The sub-directory name in which uploaded files will be stored. - @return The absolute path to use for the new directory. - - This function can be used by plugins to get the path of files or directories - that can be browsed by file dialogs. get_filebrowser_path, unlike map_path() does - not create or modify files in any way. - - The caller must free memory for the returned value with LV2_FileBrowser_Files.free_path(). - - Example: - [code] - TBD - [/code] - */ - char* (*get_upload_path)(LV2_FileBrowser_Files_Handle handle, const char* path); - - /** - Map resource bundle filenames to upload directories. - @param handle MUST be the `handle` member of this struct. - @param path File path name - @param fileBrowserDirectory relative directory of the file browser directory in which to create a link. - @return The absolute pathname of a file in the resource directory. - - The primary purpose of this method is to map file references to files in the plugin's bundle - directory to corresponding files in the a file browser directory. - - If the path is relative, the full path is resolved using the `rootResourceDirectory` path. - - If the full path is not a child of rootResourceDirectory or if the path is relative, - get_upload_path returns the supplied path unmodified. -` - If the filename is a child of rootResourceDirectory, creates a corresponding link to the file in the file browser - directory and returns the path of that link. - - The caller must free memory for the returned value with `LV2_FileBrowser_Files.free_path()`. - - Example: - */ - char* (*map_path)(LV2_FileBrowser_Files_Handle handle, const char* path, const char*rootResourceDirectory, const char*fileBrowserDirectory); - - - /** - Free a path returned by LV2_FileBrowser_Files.get_upload_path() or LV2_FileBrowser_Files.map_path() - @param handle MUST be the `handle` member of this struct. - @param path A path previously returned by a method of `LV2_FileBrowser_Files`. - - */ - void (*free_path)(LV2_FileBrowser_Files_Handle handle, char* path); -} LV2_FileBrowser_Files; - - - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // FILEBROWSERFILES_H diff --git a/src/FileBrowserFilesFeature.cpp b/src/FileBrowserFilesFeature.cpp index 3207688..92d8709 100644 --- a/src/FileBrowserFilesFeature.cpp +++ b/src/FileBrowserFilesFeature.cpp @@ -30,9 +30,11 @@ #include #include "util.hpp" #include "ofstream_synced.hpp" +#include "ModFileTypes.hpp" using namespace pipedal; using namespace pipedal::implementation; +namespace fs = std::filesystem; namespace pipedal::implementation { @@ -128,7 +130,9 @@ void FileBrowserFilesFeature::Initialize( lv2_log_logger_init(&this->lv2Logger,const_cast(lv2Map),const_cast(lv2Log)); this->bundleDirectory = bundleDirectory; - this->browserDirectory = browserDirectory; + this->browserRootDirectory = browserDirectory; + this->privateDirectory = browserDirectory / this->bundleDirectory.filename(); + MakeDirectoryMap(browserDirectory); featureData.handle = (void *)this; @@ -155,9 +159,73 @@ LV2_FileBrowser_Status FileBrowserFilesFeature::FN_publish_resource_files(LV2_Fi return ((FileBrowserFilesFeature *)handle)->PublishResourceFiles(version, resourcePath, uploadDirectory); } + +const char*FileBrowserFilesFeature::GetWellKnownDirectory(const std::string&directory) +{ + if (strcmp(directory.c_str(), "~") == 0) { + return privateDirectory.c_str(); + } + const auto &f = g_wellKnownDirectoryMap->find(directory); + if (f != g_wellKnownDirectoryMap->end()) + { + return f->second.c_str(); + } + return nullptr; +} + + +using WellKnownDirectoryMap = std::map; +std::mutex FileBrowserFilesFeature::g_DirectoryMap_mutex; + +std::unique_ptr FileBrowserFilesFeature::g_wellKnownDirectoryMap; + +void FileBrowserFilesFeature::MakeDirectoryMap(const std::filesystem::path&rootBrowserDirectory) +{ + std::lock_guard lock { g_DirectoryMap_mutex}; + + if (!g_wellKnownDirectoryMap) + { + g_wellKnownDirectoryMap = std::make_unique(); + for (const auto&modDirectory: ModFileTypes::ModDirectories()) + { + (*g_wellKnownDirectoryMap)[modDirectory.modType] = + (rootBrowserDirectory / modDirectory.pipedalPath).string(); + } + + } +} + + + char *FileBrowserFilesFeature::GetUploadPath(const char *fileBrowserPath) { - std::filesystem::path result = this->browserDirectory / fileBrowserPath; + if (fileBrowserPath == nullptr | fileBrowserPath[0] == '\0') + { + return strdup(""); + } + std::filesystem::path path {fileBrowserPath}; + + auto iter = path.begin(); + if (iter == path.end()) + { + return strdup(""); + } + std::string firstSegment = iter->string(); + + const auto f = (g_wellKnownDirectoryMap)->find(firstSegment); + if (f == g_wellKnownDirectoryMap->end()) + { + return nullptr; + } + + fs::path result = f->second; + + ++iter; + while (iter != path.end()) + { + result /= (*iter); + ++iter; + } return strdup(result.c_str()); } void FileBrowserFilesFeature::FreePath(char *path) @@ -224,7 +292,7 @@ char *FileBrowserFilesFeature::MapPath(const char *path,const char*resourcePathB } else { targetDirectory = fileBrowserPath; } - targetDirectory = this->browserDirectory / targetDirectory; + targetDirectory = this->browserRootDirectory / targetDirectory; std::filesystem::create_directories(targetDirectory); std::filesystem::path targetPath = targetDirectory / relativePath; @@ -297,7 +365,7 @@ LV2_FileBrowser_Status FileBrowserFilesFeature::PublishResourceFiles(uint32_t ve static std::string VERSION_FILENAME_PREFIX = ".versionInfo."; // generate a filename that will allow multiple plugins to install files into the same directory. - std::filesystem::path versionFileName = this->browserDirectory / uploadDirectory / + std::filesystem::path versionFileName = this->browserRootDirectory / uploadDirectory / (VERSION_FILENAME_PREFIX + GetPluginTag(this->bundleDirectory)); bool loaded = versionInfo.Load(versionFileName); @@ -325,8 +393,8 @@ LV2_FileBrowser_Status FileBrowserFilesFeature::PublishResourceFiles(uint32_t ve LogError("uploadDirectory must be relative."); return LV2_FileBrowser_Status::LV2_FileBrowser_Status_Err_InvalidParameter; } - targetDirectory = this->browserDirectory / targetDirectory; - PublishRecursive(versionInfo, sourcePath,sourcePath, this->browserDirectory / uploadDirectory); + targetDirectory = this->browserRootDirectory / targetDirectory; + PublishRecursive(versionInfo, sourcePath,sourcePath, this->browserRootDirectory / uploadDirectory); versionInfo.Save(versionFileName); } catch (const std::exception & e) { diff --git a/src/FileBrowserFilesFeature.hpp b/src/FileBrowserFilesFeature.hpp index 94eb547..6d9cb05 100644 --- a/src/FileBrowserFilesFeature.hpp +++ b/src/FileBrowserFilesFeature.hpp @@ -24,10 +24,12 @@ #pragma once -#include "FileBrowserFiles.h" +#include "lv2ext/pipedal.lv2/ext/fileBrowser.h" #include #include #include +#include +#include @@ -47,20 +49,32 @@ namespace pipedal { const std::string&browserDirectory); const LV2_Feature*GetFeature() { return &feature; } private: + static void MakeDirectoryMap(const std::filesystem::path&rootBrowserDirectory); + + using WellKnownDirectoryMap = std::map; + static std::mutex g_DirectoryMap_mutex; + static std::unique_ptr g_wellKnownDirectoryMap; + LV2_Feature feature; const LV2_URID_Map *lv2Map = nullptr; LV2_Log_Logger lv2Logger = {nullptr,0,0,0,0}; std::filesystem::path bundleDirectory; - std::filesystem::path browserDirectory; + std::filesystem::path browserRootDirectory; + std::filesystem::path privateDirectory; LV2_FileBrowser_Files featureData; + std::map wellKnownPaths; + + static char* FN_get_upload_path(LV2_FileBrowser_Files_Handle handle, const char* fileBrowserDirectory); static char* FN_map_path(LV2_FileBrowser_Files_Handle handle, const char* path, const char *resourcePathBase,const char*fileBrowserDirectory); static void FN_free_path(LV2_FileBrowser_Files_Handle handle, char* path); static LV2_FileBrowser_Status FN_publish_resource_files(LV2_FileBrowser_Files_Handle handle,uint32_t version,const char*resourcePath, const char*uploadDirectory); + const char*GetWellKnownDirectory(const std::string&directory); + void LogError(const char*message); char* GetUploadPath(const char* fileBrowserDirectory); char* MapPath(const char* path,const char*resourcePathBase,const char* fileBrowserDirectory); diff --git a/src/Ipv6Helpers.cpp b/src/Ipv6Helpers.cpp index 0e9d8b6..a3bad9a 100644 --- a/src/Ipv6Helpers.cpp +++ b/src/Ipv6Helpers.cpp @@ -136,7 +136,7 @@ static bool IsIpv4OnLocalSubnet(uint32_t ipv4Addres) for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { // TODO: Add support for AF_INET6 uint32_t netmask = htonl(((sockaddr_in *)(p->ifa_netmask))->sin_addr.s_addr); uint32_t ifAddr = htonl(((sockaddr_in *)(p->ifa_addr))->sin_addr.s_addr); @@ -310,7 +310,7 @@ bool pipedal::IsOnLocalSubnet(const std::string &fromAddress) for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr); @@ -353,7 +353,7 @@ std::vector pipedal::GetEthernetIpv4Addresses() { if (isEthernetAddress(p->ifa_name)) { - if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) uint32_t netmask = htonl(((sockaddr_in *)(p->ifa_netmask))->sin_addr.s_addr); uint32_t ifAddr = htonl(((sockaddr_in *)(p->ifa_addr))->sin_addr.s_addr); { @@ -378,7 +378,7 @@ static std::string GetInterfaceForIp4Address(uint32_t ipv4Address) std::string result; for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { // TODO: Add support for AF_INET6 uint32_t netmask = htonl(((sockaddr_in *)(p->ifa_netmask))->sin_addr.s_addr); uint32_t ifAddr = htonl(((sockaddr_in *)(p->ifa_addr))->sin_addr.s_addr); @@ -402,7 +402,7 @@ static std::string GetInterfaceForIp6Address(const in6_addr inetAddr6) std::string result; for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { // TODO: Add support for AF_INET6 struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr); struct sockaddr_in6 *pNetMask = (struct sockaddr_in6 *)(p->ifa_netmask); @@ -441,7 +441,7 @@ static std::string GetNonLinkLocalAddress(const boost::asio::ip::address_v6 &ipV // get the nameof the interface for this scopeId. for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr) { // TODO: Add support for AF_INET6 struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr); if (pAddr->sin6_scope_id == scopeId) @@ -459,7 +459,7 @@ static std::string GetNonLinkLocalAddress(const boost::asio::ip::address_v6 &ipV // an IPV4 address would be the ideal result. for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { if (strcmp(p->ifa_name, targetInterfaceName) == 0) { @@ -503,7 +503,7 @@ static std::string GetNonLinkLocalAddressForInterface(const std::string &name) for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { // TODO: Add support for AF_INET6 struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr); if (!IN6_IS_ADDR_LINKLOCAL(&(pAddr->sin6_addr))) @@ -547,8 +547,8 @@ static std::string GetNonLinkLocalAddressForIp4Interface(const std::string &name std::string result = ""; for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) { - if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) - { // TODO: Add support for AF_INET6 + if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) + { struct sockaddr_in *pAddr = (struct sockaddr_in *)(p->ifa_addr); { if (name == p->ifa_name) diff --git a/src/MimeTypes.cpp b/src/MimeTypes.cpp index 12adde9..702580d 100644 --- a/src/MimeTypes.cpp +++ b/src/MimeTypes.cpp @@ -20,99 +20,22 @@ #include "MimeTypes.hpp" #include "ss.hpp" +#include +#include using namespace pipedal; -void MimeTypes::MaybeInitialize() -{ - if (!initialized) - { - initialized = true; - AddMimeType("MP3", "audio/mpeg"); - AddMimeType("MPGA", "audio/mpeg"); - AddMimeType("M4A", "audio/mp4"); - AddMimeType("WAV", "audio/x-wav"); - AddMimeType("WAV", "audio/wav"); - AddMimeType("AMR", "audio/amr"); - AddMimeType("AWB", "audio/amr-wb"); - AddMimeType("WMA", "audio/x-ms-wma"); - AddMimeType("OGG", "audio/ogg"); - AddMimeType("OGG", "application/ogg"); - AddMimeType("OGA", "application/ogg"); - AddMimeType("AAC", "audio/aac"); - AddMimeType("AAC", "audio/aac-adts"); - AddMimeType("MKA", "audio/x-matroska"); - AddMimeType("MID", "audio/midi"); - AddMimeType("MIDI", "audio/midi"); - AddMimeType("XMF", "audio/midi"); - AddMimeType("RTTTL", "audio/midi"); - AddMimeType("SMF", "audio/sp-midi"); - AddMimeType("IMY", "audio/imelody"); - AddMimeType("RTX", "audio/midi"); - AddMimeType("OTA", "audio/midi"); - AddMimeType("MXMF", "audio/midi"); - - AddMimeType("MPEG", "video/mpeg"); - AddMimeType("MPG", "video/mpeg"); - AddMimeType("MP4", "video/mp4"); - AddMimeType("M4V", "video/mp4"); - AddMimeType("3GP", "video/3gpp"); - AddMimeType("3GPP", "video/3gpp"); - AddMimeType("3G2", "video/3gpp2"); - AddMimeType("3GPP2", "video/3gpp2"); - AddMimeType("MKV", "video/x-matroska"); - AddMimeType("WEBM", "video/webm"); - AddMimeType("TS", "video/mp2ts"); - AddMimeType("AVI", "video/avi"); - AddMimeType("WMV", "video/x-ms-wmv"); - AddMimeType("ASF", "video/x-ms-asf"); - AddMimeType("JPG", "image/jpeg"); - AddMimeType("JPEG", "image/jpeg"); - AddMimeType("GIF", "image/gif"); - AddMimeType("PNG", "image/png"); - AddMimeType("BMP", "image/x-ms-bmp"); - AddMimeType("WBMP", "image/vnd.wap.wbmp"); - AddMimeType("WEBP", "image/webp"); - - AddMimeType("M3U", "audio/x-mpegurl"); - AddMimeType("M3U", "application/x-mpegurl"); - AddMimeType("PLS", "audio/x-scpls"); - AddMimeType("WPL", "application/vnd.ms-wpl"); - AddMimeType("M3U8", "application/vnd.apple.mpegurl"); - AddMimeType("M3U8", "audio/mpegurl"); - AddMimeType("M3U8", "audio/x-mpegurl"); - AddMimeType("FL", "application/x-android-drm-fl"); - AddMimeType("TXT", "text/plain"); - AddMimeType("HTM", "text/html"); - AddMimeType("HTML", "text/html"); - AddMimeType("PDF", "application/pdf"); - AddMimeType("DOC", "application/msword"); - AddMimeType("XLS", "application/vnd.ms-excel"); - AddMimeType("PPT", "application/mspowerpoint"); - AddMimeType("FLAC", "audio/x-flac"); - AddMimeType("FLAC", "audio/flac"); - AddMimeType("ZIP", "application/zip"); - AddMimeType("MPG", "video/mp2p"); - AddMimeType("MPEG", "video/mp2p"); - - } -} - -static MimeTypes staticContruct; - static std::string empty; -const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) +const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const { - MaybeInitialize(); auto iter = extensionToMimeType.find(extension); if (iter == extensionToMimeType.end()) return empty; return iter->second; } -const std::string& MimeTypes::ExtensionFromMimeType(const std::string &mimeType) +const std::string& MimeTypes::ExtensionFromMimeType(const std::string &mimeType) const { - MaybeInitialize(); auto iter = mimeTypeToExtension.find(mimeType); if (iter == mimeTypeToExtension.end()) return empty; return iter->second; @@ -135,27 +58,132 @@ static std::string toLower(const std::string&value) return result; } -void MimeTypes::AddMimeType(const std::string&extension_, const std::string&mimeType) +void MimeTypes::AddMimeType(const std::string&extension_, const std::string&mimeType) { std::string extension = SS("." << toLower(extension_)); + mimeTypeToExtensions[mimeType].insert(extension); mimeTypeToExtension[mimeType] = extension; extensionToMimeType[extension] = mimeType; - if (mimeType.starts_with("audio/")) + if (mimeType.starts_with("audio/midi")) { + midiExtensions.insert(extension); + } else if (mimeType.starts_with("audio/")) + { + mimeTypeToExtensions["audio/*"].insert(extension); audioExtensions.insert(extension); } if (mimeType.starts_with("video/")) { + mimeTypeToExtensions["video/*"].insert(extension); videoExtensions.insert(extension); } } -std::map MimeTypes::mimeTypeToExtension; -std::map MimeTypes::extensionToMimeType; -std::set MimeTypes::audioExtensions; -std::set MimeTypes::videoExtensions; -bool MimeTypes::initialized; +const std::set &MimeTypes::AudioExtensions() const +{ + return audioExtensions; + +} +const std::set &MimeTypes::VideoExtensions() const +{ + return videoExtensions; +} +const std::set &MimeTypes::MidiExtensions() const +{ + return midiExtensions; +} + +static std::mutex m_instanceMutex; +static std::unique_ptr m_instance; + + +MimeTypes::MimeTypes() +{ + AddMimeType("MP3", "audio/mpeg"); + AddMimeType("MPGA", "audio/mpeg"); + AddMimeType("M4A", "audio/mp4"); + AddMimeType("WAV", "audio/x-wav"); + AddMimeType("WAV", "audio/wav"); + AddMimeType("AMR", "audio/amr"); + AddMimeType("AWB", "audio/amr-wb"); + AddMimeType("WMA", "audio/x-ms-wma"); + AddMimeType("OGG", "audio/ogg"); + AddMimeType("OGG", "application/ogg"); + AddMimeType("OGA", "application/ogg"); + AddMimeType("AAC", "audio/aac"); + AddMimeType("AAC", "audio/aac-adts"); + AddMimeType("MKA", "audio/x-matroska"); + AddMimeType("MID", "audio/midi"); + AddMimeType("MIDI", "audio/midi"); + AddMimeType("XMF", "audio/midi"); + AddMimeType("RTTTL", "audio/midi"); + AddMimeType("SMF", "audio/sp-midi"); + AddMimeType("IMY", "audio/imelody"); + AddMimeType("RTX", "audio/midi"); + AddMimeType("OTA", "audio/midi"); + AddMimeType("MXMF", "audio/midi"); + + AddMimeType("MPEG", "video/mpeg"); + AddMimeType("MPG", "video/mpeg"); + AddMimeType("MP4", "video/mp4"); + AddMimeType("M4V", "video/mp4"); + AddMimeType("3GP", "video/3gpp"); + AddMimeType("3GPP", "video/3gpp"); + AddMimeType("3G2", "video/3gpp2"); + AddMimeType("3GPP2", "video/3gpp2"); + AddMimeType("MKV", "video/x-matroska"); + AddMimeType("WEBM", "video/webm"); + AddMimeType("TS", "video/mp2ts"); + AddMimeType("AVI", "video/avi"); + AddMimeType("WMV", "video/x-ms-wmv"); + AddMimeType("ASF", "video/x-ms-asf"); + AddMimeType("JPG", "image/jpeg"); + AddMimeType("JPEG", "image/jpeg"); + AddMimeType("GIF", "image/gif"); + AddMimeType("PNG", "image/png"); + AddMimeType("BMP", "image/x-ms-bmp"); + AddMimeType("WBMP", "image/vnd.wap.wbmp"); + AddMimeType("WEBP", "image/webp"); + + AddMimeType("M3U", "audio/x-mpegurl"); + AddMimeType("M3U", "application/x-mpegurl"); + AddMimeType("PLS", "audio/x-scpls"); + AddMimeType("WPL", "application/vnd.ms-wpl"); + AddMimeType("M3U8", "application/vnd.apple.mpegurl"); + AddMimeType("M3U8", "audio/mpegurl"); + AddMimeType("M3U8", "audio/x-mpegurl"); + AddMimeType("FL", "application/x-android-drm-fl"); + AddMimeType("TXT", "text/plain"); + AddMimeType("HTM", "text/html"); + AddMimeType("HTML", "text/html"); + AddMimeType("PDF", "application/pdf"); + AddMimeType("DOC", "application/msword"); + AddMimeType("XLS", "application/vnd.ms-excel"); + AddMimeType("PPT", "application/mspowerpoint"); + AddMimeType("FLAC", "audio/x-flac"); + AddMimeType("FLAC", "audio/flac"); + AddMimeType("ZIP", "application/zip"); + AddMimeType("MPG", "video/mp2p"); + AddMimeType("MPEG", "video/mp2p"); + +} + +const MimeTypes&MimeTypes::instance() +{ + std::lock_guard lock { m_instanceMutex}; + if (!m_instance) { + m_instance = std::unique_ptr(new MimeTypes()); + } + return (*m_instance); +} + +const bool MimeTypes::IsValidExtension(const std::string&mimeType, const std::string&extension) const +{ + auto iter = mimeTypeToExtensions.find(mimeType); + if (iter == mimeTypeToExtensions.end()) return false; + return iter->second.contains(extension); +} diff --git a/src/MimeTypes.hpp b/src/MimeTypes.hpp index d7fd8c9..f38883d 100644 --- a/src/MimeTypes.hpp +++ b/src/MimeTypes.hpp @@ -25,17 +25,26 @@ namespace pipedal { class MimeTypes { - public: - - static const std::string& MimeTypeFromExtension(const std::string &extension); - static const std::string& ExtensionFromMimeType(const std::string &mimeType); private: - static void AddMimeType(const std::string&extension, const std::string&mimeType); - static std::map mimeTypeToExtension; - static std::map extensionToMimeType; - static std::set audioExtensions; - static std::set videoExtensions; - static void MaybeInitialize(); - static bool initialized; + MimeTypes(); + public: + static const MimeTypes&instance(); + + const std::string& MimeTypeFromExtension(const std::string &extension) const; + // the best possible extension if we had to pick one. + const std::string& ExtensionFromMimeType(const std::string &mimeType) const; + // All possible extensions that other people might have picked. + const std::set &AudioExtensions() const; + const std::set &VideoExtensions() const; + const std::set &MidiExtensions() const; + const bool IsValidExtension(const std::string&mimeType, const std::string&extension) const; + private: + void AddMimeType(const std::string&extension, const std::string&mimeType); + std::map> mimeTypeToExtensions; + std::map mimeTypeToExtension; + std::map extensionToMimeType; + std::set audioExtensions; + std::set midiExtensions; + std::set videoExtensions; }; } \ No newline at end of file diff --git a/src/ModFileTypes.cpp b/src/ModFileTypes.cpp index 9274292..05a24cb 100644 --- a/src/ModFileTypes.cpp +++ b/src/ModFileTypes.cpp @@ -22,38 +22,61 @@ #include #include #include "ss.hpp" +#include "MimeTypes.hpp" using namespace pipedal; -const std::vector ModFileTypes::ModDirectories = + +static std::mutex gModDirectoriesMutex; +static std::unique_ptr< std::vector > gModDirectories; + + +static std::vector *CreateModDirectories() +{ + return new std::vector( { {"audioloop", "shared/audio/Loops", "Loops", {"audio/*"}}, // Audio Loops, meant to be used for looper-style plugins - //"audiorecording","shared/audio/Audio Recordings", {"audio/*"}}, : Audio Recordings, triggered by plugins and stored in the unit + {"audiorecording","shared/audio/Audio Recordings", "Audio Recordings",{"audio/*"}}, // Audio Recordings, triggered by plugins and stored in the unit {"audiosample", "shared/audio/Samples", "Samples", {"audio/*"}}, // One-shot Audio Samples, meant to be used for sampler-style plugins {"audiotrack", "shared/audio/Tracks", "Tracks", {"audio/*"}}, // Audio Tracks, meant to be used as full-performance/song or backtrack {"cabsim", "CabIR", "Cab IRs", {"audio/*"}}, // Speaker Cabinets, meant as small IR audio files - /// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension + {"h2drumkit", "shared/h2drumkit", "Hydrogen Drumkits",{".h2drumkit"}}, // Hydrogen Drumkits, must use h2drumkit file extension" {"ir", "ReverbImpulseFiles", "Impulse Responses", {"audio/*"}}, // Impulse Responses {"midiclip", "shared/midiClips", "MIDI Clips", {".mid", ".midi"}}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension {"midisong", "shared/midiSongs", "MIDI Songs", {".mid", ".midi"}}, // MIDI Songs, meant to be used as full-performance/song or backtrack - {"sf2", "shared/sf2", "Sound Fonts", {"sf2", "sf3"}}, // SF2 Instruments, must have sf2 or sf3 file extension - {"sfz", "shared/sfz", "Sfz Files", {"sfz"}}, // SFZ Instruments, must have sfz file extension + {"sf2", "shared/sf2", "Sound Fonts", {".sf2", ".sf3"}}, // SF2 Instruments, must have sf2 or sf3 file extension + {"sfz", "shared/sfz", "Sfz Files", {".sfz"}}, // SFZ Instruments, must have sfz file extension // extensions observed in the field. - {"audio", "shared/audio", "Audio", {"audio/*"}}, // all audio files (Ratatoille) - {"nammodel", "NeuralAmpModels", "Neural Amp Models", {".nam"}}, // Ratatoille, Mike's NAM. - {"aidadspmodel", "shared/aidaaix", "AIDA IAX Models", {".json", ".aidaiax"}}, // Ratatoille + {"audio", "shared/audio", "Audio", {"audio/*"}}, // all audio files (Ratatouille) + {"nammodel", "NeuralAmpModels", "Neural Amp Models", {".nam"}}, // Ratatouille, Mike's NAM. + {"aidadspmodel", "shared/aidaaix", "AIDA IAX Models", {".json", ".aidaiax"}}, // Ratatouille {"mlmodel", "ToobMlModels", "ML Models", {".json"}}, // - // pipedal-specific types. use pipedal_ui:filetypes instead (or in addition to) mod:fileTypes. - {"recording", "shared/audio/Recordings", "Recordings", {"audio/*"}}, // Recordings from recordings plugins. - // -}; + // For plugins that don't secify a modDirectory (allows upload of arbitrary extension) + {"*", "shared/other", "Other", {".*"}}, // + + }); +} + + + +const std::vector&ModFileTypes::ModDirectories() +{ + std::lock_guard lock { gModDirectoriesMutex }; + + if (!gModDirectories) + { + gModDirectories = std::unique_ptr>(CreateModDirectories()); + } + return *gModDirectories; +} + const ModFileTypes::ModDirectory *ModFileTypes::GetModDirectory(const std::string &type) { - for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories) + for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories()) { if (modType.modType == type) { @@ -78,41 +101,6 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes) fileTypes_.push_back(type); } } - // for Ratatoille.lv2. - // If rootDirectories contains "nammodel" and fileTypes contains "json", add "mlmodel" too. - // if (contains(rootDirectories_,"nammodel") && contains(fileTypes_,"json")) - // { - // if (!contains(rootDirectories_,"mlmodel")) - // { - // rootDirectories_.push_back("mlmodel"); - // } - // } - if (fileTypes_.empty()) - { - for (const auto &type : types) - { - const ModDirectory *wellKnownType = GetModDirectory(type); - if (wellKnownType) - { - for (const std::string &newType : wellKnownType->defaultFileExtensions) - { - bool found = false; - for (const std::string &oldType : fileTypes_) - { - if (newType == oldType) - { - found = true; - break; - } - } - if (!found) - { - fileTypes_.push_back(newType); - } - } - } - } - } } static std::filesystem::path getModDirectoryPath( @@ -132,7 +120,7 @@ void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDir using namespace std; try { - for (const auto &modType : ModDirectories) + for (const auto &modType : ModDirectories()) { fs::path path = rootDirectory / modType.pipedalPath; @@ -158,3 +146,38 @@ void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDir } } +static std::set makeFileExtensions(const std::vector&fileTypes) +{ + std::set result; + + for (const std::string&fileType: fileTypes) + { + if (fileType.starts_with('.')) { + result.insert(fileType); + } else { + if (fileType == "audio/*") + { + const auto&audioExtensions = MimeTypes::instance().AudioExtensions(); + result.insert(audioExtensions.begin(),audioExtensions.end()); + } else if (fileType == "video/*") + { + const auto&videoExtensions = MimeTypes::instance().VideoExtensions(); + result.insert(videoExtensions.begin(),videoExtensions.end()); + } + } + } + return result; +} + +ModFileTypes::ModDirectory::ModDirectory( + const std::string modType, + const std::string pipedalPath, + const std::string displayName, + const std::vector fileTypes) + :modType(modType), + pipedalPath(pipedalPath), + displayName(displayName), + fileTypesX(fileTypes), + fileExtensions(makeFileExtensions(fileTypes)) +{ +} diff --git a/src/ModFileTypes.hpp b/src/ModFileTypes.hpp index bcf49d9..a0f5673 100644 --- a/src/ModFileTypes.hpp +++ b/src/ModFileTypes.hpp @@ -21,33 +21,45 @@ #include #include #include +#include -namespace pipedal { - class ModFileTypes { +namespace pipedal +{ + class ModFileTypes + { + public: - public: - ModFileTypes(const std::string&fileTypes); - const std::vector &rootDirectories() { return rootDirectories_; } - const std::vector &fileTypes() { return fileTypes_;} + ModFileTypes(const std::string &fileTypes); + const std::vector &rootDirectories() const { return rootDirectories_; } + std::vector &rootDirectories() { return rootDirectories_; } + const std::vector &fileTypes() const { return fileTypes_; } + + static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything) private: std::vector rootDirectories_; std::vector fileTypes_; - public: - class ModDirectory { + class ModDirectory + { public: + ModDirectory( + const std::string modType, + const std::string pipedalPath, + const std::string displayName, + const std::vector defaultFileExtensions); const std::string modType; - const std::string pipedalPath; + const std::filesystem::path pipedalPath; const std::string displayName; - const std::vector defaultFileExtensions; + const std::vector fileTypesX; // mixture of .ext and mime types. Use fileExtensions instead. + const std::set fileExtensions; + }; - const static std::vector ModDirectories; - static const ModDirectory* GetModDirectory(const std::string&modType); - - static void CreateDefaultDirectories(const std::filesystem::path&rootDirectory); + static const std::vector &ModDirectories(); + static const ModDirectory *GetModDirectory(const std::string &modType); + static void CreateDefaultDirectories(const std::filesystem::path &rootDirectory); }; } \ No newline at end of file diff --git a/src/ModFileTypesTest.cpp b/src/ModFileTypesTest.cpp new file mode 100644 index 0000000..e5a5b4d --- /dev/null +++ b/src/ModFileTypesTest.cpp @@ -0,0 +1,162 @@ +// Copyright (c) 2022 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 "catch.hpp" +#include +#include "PiPedalUI.hpp" +#include "ModFileTypes.hpp" + +using namespace pipedal; + +std::set CalculateFileExtensions(const std::string &mod__fileTypes, const std::string &modType) +{ + ModFileTypes modFileTypes(mod__fileTypes); + + auto fileProperty = + std::make_shared( + "Test", "urn:test", modFileTypes); + + return fileProperty->GetPermittedFileExtensions(modType); +} + +bool checkFile(const std::string &mod__FileTypes,const std::filesystem::path&path) +{ + ModFileTypes modFileTypes(mod__FileTypes); + + auto fileProperty = + std::make_shared( + "Test", "urn:test", modFileTypes); + + auto fileModDirectory = fileProperty->getParentModDirectory(path); + return fileProperty->IsValidExtension(path); +} + +TEST_CASE("ModFileTypes Test", "[modFileTypes]") +{ + + REQUIRE(checkFile("audio","shared/audio/Test.wav")); + REQUIRE(checkFile("audio","shared/audio/xxx/Test.flac")); + REQUIRE(!checkFile("audio","shared/audio/xxx/Test.nam")); + + REQUIRE(checkFile("audio,wav","shared/audio/Test.wav")); + REQUIRE(!checkFile("audio,wav","shared/audio/xxx/Test.flac")); + REQUIRE(!checkFile("audio","shared/audio/xxx/Test.nam")); + + REQUIRE(checkFile("cabsim,wav","CabIR/Test.wav")); + REQUIRE(!checkFile("cabsim,wav","CabIR/xxx/Test.flac")); + REQUIRE(!checkFile("cabsim","CabIR/xxx/Test.nam")); + + + + REQUIRE(checkFile("audio,wav","shared/audio/Test.wav")); + REQUIRE(!checkFile("audio,wav","shared/audio/xxx/Test.flac")); + REQUIRE(!checkFile("audio","shared/audio/xxx/Test.nam")); + + + REQUIRE(checkFile("wav","shared/audio/Test.wav")); + REQUIRE(!checkFile("wav","CabIR/Test.flac")); // only one audio directory for this case. + REQUIRE(!checkFile("wav","shared/audio/xxx/Test.nam")); + REQUIRE(!checkFile("wav","shared/midiSongs/xxx/Test.mid")); + REQUIRE(!checkFile("wav","shared/other/xxx/Test.mid")); + + + REQUIRE(checkFile("mid","shared/midiClips/Test.mid")); + REQUIRE(checkFile("mid","shared/midiSongs/Test.mid")); + REQUIRE(!checkFile("mid","CabIR/Test.flac")); // only one audio directory for this case. + + + REQUIRE(checkFile("midisong","shared/midiSongs/Test.mid")); + REQUIRE(checkFile("midisong","shared/midiSongs/xxx/Test.midi")); + REQUIRE(!checkFile("midisong","shared/midiClips/xxx/Test.nam")); + + + REQUIRE(checkFile("","shared/audio/Test.wav")); + REQUIRE(checkFile("","shared/audio/xxx/Test.flac")); + REQUIRE(!checkFile("","shared/audio/xxx/Test.nam")); + REQUIRE(!checkFile("","ToobMlModels/Test.nam")); + REQUIRE(checkFile("","ToobMlModels/Test.json")); + REQUIRE(checkFile("","ToobMlModels/a/b/c/Test.json")); + REQUIRE(checkFile("","shared/other/xxx/Test.xwav")); + REQUIRE(checkFile("","shared/other/xxx/Test")); + + + + + { + std::set result = + CalculateFileExtensions("audio", "audio"); + + REQUIRE(result.contains(".wav")); + REQUIRE(result.contains(".flac")); + + } + + { + std::set result = + CalculateFileExtensions("wav", "audio"); + + REQUIRE(result.size() == 1); + REQUIRE(result.contains(".wav")); + REQUIRE(!result.contains(".flac")); + } + { + std::set result = + CalculateFileExtensions("nam", "audio"); + + REQUIRE(result.size() == 0); + REQUIRE(!result.contains(".flac")); + REQUIRE(!result.contains(".nam")); + } + { + // extensions specified, but no modDirectory. + ModFileTypes modFileTypes("mid,midi"); + + auto fileProperty = + std::make_shared( + "Test", "urn:test", modFileTypes); + + REQUIRE(fileProperty->modDirectories().size() == 2); + for (const auto&modDirectory: fileProperty->modDirectories()) + { + auto fileExtensions = fileProperty->GetPermittedFileExtensions(modDirectory); + REQUIRE(fileExtensions.size() == 2); + REQUIRE(fileExtensions.contains(".mid")); + REQUIRE(fileExtensions.contains(".midi")); + } + } + { + // Custom directory, custom extensions. + // actually simulating a PiPedalUI::UiFileProperty load from ttl. + ModFileTypes modFileTypes("wavx"); + + modFileTypes.rootDirectories().push_back("CustomDirectory"); + + auto fileProperty = + std::make_shared( + "Test", "urn:test", modFileTypes); + + REQUIRE(fileProperty->modDirectories().size() == 1); + auto fileExtensions = fileProperty->GetPermittedFileExtensions("CustomDirectory"); + + REQUIRE(fileProperty->modDirectories().size() == 1); + REQUIRE(fileExtensions.size() == 1); + REQUIRE(fileExtensions.contains(".wavx")); + + } +} diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 79b81fb..ffb7d0b 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2309,18 +2309,6 @@ void PiPedalModel::SetSystemMidiBindings(std::vector &bindings) } } -std::vector PiPedalModel::GetFileList(const UiFileProperty &fileProperty) -{ - try - { - return this->storage.GetFileList(fileProperty); - } - catch (const std::exception &e) - { - Lv2Log::warning("GetFileList() failed: (%s)", e.what()); - return std::vector(); // don't disclose to users what the problem is. - } -} FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) { try @@ -2354,15 +2342,44 @@ std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePa std::lock_guard lock(mutex); return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); } -FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty) +FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath) { std::lock_guard lock(mutex); - return storage.GetFilePropertydirectoryTree(uiFileProperty); + return storage.GetFilePropertydirectoryTree(uiFileProperty,selectedPath); } -std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) +UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri) { - return storage.UploadUserFile(directory, patchProperty, filename, stream, contentLength); + + auto pedalboardItems = pedalboard.GetAllPlugins(); + + for (const auto&pedalboardItem: pedalboardItems) { + if (pedalboardItem->instanceId() == instanceId) + { + Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri()); + if (pluginInfo && pluginInfo->piPedalUI()) + { + for (const auto&fileProperty: pluginInfo->piPedalUI()->fileProperties()) + if (fileProperty->patchProperty() == patchPropertyUri) + { + return fileProperty; + } + } + } + } + return nullptr; + throw std::runtime_error("Permission denied. Plugin not currently loaded."); +} + +std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId,const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) +{ + UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId,patchProperty); + if (!fileProperty) + { + Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty)); + throw std::runtime_error("Permission denied."); + } + return storage.UploadUserFile(directory, fileProperty, filename, stream, contentLength); } uint64_t PiPedalModel::CreateNewPreset() diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index f544b99..2613785 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -259,6 +259,7 @@ namespace pipedal PiPedalConfiguration configuration; void CheckForResourceInitialization(Pedalboard &pedalboard); + UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); public: PiPedalModel(); @@ -447,15 +448,14 @@ namespace pipedal void SetFavorites(const std::map &favorites); void SetUpdatePolicy(UpdatePolicyT updatePolicy); void ForceUpdateCheck(); - std::vector GetFileList(const UiFileProperty &fileProperty); FileRequestResult GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty); void DeleteSampleFile(const std::filesystem::path &fileName); 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); + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath); - std::string UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength); + std::string UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength); uint64_t CreateNewPreset(); bool LoadCurrentPedalboard(); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index f1358cb..25cb8d1 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -102,6 +102,23 @@ JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath) JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty) JSON_MAP_END() +class GetFilePropertyDirectoryTreeArgs +{ +public: + UiFileProperty fileProperty_; + std::string selectedPath_; + + DECLARE_JSON_MAP(GetFilePropertyDirectoryTreeArgs); +}; + + +JSON_MAP_BEGIN(GetFilePropertyDirectoryTreeArgs) +JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, fileProperty) +JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, selectedPath) +JSON_MAP_END() + + + class Lv2StateChangedBody { public: @@ -1585,10 +1602,7 @@ public: } else if (message == "requestFileList") { - UiFileProperty fileProperty; - pReader->read(&fileProperty); - std::vector list = this->model.GetFileList(fileProperty); - this->Reply(replyTo, "requestFileList", list); + throw std::runtime_error("No longer implemented."); } else if (message == "requestFileList2") { @@ -1628,9 +1642,12 @@ public: } else if (message == "getFilePropertyDirectoryTree") { - UiFileProperty uiFileProperty; - pReader->read(&uiFileProperty); - FilePropertyDirectoryTree::ptr result = model.GetFilePropertydirectoryTree(uiFileProperty); + GetFilePropertyDirectoryTreeArgs args; + pReader->read(&args); + FilePropertyDirectoryTree::ptr result = + model.GetFilePropertydirectoryTree( + args.fileProperty_, + args.selectedPath_); this->Reply(replyTo, "GetFilePropertydirectoryTree", result); } diff --git a/src/PiPedalUI.cpp b/src/PiPedalUI.cpp index 03cb073..abe46f6 100644 --- a/src/PiPedalUI.cpp +++ b/src/PiPedalUI.cpp @@ -27,6 +27,10 @@ #include "ss.hpp" #include "MimeTypes.hpp" #include "AutoLilvNode.hpp" +#include "ModFileTypes.hpp" +#include +#include "util.hpp" +#include "MimeTypes.hpp" using namespace pipedal; @@ -122,17 +126,20 @@ UiFileType::UiFileType(PluginHost *pHost, const LilvNode *node) } if (fileExtension_ == "") { - fileExtension_ = MimeTypes::ExtensionFromMimeType(mimeType_); + fileExtension_ = MimeTypes::instance().ExtensionFromMimeType(mimeType_); } if (mimeType_ == "") { - mimeType_ = MimeTypes::MimeTypeFromExtension(fileExtension_); + mimeType_ = MimeTypes::instance().MimeTypeFromExtension(fileExtension_); if (mimeType_ == "") { mimeType_ = "application/octet-stream"; } } } + + + UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const std::filesystem::path &resourcePath) { auto pWorld = pHost->getWorld(); @@ -182,6 +189,8 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st throw std::logic_error("PipedalUI::fileProperty: must specify at least a directory."); } + this->modDirectories_.push_back(directory_); + AutoLilvNode patchProperty = lilv_world_get( pWorld, node, @@ -207,6 +216,8 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st this->resourceDirectory_ = resourceDirectory.AsString(); } this->fileTypes_ = UiFileType::GetArray(pHost, node, pHost->lilvUris->pipedalUI__fileTypes); + + PrecalculateFileExtensions(); } std::vector UiFileType::GetArray(PluginHost *pHost, const LilvNode *node, const LilvNode *uri) @@ -271,21 +282,41 @@ bool UiFileProperty::IsDirectoryNameValid(const std::string &value) return true; } -bool UiFileProperty::IsValidExtension(const std::string &extension) const -{ - if (fileTypes_.size() == 0) - { - return true; - } +// bool UiFileProperty::IsValidExtension(const std::filesystem::path&relativePath,const std::string &extension) const +// { +// std::string modDirectory = this->getParentModDirectory(relativePath); +// const auto&validExtensions = GetPermittedFileExtensions(modDirectory); +// return validExtensions.contains(extension); +// } - for (auto &fileType : fileTypes_) +std::string UiFileProperty::GetFileExtension(const std::filesystem::path&path) +{ + if (path.has_extension()) { - if (fileType.fileExtension() == extension) - { - return true; - } + return ToLower(path.extension()); } - return false; + return ""; +} + +bool UiFileProperty::IsValidExtension(const std::filesystem::path&relativePath) const +{ + std::string extension = GetFileExtension(relativePath); + if (this->fileTypes().size() != 0) { + for (const auto&fileType: fileTypes()) + { + UiFileType x; + if (fileType.IsValidExtension(extension)) return true; + } + return false; + } + if (this->modDirectories().size() != 0) + { + std::string modDirectory = this->getParentModDirectory(relativePath); + const auto&validExtensions = GetPermittedFileExtensions(modDirectory); + if (validExtensions.contains(extension)) return true; + return validExtensions.contains(".*"); + } + return true; } UiPortNotification::UiPortNotification(PluginHost *pHost, const LilvNode *node) @@ -343,6 +374,52 @@ UiPortNotification::UiPortNotification(PluginHost *pHost, const LilvNode *node) } } +UiFileProperty::UiFileProperty(const std::string&label, const std::string&patchProperty, const ModFileTypes&modFileTypes) +: label_(label), + patchProperty_(patchProperty), + directory_("") +{ + setModFileTypes(modFileTypes); +} + +void UiFileProperty::setModFileTypes(const ModFileTypes&modFileTypes) +{ + modDirectories_.resize(0); + fileTypes_.resize(0); + for (const std::string &type : modFileTypes.fileTypes()) + { + fileTypes().push_back(UiFileType(SS(type << " file"), SS('.' << type))); + } + + if (modFileTypes.rootDirectories().size() != 0) + { + for (const std::string &rootDirectory : modFileTypes.rootDirectories()) + { + modDirectories().push_back(rootDirectory); + } + } else { + if (fileTypes().size() == 0) + { + // add everything. + for (const auto&modDirectory: split(ModFileTypes::DEFAULT_FILE_TYPES,',')) + { + modDirectories().push_back(modDirectory); + } + } else { + // add only those mod directories that contain one or more of our fileTypes. + for (const auto&modDirectory: split(ModFileTypes::DEFAULT_FILE_TYPES,',')) + { + modDirectories().push_back(modDirectory); + } + } + } + + PrecalculateFileExtensions(); + + +} + + UiFileProperty::UiFileProperty(const std::string &name, const std::string &patchProperty, const std::string &directory) : label_(name), patchProperty_(patchProperty), @@ -362,6 +439,22 @@ PiPedalUI::PiPedalUI( this->fileProperties_ = std::move(fileProperties); } +bool UiFileType::IsValidExtension(const std::string&extension) const { + if (fileExtension_.length() != 0) + { + if (fileExtension_ == ".*") + { + return true; + } + return fileExtension_ == extension; + } + if (mimeType_.length() != 0) + { + return MimeTypes::instance().IsValidExtension(mimeType_,extension); + } + return false; +} + UiFileType::UiFileType(const std::string&label, const std::string &fileType) : label_(label) , fileExtension_(fileType) @@ -369,13 +462,12 @@ UiFileType::UiFileType(const std::string&label, const std::string &fileType) if (fileType.starts_with('.')) { fileExtension_ = fileType; - mimeType_ = MimeTypes::MimeTypeFromExtension(fileType); + mimeType_ = MimeTypes::instance().MimeTypeFromExtension(fileType); if (mimeType_ == "") { mimeType_ = "application/octet-stream"; } } else { - fileExtension_ = MimeTypes::ExtensionFromMimeType(fileType); // (may be blank, esp. for audio/* and video/*. mimeType_ = fileType; } if (mimeType_ == "*") @@ -443,6 +535,157 @@ UiFrequencyPlot::UiFrequencyPlot(PluginHost*pHost, const LilvNode*node, } +static std::set emptySet; + +const std::set& UiFileProperty::GetPermittedFileExtensions(const std::string &modDirectory) const { + auto result = fileExtensionsByModDirectory.find(modDirectory); + if (result != fileExtensionsByModDirectory.end()) + { + return result->second; + } + if (modDirectory == this->directory_) + { + std::set result; + for (auto &x : this->fileTypes_) + { + + } + return emptySet; + } + return emptySet; +} + +void UiFileProperty::PrecalculateFileExtensions() +{ + std::set explicitlySpecifiedExtensions; + bool allAudioTypes = false; + bool allVideoTypes = false; + for (const auto &fileType: this->fileTypes()) + { + if (!fileType.fileExtension().empty()) + { + explicitlySpecifiedExtensions.insert(fileType.fileExtension()); + } else if (fileType.mimeType().length() != 0) + { + if (fileType.mimeType() == "audio/*") + { + allAudioTypes = true; + } else if (fileType.mimeType() == "video/*") + { + allVideoTypes = true; + } else { + // The principle risk being that there is more than one commonly used extension. e.g. midi. + // If the right extension isn't picked, the client should explicitly list which extensions are wanted + // instead of using a mime type. :-/ Not a good implementation, but not a use case that's actually going to + // come up. + std::string extension = MimeTypes::instance().ExtensionFromMimeType(fileType.mimeType()); + if (extension.length() == 0) + { + Lv2Log::warning(SS("Unknown MIME type: " << fileType.mimeType())); + } else { + explicitlySpecifiedExtensions.insert(extension); + } + } + } + } + if (allAudioTypes) + { + const auto&audioExtensions = MimeTypes::instance().AudioExtensions(); + explicitlySpecifiedExtensions.insert( + audioExtensions.begin(),audioExtensions.end() + ); + } + + if (allVideoTypes) + { + const auto&videoExtensions = MimeTypes::instance().VideoExtensions(); + explicitlySpecifiedExtensions.insert( + videoExtensions.begin(),videoExtensions.end() + ); + + } + if (this->modDirectories().size() == 0) // not specified? Add all directories, (and prune later, if we have filetypes). + { + this->modDirectories_ = split(ModFileTypes::DEFAULT_FILE_TYPES,','); + } + if (fileTypes().size() != 0) + { + // delete any modDirectories that do not contain one of the given filetyeps. + auto iter = this->modDirectories_.begin(); + while(iter != this->modDirectories_.end()) + { + const auto *pDirectory = ModFileTypes::GetModDirectory(*iter); + if (pDirectory == nullptr) + { + // a custom directory! How cool is that!? + this->fileExtensionsByModDirectory[*iter] = explicitlySpecifiedExtensions; + ++iter; + } else { + std::set extensions = Intersect(pDirectory->fileExtensions,explicitlySpecifiedExtensions); + if (extensions.size() == 0) + { + iter = modDirectories_.erase(iter); + } else { + this->fileExtensionsByModDirectory[*iter] = std::move(extensions); + ++iter; + } + } + } + } else { + auto iter = this->modDirectories_.begin(); + while(iter != this->modDirectories_.end()) + { + const auto *pDirectory = ModFileTypes::GetModDirectory(*iter); + if (pDirectory) + { + this->fileExtensionsByModDirectory[*iter] = pDirectory->fileExtensions; + } else { + Lv2Log::warning(SS("Unknown Mod Directory: " << *iter)); + } + ++iter; + } + + } +} +// std::set modDirectoryFileTypes = rootModDirectory->fileExtensions; + // if (fileProperty.fileTypes().size() != 0) + // { + // std::set specifiedFileTypes; + + // for (const auto&fileType: fileProperty.fileTypes()) + // { + // if (!fileType.fileExtension().empty()) + // { + + // } + // } + // } + + + +std::string UiFileProperty::getParentModDirectory(const std::filesystem::path&path) const +{ + for (const auto &modDirectory: this->modDirectories_) + { + const auto pModDirectory = ModFileTypes::GetModDirectory(modDirectory); + if (pModDirectory) + { + if (IsChildDirectory(path,pModDirectory->pipedalPath)) + { + return modDirectory; + } + } + } + if (this->directory_.length() != 0) + { + if (IsChildDirectory(path,this->directory_)) + { + return this->directory_; + } + } + return ""; +} + JSON_MAP_BEGIN(UiPortNotification) JSON_MAP_REFERENCE(UiPortNotification, portIndex) diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index da403ae..bad425a 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -29,6 +29,8 @@ #include #include "json.hpp" #include +#include +#include "ModFileTypes.hpp" #define PIPEDAL_UI "http://github.com/rerdavies/pipedal/ui" @@ -83,7 +85,7 @@ namespace pipedal { const std::string& label() const { return label_;} const std::string &fileExtension() const { return fileExtension_; } const std::string &mimeType() const { return mimeType_; } - + bool IsValidExtension(const std::string&extension) const; public: DECLARE_JSON_MAP(UiFileType); @@ -118,13 +120,20 @@ namespace pipedal { std::string resourceDirectory_; std::vector modDirectories_; bool useLegacyModDirectory_= false; + std::map> fileExtensionsByModDirectory; // non-serialized. + void PrecalculateFileExtensions(); public: using ptr = std::shared_ptr; UiFileProperty() { } UiFileProperty(PluginHost*pHost, const LilvNode*node, const std::filesystem::path&resourcePath); - UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory); + UiFileProperty(const std::string&label, const std::string&patchProperty,const std::string &directory); + UiFileProperty( + const std::string&label, + const std::string&patchProperty, + const ModFileTypes&modFileType); + + void setModFileTypes(const ModFileTypes&modFileType); - std::vector& modDirectories() { return modDirectories_; } const std::vector& modDirectories() const { return modDirectories_; } @@ -144,11 +153,19 @@ namespace pipedal { std::vector &fileTypes() { return fileTypes_; } const std::string &patchProperty() const { return patchProperty_; } - bool IsValidExtension(const std::string&extension) const; + + bool IsValidExtension(const std::filesystem::path&relativePath) const; + static bool IsDirectoryNameValid(const std::string&value); + static std::string GetFileExtension(const std::filesystem::path&path); + const std::string&resourceDirectory() const { return resourceDirectory_; } + + const std::set& GetPermittedFileExtensions(const std::string &modDirectory) const; + + std::string getParentModDirectory(const std::filesystem::path&path) const; public: DECLARE_JSON_MAP(UiFileProperty); }; @@ -202,6 +219,8 @@ namespace pipedal { return frequencyPlots_; } + + const std::vector &portNotifications() const { return portNotifications_; } const UiFileProperty*GetFileProperty(const std::string &propertyUri) const @@ -215,14 +234,16 @@ namespace pipedal { } return nullptr; } - + bool unsupportedPatchProperty() const { return unsupportedPatchProperty_; } + void unsupportedPatchProperty(bool value) { unsupportedPatchProperty_ = value; } private: + bool unsupportedPatchProperty_ = false; // not serialized. std::vector fileProperties_; std::vector frequencyPlots_; std::vector portNotifications_; }; - // Utiltities for validating file paths received via PiPedalFileProperty-related APIs. + // utilities for validating file paths received via PiPedalFileProperty-related APIs. bool IsAlphaNumeric(const std::string&value); diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 03e3876..8391cf9 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -195,7 +195,6 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format"); mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes"); - pipedalui__fileTypes =lilv_new_uri(pWorld, PIPEDAL_UI__fileTypes); } void PluginHost::LilvUris::Free() @@ -367,7 +366,7 @@ void PluginHost::LoadPluginClassesFromLilv() LILV_FOREACH(plugin_classes, iPluginClass, classes) { const LilvPluginClass *pClass = lilv_plugin_classes_get(classes, iPluginClass); - // Get it to prepopulate the map. + // Get it to pre-populate the map. MakePluginClass(pClass); } for (auto value : classesMap) @@ -432,6 +431,9 @@ void PluginHost::Load(const char *lv2Path) if (pluginInfo->hasCvPorts()) { Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str()); + } else if (pluginInfo->hasUnsupportedPatchProperties()) + { + Lv2Log::debug("Plugin %s (%s) skipped. (Has unsupported patch parameters).", pluginInfo->name().c_str(), pluginInfo->uri().c_str()); } #if !SUPPORT_MIDI else if (pluginInfo->plugin_class() == LV2_MIDI_PLUGIN) @@ -481,10 +483,15 @@ void PluginHost::Load(const char *lv2Path) for (auto plugin : this->plugins_) { + pluginsByUri[plugin->uri()] = plugin; + Lv2PluginUiInfo info(this, plugin.get()); if (plugin->is_valid()) { #if 1 + // no plugins with more than 2 inputs or outputs. + // no zero-input or zero-output plugins (temporarily disables midi plugins) + // no zero output devices (permanent, I think) if (info.audio_inputs() > 2 || info.audio_outputs() > 2) { Lv2Log::debug( "Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(), @@ -598,7 +605,8 @@ bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plu return result; } -std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin) +Lv2PluginInfo::FindWritablePathPropertiesResult +Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin) { // example: @@ -615,6 +623,7 @@ std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost std::vector fileProperties; + bool unsupportedPatchProperty = false; LILV_FOREACH(nodes, iNode, patchWritables) { AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode); @@ -631,99 +640,91 @@ std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr); std::string strLabel = label.AsString(); - if (strLabel.length() != 0) + if (strLabel.length() == 0) { - std::filesystem::path path = this->bundle_path(); - path = path.parent_path(); - std::string lv2DirectoryName = path.filename().string(); - // we have a valid path property! - - auto fileProperty = - std::make_shared( - strLabel, propertyUri.AsUri(), lv2DirectoryName); - - - AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr); - int32_t index = indexNode.AsInt(-1); - fileProperty->index(index); - - - // if there's a pipedalui_fileTypes node, use that instead. - - AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->pipedalui__fileTypes, nullptr); - if (!mod__fileTypes) - { - mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); - } - - LILV_FOREACH(nodes, i, mod__fileTypes) - { - // "nam,nammodel" - AutoLilvNode lilvfileType{lilv_nodes_get(mod__fileTypes, i)}; - std::string fileTypes = lilvfileType.AsString(); - ModFileTypes modFileTypes(fileTypes); - - for (const std::string &rootDirectory : modFileTypes.rootDirectories()) - { - fileProperty->modDirectories().push_back(rootDirectory); - } - for (const std::string &type : modFileTypes.fileTypes()) - { - fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"), SS('.' << type))); - } - // Legacy case: audio_uploads/ exists. - - std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename(); - std::filesystem::path legacyUploadPath = lv2Host->MapPath(bundleDirectoryName.string()); - - if (std::filesystem::exists(legacyUploadPath)) - { - if (!std::filesystem::exists(legacyUploadPath / ".migrated")) - { - fileProperty->useLegacyModDirectory(true); - fileProperty->directory(bundleDirectoryName); - } - } - if (fileProperty->modDirectories().size() == 0) - { - fileProperty->directory(bundleDirectoryName); - } - else if (fileProperty->modDirectories().size() == 1 && !fileProperty->useLegacyModDirectory()) // no synthetic root. - { - auto modType = ModFileTypes::GetModDirectory(fileProperty->modDirectories()[0]); - if (modType) - { - fileProperty->directory(modType->pipedalPath); - } - } else { - // handled at request time. - } - } - if (!mod__fileTypes) - { - AutoLilvNodes dc_types = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->dc__format, nullptr); - LILV_FOREACH(nodes, i, dc_types) - { - AutoLilvNode dc_type = lilv_nodes_get(dc_types, i); - std::string fileType = dc_type.AsString(); - std::string label = ""; - fileProperty->fileTypes().push_back(UiFileType(label, fileType)); - } - } - - fileProperties.push_back( - fileProperty); + strLabel = "File"; } + + std::filesystem::path path = this->bundle_path(); + path = path.parent_path(); + std::string lv2DirectoryName = path.filename().string(); + // we have a valid path property! + + auto fileProperty = + std::make_shared( + strLabel, propertyUri.AsUri(), lv2DirectoryName); + + + AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr); + int32_t index = indexNode.AsInt(-1); + fileProperty->index(index); + + + // if there's a pipedalui_fileTypes node, use that instead. + + + AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); + + // default: everything. + std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES; + if (mod__fileTypes) + { + // "nam,nammodel" + fileTypes = mod__fileTypes.AsString(); + } + ModFileTypes modFileTypes(fileTypes); + + + + // Legacy case: audio_uploads/ exists. + + std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename(); + std::filesystem::path legacyUploadPath = lv2Host->MapPath(bundleDirectoryName.string()); + + if (std::filesystem::exists(legacyUploadPath)) + { + if (!std::filesystem::exists(legacyUploadPath / ".migrated")) + { + fileProperty->useLegacyModDirectory(true); + fileProperty->directory(bundleDirectoryName); + } + modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory! + } + fileProperty->setModFileTypes(modFileTypes); + + fileProperty->directory(bundleDirectoryName); + + fileProperties.push_back(fileProperty); + } else { + unsupportedPatchProperty = true; } } } } + FindWritablePathPropertiesResult result; + + + if (fileProperties.size() != 0) { - return std::make_shared(std::move(fileProperties)); - } + std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) { + // properies with indexes first. + int32_t indexL = left->index(); + if (indexL < 0) indexL = std::numeric_limits::max(); + int32_t indexR = right->index(); + if (indexR < 0) indexR = std::numeric_limits::max(); + if (indexL < indexR) return true; + if (indexL > indexR) return false; - return std::shared_ptr(); + // there is no natural order. TTL indexing means that the order we see them in is random. + // We can't order them sensibly. Let's at least order them consistently. + return left->label() < right->label(); + + }); + result.pipedalUi = std::make_shared(std::move(fileProperties)); + } + result.hasUnsupportedPatchProperties = unsupportedPatchProperty; + return result; } Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvPlugin *pPlugin) @@ -837,7 +838,9 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP else { // look for - this->piPedalUI_ = FindWritablePathProperties(lv2Host, pPlugin); + auto result = FindWritablePathProperties(lv2Host, pPlugin); + this->piPedalUI_ = result.pipedalUi; + this->hasUnsupportedPatchProperties_ = result.hasUnsupportedPatchProperties; } int nInputs = 0; @@ -1220,14 +1223,12 @@ std::shared_ptr PluginHost::GetLv2PluginClass() const std::shared_ptr PluginHost::GetPluginInfo(const std::string &uri) const { - for (auto i = this->plugins_.begin(); i != this->plugins_.end(); ++i) + auto ff = pluginsByUri.find(uri); + if (ff == pluginsByUri.end()) { - if ((*i)->uri() == uri) - { - return (*i); - } + return nullptr; } - return nullptr; + return ff->second; } Lv2Pedalboard *PluginHost::CreateLv2Pedalboard(Pedalboard &pedalboard, Lv2PedalboardErrorList &errorMessages) diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 35ef415..ec99211 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -346,7 +346,11 @@ namespace pipedal bool isSplit() const ; private: - std::shared_ptr FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin); + struct FindWritablePathPropertiesResult { + std::shared_ptr pipedalUi; + bool hasUnsupportedPatchProperties = false; + }; + FindWritablePathPropertiesResult FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin); bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin); std::string bundle_path_; @@ -369,6 +373,7 @@ namespace pipedal std::vector> port_groups_; bool is_valid_ = false; PiPedalUI::ptr piPedalUI_; + bool hasUnsupportedPatchProperties_ = false; bool IsSupportedFeature(const std::string &feature) const; @@ -391,6 +396,7 @@ namespace pipedal LV2_PROPERTY_GETSET(port_groups) LV2_PROPERTY_GETSET(has_factory_presets) LV2_PROPERTY_GETSET(piPedalUI) + LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties) const Lv2PortInfo &getPort(const std::string &symbol) { @@ -779,6 +785,7 @@ namespace pipedal void free_world(); std::vector> plugins_; + std::map> pluginsByUri; std::vector ui_plugins_; std::map> classesMap; diff --git a/src/Storage.cpp b/src/Storage.cpp index f60201f..e3a6e14 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -34,6 +34,9 @@ #include "ss.hpp" #include "ofstream_synced.hpp" #include "ModFileTypes.hpp" +#include +#include +#include "util.hpp" using namespace pipedal; namespace fs = std::filesystem; @@ -1388,7 +1391,7 @@ uint64_t Storage::SavePluginPreset( void Storage::UpdatePluginPresets(const PluginUiPresets &pluginPresets) { // handles deletions, renaming, and reordering only. - // If you need to add a preset, you neet to call SavePluginPreset or DulicatePluginPreset instead. + // If you need to add a preset, you need to call SavePluginPreset or DuplicatePluginPreset instead. PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_); PluginPresets newPresets; @@ -1623,51 +1626,6 @@ static void ThrowPermissionDeniedError() throw std::logic_error("Permission denied."); } -std::vector Storage::GetFileList(const UiFileProperty &fileProperty) -{ - if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory())) - { - ThrowPermissionDeniedError(); - } - - std::vector 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(); - 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(path); - } - } - } - } - } - catch (const std::exception &error) - { - throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string()); - } - } - - // sort lexicographically - auto collator = Locale::GetInstance()->GetCollator(); - - std::sort(result.begin(), result.end(), [&collator](const std::string &left, const std::string &right) - { return collator->Compare(left, right) < 0; }); - return result; -} static bool ensureNoDotDot(const std::filesystem::path &path) { @@ -1695,8 +1653,10 @@ static bool ensureNoDotDot(const std::filesystem::path &path) return true; } + static void AddFilesToResult( FileRequestResult &result, + const ModFileTypes::ModDirectory *modDirectoryInfo, //yyx const UiFileProperty &fileProperty, const fs::path &rootPath) { @@ -1705,7 +1665,12 @@ static void AddFilesToResult( return; // silently without error. } auto &resultFiles = result.files_; - try + + std::set validExtensions = fileProperty.GetPermittedFileExtensions( + modDirectoryInfo? modDirectoryInfo->modType: ""); + + + try { for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath)) { @@ -1713,9 +1678,15 @@ static void AddFilesToResult( auto name = path.filename().string(); if (dir_entry.is_regular_file()) { + std::string extension = UiFileProperty::GetFileExtension(path); + + bool match = false; if (name.length() > 0 && name[0] != '.') // don't show hidden files. { - if (fileProperty.IsValidExtension(path.extension().string())) + bool match = validExtensions.size() == 0 || validExtensions.contains(extension) + || validExtensions.contains(".*"); + + if (match) { resultFiles.push_back( FileEntry(path, name, false, false)); @@ -1778,6 +1749,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons result.breadcrumbs_.push_back({"", "Home"}); fs::path fsRelativePath{relativePath}; + const ModFileTypes::ModDirectory *rootModDirectory = nullptr; for (const auto &modDirectory : fileProperty.modDirectories()) { const ModFileTypes::ModDirectory *modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); @@ -1785,6 +1757,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons { if (isSubdirectory(fsRelativePath, uploadsDirectory / modDirectoryInfo->pipedalPath)) { + rootModDirectory = modDirectoryInfo; modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; result.breadcrumbs_.push_back({modDirectoryPath.string(), modDirectoryInfo->displayName}); break; @@ -1804,12 +1777,12 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons } } - // add remaing path segements as breadcrumbs. + // add reaming path segments as breadcrumbs. { fs::path modPath{modDirectoryPath}; fs::path rp{relativePath}; auto iRp = rp.begin(); - // skip past one or more segments in the modDiretoryPath. + // skip past one or more segments in the modDirectoryPath. for (auto iModPath = modPath.begin(); iModPath != modPath.end(); ++iModPath) { if (iRp != rp.end()) @@ -1824,7 +1797,8 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons } } - AddFilesToResult(result, fileProperty, relativePath); + + AddFilesToResult(result,rootModDirectory, fileProperty, relativePath); result.currentDirectory_ = relativePath; return result; } @@ -1911,7 +1885,14 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const { throw std::runtime_error(SS("Improper location. " << absolutePath)); } - AddFilesToResult(result, fileProperty, absolutePath); + + const ModFileTypes::ModDirectory*pModDirectory = nullptr; + if (fileProperty.modDirectories().size() > 0) + { + pModDirectory = ModFileTypes::GetModDirectory(fileProperty.modDirectories()[0]); + } + + AddFilesToResult(result,pModDirectory, fileProperty, absolutePath); return result; } @@ -2005,7 +1986,10 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co } return result; } -std::string Storage::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) +std::string Storage::UploadUserFile(const std::string &directory, + UiFileProperty::ptr uiFileProperty, + const std::string &filename, + std::istream &stream, size_t contentLength) { std::filesystem::path path; if (directory.length() != 0) @@ -2013,14 +1997,23 @@ std::string Storage::UploadUserFile(const std::string &directory, const std::str path = this->MakeUserFilePath(directory, filename); } else - { - if (patchProperty.length() == 0) - { - throw std::logic_error("Malformed request."); - } - throw std::logic_error("patchProperty directory not implemented."); + throw std::logic_error("Directory argument not supplied."); } + fs::path relativePath; + try { + relativePath = MakeRelativePath(path,this->GetPluginUploadDirectory()); + } catch (const std::exception& e) + { + throw std::logic_error("Permission denied. Path is outside the upload storage directory."); + } + + if (!uiFileProperty->IsValidExtension(relativePath)) + { + throw std::logic_error("Permission denied. Invalid file extension for this directory."); + } + + { try { @@ -2138,24 +2131,84 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree *node, const std return collator->Compare(left->directoryName_, right->directoryName_) < 0; }); } -FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty) + + + +static void GetAllExtensions(std::set&result, const std::filesystem::path &path) +{ + assert(fs::is_directory(path)); + + for (const auto&dirEnt : fs::directory_iterator(path)) + { + if (dirEnt.is_directory()) + { + GetAllExtensions(result,dirEnt.path()); + } else { + std::string filename = dirEnt.path().filename(); + if (!filename.starts_with('.')) + { + if (dirEnt.path().has_extension()) + { + std::string extension = dirEnt.path().extension(); + result.insert(extension); + } + } + } + } +} + +static std::set GetAllExtensions(const std::filesystem::path &path) +{ + std::set result; + if (fs::is_regular_file(path)) { + result.insert(UiFileProperty::GetFileExtension(path)); + } else { + GetAllExtensions(result,path); + } + return result; +} +FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::filesystem::path&selectedPath) { fs::path uploadDirectory = this->GetPluginUploadDirectory(); + fs::path relativePath; + try { + relativePath = MakeRelativePath(selectedPath,uploadDirectory); + } catch (const std::exception&e) { + // not an upload directory. + throw std::runtime_error(SS("Permission denied: " << selectedPath)); + } + + std::set fileExtensions = GetAllExtensions(selectedPath); + + if (hasSyntheticModRoot(uiFileProperty)) { + + if (relativePath.empty()) + { + throw std::runtime_error(SS("Permission denied: " << selectedPath)); + } FilePropertyDirectoryTree::ptr result = std::make_unique("", "Home"); result->isProtected_ = true; + std::string parentModDirectory = uiFileProperty.getParentModDirectory(relativePath); + for (const auto &modDirectory : uiFileProperty.modDirectories()) { auto modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); if (modDirectoryInfo) { - auto childPath = uploadDirectory / modDirectoryInfo->pipedalPath; - FilePropertyDirectoryTree::ptr child = std::make_unique( - childPath, modDirectoryInfo->displayName); - FillSampleDirectoryTree(child.get(), childPath); - result->children_.push_back(std::move(child)); + if (IsSubset(fileExtensions,modDirectoryInfo->fileExtensions) || + modDirectoryInfo->fileExtensions.contains(".*") || + modDirectoryInfo->fileExtensions.empty() // Private directory that accepts files of any type. + ) + { + auto childPath = uploadDirectory / modDirectoryInfo->pipedalPath; + FilePropertyDirectoryTree::ptr child = std::make_unique( + childPath, modDirectoryInfo->displayName); + FillSampleDirectoryTree(child.get(), childPath); + result->children_.push_back(std::move(child)); + } } } if (uiFileProperty.useLegacyModDirectory()) diff --git a/src/Storage.hpp b/src/Storage.hpp index 810e128..72220cf 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -37,6 +37,7 @@ namespace pipedal { class UiFileProperty; class Lv2PluginInfo; +class UiFileProperty; class CurrentPreset { public: @@ -153,7 +154,6 @@ public: void MoveBank(int from, int to); int64_t DeleteBank(int64_t bankId); - std::vector GetFileList(const UiFileProperty&fileProperty); FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); FileRequestResult GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty); @@ -222,13 +222,14 @@ public: void SetSystemMidiBindings(const std::vector&bindings); std::vector GetSystemMidiBindings(); void DeleteSampleFile(const std::filesystem::path &fileName); - std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,std::istream&stream, size_t contentLength); + std::string UploadUserFile(const std::string &directory, + std::shared_ptr uiFileProperty ,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); + FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); }; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 59c8337..c42488c 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -550,11 +550,21 @@ public: res.set(HttpField::content_type, "application/json"); res.set(HttpField::cache_control, "no-cache"); - std::string instanceId = request_uri.query("id"); + std::string instanceIdString = request_uri.query("id"); std::string directory = request_uri.query("directory"); std::string filename = request_uri.query("filename"); std::string patchProperty = request_uri.query("property"); + int64_t instanceId; + { + std::istringstream ss { instanceIdString}; + ss >> instanceId; + if (!ss) + { + throw PiPedalException("Invalid instanceID"); + } + } + if (patchProperty.length() == 0 && directory.length() == 0) { throw PiPedalException("Malformed request."); @@ -588,7 +598,7 @@ public: if (extensionChecker.IsValidExtension(extension)) { auto si = zipFile->GetFileInputStream(inputFile); - std::string path = this->model->UploadUserFile(directory, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); + std::string path = this->model->UploadUserFile(directory, instanceId,patchProperty, inputFile, si, zipFile->GetFileSize(inputFile)); } } } @@ -606,7 +616,7 @@ public: } else { - outputFileName = this->model->UploadUserFile(directory, patchProperty, filename, req.get_body_input_stream(), req.content_length()); + outputFileName = this->model->UploadUserFile(directory, instanceId,patchProperty, filename, req.get_body_input_stream(), req.content_length()); } if (outputFileName.is_relative()) diff --git a/src/lv2ext/pipedal.lv2/ext/fileBrowser.h b/src/lv2ext/pipedal.lv2/ext/fileBrowser.h new file mode 100644 index 0000000..60c6db4 --- /dev/null +++ b/src/lv2ext/pipedal.lv2/ext/fileBrowser.h @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2023 Robin E. R. Davies + * All rights reserved. + + * 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. + */ + +#ifndef FILEBROWSERFILES_H +#define FILEBROWSERFILES_H + +#define LV2_FILEBROWSER_URI "http://two-play.com/ns/ext/fileBrowser" +#define LV2_FILEBROWSER_PREFIX LV2_FILEBROWSER_URI "#" +#define LV2_FILEBROWSER__files LV2_FILEBROWSER_PREFIX "files" ///< http://two-play.com/ns/ext/fileBrowser#files > + +/** + @defgroup filedialog LV2_FileBrowser_Files + @ingroup lv2 + + Because PiPedal is accessed remotely through a web interface, audio and model files + have to be uploaded into standardized directories using the Upload File button, found in + the File Property Dialog. Plugins that run locally can place sample and model files + more casually, since users can browse anywhere on their computer using the system + file dialog. + + This LV2 Extension allows plugin developers to determine the location of well-known directory. + + The extension addresses three primary uses-cases: + + - Allow plugins to published factory-installed resource files that can be + selected using the Patch Property file dialog. Typically. Source files would be + stored in the bundle directory of the plugin at install time. On first use, the + plugin would create symlinks in the well-known directory that reference files in the + bundle directory. + + - Allow plugins to reference factory-installed resource files that can be selected using + the Patch Property file dialog. + + - Allow recording plugins to locate the directory in which new files should be + placed. + + The plugin recognizes the following well-known directory locations, which are based on MOD fileType + conventions. + + "audioloop" + Audio Loops, meant to be used for looper-style plugins. + "audiorecording", + Audio Recordings, triggered by plugins and stored in the unit + "audiosample", + One-shot Audio Samples, meant to be used for sampler-style plugins + "audiotrack" + Audio Tracks, meant to be used as full-performance/song or backtrack + "cabsim" + Impulse response files for convolution-based Cabinet response simulation. + "h2drumkit" + Hydrogen Drumkits, must use h2drumkit file extension. Not currently supported by PiPedal. + "ir" + Impulse response files of arbitrary size. May be either CAB IR files, or Reverbe IR files. + "midiclip" + MIDI Clips", {".mid", ".midi"}}, // MIDI Clips, to be used in sync with + host tempo, must have mid or midi file extension + "midisong" + MIDI Songs", {".mid", ".midi"}}, meant to be used as + full-performance/song or backtrack. + "sf2" + SF2 Instruments, must have sf2 or sf3 file extension + "sfz" + SFZ Instruments, must have sfz file extension + "audio" + Parent directory of all audio file directories. Not officially documented + by MOD, but has been observed in the field. + "nammodel" + .nam files: Nueral amp model's for Steven Atkin's Neural Amp Modeler library. + "aidadspmodel" + AIDA IAX model files. (file extensions of ".json") + "mlmodel" + Model files for plugins based on the ML neural net library. (file + extensions of ".json"). TooB ML, Proteus, probably others. + "~" + References a directory that is for private use by only the current plugin. + Users can upload and select files in the private directory when using + the current plugin, but no other plugins can access the files. (Not + supported by MOD). + + + Methods are are safe on the realtime thread, as they perform file i/o and allocate memory. This feature should be used + at initialization time, in the call to Activate, or else on a scheduler thread. + + @{ +*/ + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + LV2_FileBrowser_Status_Success = 0, + LV2_FileBrowser_Status_Err_InvalidParameter = 1, + LV2_FileBrowser_Status_Err_Filesystem = 2, +} LV2_FileBrowser_Status; + +typedef void* + LV2_FileBrowser_Files_Handle; ///< Opaque handle for pipedal:uploadPath feature + + +/** + Feature data for filedialog:files (@ref LV2_FILEBROWSER_file). +*/ +typedef struct { + /** + Opaque host data. + */ + LV2_FileBrowser_Files_Handle handle; + + /** + Publish a resource file provided by the plugin to a well-known directory location. + + @param handle MUST be the `handle` member of this struct. + + @param version The version number of the resource file. + + @param resourcePath The source filename./ + + @param uploadDirectory The target filename. + + @return LV2_FileBrowser_Status Zero if successful. + + The version number is the version number of the resource file(s). The host keeps track of which + resource files have previously been installed, and the version thereof. Files will only be + published once. This allows users to delete published files if they want to; and allows + upgraded plugins to write upgraded resource files. + + `resourcePath` is specified relative to the plugin's bundle directory. You can supply an + absolute path if you want, but it's generally better to place resource files in the plugin's + bundle, and specify the location with a relative path. If the resourcePath specifies a file, + only the file will be copied; if the the path specifies a directory, all files and subdirectories + will be published. + + `uploadDirectory` most be a relative path of a directory, and the first segment of the path must be one of + the well-known directory locations. + + A return code of LV2_FileBrowser_Status_Err_InvalidParameter indicates + either that the well-known directory was not found, or that an absolute + path was provided where a relative path was expected. + LV2_FileBrowser_Status_Err_Filesystem will be return if some kind of + catastrophic filesystem error occurred (out of disk space, perhaps, or + permission denied). + + Example: + [code] + // becomes /usr/lib/lv2/ToobAmp.lv2/namFactoryModels/FenderBlackface.nam (for example) + const char*sourceFile = "namFactoryModels/Fender Blackface.nam" + + // /becomes /var/pipedal/audio_uploads/NeuralAmpModels/TooB NAM (for example) + const char(*targetDirectory = "namModel/TooB NAM") + + Lv2_FileBrowserStatus ec = + feature.publish_resource_files(feature.handle,1,sourceFile,targetDirectory); + + // on success creates a symlink at + // /var/pipedal/audio_uploads/NeuralAmpModels/TooB NAM/Fender Blackface.nam + [/code] + */ + + LV2_FileBrowser_Status (*publish_resource_files)( + LV2_FileBrowser_Files_Handle handle, + uint32_t version, + const char*resourcePath, + const char*uploadDirectory); + + /** + ccc + + @param handle MUST be the `handle` member of this struct. + + @param path A relative path, starting with a well-known directory name. + + @return The absolute path to use for the new directory. + + This function can be used by plugins to get the path of files or directories + that can be browsed by file dialogs. get_filebrowser_path, unlike + map_path() does not create or modify files or directories in any way. + + The path can have multiple segments, but the first segment of the path must be + the name of a well-known directory. + + The caller must free memory for the returned value with + LV2_FileBrowser_Files.free_path(). + + Example: + [code] + const char *filePath = feature.get_upload_path(feature.handle,"audio/Tracks/Xmas/Frosty.wav"); + // returns (for example) + // /var/pipedal/audio_uploads/shared/audio/Tracks/Xmas/Frosty.wav + [/code] + */ + char* (*get_upload_path)(LV2_FileBrowser_Files_Handle handle, const char* path); + + /** + Map resource bundle filenames to upload directories. + @param handle MUST be the `handle` member of this struct. + @param path File path name + @param fileBrowserDirectory relative directory of the file browser directory in which to create a link. + @return The absolute pathname of a file in the resource directory. + + The primary purpose of this method is to map file references to files in the plugin's bundle + directory to corresponding files in the a file browser directory. + + If the path is relative, the full path is resolved using the `rootResourceDirectory` path. + + If the full path is not a child of rootResourceDirectory or if the path is relative, + get_upload_path returns the supplied path unmodified. +` + If the filename is a child of rootResourceDirectory, creates a corresponding link to the file in the file browser + directory and returns the path of that link. + + The caller must free memory for the returned value with `LV2_FileBrowser_Files.free_path()`. + + Example: + */ + char* (*map_path)(LV2_FileBrowser_Files_Handle handle, const char* path, const char*rootResourceDirectory, const char*fileBrowserDirectory); + + + /** + Free a path returned by LV2_FileBrowser_Files.get_upload_path() or LV2_FileBrowser_Files.map_path() + @param handle MUST be the `handle` member of this struct. + @param path A path previously returned by a method of `LV2_FileBrowser_Files`. + + */ + void (*free_path)(LV2_FileBrowser_Files_Handle handle, char* path); +} LV2_FileBrowser_Files; + + + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // FILEBROWSERFILES_H diff --git a/todo.txt b/todo.txt index 999bc0f..c13afca 100644 --- a/todo.txt +++ b/todo.txt @@ -14,6 +14,8 @@ - Feature for directory locations. +- center ellipsis on file properties. + - State of MIDI plugins: - plugin routing is implemented. (maybe needs a not-cheating UI) - MIDI message routing is not.