Skip to content

Commit 36f5104

Browse files
sheeruniarna
authored andcommitted
Preserve chmod and chown from overwritten file
1 parent e6d00dc commit 36f5104

File tree

5 files changed

+198
-15
lines changed

5 files changed

+198
-15
lines changed

.travis.yml

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
language: node_js
2-
sudo: false
2+
sudo: true
33
before_install:
44
- "npm -g install npm"
5+
script:
6+
- "sudo $(which node) $(which npm) test"
7+
os:
8+
- osx
9+
- linux
10+
matrix:
11+
fast_finish: true
12+
allow_failures:
13+
- os: osx
514
node_js:
6-
- "0.8"
7-
- "0.10"
8-
- "0.12"
9-
- "iojs"
1015
- "4"
16+
- "6"
17+
- "0.10"
1118
- "5"
19+
- "0.12"

index.js

+51-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
var fs = require('graceful-fs')
33
var chain = require('slide').chain
44
var MurmurHash3 = require('imurmurhash')
5+
var extend = Object.assign || require('util')._extend
56

67
function murmurhex () {
78
var hash = new MurmurHash3()
@@ -20,22 +21,63 @@ module.exports = function writeFile (filename, data, options, callback) {
2021
}
2122
if (!options) options = {}
2223
var tmpfile = getTmpname(filename)
23-
chain([
24-
[fs, fs.writeFile, tmpfile, data, options],
25-
options.chown && [fs, fs.chown, tmpfile, options.chown.uid, options.chown.gid],
26-
[fs, fs.rename, tmpfile, filename]
27-
], function (err) {
28-
err ? fs.unlink(tmpfile, function () { callback(err) })
29-
: callback()
30-
})
24+
25+
if (options.mode && options.chmod) {
26+
return thenWriteFile()
27+
} else {
28+
// Either mode or chown is not explicitly set
29+
// Default behavior is to copy it from original file
30+
return fs.stat(filename, function (err, stats) {
31+
options = extend({}, options)
32+
if (!err && stats && !options.mode) {
33+
options.mode = stats.mode
34+
}
35+
if (!err && stats && !options.chown && process.getuid) {
36+
options.chown = { uid: stats.uid, gid: stats.gid }
37+
}
38+
return thenWriteFile()
39+
})
40+
}
41+
42+
function thenWriteFile () {
43+
chain([
44+
[fs, fs.writeFile, tmpfile, data, options.encoding || 'utf8'],
45+
options.mode && [fs, fs.chmod, tmpfile, options.mode],
46+
options.chown && [fs, fs.chown, tmpfile, options.chown.uid, options.chown.gid],
47+
[fs, fs.rename, tmpfile, filename]
48+
], function (err) {
49+
err ? fs.unlink(tmpfile, function () { callback(err) })
50+
: callback()
51+
})
52+
}
3153
}
3254

