Can rename disk image files

This commit is contained in:
2025-05-31 10:45:15 +02:00
parent 4ee01cad4b
commit f850ca5cb7
5 changed files with 173 additions and 29 deletions
virtweb_backend/src
virtweb_frontend/src

@ -189,6 +189,46 @@ pub async fn handle_convert_request(
Ok(HttpResponse::Accepted().json("Successfully converted disk file"))
}
#[derive(serde::Deserialize)]
pub struct RenameDiskImageRequest {
name: String,
}
/// Rename disk image
pub async fn rename(
p: web::Path<DiskFilePath>,
req: web::Json<RenameDiskImageRequest>,
) -> HttpResult {
// Check source
if !files_utils::check_file_name(&p.filename) {
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
}
let src_path = AppConfig::get().disk_images_file_path(&p.filename);
if !src_path.exists() {
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
}
// Check destination
if !files_utils::check_file_name(&req.name) {
return Ok(HttpResponse::BadRequest().json("Invalid dst file name!"));
}
let dst_path = AppConfig::get().disk_images_file_path(&req.name);
if dst_path.exists() {
return Ok(HttpResponse::Conflict().json("Destination name already exists!"));
}
// Check extension
let disk = DiskFileInfo::load_file(&src_path)?;
if !disk.format.ext().iter().any(|e| req.name.ends_with(e)) {
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
}
// Perform rename
std::fs::rename(&src_path, &dst_path)?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete a disk image
pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
if !files_utils::check_file_name(&p.filename) {

@ -352,6 +352,10 @@ async fn main() -> std::io::Result<()> {
"/api/disk_images/{filename}/convert",
web::post().to(disk_images_controller::convert),
)
.route(
"/api/disk_images/{filename}/rename",
web::post().to(disk_images_controller::rename),
)
.route(
"/api/disk_images/{filename}",
web::delete().to(disk_images_controller::delete),

@ -94,6 +94,17 @@ export class DiskImageApi {
});
}
/**
* Rename disk image file
*/
static async Rename(file: DiskImage, name: string): Promise<void> {
await APIClient.exec({
method: "POST",
uri: `/disk_images/${file.file_name}/rename`,
jsonData: { name },
});
}
/**
* Delete disk image file
*/

@ -1,6 +1,7 @@
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import LoopIcon from "@mui/icons-material/Loop";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
Alert,
@ -8,6 +9,10 @@ import {
CircularProgress,
IconButton,
LinearProgress,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip,
Typography,
} from "@mui/material";
@ -164,15 +169,11 @@ function DiskImageList(p: {
const confirm = useConfirm();
const loadingMessage = useLoadingMessage();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
const [currConversion, setCurrConversion] = React.useState<
DiskImage | undefined
>();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
// Convert disk image file
const convertDiskImage = (entry: DiskImage) => {
setCurrConversion(entry);
};
// Download disk image file
const downloadDiskImage = async (entry: DiskImage) => {
@ -190,6 +191,11 @@ function DiskImageList(p: {
setDlProgress(undefined);
};
// Convert disk image file
const convertDiskImage = (entry: DiskImage) => {
setCurrConversion(entry);
};
// Delete disk image
const deleteDiskImage = async (entry: DiskImage) => {
if (
@ -221,7 +227,7 @@ function DiskImageList(p: {
);
const columns: GridColDef<(typeof p.list)[number]>[] = [
{ field: "file_name", headerName: "File name", flex: 3 },
{ field: "file_name", headerName: "File name", flex: 3, editable: true },
{
field: "format",
headerName: "Format",
@ -260,28 +266,21 @@ function DiskImageList(p: {
},
{
field: "actions",
type: "actions",
headerName: "",
width: 140,
renderCell(params) {
return (
<>
<Tooltip title="Convert disk image">
<IconButton onClick={() => { convertDiskImage(params.row); }}>
<LoopIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download disk image">
<IconButton onClick={() => downloadDiskImage(params.row)}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete disk image">
<IconButton onClick={() => deleteDiskImage(params.row)}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
);
width: 55,
cellClassName: "actions",
editable: false,
getActions: (params) => {
return [
<DiskImageActionMenu
key="menu"
diskImage={params.row}
onDownload={downloadDiskImage}
onConvert={convertDiskImage}
onDelete={deleteDiskImage}
/>,
];
},
},
];
@ -327,7 +326,92 @@ function DiskImageList(p: {
)}
{/* The table itself */}
<DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
<DataGrid<DiskImage>
getRowId={(c) => c.file_name}
rows={p.list}
columns={columns}
processRowUpdate={async (n, o) => {
try {
await DiskImageApi.Rename(o, n.file_name);
return n;
} catch (e) {
console.error("Failed to rename disk image!", e);
alert(`Failed to rename disk image! ${e}`);
throw e;
} finally {
p.onReload();
}
}}
/>
</>
);
}
function DiskImageActionMenu(p: {
diskImage: DiskImage;
onDownload: (d: DiskImage) => void;
onConvert: (d: DiskImage) => void;
onDelete: (d: DiskImage) => void;
}): React.ReactElement {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<>
<IconButton
aria-label="Actions"
aria-haspopup="true"
onClick={handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
{/* Download disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onDownload(p.diskImage);
}}
>
<ListItemIcon>
<DownloadIcon />
</ListItemIcon>
<ListItemText secondary={"Download disk image"}>
Download
</ListItemText>
</MenuItem>
{/* Convert disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onConvert(p.diskImage);
}}
>
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText secondary={"Convert disk image"}>Convert</ListItemText>
</MenuItem>
{/* Delete disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onDelete(p.diskImage);
}}
>
<ListItemIcon>
<DeleteIcon color="error" />
</ListItemIcon>
<ListItemText secondary={"Delete disk image"}>Delete</ListItemText>
</MenuItem>
</Menu>
</>
);
}

@ -707,6 +707,11 @@ export function TokenRightsEditor(p: {
right={{ verb: "POST", path: "/api/disk_images/*/convert" }}
label="Convert disk images"
/>
<RouteRight
{...p}
right={{ verb: "POST", path: "/api/disk_images/*/rename" }}
label="Rename disk images"
/>
<RouteRight
{...p}
right={{ verb: "DELETE", path: "/api/disk_images/*" }}