Skip to content

Commit 0e7de32

Browse files
KostiantynPopovychRyanZim
andauthoredMar 6, 2023
Preserve timestamp when moving across devices (#994)
Fixes #992 Co-authored-by: Ryan Zimmerman <opensrc@ryanzim.com>
1 parent f3a7f0b commit 0e7de32

File tree

4 files changed

+194
-2
lines changed

4 files changed

+194
-2
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use strict'
2+
3+
const fs = require('../../')
4+
const os = require('os')
5+
const path = require('path')
6+
const utimesSync = require('../../util/utimes').utimesMillisSync
7+
const assert = require('assert')
8+
const fse = require('../../index')
9+
10+
/* global beforeEach, afterEach, describe, it */
11+
12+
if (process.arch === 'ia32') console.warn('32 bit arch; skipping move timestamp tests')
13+
14+
const describeIfPractical = process.arch === 'ia32' ? describe.skip : describe
15+
16+
describeIfPractical('move() - across different devices', () => {
17+
let TEST_DIR, SRC, DEST, FILES
18+
let __skipTests = false
19+
const differentDevicePath = '/mnt'
20+
21+
// must set this up, if not, exit silently
22+
if (!fs.existsSync(differentDevicePath)) {
23+
console.log('Skipping cross-device move test')
24+
__skipTests = true
25+
}
26+
27+
// make sure we have permission on device
28+
try {
29+
fs.writeFileSync(path.join(differentDevicePath, 'file'), 'hi')
30+
} catch {
31+
console.log("Can't write to device. Skipping move test.")
32+
__skipTests = true
33+
}
34+
35+
function setupFixture (readonly) {
36+
TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-sync-preserve-timestamp')
37+
SRC = path.join(differentDevicePath, 'some/weird/dir-really-weird')
38+
DEST = path.join(TEST_DIR, 'dest')
39+
FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')]
40+
const timestamp = Date.now() / 1000 - 5
41+
FILES.forEach(f => {
42+
const filePath = path.join(SRC, f)
43+
fs.ensureFileSync(filePath)
44+
// rewind timestamps to make sure that coarser OS timestamp resolution
45+
// does not alter results
46+
utimesSync(filePath, timestamp, timestamp)
47+
if (readonly) {
48+
fs.chmodSync(filePath, 0o444)
49+
}
50+
})
51+
}
52+
53+
const _it = __skipTests ? it.skip : it
54+
55+
afterEach(() => {
56+
fse.removeSync(TEST_DIR)
57+
fse.removeSync(SRC)
58+
})
59+
60+
describe('> default behaviour', () => {
61+
;[
62+
{ subcase: 'writable', readonly: false },
63+
{ subcase: 'readonly', readonly: true }
64+
].forEach(params => {
65+
describe(`>> with ${params.subcase} source files`, () => {
66+
beforeEach(() => setupFixture(params.readonly))
67+
68+
_it('should have the same timestamps after move', done => {
69+
const originalTimestamps = FILES.map(file => {
70+
const originalPath = path.join(SRC, file)
71+
const originalStat = fs.statSync(originalPath)
72+
return {
73+
mtime: originalStat.mtime.getTime(),
74+
atime: originalStat.atime.getTime()
75+
}
76+
})
77+
fse.move(SRC, DEST, {}, (err) => {
78+
if (err) return done(err)
79+
FILES.forEach(testFile({}, originalTimestamps))
80+
done()
81+
})
82+
})
83+
})
84+
})
85+
})
86+
87+
function testFile (options, originalTimestamps) {
88+
return function (file, idx) {
89+
const originalTimestamp = originalTimestamps[idx]
90+
const currentPath = path.join(DEST, file)
91+
const currentStats = fs.statSync(currentPath)
92+
assert.strictEqual(currentStats.mtime.getTime(), originalTimestamp.mtime, 'different mtime values')
93+
assert.strictEqual(currentStats.atime.getTime(), originalTimestamp.atime, 'different atime values')
94+
}
95+
}
96+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict'
2+
3+
const fs = require('../../')
4+
const os = require('os')
5+
const path = require('path')
6+
const utimesSync = require('../../util/utimes').utimesMillisSync
7+
const assert = require('assert')
8+
const fse = require('../../index')
9+
10+
/* global beforeEach, afterEach, describe, it */
11+
12+
if (process.arch === 'ia32') console.warn('32 bit arch; skipping move timestamp tests')
13+
14+
const describeIfPractical = process.arch === 'ia32' ? describe.skip : describe
15+
16+
describeIfPractical('moveSync() - across different devices', () => {
17+
let TEST_DIR, SRC, DEST, FILES
18+
let __skipTests = false
19+
const differentDevicePath = '/mnt'
20+
21+
// must set this up, if not, exit silently
22+
if (!fs.existsSync(differentDevicePath)) {
23+
console.log('Skipping cross-device move test')
24+
__skipTests = true
25+
}
26+
27+
// make sure we have permission on device
28+
try {
29+
fs.writeFileSync(path.join(differentDevicePath, 'file'), 'hi')
30+
} catch {
31+
console.log("Can't write to device. Skipping move test.")
32+
__skipTests = true
33+
}
34+
35+
function setupFixture (readonly) {
36+
TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-sync-preserve-timestamp')
37+
SRC = path.join(differentDevicePath, 'some/weird/dir-really-weird')
38+
DEST = path.join(TEST_DIR, 'dest')
39+
FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')]
40+
const timestamp = Date.now() / 1000 - 5
41+
FILES.forEach(f => {
42+
const filePath = path.join(SRC, f)
43+
fs.ensureFileSync(filePath)
44+
// rewind timestamps to make sure that coarser OS timestamp resolution
45+
// does not alter results
46+
utimesSync(filePath, timestamp, timestamp)
47+
if (readonly) {
48+
fs.chmodSync(filePath, 0o444)
49+
}
50+
})
51+
}
52+
53+
const _it = __skipTests ? it.skip : it
54+
55+
afterEach(() => {
56+
fse.removeSync(TEST_DIR)
57+
fse.removeSync(SRC)
58+
})
59+
60+
describe('> default behaviour', () => {
61+
;[
62+
{ subcase: 'writable', readonly: false },
63+
{ subcase: 'readonly', readonly: true }
64+
].forEach(params => {
65+
describe(`>> with ${params.subcase} source files`, () => {
66+
beforeEach(() => setupFixture(params.readonly))
67+
68+
_it('should have the same timestamps after move', () => {
69+
const originalTimestamps = FILES.map(file => {
70+
const originalPath = path.join(SRC, file)
71+
const originalStat = fs.statSync(originalPath)
72+
return {
73+
mtime: originalStat.mtime.getTime(),
74+
atime: originalStat.atime.getTime()
75+
}
76+
})
77+
fse.moveSync(SRC, DEST, {})
78+
FILES.forEach(testFile({}, originalTimestamps))
79+
})
80+
})
81+
})
82+
})
83+
84+
function testFile (options, originalTimestamps) {
85+
return function (file, idx) {
86+
const originalTimestamp = originalTimestamps[idx]
87+
const currentPath = path.join(DEST, file)
88+
const currentStats = fs.statSync(currentPath)
89+
// Windows sub-second precision fixed: https://github.com/nodejs/io.js/issues/2069
90+
assert.strictEqual(currentStats.mtime.getTime(), originalTimestamp.mtime, 'different mtime values')
91+
assert.strictEqual(currentStats.atime.getTime(), originalTimestamp.atime, 'different atime values')
92+
}
93+
}
94+
})

‎lib/move/move-sync.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ function rename (src, dest, overwrite) {
4545
function moveAcrossDevice (src, dest, overwrite) {
4646
const opts = {
4747
overwrite,
48-
errorOnExist: true
48+
errorOnExist: true,
49+
preserveTimestamps: true
4950
}
5051
copySync(src, dest, opts)
5152
return removeSync(src)

‎lib/move/move.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ function rename (src, dest, overwrite, cb) {
6464
function moveAcrossDevice (src, dest, overwrite, cb) {
6565
const opts = {
6666
overwrite,
67-
errorOnExist: true
67+
errorOnExist: true,
68+
preserveTimestamps: true
6869
}
6970
copy(src, dest, opts, err => {
7071
if (err) return cb(err)

0 commit comments

Comments
 (0)
Please sign in to comment.