-
Notifications
You must be signed in to change notification settings - Fork 775
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
[WIP] Rewrite lib/copy/ncp.js #374
Conversation
Hmm! coverage decreased! I will look at it and try to add tests for the parts that are not covered yet. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, LGTM. Couple of nits below. Also a couple of questions for @jprichardson on aspects I'm not sure about.
@manidlou This needs to be rebased on top of the latest master
to avoid undoing work done on move
's tests.
fs.writeFileSync(path.join(src, FILES[0]), dat0) | ||
fs.writeFileSync(path.join(src, FILES[1]), dat1) | ||
fs.writeFileSync(path.join(src, FILES[2]), dat2) | ||
fs.writeFileSync(path.join(src, FILES[3]), dat3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use outputFile
here instead of ensureFile
and writeFile
.
}) | ||
}) | ||
|
||
it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the test's code, the description should say: when dest is 'src_src/dest'
(missing underscore).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right. In fact, description is correct, but dest path in test's code should change to match the description.
lib/copy/__tests__/copy.test.js
Outdated
@@ -159,6 +159,7 @@ describe('fs-extra', () => { | |||
}) | |||
}) | |||
|
|||
/* It is redundant. See above, we already tested for this case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just remove this test.
lib/copy/__tests__/copy.test.js
Outdated
const filter = /.html$|.css$/i | ||
|
||
fse.copy(srcFile1, destFile1, filter, (err) => { | ||
assert(!err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use assert.ifError(err)
here.
|
||
options.filter = options.filter || function () { return true } | ||
|
||
// Warn about using preserveTimestamps on 32-bit node | ||
if (options.preserveTimestamps && process.arch === 'ia32') { | ||
console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n | ||
see https://github.com/jprichardson/node-fs-extra/issues/269`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could port this to ES6 template strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RyanZim would you please say what you mean exactly? is it referring to the extra \n
in the warn?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, nevermind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that \n
necessary as with template strings we can do multi-line like normally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@manidlou We are already using template strings (I didn't notice that when I commented here). The \n
is to make a double linebreak.
Just ignore this line comment.
lib/copy/copy.js
Outdated
if (typeof options === 'function' && !callback) { | ||
callback = options | ||
const DEST_NOENT = -1 | ||
const DEST_EXISTS = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly could/should use ES6 Symbol
s here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of using Symbol
s here as one of their use cases is for unique constants.
lib/copy/copy.js
Outdated
if (err) return cb(err) | ||
if (options.preserveTimestamps) { | ||
return utimes(dest, srcStat.atime, srcStat.mtime, cb) | ||
} else return cb() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need else
here.
lib/copy/copy.js
Outdated
if (isSrcSubdir(src, res)) return cb(new Error(`Cannot copy directory '${src}' into itself '${res}'`)) | ||
if (src === res) return cb() | ||
return cpDir(src, dest, options, cb) | ||
} else return cb() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what case would this code path be taken? Just wondering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean line 141 } else return cb()
, I guess it is not needed and should be removed as we check for all possible conditions.
lib/copy/copy.js
Outdated
return cb() | ||
}).catch(err => { | ||
return cb(err) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be shortened to
})).then(() => cb())
.catch(cb)
lib/copy/copy.js
Outdated
} | ||
// if src points to dest | ||
if (resolvedSrcPath === dest) return cb() | ||
return copy(resolvedSrcPath, dest, options, cb) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit hairy on expected behavior for symlinks; @jprichardson is this correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I am not 100% sure neither. So, @jprichardson we definitely need some advice here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jprichardson ping?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RyanZim I guess you are right. I don't have a good feeling about return copy(resolvedSrcPath, dest, options, cb)
neither. I recall I used it because of the ability to handle weird cases like when src is a symlink and dest exists (either regular or another symlink) and is a kid of src as well.
return fs.symlink(resolvedSrcPath, dest, cb)
correctly throws EEXIST: file already exists, symlink 'src' -> 'dest'
for those cases.
Now, I think we should decide what we want to do here for these two cases when src is a symlink:
- dest exists and is a regular file/directory
- dest exists and is a symlink (file/directory)
Do we want to throw an error and notify user that dest exists?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now, I think we should decide what we want to do here for these two cases when src is a symlink:
What is the existing behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the existing behavior?
Currently, when src is a symlink, this is what happens for the following cases:
-
dest exists and is a regular file or directory
it throws
Error: EINVAL: invalid argument, readlink 'dest'
, and this is because the existing behavior assumes when src is a symlink, dest is also a symlink (https://github.com/jprichardson/node-fs-extra/blob/master/lib/copy/ncp.js#L182). -
dest exists and is a symlink
since
overwrite
is the default behavior, it removes dest then copies src (creates a symlink in dest that points to src).
Now, for the case 2, it works fine except if src is a kid of dest. In this case, since it removes dest, we end up losing src and consequently a broken symlink is created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry so long in getting to this.
One old type-check in two places, other than that, LGTM.
lib/copy/copy.js
Outdated
} else if (res === DEST_EXISTS) { | ||
if (isSrcSubdir(src, dest)) return cb(new Error(`Cannot copy directory '${src}' into itself '${dest}'`)) | ||
return cpDir(src, dest, options, cb) | ||
} else if (res && typeof res !== 'number') { // dest exists and is a link |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This typeof res !== 'number'
is obsolete since you are using symbols.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I just noticed that for both cases where dest exists (regular and symlink), onDir()
should check for options.overwrite
and proceed its operation accordingly; the same as onFile()
.
Edit
Is that correct because I am not 100% sure if that should be the correct behavior for copying direcotory? 😕 Since it is mentioned in docs that overwrite
would apply to both file and directory. Then, this algorithm wouldn't be consistent with docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@manidlou Historically, overwrite
only is checked for files. There's no need to nuke an entire directory if we just need to add one file to it. I'd leave it that way.
lib/copy/copy.js
Outdated
// if src points to dest | ||
if (resolvedSrcPath === dest) return cb() | ||
return copy(resolvedSrcPath, dest, options, cb) | ||
} else if (resolvedDestPath && typeof resolvedDestPath !== 'number') { // dest is a link |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again here also, when dest exists (regular and symlink), should go with the same pattern of checking options.overwrite
and operates accordingly.
lib/copy/copy.js
Outdated
} else if (options.errorOnExist) { | ||
return cb(new Error(dest + ' already exists')) | ||
} else return cb() | ||
} else return cb() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This last else
basically means dest exists and is a symlink. I believe the same pattern that is used for the case DEST_EXISTS
, which means checking for options.overwrite
and operates accordingly, should be applied for this case as well. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. @jprichardson ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed @manidlou
lib/copy/copy.js
Outdated
} else if (res === DEST_EXISTS) { | ||
if (isSrcSubdir(src, dest)) return cb(new Error(`Cannot copy directory '${src}' into itself '${dest}'`)) | ||
return cpDir(src, dest, options, cb) | ||
} else if (res && typeof res !== 'number') { // dest exists and is a link |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I just noticed that for both cases where dest exists (regular and symlink), onDir()
should check for options.overwrite
and proceed its operation accordingly; the same as onFile()
.
Edit
Is that correct because I am not 100% sure if that should be the correct behavior for copying direcotory? 😕 Since it is mentioned in docs that overwrite
would apply to both file and directory. Then, this algorithm wouldn't be consistent with docs.
lib/copy/copy.js
Outdated
// if src points to dest | ||
if (resolvedSrcPath === dest) return cb() | ||
return copy(resolvedSrcPath, dest, options, cb) | ||
} else if (resolvedDestPath && typeof resolvedDestPath !== 'number') { // dest is a link |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again here also, when dest exists (regular and symlink), should go with the same pattern of checking options.overwrite
and operates accordingly.
fs.mkdirsSync(dest) | ||
const subdir = path.join(TEST_DIR, 'dest', 'subdir') | ||
fs.mkdirsSync(subdir) | ||
fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will be changed to use fs.outputFileSync()
.
it('should not copy and return', done => { | ||
src = path.join(TEST_DIR, 'src', 'somefile.txt') | ||
fs.ensureFileSync(src) | ||
fs.writeFileSync(src, 'some data') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. It will be changed to use fs.outputFileSync()
instead.
it('should not copy and return', done => { | ||
src = path.join(TEST_DIR, 'src', 'srcfile.txt') | ||
fs.ensureFileSync(src) | ||
fs.writeFileSync(src, 'src data') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
it('should not copy and return', done => { | ||
dest = path.join(TEST_DIR, 'dest', 'somefile.txt') | ||
fs.ensureFileSync(dest) | ||
fs.writeFileSync(dest, 'some data') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
@manidlou ping? |
@RyanZim I asked a question a few comments above regarding copying when src is a symlink, the case that we are not sure about the correct behavior yet, that I've been waiting for your and @jprichardson's answers. Would you please take a look at it and give me your ideas about it? 😃 |
So, following our discussion on copying symlinks when src is a dest's kid and vice versa, to me for this case (which I guess is relatively a rare case) it is reasonable to throw error and let user decide on the action. Although we can deliberately handle these cases, but that makes it overly complex while it think it is not worth it for this case. I tried to find a short and yet descriptive error message for this case. I came up with @jprichardson @RyanZim what do you think then? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think overall LGTM
@jprichardson Please review carefully though, I might have missed something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this looks OK, @jprichardson ?
What about |
@jprichardson Thoughts on this? |
@jprichardson @RyanZim will you please take a look at https://github.com/manidlou/benchmark-fs-extra-copy? I add a quick summary here too: I've been working on an alternative implementation for So, I created a benchmark to compare the running time of these implementations:
to see which one runs faster particularly on large input sizes, when a src directory has lots of items. So, when src length is up to line chart comparison, range: 110-8190 line chart comparison, range: 110-111110 any thoughts please? |
@manidlou Sorry so terribly long in getting back (again). If you're comfortable with the |
BTW @manidlou, please don't let me just let stuff slide like this; if I don't reply within 7 days, you're welcome to ping me (I forget stuff really easily; if I'm not responding, I could be busy, but most of the time, I just forgot about it). |
@RyanZim besides perf, overall I personally have a better feeling about |
Refs: #292.
This is the first draft of reconsidering/rewriting
lib/copy/ncp.js
.As @RyanZim mentioned before:
It make sense to me too. So, it is wrapped in one file now
lib/copy/copy.js
.A few notes:
In
checkDest()
, the reason that I directly usefs.readlink()
as apposed to getting thefs.stat()
then decide accordingly, is to avoid possibility of race since we essentially need to check src against a possible resolved dest path. See handle copying between identical files #198.I added more tests for symlinks, copying identical, and copying into itself as well. These changes should resolve handle copying between identical files #198 and Prevent copying a directory into itself #83.
I activated the
move()
skipped tests: https://github.com/jprichardson/node-fs-extra/blob/master/lib/move/__tests__/move.test.js#L310. It passes now.Currently all tests pass including old and new tests.
The reason that I didn't create the branch here is I figured that'd be better to keep the upstream repo as clean as possible since this one probably take some time to discuss and decide on and whenever we want we can migrate it here.
Please feel free to point out any issues that you can think of with these changes.