diff --git a/packages/lib/file_ops.js b/packages/lib/file_ops.js index 577734b4..bc5fe91e 100644 --- a/packages/lib/file_ops.js +++ b/packages/lib/file_ops.js @@ -32,6 +32,44 @@ async function getNewestFile(directory, filter = (name) => true) { return newestFile; } +/** + * Modifies name in case it already exisist at the given directory + * + * Checks the directory passed if it already contains a file or folder with + * the given name, if it does not returns name unmodified, otherwise it + * returns a modified name that does not exist in the directory. + * + * Warning: this function should not be relied upon to be accurate for + * security sensitive applications. The selection process is inherently + * racy and a file or folder may have been created in the folder by the time + * this function returns. + * + * @param {string} directory - directory to check in. + * @param {string} name - file name to check for, may have extension. + * @param {string} extension - dot extension used for the file name. + * @returns {string} modified name with extension that likely does not exist + * in the folder + */ +async function findUnusedName(directory, name, extension = "") { + if (extension && name.endsWith(extension)) { + name = name.slice(0, -extension.length); + } + + while (true) { + if (!await fs.pathExists(path.join(directory, `${name}${extension}`))) { + return `${name}${extension}`; + } + + let match = /^(.*?)(-(\d+))?$/.exec(name); + if (!match[2]) { + name = `${match[1]}-2`; + } else { + name = `${match[1]}-${Number.parseInt(match[3], 10) + 1}`; + } + } +} + module.exports = { getNewestFile, + findUnusedName, }; diff --git a/packages/slave/slave.js b/packages/slave/slave.js index b6dc2df9..3aacbff9 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -1102,13 +1102,8 @@ class Slave extends libLink.Link { throw new Error(`Instance name ${err.message}`); } - // For now add dashes until an unused directory name is found - let dir = path.join(this.config.get("slave.instances_directory"), name); - while (await fs.pathExists(dir)) { - dir += "-"; - } - - return dir; + let instancesDir = this.config.get("slave.instances_directory"); + return path.join(instancesDir, await libFileOps.findUnusedName(instancesDir, name)); } async forwardRequestToInstance(message, request) { diff --git a/test/lib/file_ops.js b/test/lib/file_ops.js index da153c92..5b02656d 100644 --- a/test/lib/file_ops.js +++ b/test/lib/file_ops.js @@ -13,6 +13,13 @@ describe("lib/file_ops", function() { await fs.outputFile(path.join(baseDir, "test", "file.txt"), "contents"); await fs.outputFile(path.join(baseDir, "test", "another file.txt"), "more contents"); + + await fs.outputFile(path.join(baseDir, "find", "file"), "contents"); + await fs.outputFile(path.join(baseDir, "find", "file.txt"), "contents"); + await fs.outputFile(path.join(baseDir, "find", "foo-1"), "contents"); + await fs.outputFile(path.join(baseDir, "find", "foo-2"), "contents"); + await fs.outputFile(path.join(baseDir, "find", "bar-1.txt"), "contents"); + await fs.outputFile(path.join(baseDir, "find", "bar-2.txt"), "contents"); } before(setupTestingEnv); @@ -31,4 +38,32 @@ describe("lib/file_ops", function() { assert.equal(newest, null); }); }); + + describe("findUnusedName()", function() { + it("should return named unchanged if it does not exist", async function() { + let cases = [ + [["file"], "file"], + [["file", ".txt"], "file.txt"], + [["file.txt", ".txt"], "file.txt"], + ]; + for (let [args, expected] of cases) { + let actual = await libFileOps.findUnusedName(path.join(baseDir, "test", "folder"), ...args); + assert.equal(actual, expected); + } + }); + it("should return changed name if it does exist", async function() { + let cases = [ + [["file"], "file-2"], + [["file", ".txt"], "file-2.txt"], + [["file.txt", ".txt"], "file-2.txt"], + [["foo-1"], "foo-3"], + [["bar-1", ".txt"], "bar-3.txt"], + [["bar-1.txt", ".txt"], "bar-3.txt"], + ]; + for (let [args, expected] of cases) { + let actual = await libFileOps.findUnusedName(path.join(baseDir, "find"), ...args); + assert.equal(actual, expected); + } + }); + }); });