Shared upload directories

This commit is contained in:
Robin Davies
2024-10-27 19:47:05 -04:00
parent ae7e6f5abe
commit 6bff83604d
19 changed files with 1367 additions and 766 deletions
+16
View File
@@ -51,6 +51,22 @@ namespace pipedal {
} }
template <typename T>
inline bool contains(const std::vector<T>&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<std::string>&vector,const char*value)
{
return contains(vector,std::string(value));
}
class NoCopy { class NoCopy {
public: public:
NoCopy() { } NoCopy() { }
+179 -132
View File
@@ -17,6 +17,7 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // 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. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React from 'react'; import React from 'react';
import { Theme, createStyles } from '@mui/material/styles'; import { Theme, createStyles } from '@mui/material/styles';
import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder';
@@ -25,7 +26,7 @@ import Divider from '@mui/material/Divider';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MoreIcon from '@mui/icons-material/MoreVert'; 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 { isDarkMode } from './DarkMode';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileUploadIcon from '@mui/icons-material/FileUpload';
@@ -61,7 +62,7 @@ const styles = (theme: Theme) => createStyles({
}, },
}); });
const audioFileExtensions: {[name: string]: boolean} = { const audioFileExtensions: { [name: string]: boolean } = {
".wav": true, ".wav": true,
".flac": true, ".flac": true,
".ogg": true, ".ogg": true,
@@ -95,15 +96,16 @@ export interface FilePropertyDialogProps extends WithStyles<typeof styles> {
onOk: (fileProperty: UiFileProperty, selectedItem: string) => void, onOk: (fileProperty: UiFileProperty, selectedItem: string) => void,
onCancel: () => void onCancel: () => void
}; };
export interface FilePropertyDialogState { export interface FilePropertyDialogState {
fullScreen: boolean; fullScreen: boolean;
selectedFile: string; selectedFile: string;
selectedFileIsDirectory: boolean; selectedFileIsDirectory: boolean;
navDirectory: string; selectedFileProtected: boolean;
hasSelection: boolean; hasSelection: boolean;
canDelete: boolean; canDelete: boolean;
fileEntries: FileEntry[]; fileResult: FileRequestResult;
navDirectory: string;
isProtectedDirectory: boolean;
columns: number; columns: number;
columnWidth: number; columnWidth: number;
openUploadFileDialog: boolean; openUploadFileDialog: boolean;
@@ -114,19 +116,22 @@ export interface FilePropertyDialogState {
moveDialogOpen: boolean; moveDialogOpen: boolean;
}; };
function pathExtension(path: string) function pathExtension(path: string) {
{
let dotPos = path.lastIndexOf('.'); let dotPos = path.lastIndexOf('.');
if (dotPos === -1) return ""; if (dotPos === -1) return "";
let slashPos = path.lastIndexOf('/'); let slashPos = path.lastIndexOf('/');
if (slashPos !== -1) if (slashPos !== -1) {
{ if (dotPos <= slashPos + 1) return "";
if (dotPos <= slashPos+1) return "";
} }
return path.substring(dotPos); // include the '.'. 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) { function pathConcat(left: string, right: string) {
if (left === "") return right; if (left === "") return right;
if (right === "") return left; if (right === "") return left;
@@ -170,19 +175,10 @@ export default withStyles(styles, { withTheme: true })(
class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> { class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string { getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string {
if (selectedFile === "") {
// would be easier if we had the data root (/var/pipedal/audio_downloads, but could be different when we're debugging. :-/ return "";
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; return pathParentDirectory(selectedFile);
} }
constructor(props: FilePropertyDialogProps) { constructor(props: FilePropertyDialogProps) {
@@ -194,13 +190,15 @@ export default withStyles(styles, { withTheme: true })(
this.state = { this.state = {
fullScreen: this.getFullScreen(), fullScreen: this.getFullScreen(),
selectedFile: props.selectedFile, selectedFile: props.selectedFile,
selectedFileProtected: true,
selectedFileIsDirectory: false, selectedFileIsDirectory: false,
navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty), navDirectory: this.getNavDirectoryFromFile(props.selectedFile, props.fileProperty),
hasSelection: false, hasSelection: false,
canDelete: false, canDelete: false,
columns: 0, columns: 0,
columnWidth: 1, columnWidth: 1,
fileEntries: [], fileResult: new FileRequestResult(),
isProtectedDirectory: true,
openUploadFileDialog: false, openUploadFileDialog: false,
openConfirmDeleteDialog: false, openConfirmDeleteDialog: false,
menuAnchorEl: null, menuAnchorEl: null,
@@ -221,7 +219,7 @@ export default withStyles(styles, { withTheme: true })(
this.maybeScrollIntoView(); this.maybeScrollIntoView();
} }
private maybeScrollIntoView() { private maybeScrollIntoView() {
if (this.scrollRef !== null && this.state.fileEntries.length !== 0 && this.mounted && this.requestScroll) { if (this.scrollRef !== null && this.state.fileResult.files.length !== 0 && this.mounted && this.requestScroll) {
this.requestScroll = false; this.requestScroll = false;
let options: ScrollIntoViewOptions = { block: "nearest" }; let options: ScrollIntoViewOptions = { block: "nearest" };
options.block = "nearest"; options.block = "nearest";
@@ -241,7 +239,7 @@ export default withStyles(styles, { withTheme: true })(
} }
this.model.requestFileList2(navPath, this.props.fileProperty) this.model.requestFileList2(navPath, this.props.fileProperty)
.then((files) => { .then((filesResult) => {
if (this.mounted) { if (this.mounted) {
// let insertionPoint = files.length; // let insertionPoint = files.length;
// for (let i = 0; i < files.length; ++i) { // for (let i = 0; i < files.length; ++i) {
@@ -250,11 +248,25 @@ export default withStyles(styles, { withTheme: true })(
// break; // break;
// } // }
// } // }
files.splice(0, 0, { filename: "", isDirectory: false }); filesResult.files.splice(0, 0, { pathname: "", displayName: "<none>", isDirectory: false, isProtected: true });
this.setState({ fileEntries: files, hasSelection: this.isFileInList(files, this.state.selectedFile), navDirectory: navPath });
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) => { }).catch((error) => {
if (!this.mounted) return;
if (navPath !== "") // deleted sample directory maybe?
{
this.navigate("");
} else {
this.model.showAlert(error.toString()) this.model.showAlert(error.toString())
}
}); });
} }
@@ -301,6 +313,8 @@ export default withStyles(styles, { withTheme: true })(
let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty); let navDirectory = this.getNavDirectoryFromFile(this.props.selectedFile, this.props.fileProperty);
this.setState({ this.setState({
selectedFile: this.props.selectedFile, selectedFile: this.props.selectedFile,
selectedFileIsDirectory: false,
selectedFileProtected: true,
newFolderDialogOpen: false, newFolderDialogOpen: false,
renameDialogOpen: false, renameDialogOpen: false,
moveDialogOpen: false moveDialogOpen: false
@@ -313,8 +327,8 @@ export default withStyles(styles, { withTheme: true })(
} }
private isDirectory(path: string): boolean { private isDirectory(path: string): boolean {
for (var fileEntry of this.state.fileEntries) { for (var fileEntry of this.state.fileResult.files) {
if (fileEntry.filename === path) { if (fileEntry.pathname === path) {
return fileEntry.isDirectory; return fileEntry.isDirectory;
} }
} }
@@ -325,17 +339,32 @@ export default withStyles(styles, { withTheme: true })(
let hasSelection = false; let hasSelection = false;
if (file === "") return true; if (file === "") return true;
for (var listFile of files) { for (var listFile of files) {
if (listFile.filename === file) { if (listFile.pathname === file) {
hasSelection = true; hasSelection = true;
break; break;
} }
} }
return hasSelection; return hasSelection;
} }
private getFileEntry(files: FileEntry[], file: string): FileEntry | undefined {
onSelectValue(selectedFile: string, isDirectory: boolean) { if (file === "") return undefined;
for (var listFile of files) {
if (listFile.pathname === file) {
return listFile;
}
}
return undefined;
}
onSelectValue(fileEntry: FileEntry) {
this.requestScroll = true; this.requestScroll = true;
this.setState({ selectedFile: selectedFile, selectedFileIsDirectory: isDirectory, hasSelection: this.isFileInList(this.state.fileEntries, selectedFile) }) this.setState({
selectedFile: fileEntry.pathname,
selectedFileIsDirectory: fileEntry.isDirectory,
selectedFileProtected: fileEntry.isProtected,
hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname)
})
} }
onDoubleClickValue(selectedFile: string) { onDoubleClickValue(selectedFile: string) {
this.openSelectedFile(); this.openSelectedFile();
@@ -349,25 +378,31 @@ export default withStyles(styles, { withTheme: true })(
this.setState({ menuAnchorEl: null }); this.setState({ menuAnchorEl: null });
} }
handleDelete() { handleDelete() {
if (this.state.selectedFileProtected) {
return;
}
this.setState({ openConfirmDeleteDialog: true }); this.setState({ openConfirmDeleteDialog: true });
} }
handleConfirmDelete() { handleConfirmDelete() {
if (this.state.selectedFileProtected) {
return;
}
this.setState({ openConfirmDeleteDialog: false }); this.setState({ openConfirmDeleteDialog: false });
if (this.state.hasSelection) { if (this.state.hasSelection) {
let selectedFile = this.state.selectedFile; let selectedFile = this.state.selectedFile;
let position = -1; let position = -1;
for (let i = 0; i < this.state.fileEntries.length; ++i) { for (let i = 0; i < this.state.fileResult.files.length; ++i) {
let file = this.state.fileEntries[i]; let file = this.state.fileResult.files[i];
if (file.filename === selectedFile) { if (file.pathname === selectedFile) {
position = i; position = i;
} }
} }
let newSelection = ""; let newSelection: FileEntry | undefined = undefined;
if (position >= 0 && position < this.state.fileEntries.length - 1) { if (position >= 0 && position < this.state.fileResult.files.length - 1) {
newSelection = this.state.fileEntries[position + 1].filename; newSelection = this.state.fileResult.files[position + 1];
} else if (position === this.state.fileEntries.length - 1) { } else if (position === this.state.fileResult.files.length - 1) {
if (position !== 0) { if (position !== 0) {
newSelection = this.state.fileEntries[position - 1].filename; newSelection = this.state.fileResult.files[position - 1];
} }
} }
@@ -375,7 +410,23 @@ export default withStyles(styles, { withTheme: true })(
this.model.deleteUserFile(this.state.selectedFile) this.model.deleteUserFile(this.state.selectedFile)
.then( .then(
() => { () => {
this.setState({ selectedFile: newSelection, hasSelection: newSelection !== "" }); if (newSelection)
{
this.setState({
selectedFile: newSelection.pathname,
selectedFileIsDirectory: newSelection.isDirectory,
selectedFileProtected: newSelection.isProtected,
hasSelection: true
});
} else {
this.setState({
selectedFile: "",
selectedFileIsDirectory: false,
selectedFileProtected: true,
hasSelection: false
});
}
this.requestFiles(this.state.navDirectory); this.requestFiles(this.state.navDirectory);
} }
).catch( ).catch(
@@ -408,41 +459,40 @@ export default withStyles(styles, { withTheme: true })(
</Button> </Button>
) )
]; ];
let directories = this.state.navDirectory.split("/");
let target = ""; for (let i = 1; i < this.state.fileResult.breadcrumbs.length - 1; ++i) {
for (let i = 0; i < directories.length - 1; ++i) { let breadcrumb: BreadcrumbEntry = this.state.fileResult.breadcrumbs[i];
target = pathConcat(target, directories[i]);
let myTarget = target;
breadcrumbs.push(( breadcrumbs.push((
<Typography key={"x"+(i)} variant="body2" style={{marginTop: 6, marginBottom: 6}} noWrap>/</Typography> <Typography key={"x" + (i)} variant="body2" style={{ marginTop: 6, marginBottom: 6 }} noWrap>/</Typography>
) )
); );
breadcrumbs.push(( breadcrumbs.push((
<Button variant="text" <Button variant="text"
key={(i).toString()} key={"bc" + i}
color="inherit" color="inherit"
onClick={() => { this.navigate(myTarget) }} onClick={() => { this.navigate(breadcrumb.pathname) }}
style={{ minWidth: 0,textTransform: "none" , flexShrink: 0 }} style={{ minWidth: 0, textTransform: "none", flexShrink: 0 }}
> >
<Typography variant="body2" style={{}} noWrap>{directories[i]}</Typography> <Typography variant="body2" style={{}} noWrap>{breadcrumb.displayName}</Typography>
</Button> </Button>
)); ));
} }
{ if (this.state.fileResult.breadcrumbs.length > 1) {
let lastdirectory = directories[directories.length - 1]; let lastdirectory = this.state.fileResult.breadcrumbs[this.state.fileResult.breadcrumbs.length - 1];
breadcrumbs.push(( breadcrumbs.push((
<Typography key={"x"+(directories.length)} variant="body2" style={{userSelect: "none",marginTop: 6, marginBottom: 6}} noWrap>/</Typography> <Typography key={"xLast"} variant="body2" style={{ userSelect: "none", marginTop: 6, marginBottom: 6 }} noWrap>/</Typography>
) )
); );
breadcrumbs.push(( breadcrumbs.push((
<div style={{flexShrink: 0,paddingLeft: 8, paddingRight: 8, paddingTop: 6, paddingBottom: 6, overflowX:"clip"}}> <div key={"bc-last"} style={{ flexShrink: 0, paddingLeft: 8, paddingRight: 8, paddingTop: 6, paddingBottom: 6, overflowX: "clip" }}>
<Typography key={directories.length.toString()} noWrap <Typography noWrap
style={{}} variant="body2" style={{}} variant="body2"
color="text.secondary"> {lastdirectory}</Typography> color="text.secondary"> {lastdirectory.displayName}</Typography>
</div> </div>
)); ));
@@ -451,10 +501,10 @@ export default withStyles(styles, { withTheme: true })(
<div> <div>
<div aria-label="breadcrumb" <div aria-label="breadcrumb"
style={{ marginLeft: 16, marginBottom: 8, marginRight:8, textOverflow: "",display: "flex",flexFlow: "row wrap",alignItems: "center",justifyContent: "start" }}> style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}>
{breadcrumbs} {breadcrumbs}
</div> </div>
<Divider/> <Divider />
</div> </div>
); );
} }
@@ -463,12 +513,10 @@ export default withStyles(styles, { withTheme: true })(
try { try {
let storage = window.localStorage; let storage = window.localStorage;
let result = storage.getItem("fpDefaultPath"); let result = storage.getItem("fpDefaultPath");
if (result) if (result) {
{
return result; return result;
} }
} catch (e) } catch (e) {
{
} }
@@ -477,8 +525,8 @@ export default withStyles(styles, { withTheme: true })(
setDefaultPath(path: string) { setDefaultPath(path: string) {
try { try {
let storage = window.localStorage; let storage = window.localStorage;
storage.setItem("fpDefaultPath",path); storage.setItem("fpDefaultPath", path);
} catch(e) { } catch (e) {
} }
} }
@@ -487,30 +535,24 @@ export default withStyles(styles, { withTheme: true })(
} }
getFileExtensionList(uiFileProperty: UiFileProperty): string { getFileExtensionList(uiFileProperty: UiFileProperty): string {
let result = ""; let result = "";
for (var fileType of uiFileProperty.fileTypes) for (var fileType of uiFileProperty.fileTypes) {
{ if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip") {
if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip")
{
if (result !== "") result = result + ","; if (result !== "") result = result + ",";
result += fileType.fileExtension; result += fileType.fileExtension;
} }
} }
return result; return result;
} }
private getIcon(fileEntry: FileEntry) private getIcon(fileEntry: FileEntry) {
{ if (fileEntry.pathname === "") {
if (fileEntry.filename === "")
{
return (<InsertDriveFileOutlinedIcon style={{ flex: "0 0 auto", opacity: 0.5, marginRight: 8, marginLeft: 8 }} />); return (<InsertDriveFileOutlinedIcon style={{ flex: "0 0 auto", opacity: 0.5, marginRight: 8, marginLeft: 8 }} />);
} }
if (fileEntry.isDirectory) if (fileEntry.isDirectory) {
{
return ( return (
<FolderIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} /> <FolderIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
); );
} }
if (isAudioFile(fileEntry.filename)) if (isAudioFile(fileEntry.pathname)) {
{
return (<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />); return (<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />);
} }
return (<InsertDriveFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />); return (<InsertDriveFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />);
@@ -520,10 +562,17 @@ export default withStyles(styles, { withTheme: true })(
let classes = this.props.classes; let classes = this.props.classes;
let columnWidth = this.state.columnWidth; let columnWidth = this.state.columnWidth;
let okButtonText = "Select"; let okButtonText = "Select";
if (this.state.hasSelection && this.state.selectedFileIsDirectory) if (this.state.hasSelection && this.state.selectedFileIsDirectory) {
{
okButtonText = "Open"; 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 && return this.props.open &&
( (
<DialogEx <DialogEx
@@ -531,7 +580,7 @@ export default withStyles(styles, { withTheme: true })(
onClose={() => { onClose={() => {
this.props.onCancel(); this.props.onCancel();
}} }}
onEnterKey={()=> { onEnterKey={() => {
this.openSelectedFile(); this.openSelectedFile();
}} }}
open={this.props.open} tag="fileProperty" open={this.props.open} tag="fileProperty"
@@ -540,12 +589,14 @@ export default withStyles(styles, { withTheme: true })(
this.state.fullScreen ? this.state.fullScreen ?
{} {}
: :
{ style: { {
style: {
minHeight: "90%", minHeight: "90%",
maxHeight: "90%", maxHeight: "90%",
} }} }
}}
> >
<DialogTitle style={{paddingBottom: 0}} > <DialogTitle style={{ paddingBottom: 0 }} >
<Toolbar style={{ padding: 0 }}> <Toolbar style={{ padding: 0 }}>
<IconButton <IconButton
edge="start" edge="start"
@@ -566,6 +617,7 @@ export default withStyles(styles, { withTheme: true })(
color="inherit" color="inherit"
style={{ opacity: 0.6, marginRight: 8 }} style={{ opacity: 0.6, marginRight: 8 }}
onClick={(ev) => { this.onNewFolder(); }} onClick={(ev) => { this.onNewFolder(); }}
disabled={protectedDirectory}
> >
<CreateNewFolderIcon /> <CreateNewFolderIcon />
</IconButton> </IconButton>
@@ -577,6 +629,8 @@ export default withStyles(styles, { withTheme: true })(
color="inherit" color="inherit"
style={{ opacity: 0.6 }} style={{ opacity: 0.6 }}
onClick={(ev) => { this.handleMenuOpen(ev); }} onClick={(ev) => { this.handleMenuOpen(ev); }}
disabled={protectedDirectory}
> >
<MoreIcon /> <MoreIcon />
</IconButton> </IconButton>
@@ -596,16 +650,16 @@ export default withStyles(styles, { withTheme: true })(
onClose={() => { this.handleMenuClose(); }} onClose={() => { this.handleMenuClose(); }}
> >
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem> <MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
{this.hasSelectedFileOrFolder() && (<Divider />)} {canMoveOrRename && (<Divider />)}
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose();this.onMove(); }}>Move</MenuItem>)} {canMoveOrRename && (<MenuItem onClick={() => { this.handleMenuClose(); this.onMove(); }}>Move</MenuItem>)}
{this.hasSelectedFileOrFolder() && (<MenuItem onClick={() => { this.handleMenuClose();this.onRename(); }}>Rename</MenuItem>)} {canMoveOrRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => { this.handleMenuClose(); this.onRename(); }}>Rename</MenuItem>)}
</Menu> </Menu>
</Toolbar> </Toolbar>
<div style={{ flex: "0 0 auto", marginLeft: 0, marginRight: 0, overflowX: "clip" }}> <div style={{ flex: "0 0 auto", marginLeft: 0, marginRight: 0, overflowX: "clip" }}>
{this.renderBreadcrumbs()} {this.renderBreadcrumbs()}
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent style={{paddingLeft: 0, paddingRight: 0, paddingTop: 0,colorScheme: (isDarkMode() ? "dark" : "light")}}> <DialogContent style={{ paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light") }}>
<div <div
ref={(element) => this.onMeasureRef(element)} ref={(element) => this.onMeasureRef(element)}
@@ -615,20 +669,13 @@ export default withStyles(styles, { withTheme: true })(
}}> }}>
{ {
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout. (this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
this.state.fileEntries.map( this.state.fileResult.files.map(
(value: FileEntry, index: number) => { (value: FileEntry, index: number) => {
let displayValue = value.filename; let displayValue = value.displayName;
if (displayValue === "") { if (displayValue === "") {
displayValue = "<none>"; displayValue = "<none>";
} else {
if (value.isDirectory)
{
displayValue = pathFileName(displayValue);
} else {
displayValue = pathFileName(displayValue);
} }
} let selected = value.pathname === this.state.selectedFile;
let selected = value.filename === this.state.selectedFile;
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)"; let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
if (isDarkMode()) { if (isDarkMode()) {
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)"; selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
@@ -639,10 +686,11 @@ export default withStyles(styles, { withTheme: true })(
} }
return ( return (
<ButtonBase key={value.filename} <ButtonBase key={value.pathname}
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }} style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
onClick={() => this.onSelectValue(value.filename,value.isDirectory)} onDoubleClick={() => { this.onDoubleClickValue(value.filename); }} onClick={() => this.onSelectValue(value)}
onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }}
> >
<div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} /> <div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
<div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}> <div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
@@ -656,17 +704,17 @@ export default withStyles(styles, { withTheme: true })(
} }
</div> </div>
</DialogContent> </DialogContent>
<Divider/> <Divider />
<DialogActions style={{ justifyContent: "stretch" }}> <DialogActions style={{ justifyContent: "stretch" }}>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}> <div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary" <IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
disabled={!this.state.hasSelection || this.state.selectedFile === ""} disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
onClick={() => this.handleDelete()} > onClick={() => this.handleDelete()} >
<OldDeleteIcon fontSize='small' /> <OldDeleteIcon fontSize='small' />
</IconButton> </IconButton>
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />} <Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
onClick={() => { this.setState({ openUploadFileDialog: true }) }} onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
> >
<div>Upload</div> <div>Upload</div>
</Button> </Button>
@@ -694,7 +742,7 @@ export default withStyles(styles, { withTheme: true })(
} }
uploadPage={ uploadPage={
"uploadUserFile?directory=" + encodeURIComponent( "uploadUserFile?directory=" + encodeURIComponent(
pathConcat(this.props.fileProperty.directory,this.state.navDirectory) pathConcat(this.props.fileProperty.directory, this.state.navDirectory)
) )
+ "&ext=" + "&ext="
+ encodeURIComponent(this.getFileExtensionList(this.props.fileProperty)) + encodeURIComponent(this.getFileExtensionList(this.props.fileProperty))
@@ -744,11 +792,11 @@ export default withStyles(styles, { withTheme: true })(
defaultPath={this.getDefaultPath()} defaultPath={this.getDefaultPath()}
excludeDirectory={ excludeDirectory={
this.isDirectory(this.state.selectedFile) this.isDirectory(this.state.selectedFile)
? pathConcat(this.state.navDirectory,pathFileName(this.state.selectedFile)): ""} ? pathConcat(this.state.navDirectory, pathFileName(this.state.selectedFile)) : ""}
onClose={() => {this.setState({moveDialogOpen: false});}} onClose={() => { this.setState({ moveDialogOpen: false }); }}
onOk={ onOk={
(path) => { (path) => {
this.setState({moveDialogOpen: false}); this.setState({ moveDialogOpen: false });
this.setDefaultPath(path); this.setDefaultPath(path);
this.onExecuteMove(path); this.onExecuteMove(path);
} }
@@ -763,10 +811,8 @@ export default withStyles(styles, { withTheme: true })(
} }
openSelectedFile(): void { openSelectedFile(): void {
if (this.isDirectory(this.state.selectedFile)) { if (this.isDirectory(this.state.selectedFile)) {
let directoryName = pathFileName(this.state.selectedFile); this.requestFiles(this.state.selectedFile);
let navDirectory = pathConcat(this.state.navDirectory, directoryName); this.setState({ navDirectory: this.state.selectedFile });
this.requestFiles(navDirectory);
this.setState({ navDirectory: navDirectory });
} else { } else {
this.props.onOk(this.props.fileProperty, this.state.selectedFile); this.props.onOk(this.props.fileProperty, this.state.selectedFile);
} }
@@ -775,8 +821,7 @@ export default withStyles(styles, { withTheme: true })(
private renameDefaultName(): string { private renameDefaultName(): string {
let name = this.state.selectedFile; let name = this.state.selectedFile;
if (name === "") return ""; if (name === "") return "";
if (this.isDirectory(name)) if (this.isDirectory(name)) {
{
return pathFileName(name); return pathFileName(name);
} else { } else {
return pathFileNameOnly(name); return pathFileNameOnly(name);
@@ -785,17 +830,16 @@ export default withStyles(styles, { withTheme: true })(
private onMove(): void { private onMove(): void {
this.setState({ moveDialogOpen: true }); this.setState({ moveDialogOpen: true });
} }
private onExecuteMove(newDirectory: string) private onExecuteMove(newDirectory: string) {
{
let fileName = pathFileName(this.state.selectedFile); let fileName = pathFileName(this.state.selectedFile);
let oldFilePath = pathConcat(this.state.navDirectory,fileName); let oldFilePath = pathConcat(this.state.navDirectory, fileName);
let newFilePath = pathConcat(newDirectory,fileName); let newFilePath = pathConcat(newDirectory, fileName);
this.model.renameFilePropertyFile(oldFilePath,newFilePath,this.props.fileProperty) this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty)
.then(()=>{ .then(() => {
this.requestFiles(this.state.navDirectory); this.requestFiles(this.state.navDirectory);
}) })
.catch((e)=>{ .catch((e) => {
this.model.showAlert(e.toString()); this.model.showAlert(e.toString());
}); });
@@ -807,27 +851,26 @@ export default withStyles(styles, { withTheme: true })(
private onExecuteRename(newName: string) { private onExecuteRename(newName: string) {
let newPath: string = ""; let newPath: string = "";
let oldPath: string = ""; let oldPath: string = "";
if (this.isDirectory(this.state.selectedFile)) if (this.isDirectory(this.state.selectedFile)) {
{
let oldName = pathFileName(this.state.selectedFile); let oldName = pathFileName(this.state.selectedFile);
if (oldName === newName) return; if (oldName === newName) return;
oldPath = pathConcat(this.state.navDirectory,oldName); oldPath = pathConcat(this.state.navDirectory, oldName);
newPath = pathConcat(this.state.navDirectory,newName);; newPath = pathConcat(this.state.navDirectory, newName);;
} else { } else {
let oldName = pathFileNameOnly(this.state.selectedFile); let oldName = pathFileNameOnly(this.state.selectedFile);
if (oldName === newName) return; if (oldName === newName) return;
let extension = pathExtension(this.state.selectedFile); let extension = pathExtension(this.state.selectedFile);
oldPath = pathConcat(this.state.navDirectory,oldName+extension); oldPath = pathConcat(this.state.navDirectory, oldName + extension);
newPath = pathConcat(this.state.navDirectory,newName + extension); newPath = pathConcat(this.state.navDirectory, newName + extension);
} }
this.model.renameFilePropertyFile(oldPath,newPath,this.props.fileProperty) this.model.renameFilePropertyFile(oldPath, newPath, this.props.fileProperty)
.then((newPath)=>{ .then((newPath) => {
this.setState({selectedFile: newPath}); this.setState({ selectedFile: newPath });
this.requestFiles(this.state.navDirectory); this.requestFiles(this.state.navDirectory);
this.requestScroll = true this.requestScroll = true
}) })
.catch((e) =>{ .catch((e) => {
this.model.showAlert(e.toString()); this.model.showAlert(e.toString());
}); });
} }
@@ -837,9 +880,13 @@ export default withStyles(styles, { withTheme: true })(
} }
private onExecuteNewFolder(newName: string) { private onExecuteNewFolder(newName: string) {
this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory,newName), this.props.fileProperty) this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory, newName), this.props.fileProperty)
.then((newPath) => { .then((newPath) => {
this.setState({selectedFile: newPath}); this.setState({
selectedFile: newPath,
selectedFileIsDirectory:
true,selectedFileProtected: false
});
this.requestFiles(this.state.navDirectory); this.requestFiles(this.state.navDirectory);
this.requestScroll = true this.requestScroll = true
}) })
@@ -848,4 +895,4 @@ export default withStyles(styles, { withTheme: true })(
} }
); );
} }
}); });
+4
View File
@@ -246,6 +246,8 @@ export class UiFileProperty {
this.index = input.index; this.index = input.index;
this.portGroup = input.portGroup; this.portGroup = input.portGroup;
this.resourceDirectory = input.resourceDirectory ?? ""; this.resourceDirectory = input.resourceDirectory ?? "";
this.modDirectories = input.modDirectories;
this.useLegacyModDirectory = input.useLegacyModDirectory;
return this; return this;
} }
static deserialize_array(input: any): UiFileProperty[] { static deserialize_array(input: any): UiFileProperty[] {
@@ -297,6 +299,8 @@ export class UiFileProperty {
index: number = -1; index: number = -1;
portGroup: string = ""; portGroup: string = "";
resourceDirectory: string = ""; resourceDirectory: string = "";
modDirectories: string[] = [];
useLegacyModDirectory: boolean = false;
}; };
export class Lv2Plugin implements Deserializable<Lv2Plugin> { export class Lv2Plugin implements Deserializable<Lv2Plugin> {
+60
View File
@@ -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;
}
+15 -3
View File
@@ -73,8 +73,20 @@ export enum ReconnectReason {
export type ControlValueChangedHandler = (key: string, value: number) => void; export type ControlValueChangedHandler = (key: string, value: number) => void;
export interface FileEntry { export interface FileEntry {
filename: string; pathname: string;
displayName: string;
isDirectory: boolean; 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; export type PluginPresetsChangedHandler = (pluginUri: string) => void;
@@ -1989,9 +2001,9 @@ export class PiPedalModel //implements PiPedalModel
return nullCast(this.webSocket) return nullCast(this.webSocket)
.request<string[]>('requestFileList', piPedalFileProperty); .request<string[]>('requestFileList', piPedalFileProperty);
} }
requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> { requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileRequestResult> {
return nullCast(this.webSocket) return nullCast(this.webSocket)
.request<FileEntry[]>('requestFileList2', .request<FileRequestResult>('requestFileList2',
{ relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty } { relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty }
); );
} }
+2
View File
@@ -157,6 +157,7 @@ else()
endif() endif()
set (PIPEDAL_SOURCES set (PIPEDAL_SOURCES
ModFileTypes.cpp ModFileTypes.hpp
PatchPropertyWriter.hpp PatchPropertyWriter.hpp
PresetBundle.cpp PresetBundle.hpp PresetBundle.cpp PresetBundle.hpp
RealtimeMidiEventType.hpp RealtimeMidiEventType.hpp
@@ -637,6 +638,7 @@ add_executable(pipedalconfig
CommandLineParser.hpp CommandLineParser.hpp
PiPedalException.hpp PiPedalException.hpp
ConfigMain.cpp ConfigMain.cpp
ModFileTypes.cpp ModFileTypes.hpp
PiPedalConfiguration.hpp PiPedalConfiguration.cpp PiPedalConfiguration.hpp PiPedalConfiguration.cpp
JackServerSettings.hpp JackServerSettings.cpp JackServerSettings.hpp JackServerSettings.cpp
SystemConfigFile.hpp SystemConfigFile.cpp SystemConfigFile.hpp SystemConfigFile.cpp
+4
View File
@@ -23,6 +23,7 @@
#include <unistd.h> #include <unistd.h>
#include "CommandLineParser.hpp" #include "CommandLineParser.hpp"
#include "SystemConfigFile.hpp" #include "SystemConfigFile.hpp"
#include "ModFileTypes.hpp"
#include <filesystem> #include <filesystem>
#include <stdlib.h> #include <stdlib.h>
@@ -951,6 +952,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress)
} }
try try
{ {
// apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis). // apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis).
silentSysExec(SYSTEMCTL_BIN " restart polkit.service"); 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"); sysExec(SYSTEMCTL_BIN " daemon-reload");
FixPermissions(); FixPermissions();
ModFileTypes::CreateDefaultDirectories("/var/pipedal/audio_uploads");
StopService(false); StopService(false);
AvahiInstall(); AvahiInstall();
+15 -2
View File
@@ -22,7 +22,20 @@
using namespace pipedal; using namespace pipedal;
JSON_MAP_BEGIN(FileEntry) 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_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() JSON_MAP_END()
+21 -3
View File
@@ -26,15 +26,33 @@ namespace pipedal {
class FileEntry { class FileEntry {
public: public:
FileEntry() { } FileEntry() { }
FileEntry(std::string filename,bool isDirectory) FileEntry(const std::string&pathname,const std::string &displayName,bool isDirectory, bool isProtected = false)
:filename_(filename), isDirectory_(isDirectory) :pathname_(pathname), displayName_(displayName), isDirectory_(isDirectory),isProtected_(isProtected)
{ {
} }
std::string filename_; std::string pathname_;
std::string displayName_;
bool isDirectory_ = false; bool isDirectory_ = false;
bool isProtected_ = false;
DECLARE_JSON_MAP(FileEntry); DECLARE_JSON_MAP(FileEntry);
}; };
class BreadcrumbEntry {
public:
std::string pathname_;
std::string displayName_;
DECLARE_JSON_MAP(BreadcrumbEntry);
};
class FileRequestResult {
public:
std::vector<FileEntry> files_;
bool isProtected_ = false;
std::vector<BreadcrumbEntry> breadcrumbs_;
DECLARE_JSON_MAP(FileRequestResult);
};
} }
+156
View File
@@ -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 <stdexcept>
#include <iostream>
#include "ss.hpp"
using namespace pipedal;
const std::vector<ModFileTypes::ModDirectory> 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<std::string> 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;
}
}
+53
View File
@@ -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 <string>
#include <vector>
#include <filesystem>
namespace pipedal {
class ModFileTypes {
public:
ModFileTypes(const std::string&fileTypes);
const std::vector<std::string> &rootDirectories() { return rootDirectories_; }
const std::vector<std::string> &fileTypes() { return fileTypes_;}
private:
std::vector<std::string> rootDirectories_;
std::vector<std::string> fileTypes_;
public:
class ModDirectory {
public:
const std::string modType;
const std::string pipedalPath;
const std::string displayName;
const std::vector<std::string> defaultFileExtensions;
};
const static std::vector<ModDirectory> ModDirectories;
static const ModDirectory* GetModDirectory(const std::string&modType);
static void CreateDefaultDirectories(const std::filesystem::path&rootDirectory);
};
}
+2 -2
View File
@@ -2296,7 +2296,7 @@ std::vector<std::string> PiPedalModel::GetFileList(const UiFileProperty &filePro
return std::vector<std::string>(); // don't disclose to users what the problem is. return std::vector<std::string>(); // don't disclose to users what the problem is.
} }
} }
std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty)
{ {
try try
{ {
@@ -2305,7 +2305,7 @@ std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePat
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::warning("GetFileList() failed: (%s)", e.what()); Lv2Log::warning("GetFileList() failed: (%s)", e.what());
return std::vector<FileEntry>(); // don't disclose to users what the problem is. throw;
} }
} }
+1 -1
View File
@@ -432,7 +432,7 @@ namespace pipedal
void SetUpdatePolicy(UpdatePolicyT updatePolicy); void SetUpdatePolicy(UpdatePolicyT updatePolicy);
void ForceUpdateCheck(); void ForceUpdateCheck();
std::vector<std::string> GetFileList(const UiFileProperty &fileProperty); std::vector<std::string> GetFileList(const UiFileProperty &fileProperty);
std::vector<FileEntry> GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty); FileRequestResult GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty);
void DeleteSampleFile(const std::filesystem::path &fileName); void DeleteSampleFile(const std::filesystem::path &fileName);
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty); std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
+2 -2
View File
@@ -1589,8 +1589,8 @@ public:
{ {
FileRequestArgs requestArgs; FileRequestArgs requestArgs;
pReader->read(&requestArgs); pReader->read(&requestArgs);
std::vector<FileEntry> list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_);
this->Reply(replyTo, "requestFileList2", list); this->Reply(replyTo, "requestFileList2", result);
} }
else if (message == "newPreset") else if (message == "newPreset")
{ {
+2
View File
@@ -465,6 +465,8 @@ JSON_MAP_REFERENCE(UiFileProperty, patchProperty)
JSON_MAP_REFERENCE(UiFileProperty, fileTypes) JSON_MAP_REFERENCE(UiFileProperty, fileTypes)
JSON_MAP_REFERENCE(UiFileProperty, portGroup) JSON_MAP_REFERENCE(UiFileProperty, portGroup)
JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory) JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory)
JSON_MAP_REFERENCE(UiFileProperty, modDirectories)
JSON_MAP_REFERENCE(UiFileProperty, useLegacyModDirectory)
JSON_MAP_END() JSON_MAP_END()
JSON_MAP_BEGIN(UiFrequencyPlot) JSON_MAP_BEGIN(UiFrequencyPlot)
+10
View File
@@ -114,6 +114,8 @@ namespace pipedal {
std::string patchProperty_; std::string patchProperty_;
std::string portGroup_; std::string portGroup_;
std::string resourceDirectory_; std::string resourceDirectory_;
std::vector<std::string> modDirectories_;
bool useLegacyModDirectory_= false;
public: public:
using ptr = std::shared_ptr<UiFileProperty>; using ptr = std::shared_ptr<UiFileProperty>;
UiFileProperty() { } UiFileProperty() { }
@@ -121,9 +123,17 @@ namespace pipedal {
UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory); UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory);
std::vector<std::string>& modDirectories() { return modDirectories_; }
const std::vector<std::string>& modDirectories() const { return modDirectories_; }
bool useLegacyModDirectory() const { return useLegacyModDirectory_; }
void useLegacyModDirectory(bool value) { useLegacyModDirectory_ = value; }
const std::string &label() const { return label_; } const std::string &label() const { return label_; }
int32_t index() const { return index_; } int32_t index() const { return index_; }
const std::string &directory() const { return directory_; } const std::string &directory() const { return directory_; }
void directory(const std::string &path) { directory_ = path; }
const std::string&portGroup() const { return portGroup_; } const std::string&portGroup() const { return portGroup_; }
const std::vector<UiFileType> &fileTypes() const { return fileTypes_; } const std::vector<UiFileType> &fileTypes() const { return fileTypes_; }
+85 -50
View File
@@ -46,6 +46,7 @@
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include "StdErrorCapture.hpp" #include "StdErrorCapture.hpp"
#include "util.hpp" #include "util.hpp"
#include "ModFileTypes.hpp"
#include "Locale.hpp" #include "Locale.hpp"
@@ -118,7 +119,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled); core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled);
core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional); core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional);
portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); 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); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent);
core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation);
portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); 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"); 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() void PluginHost::LilvUris::Free()
@@ -478,8 +478,7 @@ void PluginHost::Load(const char *lv2Path)
const char *pb1 = left->name().c_str(); const char *pb1 = left->name().c_str();
const char *pb2 = right->name().c_str(); const char *pb2 = right->name().c_str();
return collator->Compare( return collator->Compare(
left->name(),right->name()) left->name(), right->name()) < 0;
< 0;
}; };
std::sort(this->plugins_.begin(), this->plugins_.end(), compare); std::sort(this->plugins_.begin(), this->plugins_.end(), compare);
@@ -625,15 +624,47 @@ std::shared_ptr<PiPedalUI> Lv2PluginInfo::FindWritablePathProperties(PluginHost
strLabel, propertyUri.AsUri(), lv2DirectoryName); strLabel, propertyUri.AsUri(), lv2DirectoryName);
AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); 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" // "nam,nammodel"
AutoLilvNode lilvfileType {lilv_nodes_get(mod__fileTypes, i)}; AutoLilvNode lilvfileType{lilv_nodes_get(mod__fileTypes, i)};
std::string fileTypes = lilvfileType.AsString(); 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/<plugin_directory> 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) 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->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->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->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); LilvScalePoints *pScalePoints = lilv_port_get_scale_points(plugin, pPort);
LILV_FOREACH(scale_points, iSP, pScalePoints) LILV_FOREACH(scale_points, iSP, pScalePoints)
@@ -1389,33 +1420,36 @@ std::shared_ptr<HostWorkerThread> PluginHost::GetHostWorkerThread()
return pHostWorkerThread; return pHostWorkerThread;
} }
class ResourceInfo { class ResourceInfo
{
public: public:
ResourceInfo(const std::string&filePropertyDirectory,const std::string&resourceDirectory) ResourceInfo(const std::string &filePropertyDirectory, const std::string &resourceDirectory)
:filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {} : filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {}
std::string filePropertyDirectory; std::string filePropertyDirectory;
std::string resourceDirectory; std::string resourceDirectory;
bool operator==(const ResourceInfo&other) const { bool operator==(const ResourceInfo &other) const
{
return this->filePropertyDirectory == other.filePropertyDirectory && this->resourceDirectory == other.resourceDirectory; 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) if (this->filePropertyDirectory < other.filePropertyDirectory)
return true; return true;
if (this->filePropertyDirectory > other.filePropertyDirectory) if (this->filePropertyDirectory > other.filePropertyDirectory)
return false; 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; namespace fs = std::filesystem;
try { try
if (!fs::exists(targetDirectory)) return false; {
for (auto&directoryEntry : fs::directory_iterator(sourceDirectory)) if (!fs::exists(targetDirectory))
return false;
for (auto &directoryEntry : fs::directory_iterator(sourceDirectory))
{ {
fs::path thisPath = directoryEntry.path(); fs::path thisPath = directoryEntry.path();
if (directoryEntry.is_directory()) if (directoryEntry.is_directory())
@@ -1423,11 +1457,13 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co
auto name = thisPath.filename(); auto name = thisPath.filename();
fs::path childSource = sourceDirectory / name; fs::path childSource = sourceDirectory / name;
fs::path childTarget = targetDirectory / name; fs::path childTarget = targetDirectory / name;
if (anyTargetFilesExist(childSource,childTarget)) if (anyTargetFilesExist(childSource, childTarget))
{ {
return true; return true;
} }
} else { }
else
{
fs::path targetPath = targetDirectory / thisPath.filename(); fs::path targetPath = targetDirectory / thisPath.filename();
if (fs::exists(targetPath)) 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; return false;
} }
@@ -1452,14 +1488,17 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons
{ {
fs::path childSource = dirEntry.path(); fs::path childSource = dirEntry.path();
fs::path childTarget = targetDirectory / childSource.filename(); fs::path childTarget = targetDirectory / childSource.filename();
if (dirEntry.is_directory()) { if (dirEntry.is_directory())
createTargetLinks(childSource,childTarget); {
} else { createTargetLinks(childSource, childTarget);
fs::create_symlink(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); auto plugin = GetPluginInfo(pluginUri);
@@ -1468,32 +1507,33 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con
std::filesystem::path bundlePath = plugin->bundle_path(); std::filesystem::path bundlePath = plugin->bundle_path();
if (!plugin->piPedalUI()) if (!plugin->piPedalUI())
return; return;
const auto& fileProperties = plugin->piPedalUI()->fileProperties(); const auto &fileProperties = plugin->piPedalUI()->fileProperties();
if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri)) if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri))
{ {
pluginsThatHaveBeenCheckedForResources.insert(pluginUri); pluginsThatHaveBeenCheckedForResources.insert(pluginUri);
// eliminate duplicates. // eliminate duplicates.
std::set<ResourceInfo> resourceInfoSet; std::set<ResourceInfo> resourceInfoSet;
for (const auto&fileProperty: fileProperties) for (const auto &fileProperty : fileProperties)
{ {
if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty()) if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty())
{ {
resourceInfoSet.insert(ResourceInfo(fileProperty->directory(),fileProperty->resourceDirectory())); resourceInfoSet.insert(ResourceInfo(fileProperty->directory(), fileProperty->resourceDirectory()));
} }
} }
try { try
for (const ResourceInfo&resourceInfo: resourceInfoSet) {
for (const ResourceInfo &resourceInfo : resourceInfoSet)
{ {
std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory; std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory;
std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory; std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory;
if (!anyTargetFilesExist(sourcePath,targetPath)) if (!anyTargetFilesExist(sourcePath, targetPath))
{ {
createTargetLinks(sourcePath,targetPath); createTargetLinks(sourcePath, targetPath);
} }
} }
} catch (const std::exception &e) }
catch (const std::exception &e)
{ {
Lv2Log::error(SS("CheckForResourceInitialization: " << e.what())); 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()); 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()); AtomConverter converter(GetMapFeature());
return converter.AbstractPath(json,GetPluginStoragePath()); return converter.AbstractPath(json, GetPluginStoragePath());
} }
std::string PluginHost::MapPath(const std::string &abstractPath) std::string PluginHost::MapPath(const std::string &abstractPath)
{ {
auto storagePath = GetPluginStoragePath(); auto storagePath = GetPluginStoragePath();
@@ -1523,10 +1560,8 @@ std::string PluginHost::MapPath(const std::string &abstractPath)
return ""; return "";
} }
return SS(storagePath << '/' << abstractPath); return SS(storagePath << '/' << abstractPath);
} }
std::string PluginHost::AbstractPath(const std::string&path) std::string PluginHost::AbstractPath(const std::string &path)
{ {
auto storagePath = GetPluginStoragePath(); auto storagePath = GetPluginStoragePath();
if (path.starts_with(storagePath)) if (path.starts_with(storagePath))
+193 -26
View File
@@ -22,6 +22,7 @@
#include "Storage.hpp" #include "Storage.hpp"
#include "AudioConfig.hpp" #include "AudioConfig.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include <stdexcept>
#include <sstream> #include <sstream>
#include "json.hpp" #include "json.hpp"
#include <fstream> #include <fstream>
@@ -32,14 +33,44 @@
#include "PluginHost.hpp" #include "PluginHost.hpp"
#include "ss.hpp" #include "ss.hpp"
#include "ofstream_synced.hpp" #include "ofstream_synced.hpp"
#include "ModFileTypes.hpp"
using namespace pipedal; using namespace pipedal;
namespace fs = std::filesystem;
const char *BANK_EXTENSION = ".bank"; const char *BANK_EXTENSION = ".bank";
const char *BANKS_FILENAME = "index.banks"; const char *BANKS_FILENAME = "index.banks";
#define USER_SETTINGS_FILENAME "userSettings.json"; #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() Storage::Storage()
{ {
SetConfigRoot("~/var/Config"); SetConfigRoot("~/var/Config");
@@ -1648,63 +1679,190 @@ static bool ensureNoDotDot(const std::filesystem::path&path)
return true; return true;
} }
static void AddFilesToResult(
std::vector<FileEntry> Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) 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_;
{
ThrowPermissionDeniedError();
}
std::vector<FileEntry> result;
// if fileProperty has a user-accessible directory, push the entire file path.
if (fileProperty.directory().size() != 0)
{
std::filesystem::path audioFileDirectory = this->GetPluginUploadDirectory() / fileProperty.directory() / relativePath;
try try
{ {
for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory)) for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
{ {
const auto &path = dir_entry.path();
auto name = path.filename().string();
if (dir_entry.is_regular_file()) 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 (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! resultFiles.push_back(
result.push_back(FileEntry(path,false)); FileEntry(path,name,false,false)
);
} }
} }
} else if (dir_entry.is_directory()) { } else if (dir_entry.is_directory()) {
result.push_back(FileEntry(dir_entry.path(),true)); resultFiles.push_back(FileEntry{path,name,true,fs::is_symlink(path)});
} }
} }
} }
catch (const std::exception &error) catch (const std::exception &error)
{ {
throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string()); throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string());
}
} }
// sort lexicographically // sort lexicographically
auto collator = Locale::GetInstance()->GetCollator(); 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_) if (l.isDirectory_ != r.isDirectory_)
{ {
return 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; return result;
} }
@@ -1753,11 +1911,20 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
{ {
if (std::filesystem::is_directory(fileName)) if (std::filesystem::is_directory(fileName))
{ {
if (fileName.string().length() <= 1) // guard against rm -rf / (bitter experience)
{
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) if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience)
{ {
std::filesystem::remove_all(fileName); std::filesystem::remove_all(fileName);
} }
} }
}
else { else {
std::filesystem::remove(fileName); std::filesystem::remove(fileName);
} }
+3 -1
View File
@@ -152,7 +152,9 @@ public:
int64_t DeleteBank(int64_t bankId); int64_t DeleteBank(int64_t bankId);
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty); std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
std::vector<FileEntry> GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
FileRequestResult GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty);
void SetJackChannelSelection(const JackChannelSelection&channelSelection); void SetJackChannelSelection(const JackChannelSelection&channelSelection);