From 4edc7a29492b04eeb7643ca04bbade9f9cabd076 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 27 Mar 2022 19:53:06 +0200 Subject: [PATCH] Implement rename and copy save Add ability to rename and copy existing saves on an instance. --- CHANGELOG.md | 2 +- packages/ctl/ctl.js | 32 ++++++++ packages/lib/link/messages.js | 22 +++++ packages/lib/users.js | 10 +++ packages/slave/slave.js | 40 +++++++++ packages/web_ui/src/components/SavesList.jsx | 86 +++++++++++++++++++- test/integration/clusterio.js | 35 ++++++++ 7 files changed, 225 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7844f35f..db398cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Packaged and uploaded to npm again, greatly simplifying distribution and installation of both Clusterio and plugins. - Added centralized logging for the cluster. All logs from both Clusterio and Factorio is stored in a shared log which can be inspected and queried. -- Added remote save management with the ability to list, create, upload, download, delete and load saves for instances. +- Added remote save management with the ability to list, create, rename, copy, upload, download, delete and load saves for instances. ### Features diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 1c651140..0f6dbedf 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -576,6 +576,38 @@ instanceSaveCommands.add(new libCommand.Command({ }, })); +instanceSaveCommands.add(new libCommand.Command({ + definition: ["rename ", "Rename a save on an instance", (yargs) => { + yargs.positional("instance", { describe: "Instance to rename save on", type: "string" }); + yargs.positional("old-name", { describe: "Old name of save.", type: "string" }); + yargs.positional("new-name", { describe: "New name of save.", type: "string" }); + }], + handler: async function(args, control) { + let instanceId = await libCommand.resolveInstance(control, args.instance); + await libLink.messages.renameSave.send(control, { + instance_id: instanceId, + old_name: args.oldName, + new_name: args.newName, + }); + }, +})); + +instanceSaveCommands.add(new libCommand.Command({ + definition: ["copy ", "Copy a save on an instance", (yargs) => { + yargs.positional("instance", { describe: "Instance to copy save on", type: "string" }); + yargs.positional("source", { describe: "Save to copy.", type: "string" }); + yargs.positional("destination", { describe: "Name of copy.", type: "string" }); + }], + handler: async function(args, control) { + let instanceId = await libCommand.resolveInstance(control, args.instance); + await libLink.messages.copySave.send(control, { + instance_id: instanceId, + source: args.source, + destination: args.destination, + }); + }, +})); + instanceSaveCommands.add(new libCommand.Command({ definition: ["upload ", "Upload a save to an instance", (yargs) => { yargs.positional("instance", { describe: "Instance to upload to", type: "string" }); diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 06135ff4..13f5180a 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -550,6 +550,28 @@ messages.createSave = new Request({ }, }); +messages.renameSave = new Request({ + type: "rename_save", + links: ["control-master", "master-slave"], + permission: "core.instance.save.rename", + forwardTo: "instance", + requestProperties: { + "old_name": { type: "string" }, + "new_name": { type: "string" }, + }, +}); + +messages.copySave = new Request({ + type: "copy_save", + links: ["control-master", "master-slave"], + permission: "core.instance.save.copy", + forwardTo: "instance", + requestProperties: { + "source": { type: "string" }, + "destination": { type: "string" }, + }, +}); + messages.deleteSave = new Request({ type: "delete_save", links: ["control-master", "master-slave"], diff --git a/packages/lib/users.js b/packages/lib/users.js index bbe9b98f..a5539b7c 100644 --- a/packages/lib/users.js +++ b/packages/lib/users.js @@ -392,6 +392,16 @@ definePermission({ title: "Create new save", description: "Create new savegames on instances.", }); +definePermission({ + name: "core.instance.save.rename", + title: "Rename save", + description: "Rename savegames on instances.", +}); +definePermission({ + name: "core.instance.save.copy", + title: "Copy save", + description: "Create copies of savegames on instances.", +}); definePermission({ name: "core.instance.save.delete", title: "Delete save", diff --git a/packages/slave/slave.js b/packages/slave/slave.js index cdb506b4..61e4f110 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -1449,6 +1449,46 @@ class Slave extends libLink.Link { await request.send(instanceConnection, message.data); } + async renameSaveRequestHandler(message) { + let { instance_id, old_name, new_name } = message.data; + checkRequestSaveName(old_name); + checkRequestSaveName(new_name); + let instanceInfo = this.getRequestInstanceInfo(instance_id); + try { + await fs.move( + path.join(instanceInfo.path, "saves", old_name), + path.join(instanceInfo.path, "saves", new_name), + { overwrite: false }, + ); + } catch (err) { + if (err.code === "ENOENT") { + throw new libErrors.RequestError(`${old_name} does not exist`); + } + throw err; + } + await this.sendSaveListUpdate(instance_id, path.join(instanceInfo.path, "saves")); + } + + async copySaveRequestHandler(message) { + let { instance_id, source, destination } = message.data; + checkRequestSaveName(source); + checkRequestSaveName(destination); + let instanceInfo = this.getRequestInstanceInfo(instance_id); + try { + await fs.copy( + path.join(instanceInfo.path, "saves", source), + path.join(instanceInfo.path, "saves", destination), + { overwrite: false, errorOnExist: true }, + ); + } catch (err) { + if (err.code === "ENOENT") { + throw new libErrors.RequestError(`${old_name} does not exist`); + } + throw err; + } + await this.sendSaveListUpdate(instance_id, path.join(instanceInfo.path, "saves")); + } + async sendSaveListUpdate(instance_id, savesDir) { let instanceConnection = this.instanceConnections.get(instance_id); let saveList; diff --git a/packages/web_ui/src/components/SavesList.jsx b/packages/web_ui/src/components/SavesList.jsx index 72c0554a..e63524fa 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, List, Popconfirm, Progress, Space, Table, Tooltip, Upload } from "antd"; +import { Button, Form, Input, List, Modal, Popconfirm, Progress, Space, Table, Tooltip, Upload } from "antd"; import CaretLeftOutlined from "@ant-design/icons/CaretLeftOutlined"; import LeftOutlined from "@ant-design/icons/LeftOutlined"; @@ -13,6 +13,84 @@ import { useSaves } from "../model/saves"; import { notifyErrorHandler } from "../util/notify"; +function RenameModal(props) { + let control = useContext(ControlContext); + let [visible, setVisible] = useState(false); + let [form] = Form.useForm(); + + return <> + + form.submit()} + onCancel={() => setVisible(false)} + > +
{ + libLink.messages.renameSave.send(control, + { instance_id: props.instanceId, old_name: props.save.name, new_name: values.newName } + ).then(() => { + setVisible(false); + form.resetFields(); + }).catch(notifyErrorHandler("Error renaming save")); + }} + > + + + +
+
+ ; +} + +function CopyModal(props) { + let control = useContext(ControlContext); + let [visible, setVisible] = useState(false); + let [form] = Form.useForm(); + + return <> + + form.submit()} + onCancel={() => setVisible(false)} + > +
{ + libLink.messages.copySave.send(control, + { instance_id: props.instanceId, source: props.save.name, destination: values.newName } + ).then(() => { + setVisible(false); + form.resetFields(); + }).catch(notifyErrorHandler("Error copying save")); + }} + > + + + +
+
+ ; +} + function formatBytes(bytes) { if (bytes === 0) { return "0 Bytes"; @@ -81,6 +159,12 @@ export default function SavesList(props) { ); }} >Load save} + {account.hasPermission("core.instance.save.rename") && } + {account.hasPermission("core.instance.save.copy") && } {account.hasPermission("core.instance.save.download") &&