From 99f91138cdc56c8175e069c4bcd3ffc6885ef18e Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Sat, 19 Sep 2026 01:57:06 -0400 Subject: [PATCH] Respect overwrite options when copying symlinks --- .../__tests__/copy-symlink-overwrite.test.js | 59 +++++++++++++++++++ lib/copy/copy-sync.js | 4 ++ lib/copy/copy.js | 4 ++ 3 files changed, 67 insertions(+) create mode 100644 lib/copy/__tests__/copy-symlink-overwrite.test.js diff --git a/lib/copy/__tests__/copy-symlink-overwrite.test.js b/lib/copy/__tests__/copy-symlink-overwrite.test.js new file mode 100644 index 00000000..2966a2de --- /dev/null +++ b/lib/copy/__tests__/copy-symlink-overwrite.test.js @@ -0,0 +1,59 @@ +'use strict' +/* eslint-env mocha */ + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require('../..') + +for (const method of ['copy', 'copySync']) { + describe(`${method}() / symlink overwrite options`, () => { + const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', `${method}-symlink-overwrite`) + const src = path.join(TEST_DIR, 'src') + const dest = path.join(TEST_DIR, 'dest') + const srcTarget = path.join(TEST_DIR, 'source-target') + const destTarget = path.join(TEST_DIR, 'destination-target') + + beforeEach(() => { + fs.emptyDirSync(TEST_DIR) + fs.writeFileSync(srcTarget, 'source content') + fs.symlinkSync(srcTarget, src, 'file') + }) + + afterEach(() => fs.removeSync(TEST_DIR)) + + for (const broken of [false, true]) { + describe(`with a ${broken ? 'broken' : 'valid'} destination symlink`, () => { + beforeEach(() => { + if (!broken) fs.writeFileSync(destTarget, 'destination content') + fs.symlinkSync(destTarget, dest, 'file') + }) + + for (const option of ['overwrite', 'clobber']) { + it(`does not replace the destination when ${option} is false`, async () => { + await fs[method](src, dest, { [option]: false }) + assert.strictEqual(fs.readlinkSync(dest), destTarget) + }) + } + + it('reports an existing destination when errorOnExist is true', async () => { + await assert.rejects(async () => fs[method](src, dest, { + overwrite: false, + errorOnExist: true + }), { message: `'${dest}' already exists` }) + assert.strictEqual(fs.readlinkSync(dest), destTarget) + }) + + it('still replaces the destination by default', async () => { + await fs[method](src, dest) + assert.strictEqual(fs.readlinkSync(dest), srcTarget) + }) + }) + } + + it('copies to a missing destination when overwrite is false', async () => { + await fs[method](src, dest, { overwrite: false, errorOnExist: true }) + assert.strictEqual(fs.readlinkSync(dest), srcTarget) + }) + }) +} diff --git a/lib/copy/copy-sync.js b/lib/copy/copy-sync.js index 8bb1ec59..9b2cca71 100644 --- a/lib/copy/copy-sync.js +++ b/lib/copy/copy-sync.js @@ -132,6 +132,10 @@ function onLink (destStat, src, dest, opts) { if (opts.dereference) { resolvedSrc = path.resolve(process.cwd(), resolvedSrc) } + if (destStat && !opts.overwrite) { + if (opts.errorOnExist) throw new Error(`'${dest}' already exists`) + return + } if (!destStat) { return fs.symlinkSync(resolvedSrc, dest) diff --git a/lib/copy/copy.js b/lib/copy/copy.js index ab01a5f6..eb2f38f8 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -139,6 +139,10 @@ async function onLink (destStat, src, dest, opts) { if (opts.dereference) { resolvedSrc = path.resolve(process.cwd(), resolvedSrc) } + if (destStat && !opts.overwrite) { + if (opts.errorOnExist) throw new Error(`'${dest}' already exists`) + return + } if (!destStat) { return fs.symlink(resolvedSrc, dest) }