* Some WIP on file area releated stuff - various partially implemented pieces coming together

* Some changes to database.js: Triggers for FTS were not being created properly
* Misc fixes & improvements
This commit is contained in:
Bryan Ashby 2016-09-28 21:54:25 -06:00
parent 7da0abdc39
commit 5a0b291a02
14 changed files with 675 additions and 21 deletions

View File

@ -33,6 +33,10 @@ class ACS {
return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead);
}
hasFileAreaRead(area) {
return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead);
}
getConditionalValue(condArray, memberName) {
assert(_.isArray(condArray));
assert(_.isString(memberName));
@ -59,6 +63,8 @@ class ACS {
ACS.Defaults = {
MessageAreaRead : 'GM[users]',
MessageConfRead : 'GM[users]',
FileAreaRead : 'GM[users]',
};
module.exports = ACS;

View File

@ -16,6 +16,8 @@ const async = require('async');
const util = require('util');
const _ = require('lodash');
const mkdirs = require('fs-extra').mkdirs;
const fs = require('fs');
const paths = require('path');
// our main entry point
exports.bbsMain = bbsMain;
@ -71,14 +73,23 @@ function bbsMain() {
if(err) {
console.error('Error initializing: ' + util.inspect(err));
}
callback(err);
return callback(err);
});
},
function listenConnections(callback) {
startListening(callback);
return startListening(callback);
}
],
function complete(err) {
// note this is escaped:
fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => {
console.info('ENiGMA½ Copyright (c) 2014-2016 Bryan Ashby');
if(!err) {
console.info(banner);
}
console.info('System started!');
});
if(err) {
console.error('Error initializing: ' + util.inspect(err));
}
@ -87,7 +98,9 @@ function bbsMain() {
}
function shutdownSystem() {
logger.log.info('Process interrupted, shutting down...');
const msg = 'Process interrupted. Shutting down...';
console.info(msg);
logger.log.info(msg);
async.series(
[
@ -114,7 +127,8 @@ function shutdownSystem() {
}
],
() => {
process.exit();
console.info('Goodbye!');
return process.exit();
}
);
}

View File

