import fse from 'fs-extra' import crypto from 'crypto' import path from 'path' /** * Given a path that designates a location of a file on another device, * will return a path to file in the same folder, but with a unique name * to avoid collisions. * * @param {*} destPath the path to the final location of the file being moved * @returns a unique path to a file on the same device as the file being moved */ const generateTemporaryPathOnDestinationDevice = (destPath) => { const dirName = path.dirname(destPath) // Generate a unique destination file name to get the file onto the destination filesystem const tempName = path.basename(destPath) + crypto.randomBytes(8).toString('hex') return path.join(dirName, tempName) } /** * Allows a file to be moved (renamed) even across filesystem boundaries. * * If the rename fails because the file is getting renamed across file system boundaries, * the file is first copied to the destination file system under a temporary name, * and then renamed from there. * * This is done because rename is atomic but copy is not, and can leave partially copied files. * * @param {*} oldPath the original file that should be moved * @param {*} newPath the path to move the file to */ async function safeMoveFile(oldPath, newPath) { try { // Golden path, we simply rename the file in an atomic operation await fse.rename(oldPath, newPath) } catch (err) { // The EXDEV error indicates that the rename failed because the rename was across filesystem boundaries // This might occur if a distro uses tmpfs for temporary directories if (err.code === 'EXDEV') { // Generate a unique destination file name to get the file onto the destination filesystem const tempPath = generateTemporaryPathOnDestinationDevice(newPath) // Copy onto the destination filesystem (not guaranteed to be atomic) await fse.copy(oldPath, tempPath) // Atomically move the file onto the destination path, overwriting it await fse.rename(tempPath, newPath) // Delete the old file once both the above operations succeed await fse.remove(oldPath) } else { throw err } } } export default safeMoveFile