const debug = require('debug')('streamroller:moveAndMaybeCompressFile'); const fs = require('fs-extra'); const zlib = require('zlib'); const _parseOption = function(rawOptions){ const defaultOptions = { mode: parseInt("0600", 8), compress: false, }; const options = Object.assign({}, defaultOptions, rawOptions); debug(`_parseOption: moveAndMaybeCompressFile called with option=${JSON.stringify(options)}`); return options; }; const moveAndMaybeCompressFile = async ( sourceFilePath, targetFilePath, options ) => { options = _parseOption(options); if (sourceFilePath === targetFilePath) { debug(`moveAndMaybeCompressFile: source and target are the same, not doing anything`); return; } if (await fs.pathExists(sourceFilePath)) { debug( `moveAndMaybeCompressFile: moving file from ${sourceFilePath} to ${targetFilePath} ${ options.compress ? "with" : "without" } compress` ); if (options.compress) { await new Promise((resolve, reject) => { let isCreated = false; // to avoid concurrency, the forked process which can create the file will proceed (using flags wx) const writeStream = fs.createWriteStream(targetFilePath, { mode: options.mode, flags: "wx" }) // wait until writable stream is valid before proceeding to read .on("open", () => { isCreated = true; const readStream = fs.createReadStream(sourceFilePath) // wait until readable stream is valid before piping .on("open", () => { readStream.pipe(zlib.createGzip()).pipe(writeStream); }) .on("error", (e) => { debug(`moveAndMaybeCompressFile: error reading ${sourceFilePath}`, e); // manually close writable: https://nodejs.org/api/stream.html#readablepipedestination-options writeStream.destroy(e); }); }) .on("finish", () => { debug(`moveAndMaybeCompressFile: finished compressing ${targetFilePath}, deleting ${sourceFilePath}`); // delete sourceFilePath fs.unlink(sourceFilePath) .then(resolve) .catch((e) => { debug(`moveAndMaybeCompressFile: error deleting ${sourceFilePath}, truncating instead`, e); // fallback to truncate fs.truncate(sourceFilePath) .then(resolve) .catch((e) => { debug(`moveAndMaybeCompressFile: error truncating ${sourceFilePath}`, e); reject(e); }); }); }) .on("error", (e) => { if (!isCreated) { debug(`moveAndMaybeCompressFile: error creating ${targetFilePath}`, e); // do not do anything if handled by another forked process reject(e); } else { debug(`moveAndMaybeCompressFile: error writing ${targetFilePath}, deleting`, e); // delete targetFilePath (taking as nothing happened) fs.unlink(targetFilePath) .then(() => { reject(e); }) .catch((e) => { debug(`moveAndMaybeCompressFile: error deleting ${targetFilePath}`, e); reject(e); }); } }); }).catch(() => {}); } else { debug(`moveAndMaybeCompressFile: renaming ${sourceFilePath} to ${targetFilePath}`); try { await fs.move(sourceFilePath, targetFilePath, { overwrite: true }); } catch (e) { debug(`moveAndMaybeCompressFile: error renaming ${sourceFilePath} to ${targetFilePath}`, e); /* istanbul ignore else: no need to do anything if file does not exist */ if (e.code !== "ENOENT") { debug(`moveAndMaybeCompressFile: trying copy+truncate instead`); try { await fs.copy(sourceFilePath, targetFilePath, { overwrite: true }); await fs.truncate(sourceFilePath); } catch (e) { debug(`moveAndMaybeCompressFile: error copy+truncate`, e); } } } } } }; module.exports = moveAndMaybeCompressFile;