Skip to content

Commit 4cd47bb

Browse files
committed
Preserve chmod and chown from overwritten file
1 parent e6d00dc commit 4cd47bb

File tree

6 files changed

+122
-9
lines changed

6 files changed

+122
-9
lines changed

.travis.yml

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
language: node_js
2-
sudo: false
2+
sudo: true
33
before_install:
44
- "npm -g install npm"
5+
script:
6+
- "sudo $(which npm) test"
7+
os:
8+
- osx
9+
- linux
510
node_js:
611
- "0.8"
712
- "0.10"
813
- "0.12"
914
- "iojs"
1015
- "4"
1116
- "5"
17+
- "6"

README.md

100644100755
File mode changed.

index.js

+31-7
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,37 @@ module.exports = function writeFile (filename, data, options, callback) {
2020
}
2121
if (!options) options = {}
2222
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()
23+
24+
function computeOptions (filename, cb) {
25+
if (options.mode && options.chmod) {
26+
// Fast-track, no need for fetching stat from previous file
27+
cb(null)
28+
} else {
29+
// Either mode or chown is not explicitly set
30+
// Default behavior is to copy it from original file
31+
fs.stat(filename, function (err, stats) {
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+
cb(null)
39+
})
40+
}
41+
}
42+
43+
// We can't use computeOptions as part of chain because
44+
// "options.chown &&" is evaluated before it has chance to run
45+
computeOptions(filename, function () {
46+
chain([
47+
[fs, fs.writeFile, tmpfile, data, options],
48+
options.chown && [fs, fs.chown, tmpfile, options.chown.uid, options.chown.gid],
49+
[fs, fs.rename, tmpfile, filename]
50+
], function (err) {
51+
err ? fs.unlink(tmpfile, function () { callback(err) })
52+
: callback()
53+
})
3054
})
3155
}
3256

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

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var writeFileAtomic = requireInject('../index', {
1919
if (/nounlink/.test(tmpfile)) return cb(new Error('ENOUNLINK'))
2020
cb()
2121
},
22+
stat: function (tmpfile, cb) {
23+
if (/nostat/.test(tmpfile)) return cb(new Error('ENOSTAT'))
24+
cb()
25+
},
2226
writeFileSync: function (tmpfile, data, options) {
2327
if (/nowrite/.test(tmpfile)) throw new Error('ENOWRITE')
2428
},
@@ -30,6 +34,9 @@ var writeFileAtomic = requireInject('../index', {
3034
},
3135
unlinkSync: function (tmpfile) {
3236
if (/nounlink/.test(tmpfile)) throw new Error('ENOUNLINK')
37+
},
38+
statSync: function (tmpfile) {
39+
if (/nostat/.test(tmpfile)) throw new Error('ENOSTAT')
3340
}
3441
}
3542
})

test/integration.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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, { encoding: 'utf-8' })
16+
}
17+
18+
test('writes simple file', 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', 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', 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', 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', 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+
})

0 commit comments

Comments
 (0)