mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
10 files changed
+712
-104
No files matched your search
+1
-1
@@ -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, transfer, delete and load saves for instances.
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
+74
-10
@@ -533,8 +533,11 @@ async function loadMapSettings(args) {
|
||||
};
|
||||
}
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["list-saves <instance>", "list saves on an instance", (yargs) => {
|
||||
const instanceSaveCommands = new libCommand.CommandTree({
|
||||
name: "save", alias: ["s"], description: "Instance save management",
|
||||
});
|
||||
instanceSaveCommands.add(new libCommand.Command({
|
||||
definition: ["list <instance>", "list saves on an instance", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to list saves on", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
@@ -548,8 +551,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["create-save <instance> [name]", "Create a new save on an instance", (yargs) => {
|
||||
instanceSaveCommands.add(new libCommand.Command({
|
||||
definition: ["create <instance> [name]", "Create a new save on an instance", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to create on", type: "string" });
|
||||
yargs.positional("name", { describe: "Name of save to create.", type: "string", default: "world.zip" });
|
||||
yargs.options({
|
||||
@@ -573,8 +576,40 @@ instanceCommands.add(new libCommand.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["upload-save <instance> <filepath>", "Upload a save to an instance", (yargs) => {
|
||||
instanceSaveCommands.add(new libCommand.Command({
|
||||
definition: ["rename <instance> <old-name> <new-name>", "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 <instance> <source> <destination>", "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 <instance> <filepath>", "Upload a save to an instance", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to upload to", type: "string" });
|
||||
yargs.positional("filepath", { describe: "Path to save to upload", type: "string" });
|
||||
yargs.options({
|
||||
@@ -624,8 +659,36 @@ instanceCommands.add(new libCommand.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["download-save <instance> <save>", "Download a save from an instance", (yargs) => {
|
||||
instanceSaveCommands.add(new libCommand.Command({
|
||||
definition: [
|
||||
"transfer <source-instance> <source-save> <target-instance> [target-save]",
|
||||
"Transfer a save between instances",
|
||||
(yargs) => {
|
||||
yargs.positional("source-instance", { describe: "Instance to transfer save from", type: "string" });
|
||||
yargs.positional("source-save", { describe: "Save to transfer.", type: "string" });
|
||||
yargs.positional("target-instance", { describe: "Instance to transfer to", type: "string" });
|
||||
yargs.positional("target-save", { describe: "Name to give transferred save.", type: "string" });
|
||||
yargs.options({
|
||||
"copy": { describe: "Copy instead of moving the save", type: "boolean", default: false },
|
||||
});
|
||||
},
|
||||
],
|
||||
handler: async function(args, control) {
|
||||
let sourceInstanceId = await libCommand.resolveInstance(control, args.sourceInstance);
|
||||
let targetInstanceId = await libCommand.resolveInstance(control, args.targetInstance);
|
||||
let result = await libLink.messages.transferSave.send(control, {
|
||||
instance_id: sourceInstanceId,
|
||||
source_save: args.sourceSave,
|
||||
target_instance_id: targetInstanceId,
|
||||
target_save: args.targetSave || args.sourceSave,
|
||||
copy: args.copy,
|
||||
});
|
||||
print(`Transferred as ${result.save} to ${args.targetInstance}.`);
|
||||
},
|
||||
}));
|
||||
|
||||
instanceSaveCommands.add(new libCommand.Command({
|
||||
definition: ["download <instance> <save>", "Download a save from an instance", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to download save from", type: "string" });
|
||||
yargs.positional("save", { describe: "Save to download", type: "string" });
|
||||
}],
|
||||
@@ -669,8 +732,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["delete-save <instance> <save>", "Delete a save from an instance", (yargs) => {
|
||||
instanceSaveCommands.add(new libCommand.Command({
|
||||
definition: ["delete <instance> <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" });
|
||||
}],
|
||||
@@ -682,6 +745,7 @@ instanceCommands.add(new libCommand.Command({
|
||||
});
|
||||
},
|
||||
}));
|
||||
instanceCommands.add(instanceSaveCommands);
|
||||
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: ["export-data <instance>", "Export item icons and locale from instance", (yargs) => {
|
||||
|
||||
@@ -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"],
|
||||
@@ -573,6 +595,22 @@ messages.downloadSave = new Request({
|
||||
},
|
||||
});
|
||||
|
||||
messages.transferSave = new Request({
|
||||
type: "transfer_save",
|
||||
links: ["control-master", "master-slave"],
|
||||
permission: "core.instance.save.transfer",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"source_save": { type: "string" },
|
||||
"target_instance_id": { type: "integer" },
|
||||
"target_save": { type: "string" },
|
||||
"copy": { type: "boolean" },
|
||||
},
|
||||
responseProperties: {
|
||||
"save": { type: "string" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.pullSave = new Request({
|
||||
type: "pull_save",
|
||||
links: ["master-slave"],
|
||||
|
||||
+20
-5
@@ -379,7 +379,7 @@ definePermission({
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.list",
|
||||
title: "List saves on instance",
|
||||
title: "List saves",
|
||||
description: "List the saves currently on the instance.",
|
||||
});
|
||||
definePermission({
|
||||
@@ -389,22 +389,37 @@ definePermission({
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.create",
|
||||
title: "Create new instance save",
|
||||
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 instance save",
|
||||
title: "Delete save",
|
||||
description: "Delete savegames on instances.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.upload",
|
||||
title: "Upload save to instance",
|
||||
title: "Upload save",
|
||||
description: "Upload savegames to instances.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.transfer",
|
||||
title: "Transfer save",
|
||||
description: "Transfer savegames between instances.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.instance.save.download",
|
||||
title: "Download save from instance",
|
||||
title: "Download save",
|
||||
description: "Download savegames from instances.",
|
||||
});
|
||||
definePermission({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
const jwt = require("jsonwebtoken");
|
||||
const events = require("events");
|
||||
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
@@ -159,10 +160,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async getInstanceRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
let instance = this._master.instances.get(id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${id} does not exist`);
|
||||
}
|
||||
let instance = this._master.getRequestInstance(id);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -249,11 +247,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async deleteInstanceRequestHandler(message, request) {
|
||||
let instance = this._master.instances.get(message.data.instance_id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
if (instance.config.get("instance.assigned_slave") !== null) {
|
||||
await this.forwardRequestToInstance(message, request);
|
||||
}
|
||||
@@ -266,11 +260,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async getInstanceConfigRequestHandler(message) {
|
||||
let instance = this._master.instances.get(message.data.instance_id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
return {
|
||||
serialized_config: instance.config.serialize("control"),
|
||||
};
|
||||
@@ -290,11 +280,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async setInstanceConfigFieldRequestHandler(message) {
|
||||
let instance = this._master.instances.get(message.data.instance_id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
if (message.data.field === "instance.assigned_slave") {
|
||||
throw new libErrors.RequestError("instance.assigned_slave must be set through the assign-slave interface");
|
||||
}
|
||||
@@ -309,11 +295,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async setInstanceConfigPropRequestHandler(message) {
|
||||
let instance = this._master.instances.get(message.data.instance_id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
let instance = this._master.getRequestInstance(message.data.instance_id);
|
||||
let { field, prop, value } = message.data;
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
await this.updateInstanceConfig(instance);
|
||||
@@ -321,10 +303,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async assignInstanceCommandRequestHandler(message, request) {
|
||||
let { slave_id, instance_id } = message.data;
|
||||
let instance = this._master.instances.get(instance_id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instance_id} does not exist`);
|
||||
}
|
||||
let instance = this._master.getRequestInstance(instance_id);
|
||||
|
||||
// Check if target slave is connected
|
||||
let newSlaveConnection;
|
||||
@@ -395,6 +374,85 @@ class ControlConnection extends BaseConnection {
|
||||
return { stream_id: stream.id };
|
||||
}
|
||||
|
||||
async transferSaveRequestHandler(message, request) {
|
||||
let { source_save, target_save, copy, instance_id, target_instance_id } = message.data;
|
||||
if (copy) {
|
||||
this.user.checkPermission("core.instance.save.copy");
|
||||
} else if (source_save !== target_save) {
|
||||
this.user.checkPermission("core.instance.save.rename");
|
||||
}
|
||||
|
||||
if (instance_id === target_instance_id) {
|
||||
throw new libErrors.RequestError("Source and target instance may not be the same");
|
||||
}
|
||||
let sourceInstance = this._master.getRequestInstance(instance_id);
|
||||
let targetInstance = this._master.getRequestInstance(target_instance_id);
|
||||
let sourceSlaveId = sourceInstance.config.get("instance.assigned_slave");
|
||||
let targetSlaveId = targetInstance.config.get("instance.assigned_slave");
|
||||
if (sourceSlaveId === null) {
|
||||
throw new libErrors.RequestError("Source instance is not assigned a slave");
|
||||
}
|
||||
if (targetSlaveId === null) {
|
||||
throw new libErrors.RequestError("Target instance is not assigned a slave");
|
||||
}
|
||||
|
||||
// Let slave handle request if source and target is on the same slave.
|
||||
if (sourceSlaveId === targetSlaveId) {
|
||||
return await this.forwardRequestToInstance(message, request);
|
||||
}
|
||||
|
||||
// Check connectivity
|
||||
let sourceSlaveConnection = this._master.wsServer.slaveConnections.get(sourceSlaveId);
|
||||
if (!sourceSlaveConnection || sourceSlaveConnection.closing) {
|
||||
throw new libErrors.RequestError("Source slave is not connected to the master server");
|
||||
}
|
||||
|
||||
let targetSlaveConnection = this._master.wsServer.slaveConnections.get(targetSlaveId);
|
||||
if (!targetSlaveConnection || targetSlaveConnection.closing) {
|
||||
throw new libErrors.RequestError("Target slave is not connected to the master server");
|
||||
}
|
||||
|
||||
// Create stream to proxy from target to source
|
||||
let stream = await routes.createProxyStream(this._master.app);
|
||||
stream.events.on("timeout", () => {
|
||||
if (stream.source) {
|
||||
stream.source.destroy();
|
||||
}
|
||||
stream.events.emit("error", new libErrors.RequestError("Timed out establishing transfer stream"));
|
||||
});
|
||||
|
||||
// Ignore errors if not listening for them to avoid crash.
|
||||
stream.events.on("error", () => { /* ignore */ });
|
||||
|
||||
// Establish push from source slave to stream, this is done first to
|
||||
// ensure the file size is known prior to the target slave pull.
|
||||
await Promise.all([
|
||||
this._master.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save: source_save,
|
||||
}),
|
||||
events.once(stream.events, "source"),
|
||||
]);
|
||||
|
||||
// Establish pull from target slave to stream and wait for completion.
|
||||
let result = await this._master.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
instance_id: target_instance_id,
|
||||
stream_id: stream.id,
|
||||
filename: target_save,
|
||||
});
|
||||
|
||||
// Delete source save if this is not a copy
|
||||
if (!copy) {
|
||||
await this._master.forwardRequestToInstance(libLink.messages.deleteSave, {
|
||||
instance_id,
|
||||
save: source_save,
|
||||
});
|
||||
}
|
||||
|
||||
return { save: result.save };
|
||||
}
|
||||
|
||||
async setLogSubscriptionsRequestHandler(message) {
|
||||
this.logSubscriptions = message.data;
|
||||
this.updateLogSubscriptions();
|
||||
|
||||
@@ -424,6 +424,21 @@ class Master {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance by ID for a request
|
||||
*
|
||||
* @param {number} instanceId - ID of instance to get.
|
||||
* @returns {object} instance
|
||||
* @throws {module:lib/errors.RequestError} if the instance does not exist.
|
||||
*/
|
||||
getRequestInstance(instanceId) {
|
||||
let instance = this.instances.get(instanceId);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
addInstanceHooks(instance) {
|
||||
instance.config.on("fieldChanged", (group, field, prev) => {
|
||||
if (group.name === "instance" && field === "name") {
|
||||
@@ -608,11 +623,7 @@ class Master {
|
||||
* @returns {object} data returned from the request.
|
||||
*/
|
||||
async forwardRequestToInstance(request, data) {
|
||||
let instance = this.instances.get(data.instance_id);
|
||||
if (!instance) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
let instance = this.getRequestInstance(data.instance_id);
|
||||
let slaveId = instance.config.get("instance.assigned_slave");
|
||||
if (slaveId === null) {
|
||||
throw new libErrors.RequestError("Instance is not assigned to a slave");
|
||||
|
||||
+104
-37
@@ -1077,6 +1077,15 @@ function checkFilename(name) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkRequestSaveName(name) {
|
||||
try {
|
||||
checkFilename(name);
|
||||
} catch (err) {
|
||||
throw new libErrors.RequestError(`Save name ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles running the slave
|
||||
*
|
||||
@@ -1351,6 +1360,14 @@ class Slave extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
getRequestInstanceInfo(instanceId) {
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (!instanceInfo) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
return instanceInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and connect an unloaded instance
|
||||
*
|
||||
@@ -1358,11 +1375,7 @@ class Slave extends libLink.Link {
|
||||
* @returns {module:slave/slave~InstanceConnection} connection to instance.
|
||||
*/
|
||||
async _connectInstance(instanceId) {
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (!instanceInfo) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
@@ -1424,11 +1437,7 @@ class Slave extends libLink.Link {
|
||||
return await request.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (!instanceInfo) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
return {
|
||||
list: await Instance.listSaves(path.join(instanceInfo.path, "saves"), null),
|
||||
};
|
||||
@@ -1440,6 +1449,85 @@ 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 transferSaveRequestHandler(message) {
|
||||
let { source_save, target_save, copy, instance_id, target_instance_id } = message.data;
|
||||
checkRequestSaveName(source_save);
|
||||
checkRequestSaveName(target_save);
|
||||
let sourceInstanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
let targetInstanceInfo = this.getRequestInstanceInfo(target_instance_id);
|
||||
|
||||
// For consistency with remote transfer initiated through pullSave the
|
||||
// target is renamed if it already exists.
|
||||
target_save = await libFileOps.findUnusedName(
|
||||
path.join(targetInstanceInfo.path, "saves"), target_save, ".zip"
|
||||
);
|
||||
|
||||
try {
|
||||
if (copy) {
|
||||
await fs.copy(
|
||||
path.join(sourceInstanceInfo.path, "saves", source_save),
|
||||
path.join(targetInstanceInfo.path, "saves", target_save),
|
||||
{ overwrite: true },
|
||||
);
|
||||
} else {
|
||||
await fs.move(
|
||||
path.join(sourceInstanceInfo.path, "saves", source_save),
|
||||
path.join(targetInstanceInfo.path, "saves", target_save),
|
||||
{ overwrite: true },
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new libErrors.RequestError(`${source_save} does not exist`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await this.sendSaveListUpdate(instance_id, path.join(sourceInstanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(target_instance_id, path.join(targetInstanceInfo.path, "saves"));
|
||||
|
||||
return { save: target_save };
|
||||
}
|
||||
|
||||
async sendSaveListUpdate(instance_id, savesDir) {
|
||||
let instanceConnection = this.instanceConnections.get(instance_id);
|
||||
let saveList;
|
||||
@@ -1454,15 +1542,8 @@ class Slave extends libLink.Link {
|
||||
|
||||
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`);
|
||||
}
|
||||
checkRequestSaveName(save);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
|
||||
try {
|
||||
await fs.unlink(path.join(instanceInfo.path, "saves", save));
|
||||
@@ -1477,15 +1558,8 @@ class Slave extends libLink.Link {
|
||||
|
||||
async pullSaveRequestHandler(message) {
|
||||
let { instance_id, stream_id, filename } = message.data;
|
||||
try {
|
||||
checkFilename(filename);
|
||||
} 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`);
|
||||
}
|
||||
checkRequestSaveName(filename);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
|
||||
let url = new URL(this.config.get("slave.master_url"));
|
||||
url.pathname += `api/stream/${stream_id}`;
|
||||
@@ -1528,15 +1602,8 @@ class Slave extends libLink.Link {
|
||||
|
||||
async pushSaveRequestHandler(message) {
|
||||
let { instance_id, stream_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`);
|
||||
}
|
||||
checkRequestSaveName(save);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
|
||||
let content;
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, List, Popconfirm, Progress, Space, Table, Tooltip, Upload } from "antd";
|
||||
import {
|
||||
message, Button, Checkbox, Form, Input, List, Modal,
|
||||
Popconfirm, Progress, Select, Space, Table, Tooltip, Upload,
|
||||
} from "antd";
|
||||
import CaretLeftOutlined from "@ant-design/icons/CaretLeftOutlined";
|
||||
import LeftOutlined from "@ant-design/icons/LeftOutlined";
|
||||
|
||||
@@ -9,10 +12,173 @@ import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
import CreateSaveModal from "./CreateSaveModal";
|
||||
import SectionHeader from "./SectionHeader";
|
||||
import { useInstanceList } from "../model/instance";
|
||||
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 <>
|
||||
<Button disabled={props.disabled} onClick={() => setVisible(true)}>Rename</Button>
|
||||
<Modal
|
||||
title="Rename save"
|
||||
okText="Rename"
|
||||
visible={visible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={() => setVisible(false)}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ newName: props.save.name }}
|
||||
onFinish={values => {
|
||||
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"));
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="newName"
|
||||
label="New name"
|
||||
rules={[{ required: true, message: "New name is required" }]}
|
||||
>
|
||||
<Input autoFocus />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
|
||||
function CopyModal(props) {
|
||||
let control = useContext(ControlContext);
|
||||
let [visible, setVisible] = useState(false);
|
||||
let [form] = Form.useForm();
|
||||
|
||||
return <>
|
||||
<Button disabled={props.disabled} onClick={() => setVisible(true)}>Copy</Button>
|
||||
<Modal
|
||||
title="Copy save"
|
||||
okText="Copy"
|
||||
visible={visible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={() => setVisible(false)}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ newName: props.save.name }}
|
||||
onFinish={values => {
|
||||
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"));
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="newName"
|
||||
label="New name"
|
||||
rules={[{ required: true, message: "New name is required" }]}
|
||||
>
|
||||
<Input autoFocus />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
|
||||
function TransferModal(props) {
|
||||
let account = useAccount();
|
||||
let control = useContext(ControlContext);
|
||||
let [visible, setVisible] = useState(false);
|
||||
let [form] = Form.useForm();
|
||||
let [instanceList] = useInstanceList();
|
||||
|
||||
return <>
|
||||
<Button disabled={props.disabled} onClick={() => setVisible(true)}>Transfer</Button>
|
||||
<Modal
|
||||
title="Transfer save"
|
||||
okText="Transfer"
|
||||
visible={visible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={() => setVisible(false)}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
initialValues={{ transferredName: props.save.name, copy: false }}
|
||||
onFinish={values => {
|
||||
setVisible(false);
|
||||
let hide = message.loading("Transferring save...", 0);
|
||||
libLink.messages.transferSave.send(control, {
|
||||
instance_id: props.instanceId,
|
||||
source_save: props.save.name,
|
||||
target_instance_id: values.targetInstanceId,
|
||||
target_save: values.transferredName || props.save.name,
|
||||
copy: values.copy,
|
||||
}).then(() => {
|
||||
message.success("Transfer complete");
|
||||
form.resetFields();
|
||||
}).catch(
|
||||
notifyErrorHandler("Error transfering save")
|
||||
).finally(() => hide());
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="targetInstanceId"
|
||||
label="Target Instance"
|
||||
rules={[{ required: true, message: "Target Instance is required" }]}
|
||||
>
|
||||
<Select
|
||||
autoFocus
|
||||
showSearch
|
||||
filterOption={(input, option) => option.title.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
>
|
||||
{instanceList.filter(
|
||||
instance => instance["id"] !== props.instanceId
|
||||
).map((instance) => <Select.Option
|
||||
key={instance["id"]}
|
||||
value={instance["id"]}
|
||||
title={instance["name"]}
|
||||
disabled={["unassigned", "unknown"].includes(instance["status"])}
|
||||
>
|
||||
{instance["name"]}
|
||||
{instance["status"] === "unassigned" && " (unassigned)"}
|
||||
{instance["status"] === "unknown" && " (offline)"}
|
||||
</Select.Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="transferredName"
|
||||
label="Transferred name"
|
||||
disabled={!account.hasAnyPermission("core.instance.save.rename", "core.instance.save.copy")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="copy"
|
||||
valuePropName="checked"
|
||||
label="Copy"
|
||||
tooltip="Copy instead of moving the save to the new instance."
|
||||
disabled={!account.hasPermission("core.instance.save.copy")}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) {
|
||||
return "0 Bytes";
|
||||
@@ -81,6 +247,12 @@ export default function SavesList(props) {
|
||||
);
|
||||
}}
|
||||
>Load save</Button>}
|
||||
{account.hasPermission("core.instance.save.rename") && <RenameModal
|
||||
disabled={slaveOffline} instanceId={props.instance.id} save={save}
|
||||
/>}
|
||||
{account.hasPermission("core.instance.save.copy") && <CopyModal
|
||||
disabled={slaveOffline} instanceId={props.instance.id} save={save}
|
||||
/>}
|
||||
{account.hasPermission("core.instance.save.download") && <Button
|
||||
disabled={slaveOffline}
|
||||
onClick={() => {
|
||||
@@ -95,6 +267,9 @@ export default function SavesList(props) {
|
||||
);
|
||||
}}
|
||||
>Download</Button>}
|
||||
{account.hasPermission("core.instance.save.transfer") && <TransferModal
|
||||
disabled={slaveOffline} instanceId={props.instance.id} save={save}
|
||||
/>}
|
||||
{account.hasPermission("core.instance.save.delete") && <Popconfirm
|
||||
title="Permanently delete save?"
|
||||
okText="Delete"
|
||||
|
||||
+195
-16
@@ -3,6 +3,7 @@ const assert = require("assert").strict;
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const events = require("events");
|
||||
const phin = require("phin");
|
||||
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libUsers = require("@clusterio/lib/users");
|
||||
@@ -25,6 +26,24 @@ async function checkInstanceStatus(id, status) {
|
||||
assert.equal(instances.get(44).status, status, "incorrect instance status");
|
||||
}
|
||||
|
||||
async function uploadSave(instanceId, name, content) {
|
||||
return await phin({
|
||||
url: `https://localhost:4443/api/upload-save?instance_id=${instanceId}&filename=${name}`,
|
||||
method: "POST",
|
||||
core: { rejectUnauthorized: false },
|
||||
headers: {
|
||||
"X-Access-Token": controlToken,
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Length": String(content.length),
|
||||
},
|
||||
data: content,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteSave(instanceId, save) {
|
||||
await libLink.messages.deleteSave.send(getControl(), { instance_id: instanceId, save });
|
||||
}
|
||||
|
||||
describe("Integration of Clusterio", function() {
|
||||
describe("clusteriomaster", function() {
|
||||
describe("bootstrap generate-user-token", function() {
|
||||
@@ -115,10 +134,14 @@ describe("Integration of Clusterio", function() {
|
||||
let config = "alt-slave-config.json";
|
||||
let configPath = path.join("temp", "test", config);
|
||||
await fs.remove(configPath);
|
||||
await fs.remove(path.join("temp", "test", "alt-instances"));
|
||||
await execCtl(`slave create-config --id 5 --name alt-slave --generate-token --output ${config}`);
|
||||
await exec(
|
||||
`node ../../packages/slave --config ${config} config set slave.tls_ca ../../test/file/tls/cert.pem`
|
||||
);
|
||||
await exec(
|
||||
`node ../../packages/slave --config ${config} config set slave.instances_directory alt-instances`
|
||||
);
|
||||
|
||||
let slaveProcess;
|
||||
try {
|
||||
@@ -250,11 +273,11 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance create-save", function() {
|
||||
describe("instance save create", function() {
|
||||
it("creates a save", async function() {
|
||||
slowTest(this);
|
||||
getControl().saveListUpdates = [];
|
||||
await execCtl("instance create-save test");
|
||||
await execCtl("instance save create test");
|
||||
await checkInstanceStatus(44, "stopped");
|
||||
});
|
||||
});
|
||||
@@ -266,10 +289,10 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance list-saves", function() {
|
||||
describe("instance save list", function() {
|
||||
it("lists the created save", async function() {
|
||||
slowTest(this);
|
||||
let result = await execCtl("instance list-saves test");
|
||||
let result = await execCtl("instance save list test");
|
||||
assert(/world\.zip/.test(result.stdout), "world.zip not present in list save output");
|
||||
});
|
||||
});
|
||||
@@ -530,10 +553,10 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance upload-save", function() {
|
||||
describe("instance save upload", function() {
|
||||
it("should upload a zip file", async function() {
|
||||
await fs.outputFile(path.join("temp", "test", "upload.zip"), "a test");
|
||||
await execCtl("instance upload-save 44 upload.zip");
|
||||
await execCtl("instance save upload 44 upload.zip");
|
||||
assert(
|
||||
await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "upload.zip")),
|
||||
"file not uploaded to saves directory"
|
||||
@@ -541,42 +564,198 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
it("should reject non-zip files", async function() {
|
||||
await fs.outputFile(path.join("temp", "test", "invalid"), "a test");
|
||||
await assert.rejects(execCtl("instance upload-save 44 invalid"));
|
||||
await assert.rejects(execCtl("instance save upload 44 invalid"));
|
||||
});
|
||||
it("should reject path traversal attacks", async function() {
|
||||
await fs.outputFile(path.join("temp", "test", "upload.zip"), "a test");
|
||||
await assert.rejects(execCtl("instance upload-save 44 upload.zip --name ../traversal.zip"));
|
||||
await assert.rejects(execCtl("instance save upload 44 upload.zip --name ../traversal.zip"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance download-save", function() {
|
||||
describe("instance save download", function() {
|
||||
it("should download a save", async function() {
|
||||
await fs.remove(path.join("temp", "test", "upload.zip"));
|
||||
await execCtl("instance download-save 44 upload.zip");
|
||||
await execCtl("instance save download 44 upload.zip");
|
||||
assert(await fs.pathExists(path.join("temp", "test", "upload.zip")));
|
||||
});
|
||||
it("should error if save does not exist", async function() {
|
||||
await assert.rejects(execCtl("instance download-save 44 invalid"));
|
||||
await assert.rejects(execCtl("instance save download 44 invalid"));
|
||||
});
|
||||
it("should error on path traversal attacks", async function() {
|
||||
await assert.rejects(execCtl("instance download-save 44 ../factorio-current.log"));
|
||||
await assert.rejects(execCtl("instance save download 44 ../factorio-current.log"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance delete-save", function() {
|
||||
describe("instance save copy", function() {
|
||||
it("copy a save file", async function() {
|
||||
await execCtl("instance save copy 44 upload.zip copy.zip");
|
||||
assert(await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "copy.zip")));
|
||||
});
|
||||
it("should error if destination exist", async function() {
|
||||
await assert.rejects(execCtl("instance save copy 44 upload.zip copy.zip"));
|
||||
});
|
||||
it("should error if source does not exist", async function() {
|
||||
await assert.rejects(execCtl("instance save copy 44 not-here.zip invalid.zip"));
|
||||
});
|
||||
it("should reject path traversal attacks", async function() {
|
||||
await assert.rejects(execCtl("instance save copy 44 upload.zip ../traversal.zip"));
|
||||
await assert.rejects(execCtl("instance save copy 44 ../saves/upload.zip traversal.zip "));
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance save rename", function() {
|
||||
it("rename a save file", async function() {
|
||||
await execCtl("instance save rename 44 copy.zip rename.zip");
|
||||
assert(!await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "copy.zip")));
|
||||
assert(await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "rename.zip")));
|
||||
});
|
||||
it("should error if new name exist", async function() {
|
||||
await assert.rejects(execCtl("instance save rename 44 upload.zip rename.zip"));
|
||||
});
|
||||
it("should error if old name does not exist", async function() {
|
||||
await assert.rejects(execCtl("instance save rename 44 not-here.zip invalid.zip"));
|
||||
});
|
||||
it("should reject path traversal attacks", async function() {
|
||||
await assert.rejects(execCtl("instance save rename 44 upload.zip ../traversal.zip"));
|
||||
await assert.rejects(execCtl("instance save rename 44 ../saves/upload.zip traversal.zip "));
|
||||
});
|
||||
});
|
||||
|
||||
describe("instance save transfer", function() {
|
||||
before(async function() {
|
||||
slowTest(this);
|
||||
await execCtl("instance create spam --id 66");
|
||||
await execCtl("instance assign spam 4");
|
||||
await execCtl("instance create unassign --id 77");
|
||||
});
|
||||
after(async function() {
|
||||
await execCtl("instance delete spam");
|
||||
});
|
||||
for (let remote of [false, true]) {
|
||||
let pri = 44;
|
||||
let sec = remote ? 99 : 66;
|
||||
let priSaves = path.join("temp", "test", "instances", "test", "saves");
|
||||
let secSaves = remote
|
||||
? path.join("temp", "test", "alt-instances", "alt-test", "saves")
|
||||
: path.join("temp", "test", "instances", "spam", "saves")
|
||||
;
|
||||
describe(remote ? "remote" : "local", function() {
|
||||
if (remote) {
|
||||
let slaveProcess;
|
||||
before(async function() {
|
||||
// On windows there's currently no way to automate graceful shutdown of the slave
|
||||
// process as CTRL+C is some weird terminal thing and SIGINT isn't a thing.
|
||||
if (process.platform === "win32") {
|
||||
this.skip();
|
||||
}
|
||||
slowTest(this);
|
||||
// Reuse from the clusterioslave test
|
||||
let config = "alt-slave-config.json";
|
||||
slaveProcess = await spawn(
|
||||
"alt-slave:", `node ../../packages/slave run --config ${config}`, /Started slave/
|
||||
);
|
||||
});
|
||||
after(async function() {
|
||||
if (slaveProcess) {
|
||||
slaveProcess.kill("SIGINT");
|
||||
await events.once(slaveProcess, "exit");
|
||||
}
|
||||
});
|
||||
}
|
||||
it("should transfers a save", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await execCtl(`instance save transfer ${pri} transfer.zip ${sec}`);
|
||||
assert(!await fs.pathExists(path.join(priSaves, "transfer.zip")), "save still at pri");
|
||||
assert(await fs.pathExists(path.join(secSaves, "transfer.zip")), "save not at sec");
|
||||
await deleteSave(sec, "transfer.zip");
|
||||
});
|
||||
it("should support rename", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await execCtl(`instance save transfer ${pri} transfer.zip ${sec} rename.zip`);
|
||||
assert(!await fs.pathExists(path.join(priSaves, "transfer.zip")), "save still at pri");
|
||||
assert(await fs.pathExists(path.join(secSaves, "rename.zip")), "save not at sec");
|
||||
await deleteSave(sec, "rename.zip");
|
||||
});
|
||||
it("should auto-rename if target exists", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await uploadSave(sec, "transfer.zip", "transfer.zip");
|
||||
await execCtl(`instance save transfer ${pri} transfer.zip ${sec}`);
|
||||
assert(!await fs.pathExists(path.join(priSaves, "transfer.zip")), "save still at pri");
|
||||
assert(await fs.pathExists(path.join(secSaves, "transfer-2.zip")), "save not at sec");
|
||||
await deleteSave(sec, "transfer.zip");
|
||||
await deleteSave(sec, "transfer-2.zip");
|
||||
});
|
||||
it("should copy when using --copy", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await execCtl(`instance save transfer ${pri} transfer.zip ${sec} --copy`);
|
||||
assert(await fs.pathExists(path.join(priSaves, "transfer.zip")), "save not at pri");
|
||||
assert(await fs.pathExists(path.join(secSaves, "transfer.zip")), "save not at sec");
|
||||
await deleteSave(pri, "transfer.zip");
|
||||
await deleteSave(sec, "transfer.zip");
|
||||
});
|
||||
it("should fail if save does not exist", async function() {
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} not-here.zip ${sec}`));
|
||||
});
|
||||
it("should fail if source save name is invalid", async function() {
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} nul 111`));
|
||||
});
|
||||
it("should fail if target save name is invalid", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} transfer.zip 111 nul`));
|
||||
await deleteSave(pri, "transfer.zip");
|
||||
});
|
||||
it("should reject path traversal of source save", async function() {
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} ../saves/transfer.zip 111`));
|
||||
});
|
||||
it("should reject path traversal of target save", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await assert.rejects(
|
||||
execCtl(`instance save transfer ${pri} transfer.zip 111 ../saves/transfer.zip`)
|
||||
);
|
||||
await deleteSave(pri, "transfer.zip");
|
||||
});
|
||||
if (!remote) {
|
||||
it("should fail if source and target instance is the same instance", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} transfer.zip ${pri}`));
|
||||
await deleteSave(pri, "transfer.zip");
|
||||
});
|
||||
it("should fail if source instance does not exist", async function() {
|
||||
await assert.rejects(execCtl(`instance save transfer 111 not-here.zip ${sec}`));
|
||||
});
|
||||
it("should fail if target instance does not exist", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} transfer.zip 111`));
|
||||
await deleteSave(pri, "transfer.zip");
|
||||
});
|
||||
it("should fail if source instance is not assigned", async function() {
|
||||
await assert.rejects(execCtl(`instance save transfer 77 transfer.zip ${pri}`));
|
||||
});
|
||||
it("should fail if target instance is not assigned", async function() {
|
||||
await uploadSave(pri, "transfer.zip", "transfer.zip");
|
||||
await assert.rejects(execCtl(`instance save transfer ${pri} transfer.zip 77`));
|
||||
await deleteSave(pri, "transfer.zip");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
describe("instance save delete", function() {
|
||||
it("should delete a save", async function() {
|
||||
await execCtl("instance delete-save 44 upload.zip");
|
||||
await execCtl("instance save delete 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"));
|
||||
await assert.rejects(execCtl("instance save delete 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"));
|
||||
await assert.rejects(execCtl("instance save delete 44 ../../upload.zip"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -109,7 +109,8 @@ let slaveConfigPath = path.join("temp", "test", "config-slave.json");
|
||||
let controlConfigPath = path.join("temp", "test", "config-control.json");
|
||||
|
||||
async function exec(command, options = {}) {
|
||||
console.log(command);
|
||||
// Uncomment to show commands run in tests
|
||||
// console.log(command);
|
||||
options = { cwd: path.join("temp", "test"), ...options };
|
||||
return await util.promisify(child_process.exec)(command, options);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user