Skip to content

Commit 5ef6f37

Browse files
committed
Initial commit
0 parents  commit 5ef6f37

File tree

5 files changed

+217
-0
lines changed

5 files changed

+217
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*~
2+
DEADJOE
3+
.#*
4+
node_modules

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
write-file-atomic
2+
-----------------
3+
4+
This is an extension for node's `fs.writeFile` that makes its operation
5+
atomic allows you to include uid/gid for the final file as well. It does
6+
this by initially writing to a temporary file (your filename, followed by
7+
".writeFile.atomic"), chowning it to the uid and gid you specified (if you
8+
specified any) and finally renames it to your filename.
9+
10+
### var writeFileAtomic = require('write-file-atomic')<br>writeFileAtomic(filename, data, [options], callback)
11+
12+
* filename **String**
13+
* data **String** | **Buffer**
14+
* options **Object**
15+
* chown **Object**
16+
* uid **Number**
17+
* gid **Number**
18+
* encoding **String** | **Null** default = 'utf8'
19+
* mode **Number** default = 438 (aka 0666 in Octal)
20+
callback **Function**
21+
22+
Atomically and asynchronously writes data to a file, replacing the file if it already
23+
exists. data can be a string or a buffer.
24+
25+
If provided, the **chown** option requires both **uid** and **gid** properties or else
26+
you'll get an error.
27+
28+
The **encoding** option is ignored if **data** is a buffer. It defaults to 'utf8'.
29+
30+
Example:
31+
32+
```javascript
33+
fs.writeFile('message.txt', 'Hello Node', {chown:{uid:100,gid:50}}, function (err) {
34+
if (err) throw err;
35+
console.log('It\'s saved!');
36+
});
37+
```
38+
39+
### var writeFileAtomicSync = require('write-file-atomic').sync<br>writeFileAtomicSync(filename, data, [options])
40+
41+
The synchronous version of **writeFileAtomic**.

index.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict'
2+
var fs = require('fs');
3+
var chain = require('slide').chain;
4+
var crypto = require('crypto');
5+
6+
var md5hex = function () {
7+
var hash = crypto.createHash('md5');
8+
for (var ii=0; ii<arguments.length; ++ii) hash.update(''+arguments[ii])
9+
return hash.digest('hex')
10+
}
11+
var invocations = 0;
12+
var getTmpname = function (filename) {
13+
return filename + "." + md5hex(__filename, process.pid, ++invocations)
14+
}
15+
16+
module.exports = function writeFile(filename, data, options, callback) {
17+
if (options instanceof Function) {
18+
callback = options;
19+
options = null;
20+
}
21+
if (!options) options = {};
22+
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+
})
31+
}
32+
33+
module.exports.sync = function writeFileSync(filename, data, options) {
34+
if (!options) options = {};
35+
var tmpfile = getTmpname(filename);
36+
try {
37+
fs.writeFileSync(tmpfile, data, options);
38+
if (options.chown) fs.chownSync(tmpfile, options.chown.uid, options.chown.gid);
39+
fs.renameSync(tmpfile, filename);
40+
}
41+
catch (err) {
42+
try { fs.unlinkSync(tmpfile) } catch(e) {}
43+
throw err;
44+
}
45+
}

package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "write-file-atomic",
3+
"version": "1.0.0",
4+
"description": "Write files in an atomic fashion w/configurable ownership",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "tap test/*.js"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "[email protected]:iarna/write-file-atomic.git"
12+
},
13+
"keywords": [
14+
"writeFile",
15+
"atomic"
16+
],
17+
"author": "Rebecca Turner <[email protected]> (http://re-becca.org)",
18+
"license": "ISC",
19+
"bugs": {
20+
"url": "https://github.com/iarna/write-file-atomic/issues"
21+
},
22+
"homepage": "https://github.com/iarna/write-file-atomic",
23+
"dependencies": {
24+
"slide": "^1.1.5"
25+
},
26+
"devDependencies": {
27+
"require-inject": "^1.1.0",
28+
"tap": "^0.4.12"
29+
}
30+
}