3355
module.exports.sync = function writeFileSync (filename, data, options) {
3456
if (!options) options = {}
3557
var tmpfile = getTmpname(filename)
58+
3659
try {
37-
fs.writeFileSync(tmpfile, data, options)
60+
if (!options.mode || !options.chmod) {
61+
// Either mode or chown is not explicitly set
62+
// Default behavior is to copy it from original file
63+
try {
64+
var stats = fs.statSync(filename)
65+
66+
options = extend({}, options)
67+
if (!options.mode) {
68+
options.mode = stats.mode
69+
}
70+
if (!options.chown && process.getuid) {
71+
options.chown = { uid: stats.uid, gid: stats.gid }
72+
}
73+
} catch (ex) {
74+
// ignore stat errors
75+
}
76+
}
77+
78+
fs.writeFileSync(tmpfile, data, options.encoding || 'utf8')
3879
if (options.chown) fs.chownSync(tmpfile, options.chown.uid, options.chown.gid)
80+
if (options.mode) fs.chmodSync(tmpfile, options.mode)
3981
fs.renameSync(tmpfile, filename)
4082
} catch (err) {
4183
try { fs.unlinkSync(tmpfile) } catch (e) {}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"devDependencies": {
2929
"require-inject": "^1.1.0",
3030
"standard": "^5.4.1",
31-
"tap": "^2.3.1"
31+
"tap": "^2.3.1",
32+
"tmp": "0.0.28"
3233
}
3334
}

test/basic.js

+14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ var writeFileAtomic = requireInject('../index', {
1111
if (/nochown/.test(tmpfile)) return cb(new Error('ENOCHOWN'))
1212
cb()
1313
},
14+
chmod: function (tmpfile, mode, cb) {
15+
if (/nochmod/.test(tmpfile)) return cb(new Error('ENOCHMOD'))
16+
cb()
17+
},
1418
rename: function (tmpfile, filename, cb) {
1519
if (/norename/.test(tmpfile)) return cb(new Error('ENORENAME'))
1620
cb()
@@ -19,17 +23,27 @@ var writeFileAtomic = requireInject('../index', {
1923
if (/nounlink/.test(tmpfile)) return cb(new Error('ENOUNLINK'))
2024
cb()
2125
},
26+
stat: function (tmpfile, cb) {
27+
if (/nostat/.test(tmpfile)) return cb(new Error('ENOSTAT'))
28+
cb()
29+
},
2230
writeFileSync: function (tmpfile, data, options) {
2331
if (/nowrite/.test(tmpfile)) throw new Error('ENOWRITE')
2432
},
2533
chownSync: function (tmpfile, uid, gid) {
2634
if (/nochown/.test(tmpfile)) throw new Error('ENOCHOWN')
2735
},
36+
chmodSync: function (tmpfile, mode) {
37+
if (/nochmod/.test(tmpfile)) throw new Error('ENOCHMOD')
38+
},
2839
renameSync: function (tmpfile, filename) {
2940
if (/norename/.test(tmpfile)) throw new Error('ENORENAME')
3041
},
3142
unlinkSync: function (tmpfile) {
3243
if (/nounlink/.test(tmpfile)) throw new Error('ENOUNLINK')
44+
},
45+
statSync: function (tmpfile) {
46+
if (/nostat/.test(tmpfile)) throw new Error('ENOSTAT')
3347
}
3448
}
3549
})

test/integration.js

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
'use strict'
2+
var test = require('tap').test
3+
var writeFileAtomic = require('../index')
4+
var fs = require('fs')
5+
var tmp = require('tmp')
6+
7+
function tmpPath () {
8+
// We need to manually set os temp dir because of:
9+
// https://github.com/npm/npm/issues/4531#issuecomment-226294103
10+
var dir = tmp.dirSync({ dir: '/tmp' }).name
11+
return tmp.tmpNameSync({ dir: dir })
12+
}
13+
14+
function readFile (path) {
15+
return fs.readFileSync(path).toString()
16+
}
17+
18+
test('writes simple file (async)', function (t) {
19+
t.plan(1)
20+
var path = tmpPath()
21+
writeFileAtomic(path, '42', function (err) {
22+
if (err) throw err
23+
t.is(readFile(path), '42')
24+
})
25+
})
26+
27+
test('runs chown on given file (async)', function (t) {
28+
t.plan(2)
29+
var path = tmpPath()
30+
writeFileAtomic(path, '42', { chown: { uid: 42, gid: 43 } }, function (err) {
31+
if (err) throw err
32+
var stat = fs.statSync(path)
33+
t.is(stat.uid, 42)
34+
t.is(stat.gid, 43)
35+
})
36+
})
37+
38+
test('runs chmod on given file (async)', function (t) {
39+
t.plan(1)
40+
var path = tmpPath()
41+
writeFileAtomic(path, '42', { mode: parseInt('741', 8) }, function (err) {
42+
if (err) throw err
43+
var stat = fs.statSync(path)
44+
t.is(stat.mode, parseInt('100741', 8))
45+
})
46+
})
47+
48+
test('does not change chmod by default (async)', function (t) {
49+
t.plan(1)
50+
var path = tmpPath()
51+
writeFileAtomic(path, '42', { mode: parseInt('741', 8) }, function (err) {
52+
if (err) throw err
53+
54+
writeFileAtomic(path, '43', function (err) {
55+
if (err) throw err
56+
var stat = fs.statSync(path)
57+
t.is(stat.mode, parseInt('100741', 8))
58+
})
59+
})
60+
})
61+
62+
test('does not change chown by default (async)', function (t) {
63+
t.plan(2)
64+
var path = tmpPath()
65+
writeFileAtomic(path, '42', { chown: { uid: 42, gid: 43 } }, function (err) {
66+
if (err) throw err
67+
68+
writeFileAtomic(path, '43', function (err) {
69+
if (err) throw err
70+
var stat = fs.statSync(path)
71+
t.is(stat.uid, 42)
72+
t.is(stat.gid, 43)
73+
})
74+
})
75+
})
76+
77+
test('writes simple file (sync)', function (t) {
78+
t.plan(1)
79+
var path = tmpPath()
80+
writeFileAtomic.sync(path, '42')
81+
t.is(readFile(path), '42')
82+
})
83+
84+
test('runs chown on given file (sync)', function (t) {
85+
t.plan(2)
86+
var path = tmpPath()
87+
writeFileAtomic.sync(path, '42', { chown: { uid: 42, gid: 43 } })
88+
var stat = fs.statSync(path)
89+
t.is(stat.uid, 42)
90+
t.is(stat.gid, 43)
91+
})
92+
93+
test('runs chmod on given file (sync)', function (t) {
94+
t.plan(1)
95+
var path = tmpPath()
96+
writeFileAtomic.sync(path, '42', { mode: parseInt('741', 8) })
97+
var stat = fs.statSync(path)
98+
t.is(stat.mode, parseInt('100741', 8))
99+
})
100+
101+
test('does not change chmod by default (sync)', function (t) {
102+
t.plan(1)
103+
var path = tmpPath()
104+
writeFileAtomic.sync(path, '42', { mode: parseInt('741', 8) })
105+
writeFileAtomic.sync(path, '43')
106+
var stat = fs.statSync(path)
107+
t.is(stat.mode, parseInt('100741', 8))
108+
})
109+
110+
test('does not change chown by default (sync)', function (t) {
111+
t.plan(2)
112+
var path = tmpPath()
113+
writeFileAtomic.sync(path, '42', { chown: { uid: 42, gid: 43 } })
114+
writeFileAtomic.sync(path, '43')
115+
var stat = fs.statSync(path)
116+
t.is(stat.uid, 42)
117+
t.is(stat.gid, 43)
118+
})

0 commit comments

Comments
 (0)