diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 588dca8..405b6e2 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -51,6 +51,22 @@ namespace pipedal { } + template + inline bool contains(const std::vector&vector,const T&value) + { + for (auto i = vector.begin(); i != vector.end(); ++i) + { + if ((*i) == value) { + return true; + } + } + return false; + } + inline bool contains(const std::vector&vector,const char*value) + { + return contains(vector,std::string(value)); + } + class NoCopy { public: NoCopy() { } diff --git a/react/src/FilePropertyDialog.tsx b/react/src/FilePropertyDialog.tsx index 885a210..d36112d 100644 --- a/react/src/FilePropertyDialog.tsx +++ b/react/src/FilePropertyDialog.tsx @@ -17,6 +17,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + import React from 'react'; import { Theme, createStyles } from '@mui/material/styles'; import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; @@ -25,7 +26,7 @@ import Divider from '@mui/material/Divider'; import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; import MoreIcon from '@mui/icons-material/MoreVert'; -import { PiPedalModel, PiPedalModelFactory, FileEntry } from './PiPedalModel'; +import { PiPedalModel, PiPedalModelFactory, FileEntry, BreadcrumbEntry, FileRequestResult } from './PiPedalModel'; import { isDarkMode } from './DarkMode'; import Button from '@mui/material/Button'; import FileUploadIcon from '@mui/icons-material/FileUpload'; @@ -61,7 +62,7 @@ const styles = (theme: Theme) => createStyles({ }, }); -const audioFileExtensions: {[name: string]: boolean} = { +const audioFileExtensions: { [name: string]: boolean } = { ".wav": true, ".flac": true, ".ogg": true, @@ -88,22 +89,23 @@ function isAudioFile(filename: string) { return audioFileExtensions[extension] !== undefined; } -export interface FilePropertyDialogProps extends WithStyles { +export interface FilePropertyDialogProps extends WithStyles { open: boolean, fileProperty: UiFileProperty, selectedFile: string, onOk: (fileProperty: UiFileProperty, selectedItem: string) => void, onCancel: () => void }; - export interface FilePropertyDialogState { fullScreen: boolean; selectedFile: string; selectedFileIsDirectory: boolean; - navDirectory: string; + selectedFileProtected: boolean; hasSelection: boolean; canDelete: boolean; - fileEntries: FileEntry[]; + fileResult: FileRequestResult; + navDirectory: string; + isProtectedDirectory: boolean; columns: number; columnWidth: number; openUploadFileDialog: boolean; @@ -114,19 +116,22 @@ export interface FilePropertyDialogState { moveDialogOpen: boolean; }; -function pathExtension(path: string) -{ +function pathExtension(path: string) { let dotPos = path.lastIndexOf('.'); if (dotPos === -1) return ""; let slashPos = path.lastIndexOf('/'); - if (slashPos !== -1) - { - if (dotPos <= slashPos+1) return ""; + if (slashPos !== -1) { + if (dotPos <= slashPos + 1) return ""; } return path.substring(dotPos); // include the '.'. } +function pathParentDirectory(path: string) { + let npos = path.lastIndexOf('/'); + if (npos === -1) return ""; + return path.substring(0, npos); +} function pathConcat(left: string, right: string) { if (left === "") return right; if (right === "") return left; @@ -169,683 +174,725 @@ export function pathFileName(path: string): string { export default withStyles(styles, { withTheme: true })( class FilePropertyDialog extends ResizeResponsiveComponent { - getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { - - // would be easier if we had the data root (/var/pipedal/audio_downloads, but could be different when we're debugging. :-/ - let nPos = selectedFile.indexOf("/" + fileProperty.directory + "/"); - if (nPos === -1) return ""; - let relativePath = selectedFile.substring(nPos + fileProperty.directory.length + 2); - let segments = relativePath.split('/'); - - let result = ""; - for (let i = 0; i < segments.length - 1; ++i) { - if (result !== "") result += '/'; - result += segments[i]; - } - return result; - } - - constructor(props: FilePropertyDialogProps) { - super(props); - - - this.model = PiPedalModelFactory.getInstance(); - - this.state = { - fullScreen: this.getFullScreen(), - selectedFile: props.selectedFile, - selectedFileIsDirectory: false, - navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty), - hasSelection: false, - canDelete: false, - columns: 0, - columnWidth: 1, - fileEntries: [], - openUploadFileDialog: false, - openConfirmDeleteDialog: false, - menuAnchorEl: null, - newFolderDialogOpen: false, - renameDialogOpen: false, - moveDialogOpen: false - }; - this.requestScroll = true; - } - getFullScreen() { - return window.innerWidth < 450 || window.innerHeight < 450; - } - - private scrollRef: HTMLDivElement | null = null; - - onScrollRef(element: HTMLDivElement | null) { - this.scrollRef = element; - this.maybeScrollIntoView(); - } - private maybeScrollIntoView() { - if (this.scrollRef !== null && this.state.fileEntries.length !== 0 && this.mounted && this.requestScroll) { - this.requestScroll = false; - let options: ScrollIntoViewOptions = { block: "nearest" }; - options.block = "nearest"; - - this.scrollRef.scrollIntoView(options); - } - } - private mounted: boolean = false; - private model: PiPedalModel; - - private requestFiles(navPath: string) { - if (!this.props.open) { - return; - } - if (this.props.fileProperty.directory === "") { - return; + getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { + if (selectedFile === "") { + return ""; + } + return pathParentDirectory(selectedFile); } - this.model.requestFileList2(navPath, this.props.fileProperty) - .then((files) => { - if (this.mounted) { - // let insertionPoint = files.length; - // for (let i = 0; i < files.length; ++i) { - // if (!files[i].isDirectory) { - // insertionPoint = i; - // break; - // } - // } - files.splice(0, 0, { filename: "", isDirectory: false }); - this.setState({ fileEntries: files, hasSelection: this.isFileInList(files, this.state.selectedFile), navDirectory: navPath }); - } - }).catch((error) => { - this.model.showAlert(error.toString()) - }); - } + constructor(props: FilePropertyDialogProps) { + super(props); - private lastDivRef: HTMLDivElement | null = null; - onMeasureRef(div: HTMLDivElement | null) { - this.lastDivRef = div; - if (div) { - let width = div.offsetWidth; - if (width === 0) return; - let columns = 1; - let columnWidth = (width - 40) / columns; - if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) { - this.setState({ columns: columns, columnWidth: columnWidth }); + this.model = PiPedalModelFactory.getInstance(); + + this.state = { + fullScreen: this.getFullScreen(), + selectedFile: props.selectedFile, + selectedFileProtected: true, + selectedFileIsDirectory: false, + navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty), + hasSelection: false, + canDelete: false, + columns: 0, + columnWidth: 1, + fileResult: new FileRequestResult(), + isProtectedDirectory: true, + openUploadFileDialog: false, + openConfirmDeleteDialog: false, + menuAnchorEl: null, + newFolderDialogOpen: false, + renameDialogOpen: false, + moveDialogOpen: false + }; + this.requestScroll = true; + } + getFullScreen() { + return window.innerWidth < 450 || window.innerHeight < 450; + } + + private scrollRef: HTMLDivElement | null = null; + + onScrollRef(element: HTMLDivElement | null) { + this.scrollRef = element; + this.maybeScrollIntoView(); + } + private maybeScrollIntoView() { + if (this.scrollRef !== null && this.state.fileResult.files.length !== 0 && this.mounted && this.requestScroll) { + this.requestScroll = false; + let options: ScrollIntoViewOptions = { block: "nearest" }; + options.block = "nearest"; + + this.scrollRef.scrollIntoView(options); } } - } - onWindowSizeChanged(width: number, height: number): void { + private mounted: boolean = false; + private model: PiPedalModel; - this.setState({ - fullScreen: this.getFullScreen() - }) - if (this.lastDivRef !== null) { - this.onMeasureRef(this.lastDivRef); + private requestFiles(navPath: string) { + if (!this.props.open) { + return; + } + if (this.props.fileProperty.directory === "") { + return; + } + + this.model.requestFileList2(navPath, this.props.fileProperty) + .then((filesResult) => { + if (this.mounted) { + // let insertionPoint = files.length; + // for (let i = 0; i < files.length; ++i) { + // if (!files[i].isDirectory) { + // insertionPoint = i; + // break; + // } + // } + filesResult.files.splice(0, 0, { pathname: "", displayName: "", isDirectory: false, isProtected: true }); + + let fileEntry = this.getFileEntry(filesResult.files, this.state.selectedFile); + this.setState({ + isProtectedDirectory: filesResult.isProtected, + fileResult: filesResult, + hasSelection: !!fileEntry, + selectedFileProtected: fileEntry ? fileEntry.isProtected: true, + navDirectory: navPath + }); + } + }).catch((error) => { + if (!this.mounted) return; + if (navPath !== "") // deleted sample directory maybe? + { + this.navigate(""); + } else { + this.model.showAlert(error.toString()) + } + }); } - } + private lastDivRef: HTMLDivElement | null = null; - private requestScroll: boolean = false; + onMeasureRef(div: HTMLDivElement | null) { + this.lastDivRef = div; + if (div) { + let width = div.offsetWidth; + if (width === 0) return; + let columns = 1; + let columnWidth = (width - 40) / columns; + if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) { + this.setState({ columns: columns, columnWidth: columnWidth }); + } + } + } + onWindowSizeChanged(width: number, height: number): void { - componentDidMount() { - super.componentDidMount(); - this.mounted = true; - } - componentWillUnmount() { - super.componentWillUnmount(); - this.mounted = false; - this.lastDivRef = null; - } - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { - super.componentDidUpdate?.(prevProps, prevState, snapshot); - if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) { - if (this.props.open) { - let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty); - this.setState({ - selectedFile: this.props.selectedFile, - newFolderDialogOpen: false, - renameDialogOpen: false, - moveDialogOpen: false + this.setState({ + fullScreen: this.getFullScreen() + }) + if (this.lastDivRef !== null) { + this.onMeasureRef(this.lastDivRef); + } + + } + + private requestScroll: boolean = false; + + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + this.lastDivRef = null; + } + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + super.componentDidUpdate?.(prevProps, prevState, snapshot); + if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) { + if (this.props.open) { + let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty); + this.setState({ + selectedFile: this.props.selectedFile, + selectedFileIsDirectory: false, + selectedFileProtected: true, + newFolderDialogOpen: false, + renameDialogOpen: false, + moveDialogOpen: false }); - this.requestFiles(navDirectory) - this.requestScroll = true; - } - } - - } - - private isDirectory(path: string): boolean { - for (var fileEntry of this.state.fileEntries) { - if (fileEntry.filename === path) { - return fileEntry.isDirectory; - } - } - return false; - } - private isFileInList(files: FileEntry[], file: string) { - - let hasSelection = false; - if (file === "") return true; - for (var listFile of files) { - if (listFile.filename === file) { - hasSelection = true; - break; - } - } - return hasSelection; - } - - onSelectValue(selectedFile: string, isDirectory: boolean) { - this.requestScroll = true; - this.setState({ selectedFile: selectedFile, selectedFileIsDirectory: isDirectory, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) }) - } - onDoubleClickValue(selectedFile: string) { - this.openSelectedFile(); - } - - handleMenuOpen(event: React.MouseEvent) { - this.setState({ menuAnchorEl: event.currentTarget }); - - } - handleMenuClose() { - this.setState({ menuAnchorEl: null }); - } - handleDelete() { - this.setState({ openConfirmDeleteDialog: true }); - } - handleConfirmDelete() { - this.setState({ openConfirmDeleteDialog: false }); - if (this.state.hasSelection) { - let selectedFile = this.state.selectedFile; - let position = -1; - for (let i = 0; i < this.state.fileEntries.length; ++i) { - let file = this.state.fileEntries[i]; - if (file.filename === selectedFile) { - position = i; + this.requestFiles(navDirectory) + this.requestScroll = true; } } - let newSelection = ""; - if (position >= 0 && position < this.state.fileEntries.length - 1) { - newSelection = this.state.fileEntries[position + 1].filename; - } else if (position === this.state.fileEntries.length - 1) { - if (position !== 0) { - newSelection = this.state.fileEntries[position - 1].filename; + + } + + private isDirectory(path: string): boolean { + for (var fileEntry of this.state.fileResult.files) { + if (fileEntry.pathname === path) { + return fileEntry.isDirectory; + } + } + return false; + } + private isFileInList(files: FileEntry[], file: string) { + + let hasSelection = false; + if (file === "") return true; + for (var listFile of files) { + if (listFile.pathname === file) { + hasSelection = true; + break; + } + } + return hasSelection; + } + private getFileEntry(files: FileEntry[], file: string): FileEntry | undefined { + + if (file === "") return undefined; + for (var listFile of files) { + if (listFile.pathname === file) { + return listFile; + } + } + return undefined; + } + + onSelectValue(fileEntry: FileEntry) { + this.requestScroll = true; + this.setState({ + selectedFile: fileEntry.pathname, + selectedFileIsDirectory: fileEntry.isDirectory, + selectedFileProtected: fileEntry.isProtected, + hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname) + }) + } + onDoubleClickValue(selectedFile: string) { + this.openSelectedFile(); + } + + handleMenuOpen(event: React.MouseEvent) { + this.setState({ menuAnchorEl: event.currentTarget }); + + } + handleMenuClose() { + this.setState({ menuAnchorEl: null }); + } + handleDelete() { + if (this.state.selectedFileProtected) { + return; + } + this.setState({ openConfirmDeleteDialog: true }); + } + handleConfirmDelete() { + if (this.state.selectedFileProtected) { + return; + } + this.setState({ openConfirmDeleteDialog: false }); + if (this.state.hasSelection) { + let selectedFile = this.state.selectedFile; + let position = -1; + for (let i = 0; i < this.state.fileResult.files.length; ++i) { + let file = this.state.fileResult.files[i]; + if (file.pathname === selectedFile) { + position = i; + } + } + let newSelection: FileEntry | undefined = undefined; + if (position >= 0 && position < this.state.fileResult.files.length - 1) { + newSelection = this.state.fileResult.files[position + 1]; + } else if (position === this.state.fileResult.files.length - 1) { + if (position !== 0) { + newSelection = this.state.fileResult.files[position - 1]; + } + } - } - - this.model.deleteUserFile(this.state.selectedFile) - .then( - () => { - this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" }); - this.requestFiles(this.state.navDirectory); - } - ).catch( - (e: any) => { - this.model.showAlert(e.toString()); - } - ); - } - } - - private wantsScrollRef: boolean = true; - - navigate(relativeDirectory: string) { - this.requestFiles(relativeDirectory); - this.setState({ navDirectory: relativeDirectory }); - - } - renderBreadcrumbs() { - if (this.state.navDirectory === "") { - return (); - } - let breadcrumbs: React.ReactElement[] = [( - - ) - ]; - let directories = this.state.navDirectory.split("/"); - let target = ""; - for (let i = 0; i < directories.length - 1; ++i) { - target = pathConcat(target, directories[i]); - let myTarget = target; - breadcrumbs.push(( - / - - ) - ); - breadcrumbs.push(( - - )); - } - { - let lastdirectory = directories[directories.length - 1]; - breadcrumbs.push(( - / - - ) - ); - breadcrumbs.push(( -
- {lastdirectory} -
- )); - - } - return ( -
- -
- {breadcrumbs} -
- -
- ); - } - - getDefaultPath(): string { - try { - let storage = window.localStorage; - let result = storage.getItem("fpDefaultPath"); - if (result) - { - return result; - } - } catch (e) - { - - } - - return this.state.navDirectory; - } - setDefaultPath(path: string) { - try { - let storage = window.localStorage; - storage.setItem("fpDefaultPath",path); - } catch(e) { - - } - } - hasSelectedFileOrFolder(): boolean { - return this.state.hasSelection && this.state.selectedFile !== ""; - } - getFileExtensionList(uiFileProperty: UiFileProperty): string { - let result = ""; - for (var fileType of uiFileProperty.fileTypes) - { - if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip") - { - if (result !== "") result = result + ","; - result += fileType.fileExtension; - } - } - return result; - } - private getIcon(fileEntry: FileEntry) - { - if (fileEntry.filename === "") - { - return (); - } - if (fileEntry.isDirectory) - { - return ( - - ); - } - if (isAudioFile(fileEntry.filename)) - { - return (); - } - return (); - } - - render() { - let classes = this.props.classes; - let columnWidth = this.state.columnWidth; - let okButtonText = "Select"; - if (this.state.hasSelection && this.state.selectedFileIsDirectory) - { - okButtonText = "Open"; - } - return this.props.open && - ( - { - this.props.onCancel(); - }} - onEnterKey={()=> { - this.openSelectedFile(); - }} - open={this.props.open} tag="fileProperty" - fullWidth maxWidth="md" - PaperProps={ - this.state.fullScreen ? - {} - : - { style: { - minHeight: "90%", - maxHeight: "90%", - } }} - > - - - { this.props.onCancel(); }} - > - - - - {this.props.fileProperty.label} - - - { this.onNewFolder(); }} - > - - - - - { this.handleMenuOpen(ev); }} - > - - - { this.handleMenuClose(); }} - > - { this.handleMenuClose(); this.onNewFolder(); }}>New folder - {this.hasSelectedFileOrFolder() && ()} - {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose();this.onMove(); }}>Move)} - {this.hasSelectedFileOrFolder() && ( { this.handleMenuClose();this.onRename(); }}>Rename)} - - -
- {this.renderBreadcrumbs()} -
-
- - -
this.onMeasureRef(element)} - style={{ - flex: "1 1 100%", display: "flex", flexFlow: "row wrap", - justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16 - }}> + this.model.deleteUserFile(this.state.selectedFile) + .then( + () => { + if (newSelection) { - (this.state.columns !== 0) && // don't render until we have number of columns derived from layout. - this.state.fileEntries.map( - (value: FileEntry, index: number) => { - let displayValue = value.filename; - if (displayValue === "") { - displayValue = ""; - } else { - if (value.isDirectory) - { - displayValue = pathFileName(displayValue); - } else { - displayValue = pathFileName(displayValue); - } - } - let selected = value.filename === this.state.selectedFile; - let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; - if (isDarkMode()) { - selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)"; - } - let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined; - if (selected) { - scrollRef = (element) => { this.onScrollRef(element) }; - } + this.setState({ + selectedFile: newSelection.pathname, + selectedFileIsDirectory: newSelection.isDirectory, + selectedFileProtected: newSelection.isProtected, + hasSelection: true + }); + } else { + this.setState({ + selectedFile: "", + selectedFileIsDirectory: false, + selectedFileProtected: true, + hasSelection: false + }); - return ( - this.onSelectValue(value.filename,value.isDirectory)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }} - > -
-
- {this.getIcon(value)} - {displayValue} -
- - ); - } - ) - } -
- - - -
- this.handleDelete()} > - - - - -
 
- - - -
-
- { - this.setState({ openUploadFileDialog: false }); } + this.requestFiles(this.state.navDirectory); } - uploadPage={ - "uploadUserFile?directory=" + encodeURIComponent( - pathConcat(this.props.fileProperty.directory,this.state.navDirectory) - ) - + "&ext=" - + encodeURIComponent(this.getFileExtensionList(this.props.fileProperty)) + ).catch( + (e: any) => { + this.model.showAlert(e.toString()); } - onUploaded={() => { this.requestFiles(this.state.navDirectory); }} - fileProperty={this.props.fileProperty} + ); + } + } + + private wantsScrollRef: boolean = true; + + navigate(relativeDirectory: string) { + this.requestFiles(relativeDirectory); + this.setState({ navDirectory: relativeDirectory }); + + } + renderBreadcrumbs() { + if (this.state.navDirectory === "") { + return (); + } + let breadcrumbs: React.ReactElement[] = [( + + ) + ]; + + for (let i = 1; i < this.state.fileResult.breadcrumbs.length - 1; ++i) { + let breadcrumb: BreadcrumbEntry = this.state.fileResult.breadcrumbs[i]; + + breadcrumbs.push(( + / + + ) + ); + breadcrumbs.push(( + + )); + } + if (this.state.fileResult.breadcrumbs.length > 1) { + let lastdirectory = this.state.fileResult.breadcrumbs[this.state.fileResult.breadcrumbs.length - 1]; + breadcrumbs.push(( + / + + ) + ); + breadcrumbs.push(( +
+ {lastdirectory.displayName} +
+ )); + + } + return ( +
+ +
+ {breadcrumbs} +
+ +
+ ); + } + + getDefaultPath(): string { + try { + let storage = window.localStorage; + let result = storage.getItem("fpDefaultPath"); + if (result) { + return result; + } + } catch (e) { + + } + + return this.state.navDirectory; + } + setDefaultPath(path: string) { + try { + let storage = window.localStorage; + storage.setItem("fpDefaultPath", path); + } catch (e) { + + } + } + hasSelectedFileOrFolder(): boolean { + return this.state.hasSelection && this.state.selectedFile !== ""; + } + getFileExtensionList(uiFileProperty: UiFileProperty): string { + let result = ""; + for (var fileType of uiFileProperty.fileTypes) { + if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip") { + if (result !== "") result = result + ","; + result += fileType.fileExtension; + } + } + return result; + } + private getIcon(fileEntry: FileEntry) { + if (fileEntry.pathname === "") { + return (); + } + if (fileEntry.isDirectory) { + return ( + + ); + } + if (isAudioFile(fileEntry.pathname)) { + return (); + } + return (); + } + + render() { + let classes = this.props.classes; + let columnWidth = this.state.columnWidth; + let okButtonText = "Select"; + if (this.state.hasSelection && this.state.selectedFileIsDirectory) { + okButtonText = "Open"; + } + let protectedDirectory = this.state.fileResult.isProtected; + let protectedItem = true; + if (this.state.hasSelection) + { + protectedItem = this.state.selectedFileProtected; + } + let canMoveOrRename = this.hasSelectedFileOrFolder() && !protectedItem; + + return this.props.open && + ( + { - this.setState({ openConfirmDeleteDialog: false }); + this.props.onCancel(); }} - - /> - { - this.state.newFolderDialogOpen && ( - { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }} - onClose={() => { this.setState({ newFolderDialogOpen: false }); }} - acceptActionName="OK" - /> - ) - } - { - this.state.renameDialogOpen && ( - { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} - onClose={() => { this.setState({ renameDialogOpen: false }); }} - acceptActionName="OK" - /> - - ) - } - { - this.state.moveDialogOpen && ( - ( - {this.setState({moveDialogOpen: false});}} - onOk={ - (path) => { - this.setState({moveDialogOpen: false}); - this.setDefaultPath(path); - this.onExecuteMove(path); - } + onEnterKey={() => { + this.openSelectedFile(); + }} + open={this.props.open} tag="fileProperty" + fullWidth maxWidth="md" + PaperProps={ + this.state.fullScreen ? + {} + : + { + style: { + minHeight: "90%", + maxHeight: "90%", } - + }} + > + + + { this.props.onCancel(); }} + > + + + + {this.props.fileProperty.label} + + + { this.onNewFolder(); }} + disabled={protectedDirectory} + > + + + + + { this.handleMenuOpen(ev); }} + disabled={protectedDirectory} + + > + + + { this.handleMenuClose(); }} + > + { this.handleMenuClose(); this.onNewFolder(); }}>New folder + {canMoveOrRename && ()} + {canMoveOrRename && ( { this.handleMenuClose(); this.onMove(); }}>Move)} + {canMoveOrRename && this.hasSelectedFileOrFolder() && !protectedItem && ( { this.handleMenuClose(); this.onRename(); }}>Rename)} + + +
+ {this.renderBreadcrumbs()} +
+
+ + +
this.onMeasureRef(element)} + style={{ + flex: "1 1 100%", display: "flex", flexFlow: "row wrap", + justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16 + }}> + { + (this.state.columns !== 0) && // don't render until we have number of columns derived from layout. + this.state.fileResult.files.map( + (value: FileEntry, index: number) => { + let displayValue = value.displayName; + if (displayValue === "") { + displayValue = ""; + } + let selected = value.pathname === this.state.selectedFile; + let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; + if (isDarkMode()) { + selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)"; + } + let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined; + if (selected) { + scrollRef = (element) => { this.onScrollRef(element) }; + } + + return ( + this.onSelectValue(value)} + onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }} + > +
+
+ {this.getIcon(value)} + {displayValue} +
+ + ); + } + ) + } +
+ + + +
+ this.handleDelete()} > + + + + +
 
