Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
iarna committed Sep 10, 2014
0 parents commit 5ef6f37
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*~
DEADJOE
.#*
node_modules
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
write-file-atomic
-----------------

This is an extension for node's `fs.writeFile` that makes its operation
atomic allows you to include uid/gid for the final file as well. It does
this by initially writing to a temporary file (your filename, followed by
".writeFile.atomic"), chowning it to the uid and gid you specified (if you
specified any) and finally renames it to your filename.

### var writeFileAtomic = require('write-file-atomic')<br>writeFileAtomic(filename, data, [options], callback)

* filename **String**
* data **String** | **Buffer**
* options **Object**
* chown **Object**
* uid **Number**
* gid **Number**
* encoding **String** | **Null** default = 'utf8'
* mode **Number** default = 438 (aka 0666 in Octal)
callback **Function**

Atomically and asynchronously writes data to a file, replacing the file if it already
exists. data can be a string or a buffer.

If provided, the **chown** option requires both **uid** and **gid** properties or else
you'll get an error.

The **encoding** option is ignored if **data** is a buffer. It defaults to 'utf8'.

Example:

```javascript
fs.writeFile('message.txt', 'Hello Node', {chown:{uid:100,gid:50}}, function (err) {
if (err) throw err;
console.log('It\'s saved!');
});
```

### var writeFileAtomicSync = require('write-file-atomic').sync<br>writeFileAtomicSync(filename, data, [options])

The synchronous version of **writeFileAtomic**.
45 changes: 45 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict'
var fs = require('fs');
var chain = require('slide').chain;
var crypto = require('crypto');

var md5hex = function () {
var hash = crypto.createHash('md5');
for (var ii=0; ii<arguments.length; ++ii) hash.update(''+arguments[ii])
return hash.digest('hex')
}
var invocations = 0;
var getTmpname = function (filename) {
return filename + "." + md5hex(__filename, process.pid, ++invocations)
}

module.exports = function writeFile(filename, data, options, callback) {
if (options instanceof Function) {
callback = options;
options = null;
}
if (!options) options = {};
var tmpfile = getTmpname(filename);
chain([
[fs, fs.writeFile, tmpfile, data, options],
options.chown && [fs, fs.chown, tmpfile, options.chown.uid, options.chown.gid],
[fs, fs.rename, tmpfile, filename]
], function (err) {
err ? fs.unlink(tmpfile, function () { callback(err) })
: callback()
})
}

module.exports.sync = function writeFileSync(filename, data, options) {
if (!options) options = {};
var tmpfile = getTmpname(filename);
try {
fs.writeFileSync(tmpfile, data, options);
if (options.chown) fs.chownSync(tmpfile, options.chown.uid, options.chown.gid);
fs.renameSync(tmpfile, filename);
}
catch (err) {
try { fs.unlinkSync(tmpfile) } catch(e) {}
throw err;
}
}
30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "write-file-atomic",
"version": "1.0.0",
"description": "Write files in an atomic fashion w/configurable ownership",
"main": "index.js",
"scripts": {
"test": "tap test/*.js"
},
"repository": {
"type": "git",
"url": "git@github.com:iarna/write-file-atomic.git"
},
"keywords": [
"writeFile",
"atomic"
],
"author": "Rebecca Turner <me@re-becca.org> (http://re-becca.org)",
"license": "ISC",
"bugs": {
"url": "https://github.com/iarna/write-file-atomic/issues"
},
"homepage": "https://github.com/iarna/write-file-atomic",
"dependencies": {
"slide": "^1.1.5"
},
"devDependencies": {
"require-inject": "^1.1.0",
"tap": "^0.4.12"
}
}
97 changes: 97 additions & 0 deletions test/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use strict";
var test = require('tap').test;
var requireInject = require('require-inject');
var writeFileAtomic = requireInject('../index', {
fs: {
writeFile: function (tmpfile, data, options, cb) {
if (/nowrite/.test(tmpfile)) return cb('ENOWRITE');
cb();
},
chown: function (tmpfile, uid, gid, cb) {
if (/nochown/.test(tmpfile)) return cb('ENOCHOWN');
cb();
},
rename: function (tmpfile, filename, cb) {
if (/norename/.test(tmpfile)) return cb('ENORENAME');
cb();
},
unlink: function (tmpfile, cb) {
if (/nounlink/.test(tmpfile)) return cb('ENOUNLINK');
cb();
},
writeFileSync: function (tmpfile, data, options) {
if (/nowrite/.test(tmpfile)) throw 'ENOWRITE';
},
chownSync: function (tmpfile, uid, gid) {
if (/nochown/.test(tmpfile)) throw 'ENOCHOWN';
},
renameSync: function (tmpfile, filename) {
if (/norename/.test(tmpfile)) throw 'ENORENAME';
},
unlinkSync: function (tmpfile) {
if (/nounlink/.test(tmpfile)) throw 'ENOUNLINK';
},
}
});
var writeFileAtomicSync = writeFileAtomic.sync;

test('async tests', function (t) {
t.plan(7);
writeFileAtomic('good', 'test', {mode: '0777'}, function (err) {
t.notOk(err, 'No errors occur when passing in options');
});
writeFileAtomic('good', 'test', function (err) {
t.notOk(err, 'No errors occur when NOT passing in options');
});
writeFileAtomic('nowrite', 'test', function (err) {
t.is(err, 'ENOWRITE', 'writeFile failures propagate');
});
writeFileAtomic('nochown', 'test', {chown: {uid:100,gid:100}}, function (err) {
t.is(err, 'ENOCHOWN', 'Chown failures propagate');
});
writeFileAtomic('nochown', 'test', function (err) {
t.notOk(err, 'No attempt to chown when no uid/gid passed in');
});
writeFileAtomic('norename', 'test', function (err) {
t.is(err, 'ENORENAME', 'Rename errors propagate');
});
writeFileAtomic('norename nounlink', 'test', function (err) {
t.is(err, 'ENORENAME', 'Failure to unlink the temp file does not clobber the original error');
});
});

test('sync tests', function (t) {
t.plan(7);
var throws = function (shouldthrow, msg, todo) {
var err;
try { todo() } catch (e) { err = e }
t.is(shouldthrow,err,msg);
}
var noexception = function (msg, todo) {
var err;
try { todo() } catch (e) { err = e }
t.notOk(err,msg);
}

noexception('No errors occur when passing in options',function (){
writeFileAtomicSync('good', 'test', {mode: '0777'});
})
noexception('No errors occur when NOT passing in options',function (){
writeFileAtomicSync('good', 'test');
});
throws('ENOWRITE', 'writeFile failures propagate', function () {
writeFileAtomicSync('nowrite', 'test');
});
throws('ENOCHOWN', 'Chown failures propagate', function () {
writeFileAtomicSync('nochown', 'test', {chown: {uid:100,gid:100}});
});
noexception('No attempt to chown when no uid/gid passed in', function (){
writeFileAtomicSync('nochown', 'test');
});
throws('ENORENAME', 'Rename errors propagate', function (){
writeFileAtomicSync('norename', 'test');
});
throws('ENORENAME', 'Failure to unlink the temp file does not clobber the original error', function (){
writeFileAtomicSync('norename nounlink', 'test');
});
});

0 comments on commit 5ef6f37

Please sign in to comment.