diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4543826
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*~
+DEADJOE
+.#*
+node_modules
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8bcca67
--- /dev/null
+++ b/README.md
@@ -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')
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
writeFileAtomicSync(filename, data, [options])
+
+The synchronous version of **writeFileAtomic**.
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..5f103a3
--- /dev/null
+++ b/index.js
@@ -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 (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"
+ }
+}
diff --git a/test/basic.js b/test/basic.js
new file mode 100644
index 0000000..3e0316d
--- /dev/null
+++ b/test/basic.js
@@ -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');
+ });
+});