@ -222,6 +222,33 @@ function getDefaultConfig() {
}
},
fileTransferProtocols : {
zmodem8kSz : {
name : 'ZModem 8k',
type : 'external',
external : {
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
sendArgs : [
'--zmodem', '-y', '--try-8k', '--binary', '--restricted', '{filePath}'
],
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
}
},
zmodem8kSexyz : {
name : 'ZModem 8k',
type : 'external',
external : {
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
sendCmd : 'sexyz',
sendArgs : [
'-telnet', 'sz', '{filePath}'
],
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
}
}
},
messageAreaDefaults : {
//

View File

@ -175,17 +175,23 @@ const DB_INIT_TABLE = {
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid;
END;
END;`
);
CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
DELETE FROM message_fts WHERE docid=old.rowid;
END;
END;`
);
CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;
END;`
);
CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
dbs.message.run(
`CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
END;`
);
@ -250,7 +256,7 @@ const DB_INIT_TABLE = {
file_sha1 VARCHAR NOT NULL,
file_name, /* FTS @ file_fts */
desc, /* FTS @ file_fts */
desc_long, /* FTS @ file_fts */
desc_long, /* FTS @ file_fts */
upload_by_username VARCHAR NOT NULL,
upload_timestamp DATETIME NOT NULL
);`
@ -273,18 +279,24 @@ const DB_INIT_TABLE = {
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid;
END;
CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
END;`
);
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN
DELETE FROM file_fts WHERE docid=old.rowid;
END;
END;`
);
CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, long_desc) VALUES(new.rowid, new.file_name, new.desc, new.long_desc);
END;
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
END;`
);
CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.long_desc);
dbs.file.run(
`CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN
INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long);
END;`
);

View File

@ -100,6 +100,7 @@ Door.prototype.run = function() {
}
// Expand arg strings, e.g. {dropFile} -> DOOR32.SYS
// :TODO: Use .map() here
let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified
for(let i = 0; i < args.length; ++i) {

View File

@ -19,6 +19,7 @@ class EnigError extends Error {
}
}
// :TODO: Just use EnigError for all
class EnigMenuError extends EnigError { }
exports.EnigError = EnigError;
@ -27,4 +28,5 @@ exports.EnigMenuError = EnigMenuError;
exports.Errors = {
General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode),
MenuStack : (reason, reasonCode) => new EnigMenuError('Menu stack error', -33001, reason, reasonCode),
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode),
};

31
core/file_area.js Normal file
View File

@ -0,0 +1,31 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const Config = require('./config.js').config;
const Log = require('./logger.js').log;
// deps
const _ = require('lodash');
exports.getAvailableFileAreas = getAvailableFileAreas;
exports.getFileAreaByTag = getFileAreaByTag;
function getAvailableFileAreas(client, options) {
options = options || { includeSystemInternal : false };
// perform ACS check per conf & omit system_internal if desired
return _.omit(Config.fileAreas.areas, (area, areaTag) => {
/* if(!options.includeSystemInternal && 'system_internal' === confTag) {
return true;
}*/
return !client.acs.hasFileAreaRead(area);
});
}
function getFileAreaByTag(areaTag) {
return Config.fileAreas.areas[areaTag];
}

171
core/file_entry.js Normal file
View File

@ -0,0 +1,171 @@
/* jslint node: true */
'use strict';
const fileDb = require('./database.js').dbs.file;
const Errors = require('./enig_error.js').Errors;
// deps
const async = require('async');
const _ = require('lodash');
const FILE_TABLE_MEMBERS = [
'file_id', 'area_tag', 'file_sha1', 'file_name',
'desc', 'desc_long', 'upload_by_username', 'upload_timestamp'
];
module.exports = class FileEntry {
constructor(options) {
options = options || {};
this.fileId = options.fileId || 0;
this.areaTag = options.areaTag || '';
this.meta = {};
this.hashTags = new Set();
}
load(fileId, cb) {
const self = this;
async.series(
[
function loadBasicEntry(callback) {
fileDb.get(
`SELECT ${FILE_TABLE_MEMBERS.join(', ')}
FROM file
WHERE file_id=?
LIMIT 1;`,
[ fileId ],
(err, file) => {
if(err) {
return callback(err);
}
if(!file) {
return callback(Errors.DoesNotExist('No file is available by that ID'));
}
// assign props from |file|
FILE_TABLE_MEMBERS.forEach(prop => {
self[_.camelCase(prop)] = file[prop];
});
return callback(null);
}
);
},
function loadMeta(callback) {
return self.loadMeta(callback);
},
function loadHashTags(callback) {
return self.loadHashTags(callback);
}
],
err => {
return cb(err);
}
);
}
loadMeta(cb) {
fileDb.each(
`SELECT meta_name, meta_value
FROM file_meta
WHERE file_id=?;`,
[ this.fileId ],
(err, meta) => {
if(meta) {
this.meta[meta.meta_name] = meta.meta_value;
}
},
err => {
return cb(err);
}
);
}
loadHashTags(cb) {
fileDb.each(
`SELECT ht.hash_tag_id, ht.hash_tag
FROM hash_tag ht
WHERE ht.hash_tag_id IN (
SELECT hash_tag_id
FROM file_hash_tag
WHERE file_id=?
);`,
[ this.fileId ],
(err, hashTag) => {
if(hashTag) {
this.hashTags.add(hashTag.hash_tag);
}
},
err => {
return cb(err);
}
);
}
static findFiles(criteria, cb) {
// :TODO: build search here - return [ fileid1, fileid2, ... ]
// free form
// areaTag
// tags
// order by
// sort
let sql =
`SELECT file_id
FROM file`;
let sqlWhere = '';
function appendWhereClause(clause) {
if(sqlWhere) {
sqlWhere += ' AND ';
} else {
sqlWhere += ' WHERE ';
}
sqlWhere += clause;
}
if(criteria.areaTag) {
appendWhereClause(`area_tag="${criteria.areaTag}"`);
}
if(criteria.search) {
appendWhereClause(
`file_id IN (
SELECT rowid
FROM file_fts
WHERE file_fts MATCH "${criteria.search.replace(/"/g,'""')}"
)`
);
}
if(Array.isArray(criteria.hashTags)) {
appendWhereClause(
`file_id IN (
SELECT file_id
FROM file_hash_tag
WHERE hash_tag_id IN (
SELECT hash_tag_id
FROM hash_tag
WHERE hash_tag IN (${criteria.hashTags.join(',')})
)
)`
);
}
// :TODO: criteria.orderBy
// :TODO: criteria.sort
sql += sqlWhere + ';';
const matchingFileIds = [];
fileDb.each(sql, (err, fileId) => {
if(fileId) {
matchingFileIds.push(fileId.file_id);
}
}, err => {
return cb(err, matchingFileIds);
});
}
};

View File

@ -6,6 +6,8 @@ const pad = require('./string_util.js').pad;
const stylizeString = require('./string_util.js').stylizeString;
const renderStringLength = require('./string_util.js').renderStringLength;
const renderSubstr = require('./string_util.js').renderSubstr;
const formatByteSize = require('./string_util.js').formatByteSize;
const formatByteSizeAbbr = require('./string_util.js').formatByteSizeAbbr;
// deps
const _ = require('lodash');
@ -265,6 +267,12 @@ const transformers = {
styleSmallI : (s) => stylizeString(s, 'small i'),
styleMixed : (s) => stylizeString(s, 'mixed'),
styleL33t : (s) => stylizeString(s, 'l33t'),
// toMegs(), toKilobytes(), ...
// toList(), toCommaList(),
sizeWithAbbr : (n) => formatByteSize(n, true, 2),
sizeWithoutAbbr : (n) => formatByteSize(n, false, 2),
sizeAbbr : (n) => formatByteSizeAbbr(n),
};
function transformValue(transformerName, value) {

View File

@ -13,6 +13,8 @@ exports.debugEscapedString = debugEscapedString;
exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
exports.renderSubstr = renderSubstr;
exports.renderStringLength = renderStringLength;
exports.formatByteSizeAbbr = formatByteSizeAbbr;
exports.formatByteSize = formatByteSize;
exports.cleanControlCodes = cleanControlCodes;
// :TODO: create Unicode verison of this
@ -286,6 +288,23 @@ function renderStringLength(s) {
return len;
}
const SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :)
function formatByteSizeAbbr(byteSize) {
return SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))];
}
function formatByteSize(byteSize, withAbbr, decimals) {
withAbbr = withAbbr || false;
decimals = decimals || 3;
const i = Math.floor(Math.log(byteSize) / Math.log(1024));
let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals));
if(withAbbr) {
result += ` ${SIZE_ABBRS[i]}`;
}
return result;
}
// :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's
//const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g;

132
core/transfer_file.js Normal file
View File

@ -0,0 +1,132 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule;
const Config = require('./config.js').config;
const stringFormat = require('./string_format.js');
// deps
const async = require('async');
const _ = require('lodash');
const pty = require('ptyw.js');
/*
Resources
ZModem
* http://gallium.inria.fr/~doligez/zmodem/zmodem.txt
* https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c
*/
exports.moduleInfo = {
name : 'Transfer file',
desc : 'Sends or receives a file(s)',
author : 'NuSkooler',
};
exports.getModule = class TransferFileModule extends MenuModule {
constructor(options) {
super(options);
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];
// :TODO: bring in extraArgs for path(s) to send when sending; Allow to hard code in config (e.g. for info pack/static downloads)
}
restorePipeAfterExternalProc(pipe) {
if(!this.pipeRestored) {
this.pipeRestored = true;
this.client.term.output.unpipe(pipe);
this.client.term.output.resume();
}
}
sendFiles(cb) {
async.eachSeries(this.sendQueue, (filePath, next) => {
// :TODO: built in protocols
// :TODO: use protocol passed in
this.executeExternalProtocolHandler(filePath, err => {
return next(err);
});
}, err => {
return cb(err);
});
}
executeExternalProtocolHandler(filePath, cb) {
const external = this.protocolConfig.external;
const cmd = external[`${this.config.direction}Cmd`];
const args = external[`${this.config.direction}Args`].map(arg => {
return stringFormat(arg, {
filePath : filePath,
});
});
/*this.client.term.rawWrite(new Buffer(
[
255, 253, 0, // IAC DO TRANSMIT_BINARY
255, 251, 0, // IAC WILL TRANSMIT_BINARY
]
));*/
const externalProc = pty.spawn(cmd, args, {
cols : this.client.term.termWidth,
rows : this.client.term.termHeight,
// :TODO: cwd
// :TODO: anything else??
//env : self.exeInfo.env,
});
this.client.term.output.pipe(externalProc);
/*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);
});
*/
externalProc.on('data', data => {
// needed for things like sz/rz
if(external.escapeTelnet) {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff');
this.client.term.rawWrite(new Buffer(tmp, 'binary'));
} else {
this.client.term.rawWrite(data);
}
});
externalProc.once('close', () => {
return this.restorePipeAfterExternalProc(externalProc);
});
externalProc.once('exit', exitCode => {
this.restorePipeAfterExternalProc(externalProc);
externalProc.removeAllListeners();
return cb(null);
});
}
initSequence() {
const self = this;
async.series(
[
function validateConfig(callback) {
// :TODO:
return callback(null);
},
function transferFiles(callback) {
self.sendQueue = [ '/home/nuskooler/Downloads/fdoor100.zip' ]; // :TODO: testing of course
return self.sendFiles(callback);
}
]
);
}
};

9
misc/startup_banner.asc Normal file
View File

@ -0,0 +1,9 @@
_____________________ _____ ____________________ __________\_ /
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
// __|___// | \// |// | \// | | \// \ /___ /_____
/____ _____| __________ ___|__| ____| \ / _____ \
---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/
/__ _\
<*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/
-------------------------------------------------------------------------------

222
mods/file_area_list.js Normal file
View File

@ -0,0 +1,222 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const ansi = require('../core/ansi_term.js');
const theme = require('../core/theme.js');
const FileEntry = require('../core/file_entry.js');
const stringFormat = require('../core/string_format.js');
const FileArea = require('../core/file_area.js');
// deps
const async = require('async');
const _ = require('lodash');
const moment = require('moment');
/*
Misc TODO
* Allow rating to be user defined colors & characters/etc.
*
Well known file entry meta values:
* upload_by_username
* upload_by_user_id
* file_md5
* file_sha256
* file_crc32
* est_release_year
* dl_count
* byte_size
* user_rating
*
*/
exports.moduleInfo = {
name : 'File Area List',
desc : 'Lists contents of file an file area',
author : 'NuSkooler',
};
const FormIds = {
browse : 0,
details : 1,
};
const MciViewIds = {
browse : {
desc : 1,
navMenu : 2,
// 10+: customs
},
};
exports.getModule = class FileAreaList extends MenuModule {
constructor(options) {
super(options);
const config = this.menuConfig.config;
if(options.extraArgs) {
this.filterCriteria = options.extraArgs.filterCriteria;
}
this.filterCriteria = this.filterCriteria || {
// :TODO: set area tag - all in current area by default
};
}
enter() {
super.enter();
}
leave() {
super.leave();
}
initSequence() {
const self = this;
async.series(
[
function beforeArt(callback) {
return self.beforeArt(callback);
},
function display(callback) {
return self.displayBrowsePage(false, callback);
}
],
() => {
self.finishedLoading();
}
);
}
displayBrowsePage(clearScreen, cb) {
const self = this;
const config = this.menuConfig.config;
async.waterfall(
[
function clearAndDisplayArt(callback) {
if (clearScreen) {
self.client.term.rawWrite(ansi.resetScreen());
}
theme.displayThemedAsset(
//config.art.browse,
'FBRWSE',
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.browse)) {
const vc = self.addViewController(
'browse',
new ViewController( { client : self.client, formId : FormIds.browse } )
);
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds.browse,
};
return vc.loadFromMenuConfig(loadOpts, callback);
}
self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw();
return callback(null);
},
function fetchEntryData(callback) {
return self.loadFileIds(callback);
},
function loadCurrentFileInfo(callback) {
self.currentFileEntry = new FileEntry();
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
return callback(err);
});
},
function populateViews(callback) {
if(_.isString(self.currentFileEntry.desc)) {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
if(descView) {
descView.setText(self.currentFileEntry.desc);
//descView.redraw();
}
}
const currEntry = self.currentFileEntry;
const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD';
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
const hashTagsSep = config.hashTagsSep || ', ';
const entryInfo = {
fileId : currEntry.fileId,
areaTag : currEntry.areaTag,
areaName : area.name || 'N/A',
areaDesc : area.desc || 'N/A',
fileSha1 : currEntry.fileSha1,
fileName : currEntry.fileName,
desc : currEntry.desc,
descLong : currEntry.descLong,
uploadByUsername : currEntry.uploadByUsername,
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
};
const META_NUMBERS = [ 'byte_size', 'dl_count' ];
_.forEach(self.currentFileEntry.meta, (value, name) => {
if(META_NUMBERS.indexOf(name) > -1) {
value = parseInt(value);
}
entryInfo[_.camelCase(name)] = value;
});
// entryInfo.fileSize = 1241234; // :TODO: REMOVE ME!
// 10+ are custom textviews
let textView;
let customMciId = 10;
while( (textView = self.viewControllers.browse.getView(customMciId)) ) {
const key = `browseInfoFormat${customMciId}`;
const format = config[key];
if(format) {
textView.setText(stringFormat(format, entryInfo));
}
++customMciId;
}
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
loadFileIds(cb) {
this.fileListPosition = 0;
FileEntry.findFiles(this.filterCriteria, (err, fileIds) => {
this.fileList = fileIds;
return cb(err);
});
}
};

View File

@ -127,7 +127,7 @@ MessageListModule.prototype.enter = function() {
if(this.messageAreaTag) {
this.tempMessageConfAndAreaSwitch(this.messageAreaTag);
} else {
this.messageAreaTag = this.messageAreaTag = this.client.user.properties.message_area_tag;
this.messageAreaTag = this.client.user.properties.message_area_tag;
}
};