gristlabs_grist-core/app/server/lib/docUtils.js

146 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

/**
* Functions generally useful when dealing with Grist documents.
*/
var fs = require('fs');
var fsPath = require('path');
var Promise = require('bluebird');
Promise.promisifyAll(fs);
var nonIdentRegex = /[^\w_]+/g;
/**
* Given a string, converts it to a Grist identifier. Identifiers consist of lowercase
* alphanumeric characters and the underscore.
* @param {String} name The name to convert.
* @returns {String} Identifier.
*/
function makeIdentifier(name) {
// Lowercase and replace consecutive invalid characters with underscores.
return name.toLowerCase().replace(nonIdentRegex, '_');
}
exports.makeIdentifier = makeIdentifier;
/**
* Copies a file, returning a promise that is resolved (with no value) when the copy is complete.
* TODO This needs a unittest.
*/
function copyFile(sourcePath, destPath) {
var sourceStream, destStream;
return new Promise(function(resolve, reject) {
sourceStream = fs.createReadStream(sourcePath);
destStream = fs.createWriteStream(destPath);
sourceStream.on('error', reject);
destStream.on('error', reject);
destStream.on('finish', resolve);
sourceStream.pipe(destStream);
})
.finally(function() {
if (destStream) { destStream.destroy(); }
if (sourceStream) { sourceStream.destroy(); }
});
}
exports.copyFile = copyFile;
/**
* Helper for creating numbered files. Tries to call creator() with name, then (name + separator +
* "2") and so on with incrementing numbers, as long as the promise returned by creator() is
* rejected with err.code of 'EEXIST'. Creator() must return a promise.
* @param {String} name The first name to try.
* @param {String} separator The separator between name and appended numbers.
* @param {Function} creator The function to call with successive names. Must return a promise.
* @param {Number} startNum Optional number to start with; omit to try an unnumbered name first.
* @returns {Promise} Promise for the first name for which creator() succeeded.
*/
function createNumbered(name, separator, creator, startNum) {
var fullName = name + (startNum === undefined ? '' : separator + startNum);
var nextNum = (startNum === undefined ? 2 : startNum + 1);
return creator(fullName)
.then(() => fullName)
.catch(function(err) {
if (err.cause && err.cause.code !== 'EEXIST')
throw err;
return createNumbered(name, separator, creator, nextNum);
});
}
exports.createNumbered = createNumbered;
/**
* An easier-to-use alternative to createNumbered. Pass in a template string containing the
* special token "{NUM}". It will first call creator() with "{NUM}" removed, then with "{NUM}"
2022-02-19 09:46:49 +00:00
* replaced by "-2", "-3", etc, until creator() succeeds, and will return the value for which it
* succeeded.
*/
function createNumberedTemplate(template, creator) {
const [prefix, suffix] = template.split("{NUM}");
if (typeof prefix !== "string" || typeof suffix !== "string") {
throw new Error(`createNumberedTemplate: invalid template ${template}`);
}
return createNumbered(prefix, "-", (uniqPrefix) => creator(uniqPrefix + suffix))
.then((uniqPrefix) => uniqPrefix + suffix);
}
exports.createNumberedTemplate = createNumberedTemplate;
/**
* Creates a new file, failing if the path already exists.
* @param {String} path: The path to try creating.
* @returns {Promise} Resolved if the path was created, rejected if it already existed (with
* err.cause.code === EEXIST) or if there was another error creating it.
*/
function createExclusive(path) {
return fs.openAsync(path, 'wx').then(fd => fs.closeAsync(fd));
}
exports.createExclusive = createExclusive;
/**
* Returns the canonicalized absolute path for the given path, using fs.realpath, but allowing
* non-existent paths. In case of non-existent path, the longest existing prefix is resolved and
* the rest kept unchanged.
* @param {String} path: Path to resolve.
* @return {Promise:String} Promise for the resolved path.
*/
function realPath(path) {
return fs.realpathAsync(path)
.catch(() =>
realPath(fsPath.dirname(path))
.then(dir => fsPath.join(dir, fsPath.basename(path)))
);
}
exports.realPath = realPath;
/**
* Returns a promise that resolves to true or false based on whether the path exists. If other
* errors occur, this promise may still be rejected.
*/
function pathExists(path) {
return fs.accessAsync(path)
.then(() => true)
.catch({code: 'ENOENT'}, () => false)
.catch({code: 'ENOTDIR'}, () => false);
}
exports.pathExists = pathExists;
/**
* Returns a promise that resolves to true or false based on whether the two paths point to the
* same file. If errors occur, this promise may be rejected.
*/
function isSameFile(path1, path2) {
return Promise.join(fs.lstatAsync(path1), fs.lstatAsync(path2), (stat1, stat2) => {
if (stat1.dev === stat2.dev && stat1.ino === stat2.ino) {
return true;
}
return false;
})
.catch({code: 'ENOENT'}, () => false)
.catch({code: 'ENOTDIR'}, () => false);
}
exports.isSameFile = isSameFile;