test/basic.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"use strict";
2+
var test = require('tap').test;
3+
var requireInject = require('require-inject');
4+
var writeFileAtomic = requireInject('../index', {
5+
fs: {
6+
writeFile: function (tmpfile, data, options, cb) {
7+
if (/nowrite/.test(tmpfile)) return cb('ENOWRITE');
8+
cb();
9+
},
10+
chown: function (tmpfile, uid, gid, cb) {
11+
if (/nochown/.test(tmpfile)) return cb('ENOCHOWN');
12+
cb();
13+
},
14+
rename: function (tmpfile, filename, cb) {
15+
if (/norename/.test(tmpfile)) return cb('ENORENAME');
16+
cb();
17+
},
18+
unlink: function (tmpfile, cb) {
19+
if (/nounlink/.test(tmpfile)) return cb('ENOUNLINK');
20+
cb();
21+
},
22+
writeFileSync: function (tmpfile, data, options) {
23+
if (/nowrite/.test(tmpfile)) throw 'ENOWRITE';
24+
},
25+
chownSync: function (tmpfile, uid, gid) {
26+
if (/nochown/.test(tmpfile)) throw 'ENOCHOWN';
27+
},
28+
renameSync: function (tmpfile, filename) {
29+
if (/norename/.test(tmpfile)) throw 'ENORENAME';
30+
},
31+
unlinkSync: function (tmpfile) {
32+
if (/nounlink/.test(tmpfile)) throw 'ENOUNLINK';
33+
},
34+
}
35+
});
36+
var writeFileAtomicSync = writeFileAtomic.sync;
37+
38+
test('async tests', function (t) {
39+
t.plan(7);
40+
writeFileAtomic('good', 'test', {mode: '0777'}, function (err) {
41+
t.notOk(err, 'No errors occur when passing in options');
42+
});
43+
writeFileAtomic('good', 'test', function (err) {
44+
t.notOk(err, 'No errors occur when NOT passing in options');
45+
});
46+
writeFileAtomic('nowrite', 'test', function (err) {
47+
t.is(err, 'ENOWRITE', 'writeFile failures propagate');
48+
});
49+
writeFileAtomic('nochown', 'test', {chown: {uid:100,gid:100}}, function (err) {
50+
t.is(err, 'ENOCHOWN', 'Chown failures propagate');
51+
});
52+
writeFileAtomic('nochown', 'test', function (err) {
53+
t.notOk(err, 'No attempt to chown when no uid/gid passed in');
54+
});
55+
writeFileAtomic('norename', 'test', function (err) {
56+
t.is(err, 'ENORENAME', 'Rename errors propagate');
57+
});
58+
writeFileAtomic('norename nounlink', 'test', function (err) {
59+
t.is(err, 'ENORENAME', 'Failure to unlink the temp file does not clobber the original error');
60+
});
61+
});
62+
63+
test('sync tests', function (t) {
64+
t.plan(7);
65+
var throws = function (shouldthrow, msg, todo) {
66+
var err;
67+
try { todo() } catch (e) { err = e }
68+
t.is(shouldthrow,err,msg);
69+
}
70+
var noexception = function (msg, todo) {
71+
var err;
72+
try { todo() } catch (e) { err = e }
73+
t.notOk(err,msg);
74+
}
75+
76+
noexception('No errors occur when passing in options',function (){
77+
writeFileAtomicSync('good', 'test', {mode: '0777'});
78+
})
79+
noexception('No errors occur when NOT passing in options',function (){
80+
writeFileAtomicSync('good', 'test');
81+
});
82+
throws('ENOWRITE', 'writeFile failures propagate', function () {
83+
writeFileAtomicSync('nowrite', 'test');
84+
});
85+
throws('ENOCHOWN', 'Chown failures propagate', function () {
86+
writeFileAtomicSync('nochown', 'test', {chown: {uid:100,gid:100}});
87+
});
88+
noexception('No attempt to chown when no uid/gid passed in', function (){
89+
writeFileAtomicSync('nochown', 'test');
90+
});
91+
throws('ENORENAME', 'Rename errors propagate', function (){
92+
writeFileAtomicSync('norename', 'test');
93+
});
94+
throws('ENORENAME', 'Failure to unlink the temp file does not clobber the original error', function (){
95+
writeFileAtomicSync('norename nounlink', 'test');
96+
});
97+
});

0 commit comments

Comments
 (0)