+ + + +
+
+ { + this.setState({ openUploadFileDialog: false }); + } + } + uploadPage={ + "uploadUserFile?directory=" + encodeURIComponent( + pathConcat(this.props.fileProperty.directory, this.state.navDirectory) + ) + + "&ext=" + + encodeURIComponent(this.getFileExtensionList(this.props.fileProperty)) + } + onUploaded={() => { this.requestFiles(this.state.navDirectory); }} + fileProperty={this.props.fileProperty} + + + /> + this.handleConfirmDelete()} + onClose={() => { + this.setState({ openConfirmDeleteDialog: false }); + }} + + /> + { + this.state.newFolderDialogOpen && ( + { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }} + onClose={() => { this.setState({ newFolderDialogOpen: false }); }} + acceptActionName="OK" /> ) - ) - } - - ); - } - openSelectedFile(): void { - if (this.isDirectory(this.state.selectedFile)) { - let directoryName = pathFileName(this.state.selectedFile); - let navDirectory = pathConcat(this.state.navDirectory, directoryName); - this.requestFiles(navDirectory); - this.setState({ navDirectory: navDirectory }); - } else { - this.props.onOk(this.props.fileProperty, this.state.selectedFile); + } + { + this.state.renameDialogOpen && ( + { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} + onClose={() => { this.setState({ renameDialogOpen: false }); }} + acceptActionName="OK" + /> + + ) + } + { + this.state.moveDialogOpen && ( + ( + { this.setState({ moveDialogOpen: false }); }} + onOk={ + (path) => { + this.setState({ moveDialogOpen: false }); + this.setDefaultPath(path); + this.onExecuteMove(path); + } + } + + /> + ) + ) + } + + ); } - } - - private renameDefaultName(): string { - let name = this.state.selectedFile; - if (name === "") return ""; - if (this.isDirectory(name)) - { - return pathFileName(name); - } else { - return pathFileNameOnly(name); - } - } - private onMove(): void { - this.setState({ moveDialogOpen: true }); - } - private onExecuteMove(newDirectory: string) - { - let fileName = pathFileName(this.state.selectedFile); - let oldFilePath = pathConcat(this.state.navDirectory,fileName); - let newFilePath = pathConcat(newDirectory,fileName); - - this.model.renameFilePropertyFile(oldFilePath,newFilePath,this.props.fileProperty) - .then(()=>{ - this.requestFiles(this.state.navDirectory); - }) - .catch((e)=>{ - this.model.showAlert(e.toString()); - }); - - } - - private onRename(): void { - this.setState({ renameDialogOpen: true }); - } - private onExecuteRename(newName: string) { - let newPath: string = ""; - let oldPath: string = ""; - if (this.isDirectory(this.state.selectedFile)) - { - let oldName = pathFileName(this.state.selectedFile); - if (oldName === newName) return; - oldPath = pathConcat(this.state.navDirectory,oldName); - newPath = pathConcat(this.state.navDirectory,newName);; - } else { - let oldName = pathFileNameOnly(this.state.selectedFile); - if (oldName === newName) return; - let extension = pathExtension(this.state.selectedFile); - oldPath = pathConcat(this.state.navDirectory,oldName+extension); - newPath = pathConcat(this.state.navDirectory,newName + extension); - } - this.model.renameFilePropertyFile(oldPath,newPath,this.props.fileProperty) - .then((newPath)=>{ - this.setState({selectedFile: newPath}); - this.requestFiles(this.state.navDirectory); - this.requestScroll = true - - }) - .catch((e) =>{ - this.model.showAlert(e.toString()); - }); - } - - private onNewFolder(): void { - this.setState({ newFolderDialogOpen: true }); - } - - private onExecuteNewFolder(newName: string) { - this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory,newName), this.props.fileProperty) - .then((newPath) => { - this.setState({selectedFile: newPath}); - this.requestFiles(this.state.navDirectory); - this.requestScroll = true - }) - .catch((e) => { - this.model.showAlert(e.toString()); + openSelectedFile(): void { + if (this.isDirectory(this.state.selectedFile)) { + this.requestFiles(this.state.selectedFile); + this.setState({ navDirectory: this.state.selectedFile }); + } else { + this.props.onOk(this.props.fileProperty, this.state.selectedFile); } - ); - } -}); \ No newline at end of file + } + + private renameDefaultName(): string { + let name = this.state.selectedFile; + if (name === "") return ""; + if (this.isDirectory(name)) { + return pathFileName(name); + } else { + return pathFileNameOnly(name); + } + } + private onMove(): void { + this.setState({ moveDialogOpen: true }); + } + private onExecuteMove(newDirectory: string) { + let fileName = pathFileName(this.state.selectedFile); + let oldFilePath = pathConcat(this.state.navDirectory, fileName); + let newFilePath = pathConcat(newDirectory, fileName); + + this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty) + .then(() => { + this.requestFiles(this.state.navDirectory); + }) + .catch((e) => { + this.model.showAlert(e.toString()); + }); + + } + + private onRename(): void { + this.setState({ renameDialogOpen: true }); + } + private onExecuteRename(newName: string) { + let newPath: string = ""; + let oldPath: string = ""; + if (this.isDirectory(this.state.selectedFile)) { + let oldName = pathFileName(this.state.selectedFile); + if (oldName === newName) return; + oldPath = pathConcat(this.state.navDirectory, oldName); + newPath = pathConcat(this.state.navDirectory, newName);; + } else { + let oldName = pathFileNameOnly(this.state.selectedFile); + if (oldName === newName) return; + let extension = pathExtension(this.state.selectedFile); + oldPath = pathConcat(this.state.navDirectory, oldName + extension); + newPath = pathConcat(this.state.navDirectory, newName + extension); + } + this.model.renameFilePropertyFile(oldPath, newPath, this.props.fileProperty) + .then((newPath) => { + this.setState({ selectedFile: newPath }); + this.requestFiles(this.state.navDirectory); + this.requestScroll = true + + }) + .catch((e) => { + this.model.showAlert(e.toString()); + }); + } + + private onNewFolder(): void { + this.setState({ newFolderDialogOpen: true }); + } + + private onExecuteNewFolder(newName: string) { + this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory, newName), this.props.fileProperty) + .then((newPath) => { + this.setState({ + selectedFile: newPath, + selectedFileIsDirectory: + true,selectedFileProtected: false + }); + this.requestFiles(this.state.navDirectory); + this.requestScroll = true + }) + .catch((e) => { + this.model.showAlert(e.toString()); + } + ); + } + }); \ No newline at end of file diff --git a/react/src/Lv2Plugin.tsx b/react/src/Lv2Plugin.tsx index dbf9538..c451ba8 100644 --- a/react/src/Lv2Plugin.tsx +++ b/react/src/Lv2Plugin.tsx @@ -246,6 +246,8 @@ export class UiFileProperty { this.index = input.index; this.portGroup = input.portGroup; this.resourceDirectory = input.resourceDirectory ?? ""; + this.modDirectories = input.modDirectories; + this.useLegacyModDirectory = input.useLegacyModDirectory; return this; } static deserialize_array(input: any): UiFileProperty[] { @@ -297,6 +299,8 @@ export class UiFileProperty { index: number = -1; portGroup: string = ""; resourceDirectory: string = ""; + modDirectories: string[] = []; + useLegacyModDirectory: boolean = false; }; export class Lv2Plugin implements Deserializable { diff --git a/react/src/ModFileTypes.tsx b/react/src/ModFileTypes.tsx new file mode 100644 index 0000000..2624106 --- /dev/null +++ b/react/src/ModFileTypes.tsx @@ -0,0 +1,60 @@ +// 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. + + + +export interface ModDirectory { + modType: string; + pipedalPath: string; + displayName: string; + defaultFileExtensions: string[]; +}; + + +export const modDirectories: ModDirectory[] = [ + {modType: "audioloop", pipedalPath: "shared/audio/Loops",displayName: "Loops", defaultFileExtensions: ["audio/*"]}, // Audio Loops, meant to be used for looper-style plugins + //"audiorecording","shared/audio/Audio Recordings", defaultFileExtensions: ["audio/*"]}, : Audio Recordings, triggered by plugins and stored in the unit + {modType: "audiosample", pipedalPath: "shared/audio/Samples", displayName: "Samples", defaultFileExtensions: ["audio/*"]}, // One-shot Audio Samples, meant to be used for sampler-style plugins + {modType: "audiotrack", pipedalPath: "shared/audio/Tracks",displayName: "Tracks", defaultFileExtensions: ["audio/*"]}, // Audio Tracks, meant to be used as full-performance/song or backtrack + {modType: "cabsim", pipedalPath: "CabIR", displayName: "Cab IRs", defaultFileExtensions: ["audio/*"]}, // Speaker Cabinets, meant as small IR audio files + + /// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension + {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 + // 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. + {modType: "aidadspmodel", pipedalPath: "shared/aidaaix", displayName: "AIDA IAX Models", defaultFileExtensions: [".json", ".aidaiax"]}, // Ratatoille + {modType: "mlmodel", pipedalPath: "ToobMlModels", displayName: "ML Models", defaultFileExtensions: [".json"]}, // + +]; + +export function getModDirectory(modFileType: string): ModDirectory | undefined { + for (let i = 0; i < modDirectories.length; ++i) + { + if (modDirectories[i].modType === modFileType) + { + return modDirectories[i]; + } + } + return undefined; +} \ No newline at end of file diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 196688c..fa3ed30 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -73,8 +73,20 @@ export enum ReconnectReason { export type ControlValueChangedHandler = (key: string, value: number) => void; export interface FileEntry { - filename: string; + pathname: string; + displayName: string; isDirectory: boolean; + isProtected: boolean; +}; + +export interface BreadcrumbEntry { + pathname: string; + displayName: string; +}; +export class FileRequestResult { + files: FileEntry[] = []; + isProtected: boolean = false; + breadcrumbs: BreadcrumbEntry[] = []; }; export type PluginPresetsChangedHandler = (pluginUri: string) => void; @@ -1989,9 +2001,9 @@ export class PiPedalModel //implements PiPedalModel return nullCast(this.webSocket) .request('requestFileList', piPedalFileProperty); } - requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise { + requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise { return nullCast(this.webSocket) - .request('requestFileList2', + .request('requestFileList2', { relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty } ); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d753aaf..6fdd18d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -157,6 +157,7 @@ else() endif() set (PIPEDAL_SOURCES + ModFileTypes.cpp ModFileTypes.hpp PatchPropertyWriter.hpp PresetBundle.cpp PresetBundle.hpp RealtimeMidiEventType.hpp @@ -637,6 +638,7 @@ add_executable(pipedalconfig CommandLineParser.hpp PiPedalException.hpp ConfigMain.cpp + ModFileTypes.cpp ModFileTypes.hpp PiPedalConfiguration.hpp PiPedalConfiguration.cpp JackServerSettings.hpp JackServerSettings.cpp SystemConfigFile.hpp SystemConfigFile.cpp diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 8170388..4738203 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -23,6 +23,7 @@ #include #include "CommandLineParser.hpp" #include "SystemConfigFile.hpp" +#include "ModFileTypes.hpp" #include #include @@ -951,6 +952,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) } try { + // apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis). silentSysExec(SYSTEMCTL_BIN " restart polkit.service"); @@ -1158,6 +1160,8 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) sysExec(SYSTEMCTL_BIN " daemon-reload"); FixPermissions(); + ModFileTypes::CreateDefaultDirectories("/var/pipedal/audio_uploads"); + StopService(false); AvahiInstall(); diff --git a/src/FileEntry.cpp b/src/FileEntry.cpp index f9e787e..2a6da62 100644 --- a/src/FileEntry.cpp +++ b/src/FileEntry.cpp @@ -22,7 +22,20 @@ using namespace pipedal; JSON_MAP_BEGIN(FileEntry) - JSON_MAP_REFERENCE(FileEntry,filename) + JSON_MAP_REFERENCE(FileEntry,pathname) + JSON_MAP_REFERENCE(FileEntry,displayName) + JSON_MAP_REFERENCE(FileEntry,isProtected) JSON_MAP_REFERENCE(FileEntry,isDirectory) - +JSON_MAP_END() + +JSON_MAP_BEGIN(BreadcrumbEntry) + JSON_MAP_REFERENCE(BreadcrumbEntry,pathname) + JSON_MAP_REFERENCE(BreadcrumbEntry,displayName) +JSON_MAP_END() + + +JSON_MAP_BEGIN(FileRequestResult) + JSON_MAP_REFERENCE(FileRequestResult,files) + JSON_MAP_REFERENCE(FileRequestResult,isProtected) + JSON_MAP_REFERENCE(FileRequestResult,breadcrumbs) JSON_MAP_END() diff --git a/src/FileEntry.hpp b/src/FileEntry.hpp index 0acf5be..f1dc3ff 100644 --- a/src/FileEntry.hpp +++ b/src/FileEntry.hpp @@ -26,15 +26,33 @@ namespace pipedal { class FileEntry { public: FileEntry() { } - FileEntry(std::string filename,bool isDirectory) - :filename_(filename), isDirectory_(isDirectory) + FileEntry(const std::string&pathname,const std::string &displayName,bool isDirectory, bool isProtected = false) + :pathname_(pathname), displayName_(displayName), isDirectory_(isDirectory),isProtected_(isProtected) { } - std::string filename_; + std::string pathname_; + std::string displayName_; bool isDirectory_ = false; + bool isProtected_ = false; DECLARE_JSON_MAP(FileEntry); }; + class BreadcrumbEntry { + public: + std::string pathname_; + std::string displayName_; + + DECLARE_JSON_MAP(BreadcrumbEntry); + }; + + class FileRequestResult { + public: + std::vector files_; + bool isProtected_ = false; + std::vector breadcrumbs_; + DECLARE_JSON_MAP(FileRequestResult); + + }; } \ No newline at end of file diff --git a/src/ModFileTypes.cpp b/src/ModFileTypes.cpp new file mode 100644 index 0000000..e9c6737 --- /dev/null +++ b/src/ModFileTypes.cpp @@ -0,0 +1,156 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "ModFileTypes.hpp" +#include "util.hpp" +#include +#include +#include "ss.hpp" + +using namespace pipedal; + +const std::vector ModFileTypes::ModDirectories = + { + {"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 + {"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 + {"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 + // 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 + {"mlmodel", "ToobMlModels", "ML Models", {".json"}}, // + // +}; + +const ModFileTypes::ModDirectory *ModFileTypes::GetModDirectory(const std::string &type) +{ + for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories) + { + if (modType.modType == type) + { + return &modType; + } + } + return nullptr; +} + +ModFileTypes::ModFileTypes(const std::string &fileTypes) +{ + std::vector types = split(fileTypes, ','); + for (const auto &type : types) + { + const ModDirectory *wellKnownType = GetModDirectory(type); + if (wellKnownType) + { + rootDirectories_.push_back(type); + } + else + { + 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( + const std::filesystem::path &rootDirectory, + const std::string &modType) +{ + auto wellKnownType = ModFileTypes::GetModDirectory(modType); + if (!wellKnownType) + { + throw std::runtime_error(SS("Can't find modFileType " << modType)); + } + return rootDirectory / wellKnownType->pipedalPath; +} +void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDirectory) +{ + namespace fs = std::filesystem; + using namespace std; + try + { + for (const auto &modType : ModDirectories) + { + fs::path path = rootDirectory / modType.pipedalPath; + + fs::create_directories(path); + } + + if (!fs::exists(rootDirectory / "shared" / "audio" / "Cab IR Files")) + { + fs::create_symlink( + getModDirectoryPath(rootDirectory, "cabsim"), + rootDirectory / "shared" / "audio" / "Cab IR Files"); + } + if (!fs::exists(rootDirectory / "shared" / "audio" / "Impulse Responses")) + { + fs::create_symlink( + getModDirectoryPath(rootDirectory, "ir"), + rootDirectory / "shared" / "audio" / "Impulse Responses"); + } + } + catch (const std::exception &e) + { + cout << "Can't create default MOD directories. " << e.what() << endl; + } +} + diff --git a/src/ModFileTypes.hpp b/src/ModFileTypes.hpp new file mode 100644 index 0000000..bcf49d9 --- /dev/null +++ b/src/ModFileTypes.hpp @@ -0,0 +1,53 @@ +// Copyright (c) 2024 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once +#include +#include +#include + +namespace pipedal { + class ModFileTypes { + + public: + ModFileTypes(const std::string&fileTypes); + const std::vector &rootDirectories() { return rootDirectories_; } + const std::vector &fileTypes() { return fileTypes_;} + + private: + std::vector rootDirectories_; + std::vector fileTypes_; + + + public: + class ModDirectory { + public: + const std::string modType; + const std::string pipedalPath; + const std::string displayName; + const std::vector defaultFileExtensions; + }; + + const static 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/PiPedalModel.cpp b/src/PiPedalModel.cpp index 6cbe83f..ace8873 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -2296,7 +2296,7 @@ std::vector PiPedalModel::GetFileList(const UiFileProperty &filePro return std::vector(); // don't disclose to users what the problem is. } } -std::vector PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) +FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) { try { @@ -2305,7 +2305,7 @@ std::vector PiPedalModel::GetFileList2(const std::string &relativePat catch (const std::exception &e) { Lv2Log::warning("GetFileList() failed: (%s)", e.what()); - return std::vector(); // don't disclose to users what the problem is. + throw; } } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index cc874fb..e8963d7 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -432,7 +432,7 @@ namespace pipedal void SetUpdatePolicy(UpdatePolicyT updatePolicy); void ForceUpdateCheck(); std::vector GetFileList(const UiFileProperty &fileProperty); - std::vector GetFileList2(const std::string &relativePath, 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); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 67bd417..612ee1c 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1589,8 +1589,8 @@ public: { FileRequestArgs requestArgs; pReader->read(&requestArgs); - std::vector list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); - this->Reply(replyTo, "requestFileList2", list); + FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); + this->Reply(replyTo, "requestFileList2", result); } else if (message == "newPreset") { diff --git a/src/PiPedalUI.cpp b/src/PiPedalUI.cpp index 3a2d637..03cb073 100644 --- a/src/PiPedalUI.cpp +++ b/src/PiPedalUI.cpp @@ -465,6 +465,8 @@ JSON_MAP_REFERENCE(UiFileProperty, patchProperty) JSON_MAP_REFERENCE(UiFileProperty, fileTypes) JSON_MAP_REFERENCE(UiFileProperty, portGroup) JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory) +JSON_MAP_REFERENCE(UiFileProperty, modDirectories) +JSON_MAP_REFERENCE(UiFileProperty, useLegacyModDirectory) JSON_MAP_END() JSON_MAP_BEGIN(UiFrequencyPlot) diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index 6c99527..b6e96a6 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -114,6 +114,8 @@ namespace pipedal { std::string patchProperty_; std::string portGroup_; std::string resourceDirectory_; + std::vector modDirectories_; + bool useLegacyModDirectory_= false; public: using ptr = std::shared_ptr; UiFileProperty() { } @@ -121,9 +123,17 @@ namespace pipedal { UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory); + std::vector& modDirectories() { return modDirectories_; } + const std::vector& modDirectories() const { return modDirectories_; } + + bool useLegacyModDirectory() const { return useLegacyModDirectory_; } + void useLegacyModDirectory(bool value) { useLegacyModDirectory_ = value; } const std::string &label() const { return label_; } int32_t index() const { return index_; } + const std::string &directory() const { return directory_; } + void directory(const std::string &path) { directory_ = path; } + const std::string&portGroup() const { return portGroup_; } const std::vector &fileTypes() const { return fileTypes_; } diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 9030417..d987f67 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -46,6 +46,7 @@ #include "PiPedalException.hpp" #include "StdErrorCapture.hpp" #include "util.hpp" +#include "ModFileTypes.hpp" #include "Locale.hpp" @@ -118,7 +119,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled); core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional); portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); - portprops__trigger = lilv_new_uri(pWorld,LV2_PORT_PROPS__trigger); + portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); @@ -192,8 +193,7 @@ 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"); - + mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes"); } void PluginHost::LilvUris::Free() @@ -478,8 +478,7 @@ void PluginHost::Load(const char *lv2Path) const char *pb1 = left->name().c_str(); const char *pb2 = right->name().c_str(); return collator->Compare( - left->name(),right->name()) - < 0; + left->name(), right->name()) < 0; }; std::sort(this->plugins_.begin(), this->plugins_.end(), compare); @@ -625,15 +624,47 @@ std::shared_ptr Lv2PluginInfo::FindWritablePathProperties(PluginHost strLabel, propertyUri.AsUri(), lv2DirectoryName); AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); - LILV_FOREACH(nodes,i,mod__fileTypes) + LILV_FOREACH(nodes, i, mod__fileTypes) { - // "nam,nammodel" - AutoLilvNode lilvfileType {lilv_nodes_get(mod__fileTypes, i)}; + // "nam,nammodel" + AutoLilvNode lilvfileType{lilv_nodes_get(mod__fileTypes, i)}; std::string fileTypes = lilvfileType.AsString(); + ModFileTypes modFileTypes(fileTypes); - for (std::string&type: split(fileTypes,',')) + for (const std::string &rootDirectory : modFileTypes.rootDirectories()) { - fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"),SS('.' << type))); + 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) @@ -933,7 +964,7 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled); this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri); this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional); - this->trigger_property_ = lilv_port_has_property(plugin,pPort,host->lilvUris->portprops__trigger); + this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger); LilvScalePoints *pScalePoints = lilv_port_get_scale_points(plugin, pPort); LILV_FOREACH(scale_points, iSP, pScalePoints) @@ -1389,33 +1420,36 @@ std::shared_ptr PluginHost::GetHostWorkerThread() return pHostWorkerThread; } -class ResourceInfo { +class ResourceInfo +{ public: - ResourceInfo(const std::string&filePropertyDirectory,const std::string&resourceDirectory) - :filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {} + ResourceInfo(const std::string &filePropertyDirectory, const std::string &resourceDirectory) + : filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {} std::string filePropertyDirectory; std::string resourceDirectory; - bool operator==(const ResourceInfo&other) const { + bool operator==(const ResourceInfo &other) const + { return this->filePropertyDirectory == other.filePropertyDirectory && this->resourceDirectory == other.resourceDirectory; - } - bool operator<(const ResourceInfo&other) const { + bool operator<(const ResourceInfo &other) const + { if (this->filePropertyDirectory < other.filePropertyDirectory) return true; if (this->filePropertyDirectory > other.filePropertyDirectory) return false; - return this->resourceDirectory < other.resourceDirectory; - + return this->resourceDirectory < other.resourceDirectory; } }; -static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path&targetDirectory) +static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path &targetDirectory) { namespace fs = std::filesystem; - try { - if (!fs::exists(targetDirectory)) return false; - for (auto&directoryEntry : fs::directory_iterator(sourceDirectory)) + try + { + if (!fs::exists(targetDirectory)) + return false; + for (auto &directoryEntry : fs::directory_iterator(sourceDirectory)) { fs::path thisPath = directoryEntry.path(); if (directoryEntry.is_directory()) @@ -1423,11 +1457,13 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co auto name = thisPath.filename(); fs::path childSource = sourceDirectory / name; fs::path childTarget = targetDirectory / name; - if (anyTargetFilesExist(childSource,childTarget)) + if (anyTargetFilesExist(childSource, childTarget)) { return true; } - } else { + } + else + { fs::path targetPath = targetDirectory / thisPath.filename(); if (fs::exists(targetPath)) { @@ -1435,9 +1471,9 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co } } } - - } catch (const std::exception&) { - + } + catch (const std::exception &) + { } return false; } @@ -1452,48 +1488,52 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons { fs::path childSource = dirEntry.path(); fs::path childTarget = targetDirectory / childSource.filename(); - if (dirEntry.is_directory()) { - createTargetLinks(childSource,childTarget); - } else { - fs::create_symlink(childSource,childTarget); + if (dirEntry.is_directory()) + { + createTargetLinks(childSource, childTarget); + } + else + { + fs::create_symlink(childSource, childTarget); } } } -void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,const std::filesystem::path&pluginUploadDirectory) +void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory) { auto plugin = GetPluginInfo(pluginUri); if (plugin) { std::filesystem::path bundlePath = plugin->bundle_path(); - if (!plugin->piPedalUI()) + if (!plugin->piPedalUI()) return; - const auto& fileProperties = plugin->piPedalUI()->fileProperties(); + const auto &fileProperties = plugin->piPedalUI()->fileProperties(); if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri)) { pluginsThatHaveBeenCheckedForResources.insert(pluginUri); // eliminate duplicates. std::set resourceInfoSet; - for (const auto&fileProperty: fileProperties) + for (const auto &fileProperty : fileProperties) { if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty()) { - resourceInfoSet.insert(ResourceInfo(fileProperty->directory(),fileProperty->resourceDirectory())); + resourceInfoSet.insert(ResourceInfo(fileProperty->directory(), fileProperty->resourceDirectory())); } } - try { - for (const ResourceInfo&resourceInfo: resourceInfoSet) + try { - std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory; - std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory; - if (!anyTargetFilesExist(sourcePath,targetPath)) + for (const ResourceInfo &resourceInfo : resourceInfoSet) { - createTargetLinks(sourcePath,targetPath); + std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory; + std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory; + if (!anyTargetFilesExist(sourcePath, targetPath)) + { + createTargetLinks(sourcePath, targetPath); + } } - } - } catch (const std::exception &e) + catch (const std::exception &e) { Lv2Log::error(SS("CheckForResourceInitialization: " << e.what())); } @@ -1501,20 +1541,17 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con } } -json_variant PluginHost::MapPath(const json_variant&json) +json_variant PluginHost::MapPath(const json_variant &json) { AtomConverter converter(GetMapFeature()); - return converter.MapPath(json,GetPluginStoragePath()); - + return converter.MapPath(json, GetPluginStoragePath()); } -json_variant PluginHost::AbstractPath(const json_variant&json) +json_variant PluginHost::AbstractPath(const json_variant &json) { AtomConverter converter(GetMapFeature()); - return converter.AbstractPath(json,GetPluginStoragePath()); - + return converter.AbstractPath(json, GetPluginStoragePath()); } - std::string PluginHost::MapPath(const std::string &abstractPath) { auto storagePath = GetPluginStoragePath(); @@ -1523,10 +1560,8 @@ std::string PluginHost::MapPath(const std::string &abstractPath) return ""; } return SS(storagePath << '/' << abstractPath); - - } -std::string PluginHost::AbstractPath(const std::string&path) +std::string PluginHost::AbstractPath(const std::string &path) { auto storagePath = GetPluginStoragePath(); if (path.starts_with(storagePath)) diff --git a/src/Storage.cpp b/src/Storage.cpp index fbea14f..d780d86 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -22,6 +22,7 @@ #include "Storage.hpp" #include "AudioConfig.hpp" #include "PiPedalException.hpp" +#include #include #include "json.hpp" #include @@ -32,14 +33,44 @@ #include "PluginHost.hpp" #include "ss.hpp" #include "ofstream_synced.hpp" +#include "ModFileTypes.hpp" using namespace pipedal; +namespace fs = std::filesystem; const char *BANK_EXTENSION = ".bank"; const char *BANKS_FILENAME = "index.banks"; #define USER_SETTINGS_FILENAME "userSettings.json"; + +static bool isSubdirectory(const fs::path&path, const fs::path&basePath) +{ + auto iPath = path.begin(); + for (auto i = basePath.begin(); i != basePath.end(); ++i) + { + if (iPath == path.end()) + { + return false; + } + + if ((*i) != (*iPath)) + { + return false; + } + ++iPath; + } + while (iPath != path.end()) + { + if (iPath->string() == "..") + { + return false; + } + ++iPath; + } + return true; +} + Storage::Storage() { SetConfigRoot("~/var/Config"); @@ -1648,63 +1679,190 @@ static bool ensureNoDotDot(const std::filesystem::path&path) return true; } - -std::vector Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) +static void AddFilesToResult( + FileRequestResult&result, + const UiFileProperty &fileProperty, + const fs::path&rootPath) { - if (!ensureNoDotDot(relativePath)) + if (!fs::exists(rootPath)) { - ThrowPermissionDeniedError(); + return; // silently without error. } - if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory())) + auto & resultFiles = result.files_; + try { - 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() / relativePath; - try + for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath)) { - for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory)) + const auto &path = dir_entry.path(); + auto name = path.filename().string(); + if (dir_entry.is_regular_file()) { - if (dir_entry.is_regular_file()) + if (name.length() > 0 && name[0] != '.') // don't show hidden files. { - 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())) { - if (fileProperty.IsValidExtension(path.extension().string())) - { - // a relative path! - result.push_back(FileEntry(path,false)); - } + resultFiles.push_back( + FileEntry(path,name,false,false) + ); } - } else if (dir_entry.is_directory()) { - result.push_back(FileEntry(dir_entry.path(),true)); } + } else if (dir_entry.is_directory()) { + resultFiles.push_back(FileEntry{path,name,true,fs::is_symlink(path)}); } } - catch (const std::exception &error) - { - throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string()); - } + } + catch (const std::exception &error) + { + throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string()); } // sort lexicographically auto collator = Locale::GetInstance()->GetCollator(); - std::sort(result.begin(), result.end(),[&collator](const FileEntry&l, const FileEntry&r) { + std::sort(resultFiles.begin(), resultFiles.end(),[&collator](const FileEntry&l, const FileEntry&r) { if (l.isDirectory_ != r.isDirectory_) { return l.isDirectory_ > r.isDirectory_; } - return collator->Compare(l.filename_,r.filename_) < 0; - + return collator->Compare(l.displayName_,r.displayName_) < 0; }); +} +FileRequestResult Storage::GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) +{ + FileRequestResult result; + fs::path uploadsDirectory = GetPluginUploadDirectory(); + + if (relativePath.empty()) + { + // return the synthetic root. + result.isProtected_ = true; + + for (const auto&modDirectory: fileProperty.modDirectories()) + { + const auto directoryInfo = ModFileTypes::GetModDirectory(modDirectory); + if (directoryInfo) + { + result.files_.push_back( + FileEntry(uploadsDirectory / directoryInfo->pipedalPath,directoryInfo->displayName,true,true) + ); + } + + } + if (fileProperty.useLegacyModDirectory()) + { + result.files_.push_back( + FileEntry(uploadsDirectory / fileProperty.directory(),fs::path(fileProperty.directory()).filename().string(),true,true) + ); + } + result.breadcrumbs_.push_back({"","Home"}); + return result; + } + fs::path modDirectoryPath; + + result.breadcrumbs_.push_back({"","Home"}); + fs::path fsRelativePath { relativePath}; + + for (const auto &modDirectory: fileProperty.modDirectories()) + { + const ModFileTypes::ModDirectory*modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory); + if (modDirectoryInfo) + { + if (isSubdirectory(fsRelativePath, uploadsDirectory / modDirectoryInfo->pipedalPath)) + { + modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath; + result.breadcrumbs_.push_back({modDirectoryPath.string(),modDirectoryInfo->displayName}); + break; + } + + } + } + if (modDirectoryPath.empty() && fileProperty.useLegacyModDirectory()) + { + if (isSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory())) + { + modDirectoryPath = uploadsDirectory / fileProperty.directory(); + result.breadcrumbs_.push_back({modDirectoryPath.string(),fs::path(fileProperty.directory()).filename().string()}); + } else { + ThrowPermissionDeniedError(); + } + } + + // add remaing path segements as breadcrumbs. + { + fs::path modPath {modDirectoryPath}; + fs::path rp { relativePath}; + auto iRp = rp.begin(); + // skip past one or more segments in the modDiretoryPath. + for (auto iModPath = modPath.begin(); iModPath != modPath.end(); ++iModPath) + { + if (iRp != rp.end()) { + ++iRp; + } + } + while (iRp != rp.end()) + { + result.breadcrumbs_.push_back({*iRp,*iRp}); + ++iRp; + } + } + + AddFilesToResult(result,fileProperty,relativePath); + return result; +} + + +FileRequestResult Storage::GetFileList2(const std::string &relativePath_,const UiFileProperty &fileProperty) +{ + std::string absolutePath = relativePath_; + if (!ensureNoDotDot(absolutePath)) + { + ThrowPermissionDeniedError(); + } + if (fileProperty.modDirectories().size() > 1 || (fileProperty.modDirectories().size() == 1 && fileProperty.useLegacyModDirectory())) + { + return Storage::GetModFileList2(absolutePath,fileProperty); + } + + FileRequestResult result; + + if (fileProperty.directory().empty()) + { + throw std::runtime_error("fileProperty.directory() not specified."); + } + std::filesystem::path pluginRootDirectory = this->GetPluginUploadDirectory() / fileProperty.directory(); + if (absolutePath == "") + { + absolutePath = pluginRootDirectory.string(); + } + + + { + result.breadcrumbs_.push_back({"","Home"}); + fs::path fsAbsolutePath {absolutePath}; + auto iAbsolutePath = fsAbsolutePath.begin(); + for (auto i = pluginRootDirectory.begin(); i != pluginRootDirectory.end(); ++i) + { + if (iAbsolutePath == fsAbsolutePath.end() || (*i) != *iAbsolutePath) + { + throw std::runtime_error("Directory is not a subdirectory of the plugin root directory."); + } + ++iAbsolutePath; + } + auto cumulativePath = pluginRootDirectory; + + while (iAbsolutePath != fsAbsolutePath.end()) + { + cumulativePath /= (*iAbsolutePath); + result.breadcrumbs_.push_back({cumulativePath.string(),iAbsolutePath->string()}); + ++iAbsolutePath; + } + } + if (!isSubdirectory(absolutePath,pluginRootDirectory)) + { + throw std::runtime_error(SS("Improper location. " << absolutePath)); + } + AddFilesToResult(result,fileProperty,absolutePath); return result; } @@ -1753,9 +1911,18 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName) { if (std::filesystem::is_directory(fileName)) { - if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) + if (fileName.string().length() <= 1) // guard against rm -rf / (bitter experience) { - std::filesystem::remove_all(fileName); + throw std::logic_error("Invalid filename."); + } + if (std::filesystem::is_symlink(fileName)) + { + std::filesystem::remove(fileName); + } else { + if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) + { + std::filesystem::remove_all(fileName); + } } } else { diff --git a/src/Storage.hpp b/src/Storage.hpp index e00da98..cdcfcd3 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -152,7 +152,9 @@ public: int64_t DeleteBank(int64_t bankId); std::vector GetFileList(const UiFileProperty&fileProperty); - std::vector GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); + FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); + + FileRequestResult GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty); void SetJackChannelSelection(const JackChannelSelection&channelSelection);