Skip to content

Commit

Permalink
BREAKING: copy*(): allow copying broken symlinks (#779)
Browse files Browse the repository at this point in the history
  • Loading branch information
manidlou authored and RyanZim committed Apr 1, 2021
1 parent 7498c9c commit f21048b
Show file tree
Hide file tree
Showing 20 changed files with 215 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

const fs = require('fs')
const os = require('os')
const fse = require('../../')
const fse = require('../..')
const path = require('path')
const assert = require('assert')
const copySync = require('../copy-sync')

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

describe('copy-sync / broken symlink', () => {
const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-broken-symlinks')
const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-broken-symlink')
const src = path.join(TEST_DIR, 'src')
const out = path.join(TEST_DIR, 'out')
const dest = path.join(TEST_DIR, 'dest')

beforeEach(done => {
fse.emptyDir(TEST_DIR, err => {
Expand All @@ -23,12 +23,20 @@ describe('copy-sync / broken symlink', () => {

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

it('should error if symlink is broken', () => {
assert.throws(() => copySync(src, out))
})
describe('when symlink is broken', () => {
it('should not throw error if dereference is false', () => {
let err = null
try {
copySync(src, dest)
} catch (e) {
err = e
}
assert.strictEqual(err, null)
})

it('should throw an error when dereference=true', () => {
assert.throws(() => copySync(src, out, { dereference: true }), err => err.code === 'ENOENT')
it('should throw error if dereference is true', () => {
assert.throws(() => copySync(src, dest, { dereference: true }), err => err.code === 'ENOENT')
})
})
})

Expand Down
15 changes: 15 additions & 0 deletions lib/copy-sync/__tests__/copy-sync-file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ describe('+ copySync() / file', () => {
afterEach(done => fs.remove(TEST_DIR, done))

describe('> when src is a file', () => {
describe('> when dest exists and is a directory', () => {
it('should throw error', () => {
const src = path.join(TEST_DIR, 'file.txt')
const dest = path.join(TEST_DIR, 'dir')
fs.ensureFileSync(src)
fs.ensureDirSync(dest)

try {
fs.copySync(src, dest)
} catch (err) {
assert.strictEqual(err.message, `Cannot overwrite directory '${dest}' with non-directory '${src}'.`)
}
})
})

it('should copy the file synchronously', () => {
const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_src')
const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {

describe('> when src is a directory', () => {
describe('>> when src is regular and dest is a symlink that points to src', () => {
it('should error', () => {
it('should error if dereference is true', () => {
src = path.join(TEST_DIR, 'src')
fs.mkdirsSync(src)
const subdir = path.join(TEST_DIR, 'src', 'subdir')
Expand All @@ -126,7 +126,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
const oldlen = klawSync(src).length

try {
fs.copySync(src, destLink)
fs.copySync(src, destLink, { dereference: true })
} catch (err) {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
}
Expand Down Expand Up @@ -166,7 +166,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
})

describe('>> when src and dest are symlinks that point to the exact same path', () => {
it('should error src and dest are the same', () => {
it('should error if src and dest are the same and dereferene is true', () => {
src = path.join(TEST_DIR, 'src')
fs.mkdirsSync(src)
const srcLink = path.join(TEST_DIR, 'src_symlink')
Expand All @@ -178,7 +178,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
const destlenBefore = klawSync(destLink).length

try {
fs.copySync(srcLink, destLink)
fs.copySync(srcLink, destLink, { dereference: true })
} catch (err) {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
}
Expand All @@ -203,7 +203,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {

describe('> when src is a file', () => {
describe('>> when src is regular and dest is a symlink that points to src', () => {
it('should error', () => {
it('should error if dereference is true', () => {
src = path.join(TEST_DIR, 'src', 'somefile.txt')
fs.ensureFileSync(src)
fs.writeFileSync(src, 'some data')
Expand All @@ -212,7 +212,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
fs.symlinkSync(src, destLink, 'file')

try {
fs.copySync(src, destLink)
fs.copySync(src, destLink, { dereference: true })
} catch (err) {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
}
Expand Down Expand Up @@ -243,7 +243,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
})

describe('>> when src and dest are symlinks that point to the exact same path', () => {
it('should error src and dest are the same', () => {
it('should error if src and dest are the same and dereference is true', () => {
src = path.join(TEST_DIR, 'src', 'srcfile.txt')
fs.outputFileSync(src, 'src data')

Expand All @@ -254,7 +254,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
fs.symlinkSync(src, destLink, 'file')

try {
fs.copySync(srcLink, destLink)
fs.copySync(srcLink, destLink, { dereference: true })
} catch (err) {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ describe('+ copySync() - prevent copying into itself', () => {
})

describe('>> when dest is a symlink', () => {
it('should error when dest points exactly to src', () => {
it('should error when dest points exactly to src and dereference is true', () => {
const destLink = path.join(TEST_DIR, 'dest-symlink')
fs.symlinkSync(src, destLink, 'dir')

const srclenBefore = klawSync(src).length
assert(srclenBefore > 2)

try {
fs.copySync(src, destLink)
fs.copySync(src, destLink, { dereference: true })
} catch (err) {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
}
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('+ copySync() - prevent copying into itself', () => {
}
})

it('should copy the directory successfully when src is a subdir of resolved dest path', () => {
it('should copy the directory successfully when src is a subdir of resolved dest path and dereferene is true', () => {
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
Expand All @@ -226,9 +226,9 @@ describe('+ copySync() - prevent copying into itself', () => {

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

assert(srclen > 2)
fs.copySync(srcInDest, destLink)

fs.copySync(srcInDest, destLink, { dereference: true })

const destlenAfter = klawSync(dest).length

Expand Down Expand Up @@ -323,7 +323,7 @@ describe('+ copySync() - prevent copying into itself', () => {
})

describe('>> when dest is a symlink', () => {
it('should error when resolved dest path is exactly the same as resolved src path', () => {
it('should error when resolved dest path is exactly the same as resolved src path and dereference is true', () => {
const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(src, srcLink, 'dir')
const destLink = path.join(TEST_DIR, 'dest-symlink')
Expand All @@ -335,7 +335,7 @@ describe('+ copySync() - prevent copying into itself', () => {
assert(destlenBefore > 2)

try {
fs.copySync(srcLink, destLink)
fs.copySync(srcLink, destLink, { dereference: true })
} catch (err) {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const os = require('os')
const fs = require('../../')
const fs = require('../..')
const path = require('path')
const assert = require('assert')
const copySync = require('../copy-sync')
Expand Down
13 changes: 4 additions & 9 deletions lib/copy-sync/copy-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function copySync (src, dest, opts) {
see https://github.com/jprichardson/node-fs-extra/issues/269`)
}

const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy')
const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy', opts)
stat.checkParentPathsSync(src, srcStat, dest, 'copy')
return handleFilterAndCopy(destStat, src, dest, opts)
}
Expand Down Expand Up @@ -98,13 +98,8 @@ function setDestTimestamps (src, dest) {
}

function onDir (srcStat, destStat, src, dest, opts) {
if (destStat) {
if (destStat.isDirectory()) {
return copyDir(src, dest, opts)
}
throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
}
return mkDirAndCopy(srcStat.mode, src, dest, opts)
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts)
return copyDir(src, dest, opts)
}

function mkDirAndCopy (srcMode, src, dest, opts) {
Expand All @@ -120,7 +115,7 @@ 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 } = stat.checkPathsSync(srcItem, destItem, 'copy')
const { destStat } = stat.checkPathsSync(srcItem, destItem, 'copy', opts)
return startCopy(destStat, srcItem, destItem, opts)
}

Expand Down
62 changes: 62 additions & 0 deletions lib/copy/__tests__/copy-broken-symlink.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'

const fs = require('fs')
const os = require('os')
const fse = require('../..')
const path = require('path')
const assert = require('assert')
const copy = require('../copy')

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

describe('copy / broken symlink', () => {
const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-broken-symlink')
const src = path.join(TEST_DIR, 'src')
const dest = path.join(TEST_DIR, 'dest')

beforeEach(done => {
fse.emptyDir(TEST_DIR, err => {
assert.ifError(err)
createFixtures(src, done)
})
})

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

describe('when symlink is broken', () => {
it('should not throw error if dereference is false', done => {
copy(src, dest, err => {
assert.strictEqual(err, null)
done()
})
})

it('should throw error if dereference is true', done => {
copy(src, dest, { dereference: true }, err => {
assert.strictEqual(err.code, 'ENOENT')
done()
})
})
})
})

function createFixtures (srcDir, callback) {
fs.mkdir(srcDir, err => {
let brokenFile
let brokenFileLink

if (err) return callback(err)

try {
brokenFile = path.join(srcDir, 'does-not-exist')
brokenFileLink = path.join(srcDir, 'broken-symlink')
fs.writeFileSync(brokenFile, 'does not matter')
fs.symlinkSync(brokenFile, brokenFileLink, 'file')
} catch (err) {
callback(err)
}

// break the symlink now
fse.remove(brokenFile, callback)
})
}
18 changes: 9 additions & 9 deletions lib/copy/__tests__/copy-prevent-copying-identical.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {

describe('> when src is a directory', () => {
describe('>> when src is regular and dest is a symlink that points to src', () => {
it('should error', done => {
it('should error if dereference is true', done => {
src = path.join(TEST_DIR, 'src')
fs.mkdirsSync(src)
const subdir = path.join(TEST_DIR, 'src', 'subdir')
Expand All @@ -116,7 +116,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {

const oldlen = klawSync(src).length

fs.copy(src, destLink, err => {
fs.copy(src, destLink, { dereference: true }, err => {
assert.strictEqual(err.message, 'Source and destination must not be the same.')

const newlen = klawSync(src).length
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
})

describe('>> when src and dest are symlinks that point to the exact same path', () => {
it('should error src and dest are the same', done => {
it('should error src and dest are the same and dereference is true', done => {
src = path.join(TEST_DIR, 'src')
fs.mkdirsSync(src)
const srcLink = path.join(TEST_DIR, 'src_symlink')
Expand All @@ -166,7 +166,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
const srclenBefore = klawSync(srcLink).length
const destlenBefore = klawSync(destLink).length

fs.copy(srcLink, destLink, err => {
fs.copy(srcLink, destLink, { dereference: true }, err => {
assert.strictEqual(err.message, 'Source and destination must not be the same.')

const srclenAfter = klawSync(srcLink).length
Expand Down Expand Up @@ -198,22 +198,22 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
const destLink = path.join(TEST_DIR, 'dest-symlink')
fs.symlinkSync(src, destLink, 'file')

fs.copy(src, destLink, err => {
fs.copy(src, destLink, { dereference: true }, err => {
assert.strictEqual(err.message, 'Source and destination must not be the same.')
done()
})
})
})

describe('>> when src is a symlink that points to a regular dest', () => {
it('should throw error', done => {
it('should throw error if dereference is true', done => {
dest = path.join(TEST_DIR, 'dest', 'somefile.txt')
fs.outputFileSync(dest, 'some data')

const srcLink = path.join(TEST_DIR, 'src-symlink')
fs.symlinkSync(dest, srcLink, 'file')

fs.copy(srcLink, dest, err => {
fs.copy(srcLink, dest, { dereference: true }, err => {
assert.strictEqual(err.message, 'Source and destination must not be the same.')

const link = fs.readlinkSync(srcLink)
Expand All @@ -225,7 +225,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
})

describe('>> when src and dest are symlinks that point to the exact same path', () => {
it('should error src and dest are the same', done => {
it('should error src and dest are the same and dereferene is true', done => {
src = path.join(TEST_DIR, 'src', 'srcfile.txt')
fs.outputFileSync(src, 'src data')

Expand All @@ -235,7 +235,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
const destLink = path.join(TEST_DIR, 'dest_symlink')
fs.symlinkSync(src, destLink, 'file')

fs.copy(srcLink, destLink, err => {
fs.copy(srcLink, destLink, { dereference: true }, err => {
assert.strictEqual(err.message, 'Source and destination must not be the same.')

const srcln = fs.readlinkSync(srcLink)
Expand Down
Loading

0 comments on commit f21048b

Please sign in to comment.