From e58c036c27f460873695d7b7c0a7b00da0bdf962 Mon Sep 17 00:00:00 2001 From: Thomas Shinnick Date: Sun, 11 Sep 2011 15:30:01 -0500 Subject: [PATCH] fs: add positioned file writing feature to fs.WriteStream Patterned on same feature in ReadStream; a small bit of new code added plus two refactorings of previous code; added two test files. Fixes #1645. --- doc/api/fs.markdown | 5 + lib/fs.js | 42 +++-- test/simple/test-file-write-stream2.js | 98 ++++++++++++ test/simple/test-file-write-stream3.js | 206 +++++++++++++++++++++++++ 4 files changed, 335 insertions(+), 16 deletions(-) create mode 100644 test/simple/test-file-write-stream2.js create mode 100644 test/simple/test-file-write-stream3.js diff --git a/doc/api/fs.markdown b/doc/api/fs.markdown index e2722df77da..c5ea6808146 100644 --- a/doc/api/fs.markdown +++ b/doc/api/fs.markdown @@ -496,3 +496,8 @@ Returns a new WriteStream object (See `Writable Stream`). { flags: 'w', encoding: null, mode: 0666 } + +`options` may also include a `start` option to allow writing data at +some position past the beginning of the file. Modifying a file rather +than replacing it may require a `flags` mode of `r+` rather than the +default mode `w`. diff --git a/lib/fs.js b/lib/fs.js index 7a0e6425f1d..5a7f2551ac3 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1112,6 +1112,14 @@ var WriteStream = fs.WriteStream = function(path, options) { this[key] = options[key]; } + if (this.start !== undefined) { + if (this.start < 0) { + throw new Error('start must be >= zero'); + } + + this.pos = this.start; + } + this.busy = false; this._queue = []; @@ -1153,10 +1161,18 @@ WriteStream.prototype.flush = function() { if (method == fs.write) { self.bytesWritten += arguments[1]; - } + if (cb) { + // write callback + cb(null, arguments[1]); + } - // stop flushing after close - if (method === fs.close) { + } else if (method === fs.open) { + // save reference for file pointer + self.fd = arguments[1]; + self.emit('open', self.fd); + + } else if (method === fs.close) { + // stop flushing after close if (cb) { cb(null); } @@ -1164,15 +1180,6 @@ WriteStream.prototype.flush = function() { return; } - // save reference for file pointer - if (method === fs.open) { - self.fd = arguments[1]; - self.emit('open', self.fd); - } else if (cb) { - // write callback - cb(null, arguments[1]); - } - self.flush(); }); @@ -1197,14 +1204,17 @@ WriteStream.prototype.write = function(data) { cb = arguments[arguments.length - 1]; } - if (Buffer.isBuffer(data)) { - this._queue.push([fs.write, data, 0, data.length, null, cb]); - } else { + if (!Buffer.isBuffer(data)) { var encoding = 'utf8'; if (typeof(arguments[1]) == 'string') encoding = arguments[1]; - this._queue.push([fs.write, data, undefined, encoding, cb]); + data = new Buffer('' + data, encoding); } + this._queue.push([fs.write, data, 0, data.length, this.pos, cb]); + + if (this.pos !== undefined) { + this.pos += data.length; + } this.flush(); diff --git a/test/simple/test-file-write-stream2.js b/test/simple/test-file-write-stream2.js new file mode 100644 index 00000000000..58b03db4952 --- /dev/null +++ b/test/simple/test-file-write-stream2.js @@ -0,0 +1,98 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var common = require('../common'); +var assert = require('assert'); + +var path = require('path'), + fs = require('fs'), + util = require('util'); + + +var filepath = path.join(common.tmpDir, 'write.txt'), + file; + +var EXPECTED = '012345678910'; + +var cb_expected = 'write open drain write drain close error ', + cb_occurred = ''; + +var countDrains = 0; + + +process.on('exit', function() { + removeTestFile(); + if ( cb_occurred !== cb_expected) { + console.log(' Test callback events missing or out of order:'); + console.log(' expected: %j', cb_expected); + console.log(' occurred: %j', cb_occurred); + assert.strictEqual(cb_occurred, cb_expected, + 'events missing or out of order: "' + + cb_occurred + '" !== "' + cb_expected + '"'); + } +}); + +function removeTestFile() { + try { + fs.unlinkSync(filepath); + } catch (e) {} +} + + +removeTestFile(); + +file = fs.createWriteStream(filepath); + +file.on('open', function(fd) { + cb_occurred += 'open '; + assert.equal(typeof fd, 'number'); +}); + +file.on('drain', function() { + cb_occurred += 'drain '; + ++countDrains; + if (countDrains === 1) { + assert.equal(fs.readFileSync(filepath), EXPECTED); + file.write(EXPECTED); + cb_occurred += 'write '; + } else if (countDrains == 2) { + assert.equal(fs.readFileSync(filepath), EXPECTED + EXPECTED); + file.end(); + } +}); + +file.on('close', function() { + cb_occurred += 'close '; + assert.strictEqual(file.bytesWritten, EXPECTED.length * 2); + file.write('should not work anymore'); +}); + + +file.on('error', function(err) { + cb_occurred += 'error '; + assert.ok(err.message.indexOf('not writable') >= 0); +}); + + +for (var i = 0; i < 11; i++) { + assert.strictEqual(file.write(i), false); +} +cb_occurred += 'write '; diff --git a/test/simple/test-file-write-stream3.js b/test/simple/test-file-write-stream3.js new file mode 100644 index 00000000000..ac4fe9bc057 --- /dev/null +++ b/test/simple/test-file-write-stream3.js @@ -0,0 +1,206 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var common = require('../common'); +var assert = require('assert'); + +var path = require('path'), + fs = require('fs'), + util = require('util'); + + +var filepath = path.join(common.tmpDir, 'write_pos.txt'); + + +var cb_expected = 'write open close write open close write open close ', + cb_occurred = ''; + +var fileDataInitial = 'abcdefghijklmnopqrstuvwxyz'; + +var fileDataExpected_1 = 'abcdefghijklmnopqrstuvwxyz'; +var fileDataExpected_2 = 'abcdefghij123456qrstuvwxyz'; +var fileDataExpected_3 = 'abcdefghij\u2026\u2026qrstuvwxyz'; + + +process.on('exit', function() { + removeTestFile(); + if ( cb_occurred !== cb_expected) { + console.log(' Test callback events missing or out of order:'); + console.log(' expected: %j', cb_expected); + console.log(' occurred: %j', cb_occurred); + assert.strictEqual(cb_occurred, cb_expected, + 'events missing or out of order: "' + + cb_occurred + '" !== "' + cb_expected + '"'); + } +}); + +function removeTestFile() { + try { + fs.unlinkSync(filepath); + } catch (ex) { } +} + + +removeTestFile(); + + +function run_test_1() { + var file, buffer, options; + + options = {}; + file = fs.createWriteStream(filepath, options); + console.log(' (debug: start ', file.start); + console.log(' (debug: pos ', file.pos); + + file.on('open', function(fd) { + cb_occurred += 'open '; + }); + + file.on('close', function() { + cb_occurred += 'close '; + console.log(' (debug: bytesWritten ', file.bytesWritten); + console.log(' (debug: start ', file.start); + console.log(' (debug: pos ', file.pos); + assert.strictEqual(file.bytesWritten, buffer.length); + var fileData = fs.readFileSync(filepath, 'utf8'); + console.log(' (debug: file data ', fileData); + console.log(' (debug: expected ', fileDataExpected_1); + assert.equal(fileData, fileDataExpected_1); + + run_test_2(); + }); + + file.on('error', function(err) { + cb_occurred += 'error '; + console.log(' (debug: err event ', err); + throw err; + }); + + buffer = new Buffer(fileDataInitial); + file.write(buffer); + cb_occurred += 'write '; + + file.end(); +} + + +function run_test_2() { + var file, buffer, options; + + buffer = new Buffer('123456'); + + options = { start: 10, + flags: 'r+' }; + file = fs.createWriteStream(filepath, options); + console.log(' (debug: start ', file.start); + console.log(' (debug: pos ', file.pos); + + file.on('open', function(fd) { + cb_occurred += 'open '; + }); + + file.on('close', function() { + cb_occurred += 'close '; + console.log(' (debug: bytesWritten ', file.bytesWritten); + console.log(' (debug: start ', file.start); + console.log(' (debug: pos ', file.pos); + assert.strictEqual(file.bytesWritten, buffer.length); + var fileData = fs.readFileSync(filepath, 'utf8'); + console.log(' (debug: file data ', fileData); + console.log(' (debug: expected ', fileDataExpected_2); + assert.equal(fileData, fileDataExpected_2); + + run_test_3(); + }); + + file.on('error', function(err) { + cb_occurred += 'error '; + console.log(' (debug: err event ', err); + throw err; + }); + + file.write(buffer); + cb_occurred += 'write '; + + file.end(); +} + + +function run_test_3() { + var file, buffer, options; + + var data = '\u2026\u2026', // 3 bytes * 2 = 6 bytes in UTF-8 + fileData; + + options = { start: 10, + flags: 'r+' }; + file = fs.createWriteStream(filepath, options); + console.log(' (debug: start ', file.start); + console.log(' (debug: pos ', file.pos); + + file.on('open', function(fd) { + cb_occurred += 'open '; + }); + + file.on('close', function() { + cb_occurred += 'close '; + console.log(' (debug: bytesWritten ', file.bytesWritten); + console.log(' (debug: start ', file.start); + console.log(' (debug: pos ', file.pos); + assert.strictEqual(file.bytesWritten, data.length * 3); + fileData = fs.readFileSync(filepath, 'utf8'); + console.log(' (debug: file data ', fileData); + console.log(' (debug: expected ', fileDataExpected_3); + assert.equal(fileData, fileDataExpected_3); + + run_test_4(); + }); + + file.on('error', function(err) { + cb_occurred += 'error '; + console.log(' (debug: err event ', err); + throw err; + }); + + file.write(data, 'utf8'); + cb_occurred += 'write '; + + file.end(); +} + + +function run_test_4() { + var file, options; + + options = { start: -5, + flags: 'r+' }; + + // Error: start must be >= zero + assert.throws( + function() { + file = fs.createWriteStream(filepath, options); + }, + /start must be/ + ); + +} + +run_test_1();