node-smb-server
Version:
A Pure JavaScript SMB Server Implementation
429 lines (389 loc) • 12.8 kB
JavaScript
/*
* 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.
*/
;
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;