Skip to content

Commit

Permalink
Vendor fs write stream atomic (parcel-bundler#4685)
Browse files Browse the repository at this point in the history
* Vendor

* Update package.json
  • Loading branch information
mischnic authored Jun 4, 2020
1 parent 2668bee commit 0bc67eb
Show file tree
Hide file tree
Showing 20 changed files with 749 additions and 21 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ packages/*/*/test/integration/**
packages/*/*/test/mochareporters.json
packages/core/integration-tests/test/input/**
packages/core/utils/test/input/**
packages/utils/fs-write-stream-atomic/**
packages/examples

# Generated by the build
Expand Down
18 changes: 9 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
node_modules/
.DS_Store
package-lock.json
coverage
.nyc_output
.cache
.parcel-cache
dist
lib
coverage/
.nyc_output/
.cache/
.parcel-cache/
dist/
lib/
.vscode/
.idea/
*.min.js
# Logs
logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
tmp
parcel-bundle-reports
.verdaccio_storage
parcel-bundle-reports/
.verdaccio_storage/
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"spec": "packages/*/!(parcel-bundler|integration-tests)/test/*.js",
"spec": "packages/*/!(parcel-bundler|integration-tests|fs-write-stream-atomic)/test/*.js",
"require": ["@parcel/babel-register", "@parcel/test-utils/src/mochaSetup.js"],
// TODO: Remove this when https://github.com/nodejs/node/pull/28788 is resolved
"exit": true
Expand Down
2 changes: 1 addition & 1 deletion packages/core/fs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"node": ">= 10.0.0"
},
"dependencies": {
"fs-write-stream-atomic": "github:atlassian-forks/fs-write-stream-atomic#4edf1a95433a9936229c7f768c9ea9bb5884b487",
"@parcel/fs-write-stream-atomic": "^2.0.0-alpha.3.1",
"@parcel/utils": "^2.0.0-alpha.3.1",
"@parcel/watcher": "^2.0.0-alpha.5",
"@parcel/workers": "^2.0.0-alpha.3.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/fs/src/NodeFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import {promisify} from '@parcel/utils';
import {registerSerializableClass} from '@parcel/core';
import fsWriteStreamAtomic from 'fs-write-stream-atomic';
import fsWriteStreamAtomic from '@parcel/fs-write-stream-atomic';
import watcher from '@parcel/watcher';
import packageJSON from '../package.json';

Expand Down
9 changes: 9 additions & 0 deletions packages/utils/fs-write-stream-atomic/.travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: node_js
sudo: false
before_install:
- "npm -g install npm"
node_js:
- "10"
- "8"
- "6"
- "11"
15 changes: 15 additions & 0 deletions packages/utils/fs-write-stream-atomic/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
The ISC License

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
34 changes: 34 additions & 0 deletions packages/utils/fs-write-stream-atomic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# fs-write-stream-atomic

Like `fs.createWriteStream(...)`, but atomic.

Writes to a tmp file and does an atomic `fs.rename` to move it into
place when it's done.

First rule of debugging: **It's always a race condition.**

## USAGE

```javascript
var fsWriteStreamAtomic = require('fs-write-stream-atomic');
// options are optional.
var write = fsWriteStreamAtomic('output.txt', options);
var read = fs.createReadStream('input.txt');
read.pipe(write);

// When the write stream emits a 'finish' or 'close' event,
// you can be sure that it is moved into place, and contains
// all the bytes that were written to it, even if something else
// was writing to `output.txt` at the same time.
```

### `fsWriteStreamAtomic(filename, [options])`

- `filename` {String} The file we want to write to
- `options` {Object}
- `chown` {Object} User and group to set ownership after write
- `uid` {Number}
- `gid` {Number}
- `encoding` {String} default = 'utf8'
- `mode` {Number} default = `0666`
- `flags` {String} default = `'w'`
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

require('worker_threads').parentPort.postMessage(require('../thread-id'));
198 changes: 198 additions & 0 deletions packages/utils/fs-write-stream-atomic/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
var fs = require('graceful-fs');
var Writable = require('readable-stream').Writable;
var util = require('util');
var MurmurHash3 = require('imurmurhash');
var iferr = require('iferr');
var crypto = require('crypto');
var threadId = require('./thread-id');

function murmurhex() {
var hash = MurmurHash3('');
for (var ii = 0; ii < arguments.length; ++ii) {
hash.hash('' + arguments[ii]);
}
return hash.result();
}

var invocations = 0;
function getTmpname(filename) {
return (
filename + '.' + murmurhex(__filename, process.pid, threadId, ++invocations)
);
}

var setImmediate = global.setImmediate || setTimeout;

module.exports = WriteStreamAtomic;

// Requirements:
// 1. Write everything written to the stream to a temp file.
// 2. If there are no errors:
// a. moves the temp file into its final destination
// b. emits `finish` & `closed` ONLY after the file is
// fully flushed and renamed.
// 3. If there's an error, removes the temp file.

util.inherits(WriteStreamAtomic, Writable);
function WriteStreamAtomic(path, options) {
if (!(this instanceof WriteStreamAtomic)) {
return new WriteStreamAtomic(path, options);
}
Writable.call(this, options);

this.__isWin =
options && options.hasOwnProperty('isWin')
? options.isWin
: process.platform === 'win32';

this.__atomicTarget = path;
this.__atomicTmp = getTmpname(path);

this.__atomicChown = options && options.chown;

this.__atomicClosed = false;

this.__atomicStream = fs.WriteStream(this.__atomicTmp, options);

this.__atomicStream.once('open', handleOpen(this));
this.__atomicStream.once('close', handleClose(this));
this.__atomicStream.once('error', handleError(this));
}

// We have to suppress default finish emitting, because ordinarily it
// would happen as soon as `end` is called on us and all of the
// data has been written to our target stream. So we suppress
// finish from being emitted here, and only emit it after our
// target stream is closed and we've moved everything around.
WriteStreamAtomic.prototype.emit = function(event) {
if (event === 'finish') return this.__atomicStream.end();
return Writable.prototype.emit.apply(this, arguments);
};

WriteStreamAtomic.prototype._write = function(buffer, encoding, cb) {
var flushed = this.__atomicStream.write(buffer, encoding);
if (flushed) return cb();
this.__atomicStream.once('drain', cb);
};

function handleOpen(writeStream) {
return function(fd) {
writeStream.emit('open', fd);
};
}

function handleClose(writeStream) {
return function() {
if (writeStream.__atomicClosed) return;
writeStream.__atomicClosed = true;
if (writeStream.__atomicChown) {
var uid = writeStream.__atomicChown.uid;
var gid = writeStream.__atomicChown.gid;
return fs.chown(
writeStream.__atomicTmp,
uid,
gid,
iferr(cleanup, moveIntoPlace),
);
} else {
moveIntoPlace();
}
};

function moveIntoPlace() {
fs.rename(
writeStream.__atomicTmp,
writeStream.__atomicTarget,
iferr(trapWindowsEPERM, end),
);
}

function trapWindowsEPERM(err) {
if (
writeStream.__isWin &&
err.syscall &&
err.syscall === 'rename' &&
err.code &&
err.code === 'EPERM'
) {
checkFileHashes(err);
} else {
cleanup(err);
}
}

function checkFileHashes(eperm) {
var inprocess = 2;
var tmpFileHash = crypto.createHash('sha512');
var targetFileHash = crypto.createHash('sha512');

fs.createReadStream(writeStream.__atomicTmp)
.on('data', function(data, enc) {
tmpFileHash.update(data, enc);
})
.on('error', fileHashError)
.on('end', fileHashComplete);
fs.createReadStream(writeStream.__atomicTarget)
.on('data', function(data, enc) {
targetFileHash.update(data, enc);
})
.on('error', fileHashError)
.on('end', fileHashComplete);

function fileHashError() {
if (inprocess === 0) return;
inprocess = 0;
cleanup(eperm);
}

function fileHashComplete() {
if (inprocess === 0) return;
if (--inprocess) return;
if (tmpFileHash.digest('hex') === targetFileHash.digest('hex')) {
return cleanup();
} else {
return cleanup(eperm);
}
}
}

function cleanup(err) {
fs.unlink(writeStream.__atomicTmp, function() {
if (err) {
writeStream.emit('error', err);
writeStream.emit('close');
} else {
end();
}
});
}

function end() {
// We have to use our parent class directly because we suppress `finish`
// events fired via our own emit method.
Writable.prototype.emit.call(writeStream, 'finish');

// Delay the close to provide the same temporal separation a physical
// file operation would have– that is, the close event is emitted only
// after the async close operation completes.
setImmediate(function() {
writeStream.emit('close');
});
}
}

function handleError(writeStream) {
return function(er) {
cleanupSync();
writeStream.emit('error', er);
writeStream.__atomicClosed = true;
writeStream.emit('close');
};
function cleanupSync() {
try {
fs.unlinkSync(writeStream.__atomicTmp);
} finally {
return;
}
}
}
20 changes: 20 additions & 0 deletions packages/utils/fs-write-stream-atomic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@parcel/fs-write-stream-atomic",
"version": "2.0.0-alpha.3.1",
"description": "Like `fs.createWriteStream(...)`, but atomic.",
"main": "index.js",
"directories": {
"test": "test"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"graceful-fs": "^4.1.2",
"iferr": "^1.0.2",
"imurmurhash": "^0.1.4",
"readable-stream": "1 || 2"
},
"author": "Isaac Z. Schlueter <[email protected]> (http://blog.izs.me/)",
"license": "ISC"
}
Loading

0 comments on commit 0bc67eb

Please sign in to comment.