UNPKG

node-smb-server

Version:

A Pure JavaScript SMB Server Implementation

429 lines (389 loc) 12.8 kB
/* * Copyright 2015 Adobe Systems Incorporated. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ 'use strict'; var util = require('util'); var fs = require('fs'); var Path = require('path'); var logger = require('winston').loggers.get('spi'); var perflog = require('winston').loggers.get('perf'); var async = require('async'); var File = require('../../spi/file'); var SMBError = require('../../smberror'); /** * Creates an instance of File. * * @constructor * @private * @this {FSFile} * @param {String} filePath normalized file path * @param {fs.Stats} stats fs.Stats object * @param {FSTree} tree tree object */ var FSFile = function (filePath, stats, tree) { logger.debug('[fs] file.open %s', filePath); if (!(this instanceof FSFile)) { return new FSFile(filePath, stats, tree); } this.stats = stats; this.realPath = Path.join(tree.share.path, filePath); // extract file permissions from stats.mode, convert to octagonal, check if owner write permission bit is set (00200) // see http://stackoverflow.com/questions/11775884/nodejs-file-permissions this.writeable = !!(2 & parseInt((stats.mode & parseInt('777', 8)).toString(8)[0])); File.call(this, filePath, tree); }; // the FSFile prototype inherits from File util.inherits(FSFile, File); /** * Async factory method * * @param {String} filePath normalized file path * @param {FSTree} tree tree object * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) * @param {FSFile} cb.file FSFile instance */ FSFile.createInstance = function (filePath, tree, cb) { var realPath = Path.join(tree.share.path, filePath); fs.stat(realPath, function (err, stats) { if (err) { cb(SMBError.fromSystemError(err, 'unable to create file due to unexpected error ' + filePath)); } else { cb(null, new FSFile(filePath, stats, tree)); } }); }; /** * Refreshes the stats information of the underlying file. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.refreshStats = function (cb) { var self = this; // update stats perflog.debug('%s File.refreshStats.fs.stat', this.filePath); // todo use fs.fstat if there's an open file descriptor? fs.stat(this.realPath, function (ignored, stats) { if (!ignored) { self.stats = stats; } else { logger.warn('[fs] file.refreshStats %s failed', self.filePath, ignored); } cb(); }); }; /** * Sets the read-only value of the file if needed. * * @param {Boolean} readOnly If TRUE, file will be read only; otherwise, file will be writable. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.setReadOnly = function (readOnly, cb) { var self = this; if (self.isReadOnly() != readOnly) { logger.debug('[fs] setReadOnly %s %s', readOnly, self.filePath); var self = this; fs.chmod(self.realPath, readOnly ? '444' : '644', function (err) { if (err) { cb(SMBError.fromSystemError(err, 'unable to set read only status due to unepxected error ' + self.filePath)); } else { self.writeable = !readOnly; cb(); } }); } else { cb(); } }; FSFile.prototype.getDescriptor = function (cb) { logger.debug('[fs] getDescriptor %s', this.filePath); var self = this; function openCB(err, fd) { if (err) { cb(SMBError.fromSystemError(err, 'unable to get file descriptor due to unexpeced error ' + self.filePath)); } else { self.fd = fd; cb(null, fd); } } if (this.fd) { cb(null, this.fd); } else { // open read-write fs.open(this.realPath, 'r+', function (err, fd) { if (err && err.code === 'EACCES') { // open read-only logger.debug('[fs] getDescriptor file is read-only', self.filePath); fs.open(self.realPath, 'r', openCB); } else { openCB(err, fd); } }); } }; //---------------------------------------------------------------------< File > /** * Return a flag indicating whether this is a file. * * @return {Boolean} <code>true</code> if this is a file; * <code>false</code> otherwise */ FSFile.prototype.isFile = function () { return this.stats.isFile(); }; /** * Return a flag indicating whether this is a directory. * * @return {Boolean} <code>true</code> if this is a directory; * <code>false</code> otherwise */ FSFile.prototype.isDirectory = function () { return this.stats.isDirectory(); }; /** * Return a flag indicating whether this file is read-only. * * @return {Boolean} <code>true</code> if this file is read-only; * <code>false</code> otherwise */ FSFile.prototype.isReadOnly = function () { return !this.writeable; }; /** * Return the file size. * * @return {Number} file size, in bytes */ FSFile.prototype.size = function () { logger.debug('[fs] size %s (%d bytes)', this.filePath, this.stats.size); return this.stats.size; }; /** * Return the number of bytes that are allocated to the file. * * @return {Number} allocation size, in bytes */ FSFile.prototype.allocationSize = function () { var size = this.stats.blocks * this.stats.blksize; logger.debug('[fs] allocationSize %s (%d bytes)', this.filePath, size); return size; }; /** * Return the time of last modification, in milliseconds since * Jan 1, 1970, 00:00:00.0. * * @return {Number} time of last modification */ FSFile.prototype.lastModified = function () { return this.stats.mtime.getTime(); }; /** * Sets the time of last modification, in milliseconds since * Jan 1, 1970, 00:00:00.0. * * @param {Number} ms * @return {Number} time of last modification */ FSFile.prototype.setLastModified = function (ms) { // cheatin' ... this.stats.mtime = new Date(ms); }; /** * Return the time when file status was last changed, in milliseconds since * Jan 1, 1970, 00:00:00.0. * * @return {Number} when file status was last changed */ FSFile.prototype.lastChanged = function () { return this.stats.ctime.getTime(); }; /** * Return the create time, in milliseconds since Jan 1, 1970, 00:00:00.0. * Jan 1, 1970, 00:00:00.0. * * @return {Number} time created */ FSFile.prototype.created = function () { if (this.stats.birthtime) { // node >= v0.12 return this.stats.birthtime.getTime(); } else { return this.stats.ctime.getTime(); } }; /** * Return the time of last access, in milliseconds since Jan 1, 1970, 00:00:00.0. * Jan 1, 1970, 00:00:00.0. * * @return {Number} time of last access */ FSFile.prototype.lastAccessed = function () { return this.stats.atime.getTime(); }; /** * Read bytes at a certain position inside the file. * * @param {Buffer} buffer the buffer that the data will be written to * @param {Number} offset the offset in the buffer to start writing at * @param {Number} length the number of bytes to read * @param {Number} position offset where to begin reading from in the file * @param {Function} cb callback called with the bytes actually read * @param {SMBError} cb.error error (non-null if an error occurred) * @param {Number} cb.bytesRead number of bytes actually read * @param {Buffer} cb.buffer buffer holding the bytes actually read */ FSFile.prototype.read = function (buffer, offset, length, position, cb) { logger.debug('[fs] file.read %s offset=%d, length=%d, position=%d', this.filePath, offset, length, position); var self = this; async.waterfall([ function (done) { self.getDescriptor(done); }, function (fd, done) { perflog.debug('%s File.read.fs.read %d', self.filePath, length); fs.read(fd, buffer, offset, length, position, SMBError.systemToSMBErrorTranslator(done, 'unable to read file due to unexpected error ' + self.filePath)); } ], cb ); }; /** * Write bytes at a certain position inside the file. * * @param {Buffer} data buffer to write * @param {Number} position position inside file * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.write = function (data, position, cb) { logger.debug('[fs] file.write %s data.length=%d, position=%d', this.filePath, data.length, position); var self = this; async.waterfall([ function (done) { self.getDescriptor(done); }, function (fd, done) { perflog.debug('%s File.write.fs.write %d', self.getPath(), data.length); fs.write(fd, data, 0, data.length, position, SMBError.systemToSMBErrorTranslator(done, 'unable to write file due to unexpected error ' + self.filePath)); } ], cb ); }; /** * Sets the file length. * * @param {Number} length file length * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.setLength = function (length, cb) { logger.debug('[fs] file.setLength %s length=%d', this.filePath, length); var self = this; async.series([ function (done) { // first close the file if needed self.close(done); }, function (done) { // truncate underlying file perflog.debug('%s File.setLength.fs.truncate %d', self.getPath(), length); fs.truncate(self.realPath, length, SMBError.systemToSMBErrorTranslator(done, 'unable to set file length due to unexpected error ' + self.filePath)); }, function (done) { // update stats self.refreshStats(done); } ], cb ); }; /** * Delete this file or directory. If this file denotes a directory, it must * be empty in order to be deleted. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.delete = function (cb) { logger.debug('[fs] file.delete %s', this.filePath); var self = this; async.series([ function (done) { // first close the file if needed self.close(done); }, function (done) { // delete underlying file/directory if (self.isDirectory()) { perflog.debug('%s File.delete.fs.rmdir', self.getPath()); fs.rmdir(self.realPath, SMBError.systemToSMBErrorTranslator(done, 'unable to delete directory due to unexpected error ' + self.filePath)); } else { perflog.debug('%s File.delete.fs.unlink', self.getPath()); fs.unlink(self.realPath, SMBError.systemToSMBErrorTranslator(done, 'unable to delete file due to unexpected error ' + self.filePath)); } } ], cb ); }; /** * Flush the contents of the file to disk. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.flush = function (cb) { logger.debug('[fs] file.flush %s', this.filePath); var self = this; if (this.fd) { async.series([ function (done) { // flush modified file buffers to disk perflog.debug('%s File.flush.fs.fsync', self.getPath()); fs.fsync(self.fd, SMBError.systemToSMBErrorTranslator(done, 'unable to flush file due to unexpected error ' + self.filePath)); }, function (done) { // update stats self.refreshStats(done); } ], cb ); } else { cb(); } }; /** * Close this file, releasing any resources. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ FSFile.prototype.close = function (cb) { logger.debug('[fs] file.close %s', this.filePath); var callback = SMBError.systemToSMBErrorTranslator(cb, 'unable to close file due to unexpected error ' + this.filePath); var self = this; // close file descriptor if needed if (self.fd) { perflog.debug('%s File.close.fs.close', self.getPath()); fs.close(self.fd, function (err) { self.fd = undefined; callback(err); }); } else { // nothing to do callback(); } }; module.exports = FSFile;