@sharelist/node-smb-server
Version:
Sharelist SMB Server Implementation Base On node-smb-server
553 lines (501 loc) • 17.2 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 logger = require('winston').loggers.get('default');
var async = require('async');
var _ = require('lodash');
var path = require('path');
var SMBFile = require('./smbfile');
var common = require('./common');
var utils = require('./utils');
var ntstatus = require('./ntstatus');
// timeout in ms after which a NT_TRANSACT_NOTIFY_CHANGE request will be replied with a dummy change notification.
// after receiving such a change notification the client (i.e. Finder on os-x) will send a TRANS2_FIND_FIRST2 cmd to refresh.
var CHANGE_LISTENER_TIMEOUT = 5000; // todo FIXME use configured refresh interval
/**
* Represents a tree connection established by <code>TREE_CONNECT_ANDX</code> or <code>SMB2 TREE_CONNECT</code>
*
* @param {SMBServer} smbServer
* @param {SMBShare} smbShare
* @param {Tree} spiTree
* @constructor
*/
function SMBTree(smbServer, smbShare, spiTree) {
this.smbServer = smbServer;
this.smbShare = smbShare;
this.spiTree = spiTree;
this.tid = ++SMBTree.tidCounter;
this.files = {};
this.listeners = {};
}
SMBTree.tidCounter = 0;
SMBTree.fidCounter = 0;
SMBTree.prototype.getShare = function () {
return this.smbShare;
};
SMBTree.prototype.getFile = function (fid) {
return this.files[fid];
};
SMBTree.prototype.closeFile = function (fid, cb) {
var file = this.files[fid];
if (!file) {
process.nextTick(function () { cb(new Error('no such file')); });
} else {
delete this.files[fid];
file.close(cb);
}
};
/**
* Test whether or not the specified file exists.
*
* @param {String} name file name
* @param {Function} cb callback called with the result
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {Boolean} cb.exists true if the file exists; false otherwise
*/
SMBTree.prototype.exists = function (name, cb) {
this.spiTree.exists(utils.normalizeSMBFileName(name), cb);
};
/**
* Open or create an existing file/directory.
*
* @param {String} name file name
* @param {Number} createDisposition flag specifying action if file does/does not exist
* @param {Boolean} openTargetDirectory true if target for open is a directory
* @param {Function} cb callback called with the opened file
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {SMBFile} cb.file opened file
*/
SMBTree.prototype.openOrCreate = function (name, createDisposition, openTargetDirectory, cb) {
var self = this;
name = utils.normalizeSMBFileName(name);
function create(callback) {
var createFn = openTargetDirectory ? self.createDirectory : self.createFile;
createFn.call(self, name, callback);
}
function open(callback) {
self.spiTree.open(name, function (err, file) {
if (err) {
callback(err);
return;
}
var fid = ++SMBTree.fidCounter;
// todo what's the exact difference between consts.FILE_SUPERSEDE and consts.FILE_OVERWRITE_IF ?
var openAction;
if (createDisposition === common.FILE_OVERWRITE
|| createDisposition === common.FILE_OVERWRITE_IF
|| createDisposition === common.FILE_SUPERSEDE) {
openAction = common.FILE_OVERWRITTEN;
} else {
openAction = common.FILE_OPENED;
}
var result = new SMBFile(file, self, openAction, fid);
self.files[fid] = result;
if (openAction === common.FILE_OVERWRITTEN) {
result.setLength(0, function (err) {
callback(err, result);
});
} else {
callback(null, result);
}
});
}
if (createDisposition === common.FILE_OPEN || createDisposition === common.FILE_OVERWRITE) {
// open existing
open(cb);
} else if (createDisposition === common.FILE_CREATE) {
// create new
create(cb);
} else {
// conditional create/open (consts.FILE_SUPERSEDE, consts.FILE_OPEN_IF, consts.FILE_OVERWRITE_IF)
self.exists(name, function (err, exists) {
if (err) {
cb(err);
return;
}
if (exists) {
open(cb);
} else {
create(cb);
}
});
}
};
/**
* Open an existing file/directory.
*
* @param {String} name file name
* @param {Function} cb callback called with the opened file
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {SMBFile} cb.file opened file
*/
SMBTree.prototype.open = function (name, cb) {
var self = this;
this.spiTree.open(utils.normalizeSMBFileName(name), function (err, file) {
if (err) {
cb(err);
} else {
var fid = ++SMBTree.fidCounter;
var result = new SMBFile(file, self, common.FILE_OPENED, fid);
self.files[fid] = result;
cb(null, result);
}
});
};
/**
* Reopen an existing file/directory using an already assigned fid.
* Special purpose method called when an already open SMBFile instance
* is renamed in order to make sure that the internal state of the
* wrapped File instance is consistent with the new path/name.
*
* @param {String} name file name
* @param {Number} fid file ID
* @param {Function} cb callback called with the opened file
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {SMBFile} cb.file reopened file
*/
SMBTree.prototype.reopen = function (name, fid, cb) {
var self = this;
this.spiTree.open(utils.normalizeSMBFileName(name), function (err, file) {
if (err) {
cb(err);
} else {
var result = new SMBFile(file, self, common.FILE_OPENED, fid);
self.files[fid] = result;
cb(null, result);
}
});
};
/**
* List entries, matching a specified pattern.
*
* @param {String} pattern pattern
* @param {Function} cb callback called with an array of matching files
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {SMBFile[]} cb.files array of matching files
*/
SMBTree.prototype.list = function (pattern, cb) {
var npattern = utils.normalizeSMBFileName(pattern);
var self = this;
this.spiTree.list(npattern, function (err, files) {
if (err) {
cb(err);
} else {
var results = files.map(function (file) {
return new SMBFile(file, self);
});
cb(null, results);
if (utils.getPathName(npattern) === '*') {
// emit event
self.smbServer.emit('folderListed', self.smbShare.getName(), utils.getParentPath(npattern));
}
}
});
};
/**
* Create a new file.
*
* @param {String} name file name
* @param {Function} cb callback called on completion
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {SMBFile} cb.file created file
*/
SMBTree.prototype.createFile = function (name, cb) {
var self = this;
var nname = utils.normalizeSMBFileName(name);
this.spiTree.createFile(nname, function (err, file) {
if (err) {
cb(err);
return;
}
var fid = ++SMBTree.fidCounter;
var result = new SMBFile(file, self, common.FILE_CREATED, fid);
self.files[fid] = result;
cb(null, result);
self.notifyChangeListeners(common.FILE_ACTION_ADDED, nname);
// emit event
self.smbServer.emit('fileCreated', self.smbShare.getName(), nname);
});
};
/**
* Create a new directory.
*
* @param {String} name directory name
* @param {Function} cb callback called on completion
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {SMBFile} cb.file created directory
*/
SMBTree.prototype.createDirectory = function (name, cb) {
var self = this;
var nname = utils.normalizeSMBFileName(name);
this.spiTree.createDirectory(nname, function (err, file) {
if (err) {
cb(err);
return;
}
var fid = ++SMBTree.fidCounter;
var result = new SMBFile(file, self, common.FILE_CREATED, fid);
self.files[fid] = result;
cb(null, result);
self.notifyChangeListeners(common.FILE_ACTION_ADDED, nname);
// emit event
self.smbServer.emit('folderCreated', self.smbShare.getName(), nname);
});
};
/**
* Delete a file.
*
* @param {String} name file name
* @param {Function} cb callback called with the result
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {Boolean} cb.deleted true if the file could be deleted; false otherwise
*/
SMBTree.prototype.delete = function (name, cb) {
var self = this;
var nname = utils.normalizeSMBFileName(name);
this.spiTree.delete(nname, function (err) {
cb(err);
if (!err) {
self.notifyChangeListeners(common.FILE_ACTION_REMOVED, nname);
// emit event
self.smbServer.emit('fileDeleted', self.smbShare.getName(), nname);
}
});
};
/**
* Delete a directory. It must be empty in order to be deleted.
*
* @param {String} name directory name
* @param {Function} cb callback called with the result
* @param {SMBError} cb.error error (non-null if an error occurred)
* @param {Boolean} cb.deleted true if the directory could be deleted; false otherwise
*/
SMBTree.prototype.deleteDirectory = function (name, cb) {
var self = this;
var nname = utils.normalizeSMBFileName(name);
this.spiTree.deleteDirectory(nname, function (err) {
cb(err);
if (!err) {
self.notifyChangeListeners(common.FILE_ACTION_REMOVED, nname);
// emit event
self.smbServer.emit('folderDeleted', self.smbShare.getName(), nname);
}
});
};
/**
* Rename a file or directory.
*
* @param {String|SMBFile} nameOrFile name of target file or target file
* @param {String} newName new name
* @param {Function} cb callback called on completion
* @param {SMBError} cb.error error (non-null if an error occurred)
*/
SMBTree.prototype.rename = function (nameOrFile, newName, cb) {
var self = this;
var targetFID;
var oldName;
if (typeof nameOrFile === 'string') {
oldName = nameOrFile;
} else {
targetFID = nameOrFile.getId();
oldName = nameOrFile.getPath();
}
var nOldName = utils.normalizeSMBFileName(oldName);
var nNewName = utils.normalizeSMBFileName(newName);
// todo check if source has uncommitted changes (i.e. needs flush)
// todo check if source has deleteOnClose set
this.spiTree.rename(nOldName, nNewName, function (err) {
if (err) {
cb(err);
return;
}
if (targetFID) {
self.reopen(nNewName, targetFID, cb);
} else {
cb();
}
self.notifyChangeListeners(common.FILE_ACTION_RENAMED, nOldName, nNewName);
// emit event
self.smbServer.emit('itemMoved', self.smbShare.getName(), nOldName, nOldName);
});
};
/**
* Flush the contents of all open files.
*
* @param {Function} cb callback called on completion
* @param {SMBError} cb.error error (non-null if an error occurred)
*/
SMBTree.prototype.flush = function (cb) {
async.forEachOf(this.files,
function (file, fid, callback) {
file.flush(callback);
},
cb);
};
/**
* Disconnect this tree.
*/
SMBTree.prototype.disconnect = function () {
var self = this;
// cancel any pending change listeners
_.forOwn(this.listeners, function (listener, mid) {
self.cancelChangeListener(mid);
});
// delegate to spi
this.spiTree.disconnect(function (err) {
if (err) {
logger.error('tree disconnect failed:', err);
}
});
};
/**
* Refresh a specific folder.
*
* @param {String} folderPath
* @param {Boolean} deep
* @param {Function} cb callback called on completion
* @param {String|Error} cb.error error (non-null if an error occurred)
*/
SMBTree.prototype.refresh = function (folderPath, deep, cb) {
var self = this;
// give SPI impl a chance to invalidate cache
this.spiTree.refresh(folderPath, deep, function (err) {
if (err) {
cb(err);
} else {
// dummy change notification to force client to refresh
var p = path.join(folderPath, '/'); // append trailing / to folder path (to make sure the proper listener is selected)
self.notifyChangeListeners(common.FILE_ACTION_MODIFIED, p);
cb();
}
});
};
/**
* Register a one-shot notification listener that will send a NT_TRANSACT_NOTIFY_CHANGE response.
*
* see https://msdn.microsoft.com/en-us/library/ee442155.aspx
*
* @param {Number} mid - multiplex id (msg.header.mid, identifies an SMB request within an SMB session)
* @param {SMBFile} file - directory to watch for changes
* @param {Boolean} deep - watch all subdirectories too
* @param {Number} completionFilter - completion filter bit flags
* @param {Function} cb - callback to be called on changes
* @param {Number} cb.action - file action
* @param {String} cb.name - name of file that changed
* @param {String} [cb.newName] - optional, new name if this was a rename
*/
SMBTree.prototype.registerChangeListener = function (mid, file, deep, completionFilter, cb) {
var self = this;
var listener = {
mid: mid,
path: file.getPath(),
deep: deep,
completionFilter: completionFilter,
cb: cb
};
// auto refresh after timeout if no change (via SMB server) occurred within specified period
listener.autoRefreshTimer = setTimeout(
function () {
// dummy change notification to force client to refresh
var p = path.join(listener.path, '/'); // append trailing / to folder path (to make sure the proper listener is selected)
self.notifyChangeListeners(common.FILE_ACTION_MODIFIED, p);
},
CHANGE_LISTENER_TIMEOUT
);
this.listeners[mid] = listener;
};
/**
* Notify the appropriate listener (if there is one) for some change
* and remove it from the collection of registered listeners (one shot notification).
*
* @param {Number} action file action
* @param {String} name name of file that changed
* @param {String} [newName] optional, new name of file in case of a rename
*/
SMBTree.prototype.notifyChangeListeners = function (action, name, newName) {
function trimListenerPath(name, listener) {
return name.substr(listener.path.length + ((listener.path === '/') ? 0 : 1));
}
function getSearchPredicate(path) {
return function (listener, mid) {
// todo evaluate listener.completionFilter WRT action
return (!listener.deep && utils.getParentPath(path) === listener.path)
|| (listener.deep && path.indexOf(listener.path) === 0);
};
}
var listener = _.find(this.listeners, getSearchPredicate(name));
var listenerNew;
if (action === common.FILE_ACTION_RENAMED) {
// rename
listenerNew = _.find(this.listeners, getSearchPredicate(newName));
if (listener || listenerNew) {
if (listener === listenerNew) {
// in-place rename: same listener for both old and new name
listener.cb(action, name.substr(listener.path.length + 1), trimListenerPath(newName, listener));
} else if (listener && listenerNew) {
// there's separate listeners for old and new name
listener.cb(common.FILE_ACTION_RENAMED_OLD_NAME, trimListenerPath(name, listener));
listenerNew.cb(common.FILE_ACTION_RENAMED_NEW_NAME, trimListenerPath(newName, listenerNew));
} else if (listener) {
// there's only a listener for old name
listener.cb(common.FILE_ACTION_RENAMED_OLD_NAME, trimListenerPath(name, listener));
} else {
// there's only a listener for new name
listenerNew.cb(common.FILE_ACTION_RENAMED_NEW_NAME, trimListenerPath(newName, listenerNew));
}
}
} else {
// not a rename
if (listener) {
listener.cb(action, trimListenerPath(name, listener));
}
}
// one shot notification, cancel listeners
if (listener) {
this.cancelChangeListener(listener.mid);
}
if (listenerNew) {
this.cancelChangeListener(listenerNew.mid);
}
};
/**
* Cancel the specified listener.
*
* @param {Number} mid - multiplex id (msg.header.mid, identifies an SMB request within an SMB session)
* @return {Function} cancelled listener callback or null
*/
SMBTree.prototype.cancelChangeListener = function (mid) {
var result = this.listeners[mid];
if (result) {
if (result.autoRefreshTimer) {
// cancel auto refresh timer
clearTimeout(result.autoRefreshTimer);
}
delete this.listeners[mid];
}
return result;
};
/**
* Clears the tree's cache.
* @param {function} cb Will be invoked when the operation is complete.
* @param {string|Error} cb.err Will be truthy if there were errors during the operation.
*/
SMBTree.prototype.clearCache = function (cb) {
var self = this;
if (self.spiTree) {
self.spiTree.clearCache(cb);
} else {
logger.debug('cannot clear cache because spiTree is not an object');
cb();
}
};
module.exports = SMBTree;