* WIP on upload support - protocols, modules, etc.
* Ability for KeyEntryView to only show specific/allowed keys * Start moving/adding common methods to MenuModule vs boilerplate code * menuFlags: String|StringArray: flags for menus, e.g. new 'noHistory' flag to prevent appending to history/stack * New download stats/MCI codes * Ability to redirect input stream to [protocols] temporairly
This commit is contained in:
parent
6da7d557f9
commit
0a92eec5e8
|
@ -43,6 +43,10 @@ class ACS {
|
||||||
return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead);
|
return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasFileAreaWrite(area) {
|
||||||
|
return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite);
|
||||||
|
}
|
||||||
|
|
||||||
hasFileAreaDownload(area) {
|
hasFileAreaDownload(area) {
|
||||||
return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload);
|
return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +79,7 @@ ACS.Defaults = {
|
||||||
MessageConfRead : 'GM[users]',
|
MessageConfRead : 'GM[users]',
|
||||||
|
|
||||||
FileAreaRead : 'GM[users]',
|
FileAreaRead : 'GM[users]',
|
||||||
|
FileAreaWrite : 'GM[sysops]',
|
||||||
FileAreaDownload : 'GM[users]',
|
FileAreaDownload : 'GM[users]',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -266,7 +266,7 @@ function display(client, art, options, cb) {
|
||||||
if(!options.disableMciCache && !mciMapFromCache) {
|
if(!options.disableMciCache && !mciMapFromCache) {
|
||||||
// cache our MCI findings...
|
// cache our MCI findings...
|
||||||
client.mciCache[artHash] = mciMap;
|
client.mciCache[artHash] = mciMap;
|
||||||
client.log.trace( { artHash : artHash, mciMap : mciMap }, 'Added MCI map to cache');
|
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache');
|
||||||
}
|
}
|
||||||
|
|
||||||
ansiParser.removeAllListeners(); // :TODO: Necessary???
|
ansiParser.removeAllListeners(); // :TODO: Necessary???
|
||||||
|
@ -290,7 +290,7 @@ function display(client, art, options, cb) {
|
||||||
|
|
||||||
if(mciMap) {
|
if(mciMap) {
|
||||||
mciMapFromCache = true;
|
mciMapFromCache = true;
|
||||||
client.log.trace( { artHash : artHash, mciMap : mciMap }, 'Loaded MCI map from cache');
|
client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache');
|
||||||
} else {
|
} else {
|
||||||
// no cached MCI info
|
// no cached MCI info
|
||||||
mciMap = {};
|
mciMap = {};
|
||||||
|
|
|
@ -289,22 +289,28 @@ function getDefaultConfig() {
|
||||||
external : {
|
external : {
|
||||||
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
|
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
|
||||||
sendArgs : [
|
sendArgs : [
|
||||||
'--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePath}'
|
'--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePaths}'
|
||||||
],
|
],
|
||||||
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
|
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
|
||||||
|
supportsBatch : true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
zmodem8kSexyz : {
|
zmodem8kSexyz : {
|
||||||
name : 'ZModem (SEXYZ)',
|
name : 'ZModem 8k (SEXYZ)',
|
||||||
type : 'external',
|
type : 'external',
|
||||||
external : {
|
external : {
|
||||||
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
|
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
|
||||||
sendCmd : 'sexyz',
|
sendCmd : 'sexyz',
|
||||||
sendArgs : [
|
sendArgs : [
|
||||||
'-telnet', 'sz', '{filePath}'
|
'-telnet', '-8', 'sz', '@{fileListPath}'
|
||||||
|
],
|
||||||
|
recvCmd : 'sexyz',
|
||||||
|
recvArgs : [
|
||||||
|
'-telnet', '-8', 'rz', '{uploadDir}'
|
||||||
],
|
],
|
||||||
escapeTelnet : false, // -telnet option does this for us
|
escapeTelnet : false, // -telnet option does this for us
|
||||||
|
supportsBatch : true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,7 @@ function DoorPartyModule(options) {
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
if(err) {
|
if(err) {
|
||||||
self.client.log.warn( { error : err.toString() }, 'DoorParty error');
|
self.client.log.warn( { error : err.message }, 'DoorParty error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the client is stil here, go to previous
|
// if the client is stil here, go to previous
|
||||||
|
|
|
@ -12,6 +12,14 @@ module.exports = class DownloadQueue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get items() {
|
||||||
|
return this.client.user.downloadQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.client.user.downloadQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
toggle(fileEntry) {
|
toggle(fileEntry) {
|
||||||
if(this.isQueued(fileEntry)) {
|
if(this.isQueued(fileEntry)) {
|
||||||
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
||||||
|
@ -25,10 +33,19 @@ module.exports = class DownloadQueue {
|
||||||
fileId : fileEntry.fileId,
|
fileId : fileEntry.fileId,
|
||||||
areaTag : fileEntry.areaTag,
|
areaTag : fileEntry.areaTag,
|
||||||
fileName : fileEntry.fileName,
|
fileName : fileEntry.fileName,
|
||||||
byteSize : fileEntry.meta.byteSize || 0,
|
path : fileEntry.filePath,
|
||||||
|
byteSize : fileEntry.meta.byte_size || 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeItems(fileIds) {
|
||||||
|
if(!Array.isArray(fileIds)) {
|
||||||
|
fileIds = [ fileIds ];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => ( -1 === fileIds.indexOf(e.fileId) ) );
|
||||||
|
}
|
||||||
|
|
||||||
isQueued(entryOrId) {
|
isQueued(entryOrId) {
|
||||||
if(entryOrId instanceof FileEntry) {
|
if(entryOrId instanceof FileEntry) {
|
||||||
entryOrId = entryOrId.fileId;
|
entryOrId = entryOrId.fileId;
|
||||||
|
|
|
@ -34,7 +34,7 @@ const WellKnownAreaTags = exports.WellKnownAreaTags = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAvailableFileAreas(client, options) {
|
function getAvailableFileAreas(client, options) {
|
||||||
options = options || { includeSystemInternal : false };
|
options = options || { };
|
||||||
|
|
||||||
// perform ACS check per conf & omit system_internal if desired
|
// perform ACS check per conf & omit system_internal if desired
|
||||||
const areasWithTags = _.map(Config.fileBase.areas, (area, areaTag) => Object.assign(area, { areaTag : areaTag } ) );
|
const areasWithTags = _.map(Config.fileBase.areas, (area, areaTag) => Object.assign(area, { areaTag : areaTag } ) );
|
||||||
|
@ -43,6 +43,10 @@ function getAvailableFileAreas(client, options) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(options.writeAcs && !client.acs.FileAreaWrite(area)) {
|
||||||
|
return true; // omit
|
||||||
|
}
|
||||||
|
|
||||||
return !client.acs.hasFileAreaRead(area);
|
return !client.acs.hasFileAreaRead(area);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ class FileAreaWebAccess {
|
||||||
[ hashId ]
|
[ hashId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
delete this.expireTime[hashId];
|
delete this.expireTimers[hashId];
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleExpire(hashId, expireTime) {
|
scheduleExpire(hashId, expireTime) {
|
||||||
|
|
|
@ -159,6 +159,21 @@ module.exports = class FileEntry {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static incrementAndPersistMetaValue(fileId, name, incrementBy, cb) {
|
||||||
|
incrementBy = incrementBy || 1;
|
||||||
|
fileDb.run(
|
||||||
|
`UPDATE file_meta
|
||||||
|
SET meta_value = meta_value + ?
|
||||||
|
WHERE file_id = ? AND meta_name = ?;`,
|
||||||
|
[ incrementBy, fileId, name ],
|
||||||
|
err => {
|
||||||
|
if(cb) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadMeta(cb) {
|
loadMeta(cb) {
|
||||||
fileDb.each(
|
fileDb.each(
|
||||||
`SELECT meta_name, meta_value
|
`SELECT meta_name, meta_value
|
||||||
|
|
|
@ -15,22 +15,30 @@ module.exports = class KeyEntryView extends View {
|
||||||
|
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.eatTabKey = options.eatTabKey || true;
|
this.eatTabKey = options.eatTabKey || true;
|
||||||
this.caseInsensitive = options.caseInsensitive || true;
|
this.caseInsensitive = options.caseInsensitive || true;
|
||||||
|
|
||||||
// :TODO: allow (by default) only supplied keys[] to even draw
|
if(Array.isArray(options.keys)) {
|
||||||
|
if(this.caseInsensitive) {
|
||||||
|
this.keys = options.keys.map( k => k.toUpperCase() );
|
||||||
|
} else {
|
||||||
|
this.keys = options.keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyPress(ch, key) {
|
onKeyPress(ch, key) {
|
||||||
if(ch && isPrintable(ch)) {
|
const drawKey = ch;
|
||||||
this.redraw(); // sets position
|
|
||||||
this.client.term.write(stylizeString(ch, this.textStyle));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ch && this.caseInsensitive) {
|
if(ch && this.caseInsensitive) {
|
||||||
ch = ch.toUpperCase();
|
ch = ch.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) {
|
||||||
|
this.redraw(); // sets position
|
||||||
|
this.client.term.write(stylizeString(ch, this.textStyle));
|
||||||
|
}
|
||||||
|
|
||||||
this.keyEntered = ch || key.name;
|
this.keyEntered = ch || key.name;
|
||||||
|
|
||||||
if(key && 'tab' === key.name && !this.eatTabKey) {
|
if(key && 'tab' === key.name && !this.eatTabKey) {
|
||||||
|
@ -54,6 +62,12 @@ module.exports = class KeyEntryView extends View {
|
||||||
this.caseInsensitive = propValue;
|
this.caseInsensitive = propValue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'keys' :
|
||||||
|
if(Array.isArray(propValue)) {
|
||||||
|
this.keys = propValue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.setPropertyValue(propName, propValue);
|
super.setPropertyValue(propName, propValue);
|
||||||
|
|
|
@ -3,14 +3,12 @@
|
||||||
|
|
||||||
var PluginModule = require('./plugin_module.js').PluginModule;
|
var PluginModule = require('./plugin_module.js').PluginModule;
|
||||||
var theme = require('./theme.js');
|
var theme = require('./theme.js');
|
||||||
var art = require('./art.js');
|
|
||||||
var Log = require('./logger.js').log;
|
|
||||||
var ansi = require('./ansi_term.js');
|
var ansi = require('./ansi_term.js');
|
||||||
var asset = require('./asset.js');
|
|
||||||
var ViewController = require('./view_controller.js').ViewController;
|
var ViewController = require('./view_controller.js').ViewController;
|
||||||
var menuUtil = require('./menu_util.js');
|
var menuUtil = require('./menu_util.js');
|
||||||
var Config = require('./config.js').config;
|
var Config = require('./config.js').config;
|
||||||
|
|
||||||
|
// deps
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
@ -236,6 +234,11 @@ MenuModule.prototype.gotoMenu = function(name, options, cb) {
|
||||||
this.client.menuStack.goto(name, options, cb);
|
this.client.menuStack.goto(name, options, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MenuModule.prototype.popAndGotoMenu = function(name, options, cb) {
|
||||||
|
this.client.menuStack.pop();
|
||||||
|
this.client.menuStack.goto(name, options, cb);
|
||||||
|
};
|
||||||
|
|
||||||
MenuModule.prototype.leave = function() {
|
MenuModule.prototype.leave = function() {
|
||||||
this.detachViewControllers();
|
this.detachViewControllers();
|
||||||
};
|
};
|
||||||
|
@ -322,3 +325,65 @@ MenuModule.prototype.finishedLoading = function() {
|
||||||
MenuModule.prototype.getMenuResult = function() {
|
MenuModule.prototype.getMenuResult = function() {
|
||||||
// nothing in base
|
// nothing in base
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MenuModule.prototype.displayAsset = function(name, options, cb) {
|
||||||
|
|
||||||
|
if(_.isFunction(options)) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.clearScreen) {
|
||||||
|
this.client.term.rawWrite(ansi.clearScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme.displayThemedAsset(
|
||||||
|
name,
|
||||||
|
this.client,
|
||||||
|
Object.merge( { font : this.menuConfig.config }, options ),
|
||||||
|
(err, artData) => {
|
||||||
|
if(cb) {
|
||||||
|
return cb(err, artData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
MenuModule.prototype.prepViewController = function(name, formId, artData, cb) {
|
||||||
|
|
||||||
|
if(_.isUndefined(this.viewControllers[name])) {
|
||||||
|
const vcOpts = {
|
||||||
|
client : this.client,
|
||||||
|
formId : formId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const vc = this.addViewController(name, new ViewController(vcOpts));
|
||||||
|
|
||||||
|
const loadOpts = {
|
||||||
|
callingMenu : this,
|
||||||
|
mciMap : artData.mciMap,
|
||||||
|
formId : formId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return vc.loadFromMenuConfig(loadOpts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.viewControllers[name].setFocus(true);
|
||||||
|
return cb(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
MenuModule.prototype.prepViewControllerWithArt = function(name, formId, options, cb) {
|
||||||
|
this.displayAsset(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
(err, artData) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prepViewController(name, formId, artData, cb);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
|
@ -135,11 +135,21 @@ module.exports = class MenuStack {
|
||||||
currentModuleInfo.instance.leave();
|
currentModuleInfo.instance.leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push({
|
const noHistory = modInst.menuConfig.options.menuFlags.indexOf('noHistory') > -1;
|
||||||
name : name,
|
|
||||||
instance : modInst,
|
const stackToLog = _.map(self.stack, stackEntry => stackEntry.name);
|
||||||
extraArgs : loadOpts.extraArgs,
|
|
||||||
});
|
if(!noHistory) {
|
||||||
|
self.push({
|
||||||
|
name : name,
|
||||||
|
instance : modInst,
|
||||||
|
extraArgs : loadOpts.extraArgs,
|
||||||
|
});
|
||||||
|
|
||||||
|
stackToLog.push(name);
|
||||||
|
} else {
|
||||||
|
stackToLog.push(`${name} (noHistory)`);
|
||||||
|
}
|
||||||
|
|
||||||
// restore previous state if requested
|
// restore previous state if requested
|
||||||
if(options && options.savedState) {
|
if(options && options.savedState) {
|
||||||
|
@ -149,8 +159,11 @@ module.exports = class MenuStack {
|
||||||
modInst.enter();
|
modInst.enter();
|
||||||
|
|
||||||
self.client.log.trace(
|
self.client.log.trace(
|
||||||
{ stack : _.map(self.stack, stackEntry => stackEntry.name) },
|
{
|
||||||
'Updated menu stack');
|
stack : stackToLog
|
||||||
|
},
|
||||||
|
'Updated menu stack'
|
||||||
|
);
|
||||||
|
|
||||||
if(cb) {
|
if(cb) {
|
||||||
cb(null);
|
cb(null);
|
||||||
|
|
|
@ -64,6 +64,12 @@ function loadMenu(options, cb) {
|
||||||
},
|
},
|
||||||
function loadMenuModule(menuConfig, callback) {
|
function loadMenuModule(menuConfig, callback) {
|
||||||
|
|
||||||
|
menuConfig.options = menuConfig.options || {};
|
||||||
|
menuConfig.options.menuFlags = menuConfig.options.menuFlags || [];
|
||||||
|
if(!Array.isArray(menuConfig.options.menuFlags)) {
|
||||||
|
menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ];
|
||||||
|
}
|
||||||
|
|
||||||
const modAsset = asset.getModuleAsset(menuConfig.module);
|
const modAsset = asset.getModuleAsset(menuConfig.module);
|
||||||
const modSupplied = null !== modAsset;
|
const modSupplied = null !== modAsset;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ const getMessageConferenceByTag = require('./message_area.js').getMessageConfe
|
||||||
const clientConnections = require('./client_connections.js');
|
const clientConnections = require('./client_connections.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const FileBaseFilters = require('./file_base_filter.js');
|
const FileBaseFilters = require('./file_base_filter.js');
|
||||||
|
const formatByteSize = require('./string_util.js').formatByteSize;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
@ -85,6 +86,13 @@ function getPredefinedMCIValue(client, code) {
|
||||||
const activeFilter = FileBaseFilters.getActiveFilter(client);
|
const activeFilter = FileBaseFilters.getActiveFilter(client);
|
||||||
return activeFilter ? activeFilter.name : '';
|
return activeFilter ? activeFilter.name : '';
|
||||||
},
|
},
|
||||||
|
DN : function userNumDownloads() { return StatLog.getUserStat(client.user, 'dl_total_count'); }, // Obv/2
|
||||||
|
DK : function userByteDownload() { // Obv/2
|
||||||
|
const byteSize = parseInt(StatLog.getUserStat(client.user, 'dl_total_bytes')) || 0;
|
||||||
|
return formatByteSize(byteSize, true);
|
||||||
|
},
|
||||||
|
// :TODO: Up/down ratio (count)
|
||||||
|
// :TODO: Up/down ratio (bytes)
|
||||||
|
|
||||||
MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
|
MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
|
||||||
CS : function currentStatus() { return client.currentStatus; },
|
CS : function currentStatus() { return client.currentStatus; },
|
||||||
|
@ -168,6 +176,12 @@ function getPredefinedMCIValue(client, code) {
|
||||||
return StatLog.getSystemStat('random_rumor');
|
return StatLog.getSystemStat('random_rumor');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// System File Base, Up/Download Info
|
||||||
|
//
|
||||||
|
// :TODO: DD - Today's # of downloads (iNiQUiTY)
|
||||||
|
//
|
||||||
|
|
||||||
//
|
//
|
||||||
// Special handling for XY
|
// Special handling for XY
|
||||||
//
|
//
|
||||||
|
|
|
@ -438,6 +438,65 @@ function TelnetClient(input, output) {
|
||||||
newEnvironRequested : false,
|
newEnvironRequested : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.setTemporaryDataHandler = function(handler) {
|
||||||
|
this.input.removeAllListeners();
|
||||||
|
this.input.on('data', handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.restoreDataHandler = function() {
|
||||||
|
this.input.removeAllListeners();
|
||||||
|
this.input.on('data', this.dataHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dataHandler = function(b) {
|
||||||
|
bufs.push(b);
|
||||||
|
|
||||||
|
let i;
|
||||||
|
while((i = bufs.indexOf(IAC_BUF)) >= 0) {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Some clients will send even IAC separate from data
|
||||||
|
//
|
||||||
|
if(bufs.length <= (i + 1)) {
|
||||||
|
i = MORE_DATA_REQUIRED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(bufs.length > (i + 1));
|
||||||
|
|
||||||
|
if(i > 0) {
|
||||||
|
self.emit('data', bufs.splice(0, i).toBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
i = parseBufs(bufs);
|
||||||
|
|
||||||
|
if(MORE_DATA_REQUIRED === i) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if(i.option) {
|
||||||
|
self.emit(i.option, i); // "transmit binary", "echo", ...
|
||||||
|
}
|
||||||
|
|
||||||
|
self.handleTelnetEvent(i);
|
||||||
|
|
||||||
|
if(i.data) {
|
||||||
|
self.emit('data', i.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(MORE_DATA_REQUIRED !== i && bufs.length > 0) {
|
||||||
|
//
|
||||||
|
// Standard data payload. This can still be "non-user" data
|
||||||
|
// such as ANSI control, but we don't handle that here.
|
||||||
|
//
|
||||||
|
self.emit('data', bufs.splice(0).toBuffer());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.input.on('data', this.dataHandler);
|
||||||
|
|
||||||
|
/*
|
||||||
this.input.on('data', b => {
|
this.input.on('data', b => {
|
||||||
bufs.push(b);
|
bufs.push(b);
|
||||||
|
|
||||||
|
@ -482,8 +541,8 @@ function TelnetClient(input, output) {
|
||||||
//
|
//
|
||||||
self.emit('data', bufs.splice(0).toBuffer());
|
self.emit('data', bufs.splice(0).toBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
this.input.on('end', () => {
|
this.input.on('end', () => {
|
||||||
self.emit('end');
|
self.emit('end');
|
||||||
|
|
|
@ -118,6 +118,10 @@ class StatLog {
|
||||||
return user.persistProperty(statName, statValue, cb);
|
return user.persistProperty(statName, statValue, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserStat(user, statName) {
|
||||||
|
return user.properties[statName];
|
||||||
|
}
|
||||||
|
|
||||||
incrementUserStat(user, statName, incrementBy, cb) {
|
incrementUserStat(user, statName, incrementBy, cb) {
|
||||||
incrementBy = incrementBy || 1;
|
incrementBy = incrementBy || 1;
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,22 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// enigma-bbs
|
// enigma-bbs
|
||||||
const MenuModule = require('./menu_module.js').MenuModule;
|
const MenuModule = require('./menu_module.js').MenuModule;
|
||||||
const Config = require('./config.js').config;
|
const Config = require('./config.js').config;
|
||||||
const stringFormat = require('./string_format.js');
|
const stringFormat = require('./string_format.js');
|
||||||
|
const Errors = require('./enig_error.js').Errors;
|
||||||
|
const DownloadQueue = require('./download_queue.js');
|
||||||
|
const StatLog = require('./stat_log.js');
|
||||||
|
const FileEntry = require('./file_entry.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const pty = require('ptyw.js');
|
const pty = require('ptyw.js');
|
||||||
|
const temp = require('temp').track(); // track() cleans up temp dir/files for us
|
||||||
|
const paths = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Resources
|
Resources
|
||||||
|
@ -30,67 +38,281 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.config = this.menuConfig.config || {};
|
this.config = this.menuConfig.config || {};
|
||||||
this.config.protocol = this.config.protocol || 'zmodem8kSz';
|
|
||||||
this.config.direction = this.config.direction || 'send';
|
|
||||||
|
|
||||||
this.protocolConfig = Config.fileTransferProtocols[this.config.protocol];
|
//
|
||||||
|
// Most options can be set via extraArgs or config block
|
||||||
|
//
|
||||||
|
if(options.extraArgs) {
|
||||||
|
if(options.extraArgs.protocol) {
|
||||||
|
this.protocolConfig = Config.fileTransferProtocols[options.extraArgs.protocol];
|
||||||
|
}
|
||||||
|
|
||||||
// :TODO: bring in extraArgs for path(s) to send when sending; Allow to hard code in config (e.g. for info pack/static downloads)
|
if(options.extraArgs.direction) {
|
||||||
|
this.direction = options.extraArgs.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.extraArgs.sendQueue) {
|
||||||
|
this.sendQueue = options.extraArgs.sendQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.extraArgs.recvFileName) {
|
||||||
|
this.recvFileName = options.extraArgs.recvFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.extraArgs.recvDirectory) {
|
||||||
|
this.recvDirectory = options.extraArgs.recvDirectory;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(this.config.protocol) {
|
||||||
|
this.protocolConfig = Config.fileTransferProtocols[this.config.protocol];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.config.direction) {
|
||||||
|
this.direction = this.config.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.config.sendQueue) {
|
||||||
|
this.sendQueue = this.config.sendQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.config.recvFileName) {
|
||||||
|
this.recvFileName = this.config.recvFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.config.recvDirectory) {
|
||||||
|
this.recvDirectory = this.config.recvDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.protocolConfig = this.protocolConfig || Config.fileTransferProtocols.zmodem8kSz; // try for *something*
|
||||||
|
this.direction = this.direction || 'send';
|
||||||
|
this.sendQueue = this.sendQueue || [];
|
||||||
|
|
||||||
|
// Ensure sendQueue is an array of objects that contain at least a 'path' member
|
||||||
|
this.sendQueue = this.sendQueue.map(item => {
|
||||||
|
if(_.isString(item)) {
|
||||||
|
return { path : item };
|
||||||
|
} else {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sentFileIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSending() {
|
||||||
|
return 'send' === this.direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
restorePipeAfterExternalProc(pipe) {
|
restorePipeAfterExternalProc(pipe) {
|
||||||
if(!this.pipeRestored) {
|
if(!this.pipeRestored) {
|
||||||
this.pipeRestored = true;
|
this.pipeRestored = true;
|
||||||
|
|
||||||
|
this.client.restoreDataHandler();
|
||||||
|
|
||||||
this.client.term.output.unpipe(pipe);
|
//this.client.term.output.unpipe(pipe);
|
||||||
this.client.term.output.resume();
|
//this.client.term.output.resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFiles(cb) {
|
sendFiles(cb) {
|
||||||
async.eachSeries(this.sendQueue, (filePath, next) => {
|
// :TODO: built in/native protocol support
|
||||||
// :TODO: built in protocols
|
|
||||||
// :TODO: use protocol passed in
|
if(this.protocolConfig.external.supportsBatch) {
|
||||||
this.executeExternalProtocolHandler(filePath, err => {
|
const allFiles = this.sendQueue.map(f => f.path);
|
||||||
return next(err);
|
this.executeExternalProtocolHandlerForSend(allFiles, err => {
|
||||||
|
if(err) {
|
||||||
|
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' );
|
||||||
|
} else {
|
||||||
|
const sentFiles = [];
|
||||||
|
this.sendQueue.forEach(f => {
|
||||||
|
f.sent = true;
|
||||||
|
sentFiles.push(f.path);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` );
|
||||||
|
}
|
||||||
|
return cb(err);
|
||||||
});
|
});
|
||||||
}, err => {
|
} else {
|
||||||
return cb(err);
|
// :TODO: we need to prompt between entries such that users can prepare their clients
|
||||||
|
async.eachSeries(this.sendQueue, (queueItem, next) => {
|
||||||
|
this.executeExternalProtocolHandlerForSend(queueItem.path, err => {
|
||||||
|
if(err) {
|
||||||
|
this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' );
|
||||||
|
} else {
|
||||||
|
queueItem.sent = true;
|
||||||
|
|
||||||
|
this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' );
|
||||||
|
}
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
}, err => {
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recvFiles(cb) {
|
||||||
|
this.executeExternalProtocolHandlerForRecv( (err, tempWorkingDir) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.receivedFiles = [];
|
||||||
|
|
||||||
|
if(this.recvFileName) {
|
||||||
|
// file name specified - we expect a single file in |tempWorkingDir|
|
||||||
|
return cb(null);
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
// blind recv (upload) - files in |tempWorkingDir| should be named appropriately already
|
||||||
|
// move files to |this.recvDirectory|
|
||||||
|
//
|
||||||
|
fs.readdir(tempWorkingDir, (err, files) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.each(files, (file, nextFile) => {
|
||||||
|
fse.move(
|
||||||
|
paths.join(tempWorkingDir, file),
|
||||||
|
paths.join(this.recvDirectory, file),
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
// :TODO: IMPORTANT: Handle collisions - rename to FILE(1).EXT, etc.
|
||||||
|
this.client.log.warn(
|
||||||
|
{ tempWorkingDir : tempWorkingDir, recvDirectory : this.recvDirectory, file : file, error : err.message },
|
||||||
|
'Failed to move upload file to destination directory'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.receivedFiles.push(file);
|
||||||
|
}
|
||||||
|
return nextFile(null); // don't pass along err; try next
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, () => {
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
executeExternalProtocolHandler(filePath, cb) {
|
pathWithTerminatingSeparator(path) {
|
||||||
const external = this.protocolConfig.external;
|
if(path && paths.sep !== path.charAt(path.length - 1)) {
|
||||||
const cmd = external[`${this.config.direction}Cmd`];
|
path = path + paths.sep;
|
||||||
const args = external[`${this.config.direction}Args`].map(arg => {
|
}
|
||||||
return stringFormat(arg, {
|
return path;
|
||||||
filePath : filePath,
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*this.client.term.rawWrite(new Buffer(
|
prepAndBuildSendArgs(filePaths, cb) {
|
||||||
[
|
const external = this.protocolConfig.external;
|
||||||
255, 253, 0, // IAC DO TRANSMIT_BINARY
|
const externalArgs = external[`${this.direction}Args`];
|
||||||
255, 251, 0, // IAC WILL TRANSMIT_BINARY
|
const self = this;
|
||||||
]
|
let tempWorkingDir;
|
||||||
));*/
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function getTempFileListPath(callback) {
|
||||||
|
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) );
|
||||||
|
if(!hasFileList) {
|
||||||
|
temp.mkdir('enigdl-', (err, tempDir) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
tempWorkingDir = self.pathWithTerminatingSeparator(tempDir);
|
||||||
|
return callback(null, null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
temp.open( { prefix : 'enigdl-', suffix : '.txt' }, (err, tempFileInfo) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err); // failed to create it
|
||||||
|
}
|
||||||
|
|
||||||
|
tempWorkingDir = self.pathWithTerminatingSeparator(paths.dirname(tempFileInfo.path));
|
||||||
|
|
||||||
|
fs.write(tempFileInfo.fd, filePaths.join('\n'));
|
||||||
|
fs.close(tempFileInfo.fd, err => {
|
||||||
|
return callback(err, tempFileInfo.path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function createArgs(tempFileListPath, callback) {
|
||||||
|
// initial args: ignore {filePaths} as we must break that into it's own sep array items
|
||||||
|
const args = externalArgs.map(arg => {
|
||||||
|
return '{filePaths}' === arg ? arg : stringFormat(arg, {
|
||||||
|
fileListPath : tempFileListPath || '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePathsPos = args.indexOf('{filePaths}');
|
||||||
|
if(filePathsPos > -1) {
|
||||||
|
// replace {filePaths} with 0:n individual entries in |args|
|
||||||
|
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null, args);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
(err, args) => {
|
||||||
|
return cb(err, args, tempWorkingDir);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepAndBuildRecvArgs(cb) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function getTempRecvPath(callback) {
|
||||||
|
temp.mkdir('enigrcv-', (err, tempWorkingDir) => {
|
||||||
|
tempWorkingDir = self.pathWithTerminatingSeparator(tempWorkingDir);
|
||||||
|
return callback(err, tempWorkingDir);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function createArgs(tempWorkingDir, callback) {
|
||||||
|
const externalArgs = self.protocolConfig.external[`${self.direction}Args`];
|
||||||
|
const args = externalArgs.map(arg => stringFormat(arg, {
|
||||||
|
uploadDir : tempWorkingDir,
|
||||||
|
fileName : self.recvFileName || '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return callback(null, args, tempWorkingDir);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
(err, args, tempWorkingDir) => {
|
||||||
|
return cb(err, args, tempWorkingDir);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeExternalProtocolHandler(args, tempWorkingDir, cb) {
|
||||||
|
const external = this.protocolConfig.external;
|
||||||
|
const cmd = external[`${this.direction}Cmd`];
|
||||||
|
|
||||||
|
this.client.log.debug(
|
||||||
|
{ cmd : cmd, args : args, tempDir : tempWorkingDir, direction : this.direction },
|
||||||
|
'Executing external protocol'
|
||||||
|
);
|
||||||
|
|
||||||
const externalProc = pty.spawn(cmd, args, {
|
const externalProc = pty.spawn(cmd, args, {
|
||||||
cols : this.client.term.termWidth,
|
cols : this.client.term.termWidth,
|
||||||
rows : this.client.term.termHeight,
|
rows : this.client.term.termHeight,
|
||||||
// :TODO: cwd
|
cwd : tempWorkingDir,
|
||||||
// :TODO: anything else??
|
|
||||||
//env : self.exeInfo.env,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.term.output.pipe(externalProc);
|
this.client.setTemporaryDataHandler(data => {
|
||||||
|
externalProc.write(data);
|
||||||
/*this.client.term.output.on('data', data => {
|
|
||||||
// let tmp = data.toString('binary').replace(/\xff\xff/g, '\xff');
|
|
||||||
// proc.write(new Buffer(tmp, 'binary'));
|
|
||||||
proc.write(data);
|
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
|
//this.client.term.output.pipe(externalProc);
|
||||||
|
|
||||||
externalProc.on('data', data => {
|
externalProc.on('data', data => {
|
||||||
// needed for things like sz/rz
|
// needed for things like sz/rz
|
||||||
if(external.escapeTelnet) {
|
if(external.escapeTelnet) {
|
||||||
|
@ -105,7 +327,9 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
return this.restorePipeAfterExternalProc(externalProc);
|
return this.restorePipeAfterExternalProc(externalProc);
|
||||||
});
|
});
|
||||||
|
|
||||||
externalProc.once('exit', exitCode => {
|
externalProc.once('exit', (exitCode) => {
|
||||||
|
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' );
|
||||||
|
|
||||||
this.restorePipeAfterExternalProc(externalProc);
|
this.restorePipeAfterExternalProc(externalProc);
|
||||||
externalProc.removeAllListeners();
|
externalProc.removeAllListeners();
|
||||||
|
|
||||||
|
@ -113,20 +337,162 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeExternalProtocolHandlerForSend(filePaths, cb) {
|
||||||
|
if(!Array.isArray(filePaths)) {
|
||||||
|
filePaths = [ filePaths ];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepAndBuildSendArgs(filePaths, (err, args, tempWorkingDir) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.executeExternalProtocolHandler(args, tempWorkingDir, err => {
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
executeExternalProtocolHandlerForRecv(cb) {
|
||||||
|
this.prepAndBuildRecvArgs( (err, args, tempWorkingDir) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.executeExternalProtocolHandler(args, tempWorkingDir, err => {
|
||||||
|
return cb(err, tempWorkingDir);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuResult() {
|
||||||
|
return { sentFileIds : this.sentFileIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSendStats(cb) {
|
||||||
|
let downloadBytes = 0;
|
||||||
|
let downloadCount = 0;
|
||||||
|
let fileIds = [];
|
||||||
|
|
||||||
|
async.each(this.sendQueue, (queueItem, next) => {
|
||||||
|
if(!queueItem.sent) {
|
||||||
|
return next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(queueItem.fileId) {
|
||||||
|
fileIds.push(queueItem.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadCount += 1;
|
||||||
|
|
||||||
|
if(_.isNumber(queueItem.byteSize)) {
|
||||||
|
downloadBytes += queueItem.byteSize;
|
||||||
|
return next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we just have a path - figure it out
|
||||||
|
fs.stat(queueItem.path, (err, stats) => {
|
||||||
|
if(err) {
|
||||||
|
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
|
||||||
|
} else {
|
||||||
|
downloadBytes += stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(null);
|
||||||
|
});
|
||||||
|
}, () => {
|
||||||
|
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
|
||||||
|
StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount);
|
||||||
|
StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes);
|
||||||
|
StatLog.incrementSystemStat('dl_total_count', downloadCount);
|
||||||
|
StatLog.incrementSystemStat('dl_total_bytes', downloadBytes);
|
||||||
|
|
||||||
|
fileIds.forEach(fileId => {
|
||||||
|
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRecvStats(cb) {
|
||||||
|
// :TODO: update user & system upload stats
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
// :TODO: break this up to send|recv
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function validateConfig(callback) {
|
function validateConfig(callback) {
|
||||||
// :TODO:
|
if(self.isSending) {
|
||||||
|
if(!Array.isArray(self.sendQueue)) {
|
||||||
|
self.sendQueue = [ self.sendQueue ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
},
|
},
|
||||||
function transferFiles(callback) {
|
function transferFiles(callback) {
|
||||||
self.sendQueue = [ '/home/nuskooler/Downloads/fdoor100.zip' ]; // :TODO: testing of course
|
if(self.isSending) {
|
||||||
return self.sendFiles(callback);
|
self.sendFiles( err => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentFileIds = [];
|
||||||
|
self.sendQueue.forEach(queueItem => {
|
||||||
|
if(queueItem.sent && queueItem.fileId) {
|
||||||
|
sentFileIds.push(queueItem.fileId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(sentFileIds.length > 0) {
|
||||||
|
// remove items we sent from the D/L queue
|
||||||
|
const dlQueue = new DownloadQueue(self.client);
|
||||||
|
dlQueue.removeItems(sentFileIds);
|
||||||
|
|
||||||
|
self.sentFileIds = sentFileIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.recvFiles( err => {
|
||||||
|
return callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function cleanupTempFiles(callback) {
|
||||||
|
temp.cleanup( err => {
|
||||||
|
if(err) {
|
||||||
|
self.client.log.warn( { error : err.message }, 'Failed to clean up temporary file/directory(s)' );
|
||||||
|
}
|
||||||
|
return callback(null); // ignore err
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function updateUserAndSystemStats(callback) {
|
||||||
|
if(self.isSending) {
|
||||||
|
return self.updateSendStats(callback);
|
||||||
|
} else {
|
||||||
|
return self.updateRecvStats(callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
self.client.log.warn( { error : err.message }, 'File transfer error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a key press - attempt to avoid issues with some terminals after xfer
|
||||||
|
self.client.term.write('|00\nTransfer(s) complete. Press a key\n');
|
||||||
|
self.client.waitForKeyPress( () => {
|
||||||
|
self.prevMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
1
main.js
1
main.js
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -10,7 +10,6 @@ const stringFormat = require('../core/string_format.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'File Area Filter Editor',
|
name : 'File Area Filter Editor',
|
||||||
|
|
|
@ -496,7 +496,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
if('re-cached' === cacheStatus) {
|
if('re-cached' === cacheStatus) {
|
||||||
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}';
|
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here?
|
||||||
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
|
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
|
||||||
|
|
||||||
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
|
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
|
const ViewController = require('../core/view_controller.js').ViewController;
|
||||||
|
const DownloadQueue = require('../core/download_queue.js');
|
||||||
|
const theme = require('../core/theme.js');
|
||||||
|
const ansi = require('../core/ansi_term.js');
|
||||||
|
const Errors = require('../core/enig_error.js').Errors;
|
||||||
|
const stringFormat = require('../core/string_format.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'File Base Download Queue Manager',
|
||||||
|
desc : 'Module for interacting with download queue/batch',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormIds = {
|
||||||
|
queueManager : 0,
|
||||||
|
details : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MciViewIds = {
|
||||||
|
queueManager : {
|
||||||
|
queue : 1,
|
||||||
|
navMenu : 2,
|
||||||
|
},
|
||||||
|
details : {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
|
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||||
|
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||||
|
|
||||||
|
this.menuMethods = {
|
||||||
|
downloadAll : (formData, extraArgs, cb) => {
|
||||||
|
const modOpts = {
|
||||||
|
extraArgs : {
|
||||||
|
sendQueue : this.dlQueue.items,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb);
|
||||||
|
},
|
||||||
|
viewItemInfo : (formData, extraArgs, cb) => {
|
||||||
|
},
|
||||||
|
removeItem : (formData, extraArgs, cb) => {
|
||||||
|
const selectedItem = formData.value.queueItem;
|
||||||
|
this.dlQueue.removeItems(selectedItem);
|
||||||
|
return this.updateDownloadQueueView(cb);
|
||||||
|
},
|
||||||
|
clearQueue : (formData, extraArgs, cb) => {
|
||||||
|
this.dlQueue.clear();
|
||||||
|
return this.updateDownloadQueueView(cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
initSequence() {
|
||||||
|
if(0 === this.dlQueue.items.length) {
|
||||||
|
if(this.sendFileIds) {
|
||||||
|
// we've finished everything up - just fall back
|
||||||
|
return this.prevMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply an empty D/L queue: Present a specialized "empty queue" page
|
||||||
|
// :TODO: This technique can be applied in many areas of the code; probablly need a better name than 'popAndGotoMenu' though
|
||||||
|
// ...actually, the option to not append to the stack would be better here
|
||||||
|
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
||||||
|
//return this.popAndGotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue');
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function beforeArt(callback) {
|
||||||
|
return self.beforeArt(callback);
|
||||||
|
},
|
||||||
|
function display(callback) {
|
||||||
|
return self.displayQueueManagerPage(false, callback);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
return self.finishedLoading();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDownloadQueueView(cb) {
|
||||||
|
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||||
|
if(!queueView) {
|
||||||
|
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}';
|
||||||
|
const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat;
|
||||||
|
|
||||||
|
queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) );
|
||||||
|
queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) );
|
||||||
|
|
||||||
|
queueView.redraw();
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayQueueManagerPage(clearScreen, cb) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function prepArtAndViewController(callback) {
|
||||||
|
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback);
|
||||||
|
},
|
||||||
|
function populateViews(callback) {
|
||||||
|
return self.updateDownloadQueueView(callback);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
if(cb) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayArtAndPrepViewController(name, options, cb) {
|
||||||
|
const self = this;
|
||||||
|
const config = this.menuConfig.config;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function readyAndDisplayArt(callback) {
|
||||||
|
if(options.clearScreen) {
|
||||||
|
self.client.term.rawWrite(ansi.clearScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.displayThemedAsset(
|
||||||
|
config.art[name],
|
||||||
|
self.client,
|
||||||
|
{ font : self.menuConfig.font, trailingLF : false },
|
||||||
|
(err, artData) => {
|
||||||
|
return callback(err, artData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function prepeareViewController(artData, callback) {
|
||||||
|
if(_.isUndefined(self.viewControllers[name])) {
|
||||||
|
const vcOpts = {
|
||||||
|
client : self.client,
|
||||||
|
formId : FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!_.isUndefined(options.noInput)) {
|
||||||
|
vcOpts.noInput = options.noInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vc = self.addViewController(name, new ViewController(vcOpts));
|
||||||
|
|
||||||
|
const loadOpts = {
|
||||||
|
callingMenu : self,
|
||||||
|
mciMap : artData.mciMap,
|
||||||
|
formId : FormIds[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.viewControllers[name].setFocus(true);
|
||||||
|
return callback(null);
|
||||||
|
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,126 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// enigma-bbs
|
||||||
|
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
|
const Config = require('../core/config.js').config;
|
||||||
|
const stringFormat = require('../core/string_format.js');
|
||||||
|
const ViewController = require('../core/view_controller.js').ViewController;
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'File transfer protocol selection',
|
||||||
|
desc : 'Select protocol / method for file transfer',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MciViewIds = {
|
||||||
|
protList : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.config = this.menuConfig.config || {};
|
||||||
|
this.config.direction = this.config.direction || 'send';
|
||||||
|
|
||||||
|
this.loadAvailProtocols();
|
||||||
|
|
||||||
|
this.extraArgs = options.extraArgs;
|
||||||
|
|
||||||
|
if(_.has(options, 'lastMenuResult.sentFileIds')) {
|
||||||
|
this.sentFileIds = options.lastMenuResult.sentFileIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fallbackOnly = options.lastMenuResult ? true : false;
|
||||||
|
|
||||||
|
this.menuMethods = {
|
||||||
|
selectProtocol : (formData, extraArgs, cb) => {
|
||||||
|
const protocol = this.protocols[formData.value.protocol];
|
||||||
|
const finalExtraArgs = this.extraArgs || {};
|
||||||
|
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs );
|
||||||
|
|
||||||
|
const modOpts = {
|
||||||
|
extraArgs : finalExtraArgs,
|
||||||
|
};
|
||||||
|
|
||||||
|
if('send' === this.config.direction) {
|
||||||
|
return this.gotoMenu(this.config.downloadFilesMenu || 'downloadFiles', modOpts, cb);
|
||||||
|
} else {
|
||||||
|
return this.gotoMenu(this.config.uploadFilesMenu || 'uploadFiles', modOpts, cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuResult() {
|
||||||
|
if(this.sentFileIds) {
|
||||||
|
return { sentFileIds : this.sentFileIds };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initSequence() {
|
||||||
|
if(this.sentFileIds) {
|
||||||
|
// nothing to do here; move along
|
||||||
|
this.prevMenu();
|
||||||
|
} else {
|
||||||
|
super.initSequence();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mciReady(mciData, cb) {
|
||||||
|
super.mciReady(mciData, err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function loadFromConfig(callback) {
|
||||||
|
const loadOpts = {
|
||||||
|
callingMenu : self,
|
||||||
|
mciMap : mciData.menu
|
||||||
|
};
|
||||||
|
|
||||||
|
return vc.loadFromMenuConfig(loadOpts, callback);
|
||||||
|
},
|
||||||
|
function populateList(callback) {
|
||||||
|
const protListView = vc.getView(MciViewIds.protList);
|
||||||
|
|
||||||
|
const protListFormat = self.config.protListFormat || '{name}';
|
||||||
|
const protListFocusFormat = self.config.protListFocusFormat || protListFormat;
|
||||||
|
|
||||||
|
protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) );
|
||||||
|
protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) );
|
||||||
|
|
||||||
|
protListView.redraw();
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAvailProtocols() {
|
||||||
|
this.protocols = _.map(Config.fileTransferProtocols, (protInfo, protocol) => {
|
||||||
|
return {
|
||||||
|
protocol : protocol,
|
||||||
|
name : protInfo.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.protocols.sort( (a, b) => a.name.localeCompare(b.name) );
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue