Skip to content

Commit

Permalink
Refactor copy API in async/await
Browse files Browse the repository at this point in the history
  • Loading branch information
SukkaW committed Oct 19, 2023
1 parent 47bc43d commit 56428e3
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 163 deletions.
312 changes: 150 additions & 162 deletions lib/copy/copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

const fs = require('graceful-fs')
const path = require('path')
const mkdirs = require('../mkdirs').mkdirs
const pathExists = require('../path-exists').pathExists
const utimesMillis = require('../util/utimes').utimesMillis
const { mkdirs } = require('../mkdirs')
const { pathExists } = require('../path-exists')
const { utimesMillisAsync } = require('../util/utimes')
const stat = require('../util/stat')

function copy (src, dest, opts, cb) {
if (typeof opts === 'function' && !cb) {
cb = opts
opts = {}
} else if (typeof opts === 'function') {
const { promisify } = require('util')

// TODO: re-exports promisified version of graceful-fs functions from lib/fs/promise.js
const fs$stat = promisify(fs.stat)
const fs$lstat = promisify(fs.lstat)
const fs$unlink = promisify(fs.unlink)
const fs$copyFile = promisify(fs.copyFile)
const fs$mkdir = promisify(fs.mkdir)
const fs$chmod = promisify(fs.chmod)
const fs$readdir = promisify(fs.readdir)
const fs$readlink = promisify(fs.readlink)
const fs$symlink = promisify(fs.symlink)

async function copy (src, dest, opts) {
if (typeof opts === 'function') {
opts = { filter: opts }
}

cb = cb || function () {}
opts = opts || {}

opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
Expand All @@ -30,209 +39,188 @@ function copy (src, dest, opts, cb) {
)
}

stat.checkPaths(src, dest, 'copy', opts, (err, stats) => {
if (err) return cb(err)
const { srcStat, destStat } = stats
stat.checkParentPaths(src, srcStat, dest, 'copy', err => {
if (err) return cb(err)
runFilter(src, dest, opts, (err, include) => {
if (err) return cb(err)
if (!include) return cb()
const { srcStat, destStat } = await stat.checkPathsAsync(src, dest, 'copy', opts)

checkParentDir(destStat, src, dest, opts, cb)
})
})
})
await stat.checkParentPathsAsync(src, srcStat, dest, 'copy')

const include = await runFilter(src, dest, opts)

if (!include) return

return checkParentDir(destStat, src, dest, opts)
}

function checkParentDir (destStat, src, dest, opts, cb) {
async function checkParentDir (destStat, src, dest, opts) {
const destParent = path.dirname(dest)
pathExists(destParent, (err, dirExists) => {
if (err) return cb(err)
if (dirExists) return getStats(destStat, src, dest, opts, cb)
mkdirs(destParent, err => {
if (err) return cb(err)
return getStats(destStat, src, dest, opts, cb)
})
})

const dirExists = await pathExists(destParent)

if (dirExists) return getStats(destStat, src, dest, opts)

const parentDirExists = await pathExists(destParent)
if (parentDirExists) return getStats(destStat, src, dest, opts)

await mkdirs(destParent)

return getStats(destStat, src, dest, opts)
}

function runFilter (src, dest, opts, cb) {
if (!opts.filter) return cb(null, true)
Promise.resolve(opts.filter(src, dest))
.then(include => cb(null, include), error => cb(error))
async function runFilter (src, dest, opts) {
if (!opts.filter) return true
return opts.filter(src, dest)
}

function getStats (destStat, src, dest, opts, cb) {
const stat = opts.dereference ? fs.stat : fs.lstat
stat(src, (err, srcStat) => {
if (err) return cb(err)
async function getStats (destStat, src, dest, opts) {
const statFn = opts.dereference ? fs$stat : fs$lstat
const srcStat = await statFn(src)

if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb)
else if (srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb)
else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts, cb)
else if (srcStat.isSocket()) return cb(new Error(`Cannot copy a socket file: ${src}`))
else if (srcStat.isFIFO()) return cb(new Error(`Cannot copy a FIFO pipe: ${src}`))
return cb(new Error(`Unknown file: ${src}`))
})
if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)

if (
srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()
) return onFile(srcStat, destStat, src, dest, opts)

if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts)
if (srcStat.isSocket()) throw new Error(`Cannot copy a socket file: ${src}`)
if (srcStat.isFIFO()) throw new Error(`Cannot copy a FIFO pipe: ${src}`)
throw new Error(`Unknown file: ${src}`)
}

