diff --git a/.gitignore b/.gitignore index 704aecb..f21aec7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store node_modules test.js +.vscode diff --git a/README.md b/README.md index e1ce82f..d2e6c52 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,15 @@ npm install -S smb2 ### var smb2Client = new SMB2 ( options ) The SMB2 class is the constructor of your SMB2 client. -the parameter ```options``` accepts this list of attributes: +the parameter `options` accepts this list of attributes: -- ```share``` (mandatory): the share you want to access -- ```domain``` (mandatory): the domain of which the user is registred -- ```username``` (mandatory): the username of the user that access the share -- ```password``` (mandatory): the password -- ```port``` (optional): default ```445```, the port of the SMB server -- ```packetConcurrency``` (optional): default ```20```, the number of simulatanous packet when writting / reading data from the share -- ```autoCloseTimeout``` (optional): default ```10000```, the timeout in milliseconds before to close the SMB2 session and the socket, if setted to ```0``` the connection will never be closed unless you do it +- `share` (mandatory): the share you want to access +- `domain` (mandatory): the domain of which the user is registred +- `username` (mandatory): the username of the user that access the share +- `password` (mandatory): the password +- `port` (optional): default `445`, the port of the SMB server +- `packetConcurrency` (optional): default `20`, the number of simulatanous packet when writting / reading data from the share +- `autoCloseTimeout` (optional): default `10000`, the timeout in milliseconds before to close the SMB2 session and the socket, if setted to `0` the connection will never be closed unless you do it Example: ```javascript @@ -55,10 +55,10 @@ smb2Client.readdir('Windows\\System32', function(err, files){ ``` ### smb2Client.readFile ( filename, [options], callback ) -- ```filename``` String -- ```options``` Object - - ```encoding``` String | Null default = null -- ```callback``` Function +- `filename` String +- `options` Object + - `encoding` String | Null default = null +- `callback` Function Asynchronously reads the entire contents of a file. Example: ```javascript @@ -72,11 +72,11 @@ The callback is passed two arguments (err, data), where data is the contents of If no encoding is specified, then the raw buffer is returned. ### smb2Client.writeFile ( filename, data, [options], callback ) -- ```filename``` String -- ```data``` String | Buffer -- ```options``` Object - - ```encoding``` String | Null default = 'utf8' -- ```callback``` Function +- `filename` String +- `data` String | Buffer +- `options` Object + - `encoding` String | Null default = 'utf8' +- `callback` Function Asynchronously writes data to a file, replacing the file if it already exists. data can be a string or a buffer. @@ -139,6 +139,44 @@ smb2Client.rename('path\\to\\my\\file.txt', 'new\\path\\to\\my\\new-file-name.tx }); ``` +### smb2Client.createReadStream ( path, [options] ) +- `path` String +- `options` Object + - `start` Number default = '0' + - `end` Number default = 'Inf' + +Creates a readable stream. +`options` can include `start` and `end` values to read a range of bytes from the file instead of the entire file. Both `start` and `end` are inclusive and start counting at 0. + +```javascript +var smbStream = smb2Client.createReadStream('path\\to\\my\\file.txt'); +smbStream.pipe(fs.createWriteStream('path\\to\\local\\file.txt')); +smbStream.on('end', function () { + console.log('File copied'); +}); +``` + +An example to read the last 10 bytes of a file which is 100 bytes long: + +```javascript +smb2Client.createReadStream('sample.txt', { start: 90, end: 99 }); +``` + +Note that when using readable streams, it might make sense to disable auto closing of the SMB2 session by setting the `autoCloseTimeout`to `0`. +Note that when `start` > file lenght stream fails with error. (`end` still can be greater than file lenght e.g. `Inf`) + +### smb2Client.createWriteStream ( path ) +Creates a writeable stream. +```javascript +var smbStream = smb2Client.createWriteStream('path\\to\\my\\file.txt'); +fs.createReadStream('path\\to\\local\\file.txt').pipe(smbStream); +smbStream.on('finish', function () { + console.log('File copied'); +}); +``` + +Note that when using writeable streams, it might make sense to disable auto closing of the SMB2 session by setting the `autoCloseTimeout`to `0`. + ### smb2Client.close ( ) This function will close the open connection if opened, it will be called automatically after ```autoCloseTimeout``` ms of no SMB2 call on the server. @@ -147,6 +185,8 @@ This function will close the open connection if opened, it will be called automa - [Fabrice Marsaud](https://github.com/marsaud) - [Jay McAliley](https://github.com/jaymcaliley) - [eldrago](https://github.com/eldrago) +- [Friðjón Guðjohnsen](https://github.com/fridjon) +- [Bartłomiej Wierciński](https://github.com/bwiercinski) ## References diff --git a/TODO.md b/TODO.md index 410b87f..4ac5ff1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,12 @@ # TODO: ## New functions -- fs.appendFile(filename, data, [options], callback) -- fs.chmod(path, mode, callback) -- fs.stat(path, callback) -- fs.watchFile(filename, [options], listener) -- fs.unwatchFile(filename, [listener]) -- fs.watch(filename, [options], [listener]) +- `fs.appendFile(filename, data, [options], callback)` +- `fs.chmod(path, mode, callback)` +- `fs.stat(path, callback)` +- `fs.watchFile(filename, [options], listener)` +- `fs.unwatchFile(filename, [listener])` +- `fs.watch(filename, [options], [listener])` ## Implementation on existing functions - support of mode in mkdir diff --git a/lib/api/createreadstream.js b/lib/api/createreadstream.js new file mode 100644 index 0000000..250cf19 --- /dev/null +++ b/lib/api/createreadstream.js @@ -0,0 +1,148 @@ +var RS = require('readable-stream') + , SMB2Forge = require('../tools/smb2-forge') + , SMB2Request = SMB2Forge.request + , bigint = require('../tools/bigint') + ; + +/* +* createReadStream +* ================ +* +* Return a read stream for a file on the share +*/ +module.exports = function(filename, options){ + options = processOptions(options); + + var connection = this + , file + , bufferedChunks = [] + , chunckPushed = 0 + , chunckRequested = 0 + , opened = false + , fileLength = 0 + , offset = new bigint(8).add(options.start) + , stop = false + , nbRemainingPackets = 0 + , maxPacketSize = 0x00010000 + , readable = new RS.Readable({ + read: function(size) { + if (opened) { + readNext(); + } + } + }) + ; + + var close = function () { + stop = true; + if (opened) { + opened = false; + SMB2Request('close', file, connection, function(err){ + if(err){ + readable.emit('error', err); + } + }); + } + }; + + SMB2Request('open', {path:filename}, connection, function(err, openedFile) { + file = openedFile; + opened = true; + if(err) { + readable.emit('error', err); + return close(); + } else { + fileLength = file.EndofFile.readUInt16LE(); + if(options.start >= fileLength) { + readable.emit('error', new Error('start (' + options.start + ') is greater equal than file length (' + fileLength + ')!')); + return close(); + } + fileLength = Math.min(fileLength, options.end + 1); + readNext(); + } + }); + + + function callback(position, offset) { + return function(err, content){ + if(stop) { + return close(); + } + if(err) { + readable.emit('error', err) + return close(); + } else { + bufferedChunks[position] = { loaded: true, content: content }; + if (position === chunckPushed) { + while (bufferedChunks[chunckPushed] && bufferedChunks[chunckPushed].loaded) { + readable.push(bufferedChunks[chunckPushed].content); + delete bufferedChunks[chunckPushed]; + chunckPushed++; + } + } + nbRemainingPackets--; + readNext(); + if (chunckPushed == chunckRequested && nbRemainingPackets == 0) { + return close(); + } + } + } + } + + function readNext() { + while (nbRemainingPackets < connection.packetConcurrency && offset.lt(fileLength)){ + // process packet size + var rest = offset.sub(fileLength).neg(); + var packetSize = rest.gt(maxPacketSize) ? maxPacketSize : rest.toNumber(); + // generate buffer + SMB2Request('read', { + 'FileId':file.FileId + , 'Length':packetSize + , 'Offset':offset.toBuffer() + }, connection, callback(chunckRequested, offset)); + offset = offset.add(packetSize); + chunckRequested++; + nbRemainingPackets++; + } + } + + return readable; +} + +function processOptions(options) { + + // create the options object + if (options === undefined) { + options = {}; + } else if (typeof options === 'string') { + options = { encoding: options }; + } else if (typeof options === 'object') { + // this is a valid options object + } else { + throw new TypeError('"options" argument must be a string or an object'); + } + + // check the start option + if (options.start === undefined) { + options.start = 0; + } else if (typeof options.start !== 'number') { + throw new TypeError('start (' + options.start + ') must be a Number'); + } + + // check the end option + if (options.end === undefined) { + options.end = Infinity; + } else if (typeof options.end !== 'number') { + throw new TypeError('end (' + options.end + ') must be a Number'); + } + + // check logical values for start and end + if (options.start > options.end) { + throw new Error('start (' + options.start + ') must be <= end (' + options.end + ')'); + } else if (options.start < 0) { + throw new Error('start (' + options.start + ') must be >= zero'); + } + + return options; + +} diff --git a/lib/api/createwritestream.js b/lib/api/createwritestream.js new file mode 100644 index 0000000..dd89b40 --- /dev/null +++ b/lib/api/createwritestream.js @@ -0,0 +1,175 @@ +var RS = require('readable-stream') + , SMB2Forge = require('../tools/smb2-forge') + , SMB2Request = SMB2Forge.request + , SMB2Connection = require('../tools/smb2-connection') + , bigint = require('../tools/bigint') + ; + +/* +* createWriteStream +* ========= +* +* create and return a writeStream to a new file on the share +* +*/ + +module.exports = function(filename) { + var connection = this + , file + , currFileLength = new bigint(8, 0) + , offset = new bigint(8, 0) + , maxPacketSize = new bigint(8, 0x00010000 - 0x71) + , nbRemainingPackets = 0 + , chunkOffset = 0 + , writable = new RS.Writable({ + write: write, + writev: writev, + final: final + }) + , pendingError + , stop = false + , created = false + , writePending = false + , incomingWriteBuffer = [] + , outgoingWriteBuffer = [] + , finalCb = null + ; + + function write(chunk, encoding, cb) { + writev([{chunk: chunk, encoding: encoding}], cb); + } + + function writev(chunks, cb) { + incomingWriteBuffer.push({ chunks: chunks, cb: cb}); + writePending = true; + if (pendingError) { + stop = true; + cb(pendingError); + } else { + if (created) { + writeNext(); + } + } + } + + function final(cb) { + finalCb = cb; + writeNext(); + } + + + var createFile = SMB2Connection.requireConnect(function(cb) { + SMB2Request('create', {path:filename}, connection, cb); + }).bind(this); + + createFile(function(err, f) { + if(err) { + if (outgoingWriteBuffer.length > 0) { + outgoingWriteBuffer[0].cb(err); + stop = true; + } else { + pendingError = err; + } + } + else { + created = true; + file = f; + if (writePending) { + writeNext(); + } + } + }); + + function writeNext() { + if (outgoingWriteBuffer.length !== 0) return; + outgoingWriteBuffer = incomingWriteBuffer; + incomingWriteBuffer = []; + if (outgoingWriteBuffer.length !== 0) startOutgoingBufferWrite(); + if (outgoingWriteBuffer.length === 0 && finalCb) { + SMB2Request('close', file, connection, function(err){ + if(err) finalCb(err); + else { + file = null; + finalCb(); + } + }); + } + } + + function getLengthOfChunksInBuffer(buffer) { + return buffer.reduce(function(acc, curr) { + return acc + curr.chunks.reduce(function(acc2, curr2) { + return acc2 + curr2.chunk.length; + },0); + }, 0) + } + + function startOutgoingBufferWrite() { + currFileLength = currFileLength.add(getLengthOfChunksInBuffer(outgoingWriteBuffer)); + SMB2Request('set_info', {FileId:file.FileId, FileInfoClass:'FileEndOfFileInformation', Buffer:currFileLength.toBuffer()}, connection, function(err){ + if(err) { + stop = true; + outgoingWriteBuffer[0].cb(err); + } + else { + chunkOffset = new bigint(8, 0); + continueOutgoingBufferWrite(); + } + }); + } + + function callback(cb) { + return function(err) { + if(stop) return; + if(err) { + cb(err); + stop = true; + } else { + nbRemainingPackets--; + continueOutgoingBufferWrite(); + } + } + } + + function continueOutgoingBufferWrite() { + if (stop || outgoingWriteBuffer.length === 0) return; + var currChunk = outgoingWriteBuffer[0].chunks[0].chunk; + var currChunkLen = new bigint(8, currChunk.length); + var currCb = outgoingWriteBuffer[0].cb; + while(nbRemainingPackets= currChunk.length) { + outgoingWriteBuffer[0].chunks.shift(); + if (outgoingWriteBuffer[0].chunks.length > 0) { + chunkOffset = new bigint(8, 0); + currChunk = outgoingWriteBuffer[0].chunks[0].chunk; + currChunkLen = new bigint(8, currChunk.length); + } else { + outgoingWriteBuffer[0].cb(); + outgoingWriteBuffer.shift(); + if (outgoingWriteBuffer.length > 0) { + chunkOffset = new bigint(8, 0); + currChunk = outgoingWriteBuffer[0].chunks[0].chunk; + currChunkLen = new bigint(8, currChunk.length); + } else { + chunkOffset = new bigint(8, 0); + } + } + } + nbRemainingPackets++; + } + writeNext(); + } + + + return writable; +}; \ No newline at end of file diff --git a/lib/smb2.js b/lib/smb2.js index c2d700b..37d6002 100644 --- a/lib/smb2.js +++ b/lib/smb2.js @@ -99,6 +99,5 @@ proto.readdir = SMB2Connection.requireConnect(require('./api/readdir')); proto.rmdir = SMB2Connection.requireConnect(require('./api/rmdir')); proto.mkdir = SMB2Connection.requireConnect(require('./api/mkdir')); - - - +proto.createReadStream = require('./api/createreadstream'); +proto.createWriteStream = require('./api/createwritestream'); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0101846..8ef0d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,10 +4,43 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, "ntlm": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/ntlm/-/ntlm-0.1.3.tgz", "integrity": "sha1-O4FOvFMKHmzXEtzwz1kBVZMRlcE=" + }, + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" } } } diff --git a/package.json b/package.json index 72d33c8..d209363 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "smb2", "description": "SMB2 Client", "homepage": "https://github.com/bchelli/node-smb2", - "version": "0.2.11", + "version": "0.2.12", "engines": [ "node" ], @@ -17,7 +17,8 @@ "url": "https://github.com/bchelli/node-smb2" }, "dependencies": { - "ntlm": "^0.1.3" + "ntlm": "^0.1.3", + "readable-stream": "^3.1.1" }, "keywords": [ "SMB",