-
Notifications
You must be signed in to change notification settings - Fork 772
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
Update dest atime after copy/copy-sync #633
Update dest atime after copy/copy-sync #633
Conversation
@@ -19,11 +19,18 @@ describeIfPractical('copySync() - preserveTimestamps option', () => { | |||
let TEST_DIR, SRC, DEST, FILES | |||
|
|||
beforeEach(done => { | |||
TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-preserve-time') | |||
TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-preserve-timestamp') |
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 making this identical to its async counterpart.
SRC = path.join(TEST_DIR, 'src') | ||
DEST = path.join(TEST_DIR, 'dest') | ||
FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')] | ||
FILES.forEach(f => fs.ensureFileSync(path.join(SRC, f))) | ||
const timestamp = Date.now() / 1000 - 5 |
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 prefer, I could just hard-code a date in the past rather than use "now minus 5 seconds".
assert.strictEqual(toStat.mtime.getTime(), utimes.timeRemoveMillis(fromStat.mtime.getTime())) | ||
assert.strictEqual(toStat.atime.getTime(), utimes.timeRemoveMillis(fromStat.atime.getTime())) | ||
} | ||
// Windows has sub-second support: https://github.com/nodejs/io.js/issues/2069 |
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 updated the comment after checking the nodejs/node#2069 thread - according to its conclusion, we should no longer need to RemoveMillis for Windows (so maybe I should have switched the conditions around instead, or what...? I'm open to reviewing this section)
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.
Update - I realized afterwards that utimes.utimesMillisSync
properly implements sub-second timestamp changes by using fs.futimes
rather than fs.utimes
- that looks like a bug in Node's fs.utimes
function.
Still, I updated the comment and code since the bug reported by nodejs/node#2069 has been fixed years ago, so it seems safe to no longer have a different check for Windows. Tests will tell us if it's valid (I can't check on Windows atm)
518036c
to
ca81e73
Compare
ca81e73
to
b9edbbe
Compare
Rebased on top of latest |
lib/copy/copy.js
Outdated
return fs.chmod(dest, srcStat.mode, cb) | ||
} | ||
|
||
const next = (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.
This part is a bit tricky. (Check the sync version to better understand.)
Basically, the flow is:
1 -> (optional) set writable if not writable
2 --> re-read atime from source
3 ---> set dest atime
4 ----> set dest mode
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.
The complexity stems from the optional step. I'm open to suggestions for making this more readable.
Initial quick review LGTM; but I'd like @manidlou to take a detailed look, since I'm not very familiar with this part of the codebase. |
I checked the coverage loss.
|
lib/copy-sync/copy-sync.js
Outdated
return utimesSync(dest, srcStat.atime, srcStat.mtime) | ||
// Make sure the file is writable first | ||
if ((srcStat.mode & 0o200) === 0) { | ||
fs.chmodSync(dest, srcStat.mode | 0o200) |
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 seems unnecessary! we are chmod
ing dest at the end anyway.
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.
Actually, it is required. One of the tests I added reveals that it doesn't work without this if the source file is read-only (also manually tested on both Windows and Mac).
- If
src
is readonly, then so isdest
afterfs.copyFileSync(src, dest)
utimesSync
then fails with a permission error because it's not allowed to open thedest
file for write.
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.
Simple repro on Node 8.11.3, with latest head (9f1c029). Assumed to be run from the fse directory.
const fse = require('.')
fse.ensureFileSync('src')
fse.chmodSync('src', 0x111)
fse.copySync('src', 'destNoPreserve') // works
fse.copySync('src', 'destPreserve', { preserveTimestamps: true }) // fails with the following stack trace (Windows example):
Error: EPERM: operation not permitted, open 'C:\Users\maximeb\code\node-fs-extra\destPreserve'
at Object.fs.openSync (fs.js:646:18)
at utimesMillisSync (C:\Users\maximeb\code\node-fs-extra\lib\util\utimes.js:68:17)
at copyFile (C:\Users\maximeb\code\node-fs-extra\lib\copy-sync\copy-sync.js:79:7)
at onFile (C:\Users\maximeb\code\node-fs-extra\lib\copy-sync\copy-sync.js:55:25)
at getStats (C:\Users\maximeb\code\node-fs-extra\lib\copy-sync\copy-sync.js:50:44)
at startCopy (C:\Users\maximeb\code\node-fs-extra\lib\copy-sync\copy-sync.js:40:10)
at handleFilterAndCopy (C:\Users\maximeb\code\node-fs-extra\lib\copy-sync\copy-sync.js:35:10)
at Object.copySync (C:\Users\maximeb\code\node-fs-extra\lib\copy-sync\copy-sync.js:28:10)
With this proposed change, no exception. If I comment out line 74, then I get the same exception.
If you don't want to keep this as-is, I see a few options:
- Make sure
copyFileSync
does not set thedest
mode to be the same assrc
- Change
utimesMillisSync
to be more robust if it fails with EPERM - Change
utimesMillisSync
to avoid this problem by not usingfs.openSync(<path>, 'r+')
+fs.futimesSync
but straight outfs.utimesSync
which works even on readonly files (note: I'm not sure thefutimes
andutimes
interfaces are fully compatible, this might be the reason for usingfutimes
inutimesMillisSync
in the first place...) - Change this code to implement the chmod in a try/catch around utimesSync... but then it's no simpler than an
if
, just more costly.
Any other idea is also welcome.
if (opts.preserveTimestamps) { | ||
const updatedSrcStat = fs.statSync(src) | ||
fs.futimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime) | ||
} | ||
|
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 realized we forgot to chmod
again here before closing fd
s. copy
does that and we need to be consistent. So I think it is better we have the same function setDestModeAndTimestamps()
like in copy
so that we can call it after both native copy and fallback.
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 would make sense. I'll try it out.
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.
Question, though: if the fallback copy is used, wouldn't it be much better to use the futimes
and fchmod
operations directly to make the operation both atomic and faster, rather than closing the file descriptor and calling the path-based setDestModeAndTimestamps()
function?
In fact, the setDestModeAndTimestamps
function could be split in two, one taking paths in, and the other taking file descriptors in. Something like:
- copyFile -> fs.copy -> setDestModeAndTimestamps(src, dest...) -> setDestModeAndTimestampsFd(fsd, fwd...)
- copyFileFallback -> fs.open -> (copy) -> setDestModeAndTimestampsFd(fsd, fwd...) -> fs.close
What do you think?
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.
Not sure the latter works for async since the async copyFileFallback uses streams with implicit FDs. But it seems a good solution for the sync version.
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 fix looks important, what is the status? |
@brodybits I just pushed commits that hopefully address review feedback. |
@manidlou What did you think of the changes I made after your initial review? (I will squash once approved.) |
so this is my suggestion:
function setDestTimestampsAndMode (srcStat, src, dest, opts) {
const utimesSync = typeof dest === 'string' ? fs.utimesSync : fs.futimesSync
const chmodSync = typeof dest === 'string' ? fs.chmodSync : fs.fchmodSync
if (opts.preserveTimestamps) {
const updatedSrcStat = fs.statSync(src)
utimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime)
}
chmodSync(dest, srcStat.mode)
} |
That would certainly be more straightforward. Let me take a stab at that tonight. |
SECOND EDIT: I found an issue in my test implementation that (presumably) re-ran copy[Sync] tests with the fallback implementation: it didn't work as expected, and actually caused these tests to only run the Fallback path, twice. Fixing this surfaced a problem: the native (And passing the |
@manidlou 2b7c0e5 fixes the tests and now both the "fs.copyFile[Sync]" path and "copyFile[Sync]Fallback" path are tested. In fb89a29, I simplified the implementation following your last idea, but I had to keep some of the complexity you didn't like, because of the catch-22 between |
Apparently |
I don't understand why these failures are happening. I cannot reproduce them locally, on macOS (10.14), not even with the same node/npm/nvs versions. All these tests pass. The Linux builder also passes. The failing builds are using macOS 10.13.3, but passing ones also seem to be using 10.13.3. Maybe a difference between HFS+ and APFS? Could it be there is a race condition between nodejs and the OS APIs, e.g. the source atime is only modified from reading it after the copy is done rather than immediately? (Edited: the build machines are 10.13.3 only, so it should only be using APFS.) |
I locally tested using a HFS+ partition, no luck. This simply isn't happening on macOS 10.14.6. |
@manidlou f928144 fixes the test failures that I can't quite explain. (Or rather, I can't explain why they were not failures in some builds, considering the Node documentation.) Unfortunately it does so by reviving the "re-read the source file stats" but at least in a way that should be much easier to follow thanks to the suggestions you made in review. |
Note: in #633 (comment), you suggested not setting the mode after the initial copy so we wouldn't need to check it/make writable before updating the timestamps. Unfortunately, the only way I can think of implementing this is to not call |
... I don't get it. I locally see the coverage of the EDIT ... because it's running on the commit resulting from merging master, where graceful-fs was re-enabled! Makes sense now. Fixing. |
d288d84
to
182e112
Compare
(Yes, I'll squash it all when it's approved.) |
@manidlou what's the status 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.
LGTM! Thank you @mbargiel!
75756c4
to
3121749
Compare
Squashed into Glad to have been of any help! |
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.
Actual code looks OK to me; however, please remove the fallback test files. We will simply test the fallback on the old Node versions on the CI where copyFile
doesn't exist.
Besides, the fallback will be getting killed soon, as we upgrade minimum required Node version.
Sure thing. You might want to explicitly opt out of code coverage for "fallback" paths like these in the future? That caused me to run into "code coverage loss" while working on this PR. |
3121749
to
d09b23b
Compare
This PR provides a fix for the recently failing "copy-preserve-timestamps" (sync & async) tests on MacOS (partial fix for #571). (I believe they have been incorrect for a while but passed by chance due to timing and the coarse 1-s resolution time of HFS+.)
The main observation is that after copying a source file, the
atime
of that file may be modified (probably always will, due to the copy operation implicitly updating theatime
when itread
s from the file according to https://nodejs.org/docs/latest-v8.x/api/fs.html#fs_stat_time_values), so if we want to provide a truepreserveTimestamps
option, we need to re-read the sourceatime
and use that value when updating the destination timestamps.