From 35f54a0ee855ae04b1fc901c90801b90251e30e7 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 31 May 2021 13:49:39 +0200 Subject: [PATCH] Add delete save feature Add command and web interface button to delete saves. --- packages/ctl/ctl.js | 14 ++++++ packages/lib/link/messages.js | 10 +++++ packages/lib/users.js | 5 +++ packages/slave/slave.js | 45 ++++++++++++++++---- packages/web_ui/src/components/SavesList.jsx | 15 ++++++- test/integration/clusterio.js | 17 ++++++++ 6 files changed, 96 insertions(+), 10 deletions(-) diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 86578c6e..4637560a 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -429,6 +429,20 @@ instanceCommands.add(new libCommand.Command({ }, })); +instanceCommands.add(new libCommand.Command({ + definition: ["delete-save ", "Delete a save from an instance", (yargs) => { + yargs.positional("instance", { describe: "Instance to delete save from", type: "string" }); + yargs.positional("save", { describe: "Save to delete", type: "string" }); + }], + handler: async function(args, control) { + let instanceId = await libCommand.resolveInstance(control, args.instance); + await libLink.messages.deleteSave.send(control, { + instance_id: instanceId, + save: args.save, + }); + }, +})); + instanceCommands.add(new libCommand.Command({ definition: ["export-data ", "Export item icons and locale from instance", (yargs) => { yargs.positional("instance", { describe: "Instance to export from", type: "string" }); diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index b96ff115..c6e0d2fe 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -533,6 +533,16 @@ messages.createSave = new Request({ }, }); +messages.deleteSave = new Request({ + type: "delete_save", + links: ["control-master", "master-slave"], + permission: "core.instance.save.delete", + forwardTo: "instance", + requestProperties: { + "save": { type: "string" }, + }, +}); + messages.pullSave = new Request({ type: "pull_save", links: ["master-slave"], diff --git a/packages/lib/users.js b/packages/lib/users.js index f637c7e1..ea132653 100644 --- a/packages/lib/users.js +++ b/packages/lib/users.js @@ -381,6 +381,11 @@ definePermission({ title: "Create new instance save", description: "Create new savegames on instances.", }); +definePermission({ + name: "core.instance.save.delete", + title: "Delete instance save", + description: "Delete savegames on instances.", +}); definePermission({ name: "core.instance.save.upload", title: "Upload save to instance", diff --git a/packages/slave/slave.js b/packages/slave/slave.js index c0875fb0..13b6a9b6 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -1375,6 +1375,41 @@ class Slave extends libLink.Link { await request.send(instanceConnection, message.data); } + async sendSaveListUpdate(instance_id, savesDir) { + let instanceConnection = this.instanceConnections.get(instance_id); + let saveList; + if (instanceConnection) { + saveList = (await libLink.messages.listSaves.send(instanceConnection, { instance_id })).list; + } else { + saveList = await Instance.listSaves(savesDir, null); + } + + libLink.messages.saveListUpdate.send(this, { instance_id, list: saveList }); + } + + async deleteSaveRequestHandler(message) { + let { instance_id, save } = message.data; + try { + checkFilename(save); + } catch (err) { + throw new libErrors.RequestError(`Save name ${err.message}`); + } + let instanceInfo = this.instanceInfos.get(instance_id); + if (!instanceInfo) { + throw new libErrors.RequestError(`Instance with ID ${instance_id} does not exist`); + } + + try { + await fs.unlink(path.join(instanceInfo.path, "saves", save)); + } catch (err) { + if (err.code === "ENOENT") { + throw new libErrors.RequestError(`${save} does not exist`); + } + throw err; + } + await this.sendSaveListUpdate(instance_id, path.join(instanceInfo.path, "saves")); + } + async pullSaveRequestHandler(message) { let { instance_id, stream_id, filename } = message.data; try { @@ -1422,15 +1457,7 @@ class Slave extends libLink.Link { filename = await libFileOps.findUnusedName(savesDir, filename, ".zip"); await fs.rename(path.join(savesDir, tempFilename), path.join(savesDir, filename)); - let instanceConnection = this.instanceConnections.get(instance_id); - let saveList; - if (instanceConnection) { - saveList = (await libLink.messages.listSaves.send(instanceConnection, { instance_id })).list; - } else { - saveList = await Instance.listSaves(savesDir, null); - } - - libLink.messages.saveListUpdate.send(this, { instance_id, list: saveList }); + await this.sendSaveListUpdate(instance_id, savesDir); return { save: filename }; } diff --git a/packages/web_ui/src/components/SavesList.jsx b/packages/web_ui/src/components/SavesList.jsx index fcf2e6b4..7f5b9f11 100644 --- a/packages/web_ui/src/components/SavesList.jsx +++ b/packages/web_ui/src/components/SavesList.jsx @@ -1,5 +1,5 @@ import React, { useContext, useState } from "react"; -import { Button, Space, Table, Tooltip, Upload } from "antd"; +import { Button, Popconfirm, Space, Table, Tooltip, Upload } from "antd"; import CaretLeftOutlined from "@ant-design/icons/CaretLeftOutlined"; import LeftOutlined from "@ant-design/icons/LeftOutlined"; @@ -77,6 +77,19 @@ export default function SavesList(props) { ); }} >Load save + { + libLink.messages.deleteSave.send( + control, { instance_id: props.instance.id, save: save.name } + ).catch(notifyErrorHandler("Error deleting save")); + }} + > + + , }} />; diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 64eb0a17..4f875105 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -377,6 +377,23 @@ describe("Integration of Clusterio", function() { }); }); + describe("instance delete-save", function() { + it("should delete a save", async function() { + await execCtl("instance delete-save 44 upload.zip"); + assert( + !await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "upload.zip")), + "file not deleted" + ); + }); + it("should error if save does not exist", async function() { + await assert.rejects(execCtl("instance delete-save 44 upload.zip")); + }); + it("should error on path traversal attacks", async function() { + await fs.outputFile(path.join("temp", "test", "upload.zip"), "a test"); + await assert.rejects(execCtl("instance delete-save 44 ../../upload.zip")); + }); + }); + describe("instance delete", function() { it("deletes the instance", async function() { slowTest(this);