function onFile (srcStat, destStat, src, dest, opts, cb) {
if (!destStat) return copyFile(srcStat, src, dest, opts, cb)
return mayCopyFile(srcStat, src, dest, opts, cb)
function onFile (srcStat, destStat, src, dest, opts) {
if (!destStat) return copyFile(srcStat, src, dest, opts)
return mayCopyFile(srcStat, src, dest, opts)
}

function mayCopyFile (srcStat, src, dest, opts, cb) {
async function mayCopyFile (srcStat, src, dest, opts) {
if (opts.overwrite) {
fs.unlink(dest, err => {
if (err) return cb(err)
return copyFile(srcStat, src, dest, opts, cb)
})
} else if (opts.errorOnExist) {
return cb(new Error(`'${dest}' already exists`))
} else return cb()
await fs$unlink(dest)
return copyFile(srcStat, src, dest, opts)
}
if (opts.errorOnExist) {
throw new Error(`'${dest}' already exists`)
}
}

function copyFile (srcStat, src, dest, opts, cb) {
fs.copyFile(src, dest, err => {
if (err) return cb(err)
if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb)
return setDestMode(dest, srcStat.mode, cb)
})
async function copyFile (srcStat, src, dest, opts) {
await fs$copyFile(src, dest)
if (opts.preserveTimestamps) {
return handleTimestampsAndMode(srcStat.mode, src, dest)
}
return setDestMode(dest, srcStat.mode)
}

function handleTimestampsAndMode (srcMode, src, dest, cb) {
async function handleTimestampsAndMode (srcMode, src, dest) {
// Make sure the file is writable before setting the timestamp
// otherwise open fails with EPERM when invoked with 'r+'
// (through utimes call)
if (fileIsNotWritable(srcMode)) {
return makeFileWritable(dest, srcMode, err => {
if (err) return cb(err)
return setDestTimestampsAndMode(srcMode, src, dest, cb)
})
await makeFileWritable(dest, srcMode)
return setDestTimestampsAndMode(srcMode, src, dest)
}
return setDestTimestampsAndMode(srcMode, src, dest, cb)
return setDestTimestampsAndMode(srcMode, src, dest)
}

function fileIsNotWritable (srcMode) {
return (srcMode & 0o200) === 0
}

function makeFileWritable (dest, srcMode, cb) {
return setDestMode(dest, srcMode | 0o200, cb)
function makeFileWritable (dest, srcMode) {
return setDestMode(dest, srcMode | 0o200)
}

function setDestTimestampsAndMode (srcMode, src, dest, cb) {
setDestTimestamps(src, dest, err => {
if (err) return cb(err)
return setDestMode(dest, srcMode, cb)
})
async function setDestTimestampsAndMode (srcMode, src, dest) {
await setDestTimestamps(src, dest)
return setDestMode(dest, srcMode)
}

function setDestMode (dest, srcMode, cb) {
return fs.chmod(dest, srcMode, cb)
function setDestMode (dest, srcMode) {
return fs$chmod(dest, srcMode)
}

function setDestTimestamps (src, dest, cb) {
async function setDestTimestamps (src, dest) {
// The initial srcStat.atime cannot be trusted
// because it is modified by the read(2) system call
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
fs.stat(src, (err, updatedSrcStat) => {
if (err) return cb(err)
return utimesMillis(dest, updatedSrcStat.atime, updatedSrcStat.mtime, cb)
})
// (See https://nodejs.org/api/fs.html#fs$stat_time_values)
const updatedSrcStat = await fs$stat(src)

return utimesMillisAsync(dest, updatedSrcStat.atime, updatedSrcStat.mtime)
}

function onDir (srcStat, destStat, src, dest, opts, cb) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts, cb)
return copyDir(src, dest, opts, cb)
function onDir (srcStat, destStat, src, dest, opts) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts)
return copyDir(src, dest, opts)
}

function mkDirAndCopy (srcMode, src, dest, opts, cb) {
fs.mkdir(dest, err => {
if (err) return cb(err)
copyDir(src, dest, opts, err => {
if (err) return cb(err)
return setDestMode(dest, srcMode, cb)
})
})
async function mkDirAndCopy (srcMode, src, dest, opts) {
await fs$mkdir(dest)
await copyDir(src, dest, opts)

return setDestMode(dest, srcMode)
}

function copyDir (src, dest, opts, cb) {
fs.readdir(src, (err, items) => {
if (err) return cb(err)
return copyDirItems(items, src, dest, opts, cb)
})
async function copyDir (src, dest, opts) {
const items = await fs$readdir(src)
return copyDirItems(items, src, dest, opts)
}

function copyDirItems (items, src, dest, opts, cb) {
function copyDirItems (items, src, dest, opts) {
const item = items.pop()
if (!item) return cb()
return copyDirItem(items, item, src, dest, opts, cb)
if (!item) return
return copyDirItem(items, item, src, dest, opts)
}

function copyDirItem (items, item, src, dest, opts, cb) {
async function copyDirItem (items, item, src, dest, opts) {
const srcItem = path.join(src, item)
const destItem = path.join(dest, item)
runFilter(srcItem, destItem, opts, (err, include) => {
if (err) return cb(err)
if (!include) return copyDirItems(items, src, dest, opts, cb)

stat.checkPaths(srcItem, destItem, 'copy', opts, (err, stats) => {
if (err) return cb(err)
const { destStat } = stats
getStats(destStat, srcItem, destItem, opts, err => {
if (err) return cb(err)
return copyDirItems(items, src, dest, opts, cb)
})
})
})
}

function onLink (destStat, src, dest, opts, cb) {
fs.readlink(src, (err, resolvedSrc) => {
if (err) return cb(err)
if (opts.dereference) {
resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
}

if (!destStat) {
return fs.symlink(resolvedSrc, dest, cb)
} else {
fs.readlink(dest, (err, resolvedDest) => {
if (err) {
// dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
// fs throws error anyway, so no need to guard against it here.
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest, cb)
return cb(err)
}
if (opts.dereference) {
resolvedDest = path.resolve(process.cwd(), resolvedDest)
}
if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`))
}

// do not copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
if (stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
}
return copyLink(resolvedSrc, dest, cb)
})
}
})
}

function copyLink (resolvedSrc, dest, cb) {
fs.unlink(dest, err => {
if (err) return cb(err)
return fs.symlink(resolvedSrc, dest, cb)
})

const include = await runFilter(srcItem, destItem, opts)
if (!include) return copyDirItems(items, src, dest, opts)

const { destStat } = await stat.checkPathsAsync(srcItem, destItem, 'copy', opts)

await getStats(destStat, srcItem, destItem, opts)

return copyDirItems(items, src, dest, opts)
}

async function onLink (destStat, src, dest, opts) {
let resolvedSrc = await fs$readlink(src)
if (opts.dereference) {
resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
}
if (!destStat) {
return fs$symlink(resolvedSrc, dest)
}

let resolvedDest = null
try {
resolvedDest = await fs$readlink(dest)
} catch (e) {
if (e.code === 'EINVAL' || e.code === 'UNKNOWN') return fs$symlink(resolvedSrc, dest)
throw e
}
if (opts.dereference) {
resolvedDest = path.resolve(process.cwd(), resolvedDest)
}
if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
}

// do not copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
if (stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)
}

return copyLink(resolvedSrc, dest)
}

async function copyLink (resolvedSrc, dest) {
await fs$unlink(dest)
return fs$symlink(resolvedSrc, dest)
}

module.exports = copy
2 changes: 1 addition & 1 deletion lib/copy/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const u = require('universalify').fromCallback
const u = require('universalify').fromPromise
module.exports = {
copy: u(require('./copy')),
copySync: require('./copy-sync')
Expand Down
Loading

0 comments on commit 56428e3

Please sign in to comment.