Skip to content

Commit

Permalink
copy*(): add new option checkPathsBeforeCopying
Browse files Browse the repository at this point in the history
  • Loading branch information
manidlou committed Apr 1, 2019
1 parent 562902f commit 3d24bbf
Show file tree
Hide file tree
Showing 6 changed files with 624 additions and 19 deletions.
296 changes: 296 additions & 0 deletions lib/copy-sync/__tests__/copy-sync-without-checking-paths.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
'use strict'

const assert = require('assert')
const os = require('os')
const path = require('path')
const fs = require('../../')
const klawSync = require('klaw-sync')

/* global beforeEach, afterEach, describe, it */

const FILES = [
'file0.txt',
path.join('dir1', 'file1.txt'),
path.join('dir1', 'dir2', 'file2.txt'),
path.join('dir1', 'dir2', 'dir3', 'file3.txt')
]

const dat0 = 'file0'
const dat1 = 'file1'
const dat2 = 'file2'
const dat3 = 'file3'

describe('+ copySync() - without checking paths', () => {
let TEST_DIR, src

beforeEach(done => {
TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-without-checking-paths')
src = path.join(TEST_DIR, 'src')
fs.mkdirpSync(src)

fs.outputFileSync(path.join(src, FILES[0]), dat0)
fs.outputFileSync(path.join(src, FILES[1]), dat1)
fs.outputFileSync(path.join(src, FILES[2]), dat2)
fs.outputFileSync(path.join(src, FILES[3]), dat3)
done()
})

afterEach(done => fs.remove(TEST_DIR, done))

describe('> when source is a file', () => {
it('should copy the file successfully even if dest parent is a subdir of src', () => {
const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt')
const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt')
fs.writeFileSync(srcFile, dat0)

fs.copySync(srcFile, destFile, { checkPathsBeforeCopying: false })

assert(fs.existsSync(destFile))
const out = fs.readFileSync(destFile, 'utf8')
assert.strictEqual(out, dat0)
})
})

// test for these cases:
// - src is directory, dest is directory
// - src is directory, dest is symlink
// - src is symlink, dest is directory
// - src is symlink, dest is symlink

describe('> when source is a directory', () => {
describe('>> when dest is a directory', () => {
it(`of not itself`, () => {
const dest = path.join(TEST_DIR, src.replace(/^\w:/, ''))
return testSuccess(src, dest)
})
it(`should copy the directory successfully when dest is 'src_dest'`, () => {
const dest = path.join(TEST_DIR, 'src_dest')
return testSuccess(src, dest)
})
it(`should copy the directory successfully when dest is 'src-dest'`, () => {
const dest = path.join(TEST_DIR, 'src-dest')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'dest_src'`, () => {
const dest = path.join(TEST_DIR, 'dest_src')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'src_dest/src'`, () => {
const dest = path.join(TEST_DIR, 'src_dest', 'src')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'src-dest/src'`, () => {
const dest = path.join(TEST_DIR, 'src-dest', 'src')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'dest_src/src'`, () => {
const dest = path.join(TEST_DIR, 'dest_src', 'src')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'src_src/dest'`, () => {
const dest = path.join(TEST_DIR, 'src_src', 'dest')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'src-src/dest'`, () => {
const dest = path.join(TEST_DIR, 'src-src', 'dest')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'srcsrc/dest'`, () => {
const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
return testSuccess(src, dest)
})

it(`should copy the directory successfully when dest is 'dest/src'`, () => {
const dest = path.join(TEST_DIR, 'dest', 'src')
return testSuccess(src, dest)
})

it('should copy the directory successfully when dest is very nested that all its parents need to be created', () => {
const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo',
'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply',
'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest')
return testSuccess(src, dest)
})
})

describe('>> when dest is a symlink', () => {
it('should copy the directory successfully when src is a subdir of resolved dest path', () => {
const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
const destLink = path.join(TEST_DIR, 'dest-symlink')
fs.copySync(src, srcInDest) // put some stuff in srcInDest

const dest = path.join(TEST_DIR, 'dest')
fs.symlinkSync(dest, destLink, 'dir')

const srclen = klawSync(srcInDest).length
const destlenBefore = klawSync(dest).length

assert(srclen > 2)
fs.copySync(srcInDest, destLink, { checkPathsBeforeCopying: false })

const destlenAfter = klawSync(dest).length

// assert dest length is oldlen + length of stuff copied from src
assert.strictEqual(destlenAfter, destlenBefore + srclen, 'dest length should be equal to old length + copied legnth')

FILES.forEach(f => assert(fs.existsSync(path.join(dest, f))))

const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')

assert.strictEqual(o0, dat0)
assert.strictEqual(o1, dat1)
assert.strictEqual(o2, dat2)
assert.strictEqual(o3, dat3)
})
})
})

describe('> when source is a symlink', () => {
describe('>> when dest is a directory', () => {
it('should error when resolved src path points to dest', () => {
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(src, srcLink, 'dir')
const dest = path.join(TEST_DIR, 'src')
let errThrown
try {
fs.copySync(srcLink, dest, { checkPathsBeforeCopying: false })
} catch (err) {
errThrown = err
}
assert(errThrown)

// assert source not affected
const link = fs.readlinkSync(srcLink)
assert.strictEqual(link, src)
})

it('should error when dest is a subdir of resolved src path', () => {
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(src, srcLink, 'dir')

const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
fs.mkdirsSync(dest)

let errThrown
try {
fs.copySync(srcLink, dest, { checkPathsBeforeCopying: false })
} catch (err) {
errThrown = err
}
assert(errThrown)
const link = fs.readlinkSync(srcLink)
assert.strictEqual(link, src)
})

it('should error when resolved src path is a subdir of dest', () => {
const dest = path.join(TEST_DIR, 'dest')

const resolvedSrcPath = path.join(dest, 'contains', 'src')
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.copySync(src, resolvedSrcPath)

// make symlink that points to a subdir in dest
fs.symlinkSync(resolvedSrcPath, srcLink, 'dir')

let errThrown
try {
fs.copySync(srcLink, dest, { checkPathsBeforeCopying: false })
} catch (err) {
errThrown = err
}
assert(errThrown)
})

it(`should copy the directory successfully when dest is 'src_src/dest'`, () => {
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(src, srcLink, 'dir')

const dest = path.join(TEST_DIR, 'src_src', 'dest')
testSuccess(srcLink, dest)
const link = fs.readlinkSync(dest)
assert.strictEqual(link, src)
})

it(`should copy the directory successfully when dest is 'srcsrc/dest'`, () => {
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(src, srcLink, 'dir')

const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
testSuccess(srcLink, dest)
const link = fs.readlinkSync(dest)
assert.strictEqual(link, src)
})
})

describe('>> when dest is a symlink', () => {
it('should error when resolved dest path is a subdir of resolved src path', () => {
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(src, srcLink, 'dir')

const destLink = path.join(TEST_DIR, 'dest-symlink')
const resolvedDestPath = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
fs.ensureFileSync(path.join(resolvedDestPath, 'subdir', 'somefile.txt'))
fs.symlinkSync(resolvedDestPath, destLink, 'dir')

let errThrown
try {
fs.copySync(srcLink, destLink, { checkPathsBeforeCopying: false })
} catch (err) {
errThrown = err
}
assert.strictEqual(errThrown.message, `Cannot copy '${src}' to a subdirectory of itself, '${resolvedDestPath}'.`)
const destln = fs.readlinkSync(destLink)
assert.strictEqual(destln, resolvedDestPath)
})

it('should error when resolved src path is a subdir of resolved dest path', () => {
const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
const srcLink = path.join(TEST_DIR, 'src-symlink')
const destLink = path.join(TEST_DIR, 'dest-symlink')
const dest = path.join(TEST_DIR, 'dest')

fs.ensureDirSync(srcInDest)
fs.ensureSymlinkSync(srcInDest, srcLink, 'dir')
fs.ensureSymlinkSync(dest, destLink, 'dir')

let errThrown
try {
fs.copySync(srcLink, destLink, { checkPathsBeforeCopying: false })
} catch (err) {
errThrown = err
}
assert.strictEqual(errThrown.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`)
const destln = fs.readlinkSync(destLink)
assert.strictEqual(destln, dest)
})
})
})
})

function testSuccess (src, dest) {
const srclen = klawSync(src).length
assert(srclen > 2)
fs.copySync(src, dest, { checkPathsBeforeCopying: false })

FILES.forEach(f => assert(fs.existsSync(path.join(dest, f))))

const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')

assert.strictEqual(o0, dat0)
assert.strictEqual(o1, dat1)
assert.strictEqual(o2, dat2)
assert.strictEqual(o3, dat3)
}
19 changes: 13 additions & 6 deletions lib/copy-sync/copy-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ function copySync (src, dest, opts) {
see https://github.com/jprichardson/node-fs-extra/issues/269`)
}

const {srcStat, destStat} = checkPaths(src, dest)
checkParentStats(src, srcStat, dest)
if (opts.checkPathsBeforeCopying === false) {
const { destStat } = checkStats(src, dest)
return handleFilterAndCopy(destStat, src, dest, opts)
}
const { srcStat, destStat } = checkPaths(src, dest)
checkParentPaths(src, srcStat, dest)
return handleFilterAndCopy(destStat, src, dest, opts)
}

function handleFilterAndCopy (destStat, src, dest, opts) {
if (opts.filter && !opts.filter(src, dest)) return

const destParent = path.dirname(dest)
if (!fs.existsSync(destParent)) mkdirpSync(destParent)
return startCopy(destStat, src, dest, opts)
Expand Down Expand Up @@ -115,7 +121,8 @@ function copyDir (src, dest, opts) {
function copyDirItem (item, src, dest, opts) {
const srcItem = path.join(src, item)
const destItem = path.join(dest, item)
const {destStat} = checkPaths(srcItem, destItem)
const check = opts.checkPathsBeforeCopying === false ? checkStats : checkPaths
const { destStat } = check(srcItem, destItem)
return startCopy(destStat, srcItem, destItem, opts)
}

Expand Down Expand Up @@ -185,7 +192,7 @@ function checkStats (src, dest) {
// It works for all file types including symlinks since it
// checks the src and dest inodes. It starts from the deepest
// parent and stops once it reaches the src parent or the root path.
function checkParentStats (src, srcStat, dest) {
function checkParentPaths (src, srcStat, dest) {
const destParent = path.dirname(dest)
if (destParent && (destParent === path.dirname(src) || destParent === path.parse(destParent).root)) return
let destStat
Expand All @@ -198,7 +205,7 @@ function checkParentStats (src, srcStat, dest) {
if (destStat.ino && destStat.ino === srcStat.ino) {
throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
}
return checkParentStats(src, srcStat, destParent)
return checkParentPaths(src, srcStat, destParent)
}

function checkPaths (src, dest) {
Expand Down
2 changes: 0 additions & 2 deletions lib/copy/__tests__/copy-case-insensitive-paths.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ const path = require('path')
const fs = require('../../')
const platform = os.platform()

console.log('platform: ' + platform)

/* global beforeEach, afterEach, describe, it */

describe('+ copy() - case insensitive paths', () => {
Expand Down

0 comments on commit 3d24bbf

Please sign in to comment.