Files
factorio-clusterio/packages/lib/file_ops.js
T
2021-11-21 20:26:17 +11:00

117 lines
4.0 KiB
JavaScript

/**
* @module lib/file_ops
*/
"use strict";
const fs = require("fs-extra");
const path = require("path");
const crypto = require("crypto"); // needed for getTempFile
/**
* Returns the newest file in a directory
*
* @param {string} directory - The directory to look for the newest file in.
* @param {function(string): boolean} filter -
* Optional function to filter out files to ignore. Should return true
* if the file is to be considered.
* @returns {?string}
* Name of the file with the newest timestamp or null if the
* directory contains no files.
*/
async function getNewestFile(directory, filter = (name) => true) {
let newestTime = new Date(0);
let newestFile = null;
for (let entry of await fs.readdir(directory, { withFileTypes: true })) {
if (entry.isFile()) {
let stat = await fs.stat(path.join(directory, entry.name));
if (filter(entry.name) && stat.mtime > newestTime) {
newestTime = stat.mtime;
newestFile = entry.name;
}
}
}
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}`;
}
}
}
/**
* 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} prefix - Prefix for file
* @param {string} suffix - Suffix for file
* @param {string} tmpdir - Directory for temp file
*/
async function getTempFile(prefix, suffix, tmpdir) {
prefix = (typeof prefix !== "undefined") ? prefix : "tmp.";
suffix = (typeof suffix !== "undefined") ? suffix : "";
tmpdir = (typeof tmpdir !== "undefined") ? tmpdir : "./";
let fileName = path.join(prefix + crypto.randomBytes(16).toString("hex") + suffix);
let freeFile = await findUnusedName(tmpdir, fileName);
let fullPath = path.join(tmpdir, freeFile);
return fullPath;
}
/**
* Safely write data to a file
*
* Same as fs-extra.outputFile except the data is written to a temporary
* file that's renamed over the target file. The name of the temporary file
* is the same as the target file with the suffix `.tmp` added.
*
* If the operation fails it may leave behind the temporary file. This
* should not be too much of an issue as the next time the same file is
* written the temporary will be overwritten and renamed to the target file.
*
* @param {string} file - Path to file to write.
* @param {string|Buffer} data - Content to write.
* @param {object|string} options - see fs.writeFile, `flag` must not be set.
*/
async function safeOutputFile(file, data, options={}) {
let temporary = `${file}.tmp`;
await fs.outputFile(temporary, data, options);
await fs.rename(temporary, file);
}
module.exports = {
getNewestFile,
findUnusedName,
getTempFile,
safeOutputFile,
};