Files
factorio-clusterio/test/lib/file_ops.js
Hornwitser d5bcd34cdb Use .tmp before the file extension (#529)
When creating temporary files use the pattern `.tmp.ext` instead of
`.ext.tmp` to be consistent with Factorio which creates `.tmp.zip` files
during saving.

If patching fails this will leave a `.tmp.zip` file in the instance saves
directory which is ignored by the default save selector.

Resolves #486.
2023-11-30 01:21:06 +01:00

222 lines
7.3 KiB
JavaScript

"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const lib = require("@clusterio/lib");
describe("lib/file_ops", function() {
let baseDir = path.join("temp", "test", "file_ops");
async function setupTestingEnv() {
await fs.ensureDir(path.join(baseDir, "test", "folder"));
await fs.ensureDir(path.join(baseDir, "test", "another folder"));
await fs.remove(path.join(baseDir, "safe"));
await fs.ensureDir(path.join(baseDir, "safe"));
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);
describe("getNewestFile()", function() {
it("returns a string in a directory with files", async function() {
let newest = await lib.getNewestFile(path.join(baseDir, "test"));
assert.equal(typeof newest, "string");
});
it("returns undefined if all entries were filtered out", async function() {
let newest = await lib.getNewestFile(path.join(baseDir, "test"), (name) => !name.endsWith(".txt"));
assert.equal(newest, undefined);
});
it("returns undefined if directory is empty", async function() {
let newest = await lib.getNewestFile(path.join(baseDir, "test", "folder"));
assert.equal(newest, undefined);
});
});
describe("getNewestFile()", function() {
it("returns 0 if directory does not exist", async function() {
let size = await lib.directorySize(path.join(baseDir, "invalid"));
assert.equal(size, 0);
});
it("returns 0 if directory is empty", async function() {
let size = await lib.directorySize(path.join(baseDir, "test", "folder"));
assert.equal(size, 0);
});
it("returns size of files in directory", async function() {
let size = await lib.directorySize(path.join(baseDir, "test"));
assert.equal(size, 21);
});
});
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 lib.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 lib.findUnusedName(path.join(baseDir, "find"), ...args);
assert.equal(actual, expected);
}
});
});
describe("safeOutputFile()", function() {
it("should write new target file", async function() {
let target = path.join(baseDir, "safe", "simple.txt");
await lib.safeOutputFile(target, "a text file", "utf8");
assert(!await fs.pathExists(target.replace(".txt", ".tmp.txt")), "temporary was left behind");
assert.equal(await fs.readFile(target, "utf8"), "a text file");
});
it("should overwrite existing target file", async function() {
let target = path.join(baseDir, "safe", "exists.txt");
await fs.outputFile(target, "previous", "utf8");
await lib.safeOutputFile(target, "current", "utf8");
assert(!await fs.pathExists(target.replace(".txt", ".tmp.txt")), "temporary was left behind");
assert.equal(await fs.readFile(target, "utf8"), "current");
});
it("should handle creating file in current working directory", async function() {
let target = "temporary-file-made-to-test-cwd.txt";
try {
await lib.safeOutputFile(target, "a text file", "utf8");
} finally {
try {
await fs.unlink(target);
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
}
}
});
});
describe("checkFilename()", function() {
it("should allow a basic name", function() {
lib.checkFilename("file");
});
function check(item, msg) {
assert.throws(() => lib.checkFilename(item), new Error(msg));
}
it("should throw on non-string", function() {
check(undefined, "must be a string");
check(null, "must be a string");
check({}, "must be a string");
check([], "must be a string");
check(0, "must be a string");
check(false, "must be a string");
});
it("should throw on empty name", function() {
check("", "cannot be empty");
});
it("should throw on <>:\"\\/|?* \\x00\\r\\n\\t", function() {
for (let char of '<>:"\\/|?*\x00\r\n\t') {
check(char, 'cannot contain <>:"\\/|=* or control characters');
}
});
it("should throw on CON, PRN, AUX, NUL, COM1, LPT1", function() {
for (let bad of ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"]) {
check(bad, "cannot be named any of CON PRN AUX NUL COM1-9 and LPT1-9");
check(`${bad}.zip`, "cannot be named any of CON PRN AUX NUL COM1-9 and LPT1-9");
check(`${bad}.anything.txt`, "cannot be named any of CON PRN AUX NUL COM1-9 and LPT1-9");
check(`${bad}....a`, "cannot be named any of CON PRN AUX NUL COM1-9 and LPT1-9");
check(`${bad}.`, "cannot be named any of CON PRN AUX NUL COM1-9 and LPT1-9");
}
});
it("should throw on . and ..", function() {
for (let bad of [".", ".."]) {
check(bad, `cannot be named ${bad}`);
}
});
it("should throw on names ending with . or space", function() {
check("a ", "cannot end with . or space");
check("a.", "cannot end with . or space");
});
});
describe("cleanFilename()", function() {
function clean(item, expected) {
assert.equal(lib.cleanFilename(item), expected);
}
it("should allow a basic name", function() {
clean("file", "file");
});
function check(item, msg) {
assert.throws(() => lib.cleanFilename(item), new Error(msg));
}
it("should throw on non-string", function() {
check(undefined, "name must be a string");
check(null, "name must be a string");
check({}, "name must be a string");
check([], "name must be a string");
check(0, "name must be a string");
check(false, "name must be a string");
});
it("should clean empty name", function() {
clean("", "_");
});
it("should clean <>:\"\\/|?* \\x00\\r\\n\\t", function() {
for (let char of '<>:"\\/|?*\x00\r\n\t') {
clean(char, "_");
}
});
it("should clean CON, PRN, AUX, NUL, COM1, LPT1", function() {
for (let bad of ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"]) {
clean(bad, `${bad}_`);
clean(`${bad}.zip`, `${bad}_.zip`);
clean(`${bad}.anything.txt`, `${bad}_.anything.txt`);
clean(`${bad}....a`, `${bad}_....a`);
clean(`${bad}.`, `${bad}__`);
}
});
it("should clean . and ..", function() {
for (let bad of [".", ".."]) {
clean(bad, `${bad}_`);
}
});
it("should clean names ending with . or space", function() {
clean("a ", "a_");
clean("a.", "a_");
});
});
});