Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[breaking] copy*(): allow copying broken symlinks #779

Merged
merged 3 commits into from May 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
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
@@ -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
7 changes: 2 additions & 5 deletions lib/copy-sync/copy-sync.js
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 @@ -99,9 +99,6 @@ function setDestTimestamps (src, dest) {

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

Expand All @@ -118,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
@@ -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
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