serverless/lib/utils/fs/safe-move-file.js
2024-05-30 16:31:52 -04:00

57 lines
2.2 KiB
JavaScript

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