Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs
This commit is contained in:
commit
87d122b34c
|
@ -41,7 +41,12 @@ function hasMessageConferenceAndArea(config) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(configPath, cb) {
|
function init(configPath, options, cb) {
|
||||||
|
if(!cb && _.isFunction(options)) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function loadUserConfig(callback) {
|
function loadUserConfig(callback) {
|
||||||
|
@ -56,7 +61,7 @@ function init(configPath, cb) {
|
||||||
|
|
||||||
let configJson;
|
let configJson;
|
||||||
try {
|
try {
|
||||||
configJson = hjson.parse(configData);
|
configJson = hjson.parse(configData, options);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return callback(e);
|
return callback(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
const fileDb = require('./database.js').dbs.file;
|
const fileDb = require('./database.js').dbs.file;
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||||
const Config = require('./config.js').config;
|
const Config = require('./config.js').config;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
|
||||||
const FILE_TABLE_MEMBERS = [
|
const FILE_TABLE_MEMBERS = [
|
||||||
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag',
|
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag',
|
||||||
|
@ -377,7 +378,7 @@ module.exports = class FileEntry {
|
||||||
} else {
|
} else {
|
||||||
sql =
|
sql =
|
||||||
`SELECT f.file_id
|
`SELECT f.file_id
|
||||||
FROM file`;
|
FROM file f`;
|
||||||
|
|
||||||
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
|
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
|
||||||
}
|
}
|
||||||
|
@ -387,6 +388,10 @@ module.exports = class FileEntry {
|
||||||
appendWhereClause(`f.area_tag="${filter.areaTag}"`);
|
appendWhereClause(`f.area_tag="${filter.areaTag}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(filter.storageTag && filter.storageTag.length > 0) {
|
||||||
|
appendWhereClause(`f.storage_tag="${filter.storageTag}"`);
|
||||||
|
}
|
||||||
|
|
||||||
if(filter.terms && filter.terms.length > 0) {
|
if(filter.terms && filter.terms.length > 0) {
|
||||||
appendWhereClause(
|
appendWhereClause(
|
||||||
`f.file_id IN (
|
`f.file_id IN (
|
||||||
|
@ -425,4 +430,49 @@ module.exports = class FileEntry {
|
||||||
return cb(err, matchingFileIds);
|
return cb(err, matchingFileIds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) {
|
||||||
|
if(!cb && _.isFunction(destFileName)) {
|
||||||
|
cb = destFileName;
|
||||||
|
destFileName = srcFileEntry.fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcPath = srcFileEntry.filePath;
|
||||||
|
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
|
||||||
|
|
||||||
|
|
||||||
|
if(!dstDir) {
|
||||||
|
return cb(Errors.Invalid('Invalid storage tag'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dstPath = paths.join(dstDir, destFileName);
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function movePhysFile(callback) {
|
||||||
|
if(srcPath === dstPath) {
|
||||||
|
return callback(null); // don't need to move file, but may change areas
|
||||||
|
}
|
||||||
|
|
||||||
|
fse.move(srcPath, dstPath, err => {
|
||||||
|
return callback(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function updateDatabase(callback) {
|
||||||
|
fileDb.run(
|
||||||
|
`UPDATE file
|
||||||
|
SET area_tag = ?, file_name = ?, storage_tag = ?
|
||||||
|
WHERE file_id = ?;`,
|
||||||
|
[ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ],
|
||||||
|
err => {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,7 +31,7 @@ module.exports = class Address {
|
||||||
this.zone === other.zone &&
|
this.zone === other.zone &&
|
||||||
this.point === other.point &&
|
this.point === other.point &&
|
||||||
this.domain === other.domain
|
this.domain === other.domain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchAddr(pattern) {
|
getMatchAddr(pattern) {
|
||||||
|
@ -40,7 +40,7 @@ module.exports = class Address {
|
||||||
let addr = { };
|
let addr = { };
|
||||||
|
|
||||||
if(m[1]) {
|
if(m[1]) {
|
||||||
addr.zone = m[1].slice(0, -1)
|
addr.zone = m[1].slice(0, -1);
|
||||||
if('*' !== addr.zone) {
|
if('*' !== addr.zone) {
|
||||||
addr.zone = parseInt(addr.zone);
|
addr.zone = parseInt(addr.zone);
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ module.exports = class Address {
|
||||||
('*' === addr.zone || this.zone === addr.zone) &&
|
('*' === addr.zone || this.zone === addr.zone) &&
|
||||||
('*' === addr.point || this.point === addr.point) &&
|
('*' === addr.point || this.point === addr.point) &&
|
||||||
('*' === addr.domain || this.domain === addr.domain)
|
('*' === addr.domain || this.domain === addr.domain)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -193,6 +193,6 @@ module.exports = class Address {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (left.domain || '').localeCompare(right.domain || '');
|
return (left.domain || '').localeCompare(right.domain || '');
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -90,7 +90,7 @@ function loadMenu(options, cb) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function createModuleInstance(modData, callback) {
|
function createModuleInstance(modData, callback) {
|
||||||
Log.debug(
|
Log.trace(
|
||||||
{ moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo },
|
{ moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo },
|
||||||
'Creating menu module instance');
|
'Creating menu module instance');
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,8 @@ exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent;
|
||||||
|
|
||||||
function getAvailableMessageConferences(client, options) {
|
function getAvailableMessageConferences(client, options) {
|
||||||
options = options || { includeSystemInternal : false };
|
options = options || { includeSystemInternal : false };
|
||||||
|
|
||||||
|
assert(client || true === options.noClient);
|
||||||
|
|
||||||
// perform ACS check per conf & omit system_internal if desired
|
// perform ACS check per conf & omit system_internal if desired
|
||||||
return _.omitBy(Config.messageConferences, (conf, confTag) => {
|
return _.omitBy(Config.messageConferences, (conf, confTag) => {
|
||||||
|
@ -42,7 +44,7 @@ function getAvailableMessageConferences(client, options) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !client.acs.hasMessageConfRead(conf);
|
return client && !client.acs.hasMessageConfRead(conf);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ const async = require('async');
|
||||||
|
|
||||||
exports.printUsageAndSetExitCode = printUsageAndSetExitCode;
|
exports.printUsageAndSetExitCode = printUsageAndSetExitCode;
|
||||||
exports.getDefaultConfigPath = getDefaultConfigPath;
|
exports.getDefaultConfigPath = getDefaultConfigPath;
|
||||||
|
exports.getConfigPath = getConfigPath;
|
||||||
exports.initConfigAndDatabases = initConfigAndDatabases;
|
exports.initConfigAndDatabases = initConfigAndDatabases;
|
||||||
exports.getAreaAndStorage = getAreaAndStorage;
|
exports.getAreaAndStorage = getAreaAndStorage;
|
||||||
|
|
||||||
|
@ -40,10 +41,14 @@ function getDefaultConfigPath() {
|
||||||
return resolvePath('~/.config/enigma-bbs/config.hjson');
|
return resolvePath('~/.config/enigma-bbs/config.hjson');
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConfig(cb) {
|
function getConfigPath() {
|
||||||
const configPath = argv.config ? argv.config : config.getDefaultPath();
|
return argv.config ? argv.config : config.getDefaultPath();
|
||||||
|
}
|
||||||
|
|
||||||
config.init(configPath, cb);
|
function initConfig(cb) {
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
|
config.init(configPath, { keepWsc : true }, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConfigAndDatabases(cb) {
|
function initConfigAndDatabases(cb) {
|
||||||
|
@ -68,6 +73,7 @@ function getAreaAndStorage(tags) {
|
||||||
const entry = {
|
const entry = {
|
||||||
areaTag : parts[0],
|
areaTag : parts[0],
|
||||||
};
|
};
|
||||||
|
entry.pattern = entry.areaTag; // handy
|
||||||
if(parts[1]) {
|
if(parts[1]) {
|
||||||
entry.storageTag = parts[1];
|
entry.storageTag = parts[1];
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,10 @@ const resolvePath = require('../../core/misc_util.js').resolvePath;
|
||||||
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
|
||||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
||||||
const argv = require('./oputil_common.js').argv;
|
const argv = require('./oputil_common.js').argv;
|
||||||
const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath;
|
const getConfigPath = require('./oputil_common.js').getConfigPath;
|
||||||
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||||
|
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
|
||||||
|
const Errors = require('../../core/enig_error.js').Errors;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -17,6 +19,7 @@ const mkdirsSync = require('fs-extra').mkdirsSync;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const hjson = require('hjson');
|
const hjson = require('hjson');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.handleConfigCommand = handleConfigCommand;
|
exports.handleConfigCommand = handleConfigCommand;
|
||||||
|
|
||||||
|
@ -38,7 +41,7 @@ const QUESTIONS = {
|
||||||
{
|
{
|
||||||
name : 'configPath',
|
name : 'configPath',
|
||||||
message : 'Configuration path:',
|
message : 'Configuration path:',
|
||||||
default : argv.config ? argv.config : getDefaultConfigPath(),
|
default : getConfigPath(),
|
||||||
when : answers => answers.createNewConfig
|
when : answers => answers.createNewConfig
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -232,27 +235,326 @@ function askNewConfigQuestions(cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeConfig(config, path) {
|
||||||
|
config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t', keepWsc : true, quotes : 'strings' } );
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path, config, 'utf8');
|
||||||
|
return true;
|
||||||
|
} catch(e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNewConfig() {
|
||||||
|
askNewConfigQuestions( (err, configPath, config) => {
|
||||||
|
if(err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(writeConfig(config, configPath)) {
|
||||||
|
console.info('Configuration generated');
|
||||||
|
} else {
|
||||||
|
console.error('Failed writing configuration');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUplinks(uplinks) {
|
||||||
|
const ftnAddress = require('../../core/ftn_address.js');
|
||||||
|
const valid = uplinks.every(ul => {
|
||||||
|
const addr = ftnAddress.fromString(ul);
|
||||||
|
return addr;
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMsgAreaImportType(path) {
|
||||||
|
if(argv.type) {
|
||||||
|
return argv.type.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = paths.extname(path).toLowerCase().substr(1);
|
||||||
|
return ext; // .bbs|.na|...
|
||||||
|
}
|
||||||
|
|
||||||
|
function importAreas() {
|
||||||
|
const importPath = argv._[argv._.length - 1];
|
||||||
|
if(argv._.length < 3 || !importPath || 0 === importPath.length) {
|
||||||
|
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importType = getMsgAreaImportType(importPath);
|
||||||
|
if('na' !== importType && 'bbs' !== importType) {
|
||||||
|
return console.error(`"${importType}" is not a recognized import file type`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional data - we'll prompt if for anything not found
|
||||||
|
let confTag = argv.conf;
|
||||||
|
let networkName = argv.network;
|
||||||
|
let uplinks = argv.uplinks;
|
||||||
|
if(uplinks) {
|
||||||
|
uplinks = uplinks.split(/[\s,]+/);
|
||||||
|
}
|
||||||
|
|
||||||
|
let importEntries;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function readImportFile(callback) {
|
||||||
|
fs.readFile(importPath, 'utf8', (err, importData) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
importEntries = getImportEntries(importType, importData);
|
||||||
|
if(0 === importEntries.length) {
|
||||||
|
return callback(Errors.Invalid('Invalid or empty import file'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have enough to validate uplinks
|
||||||
|
if('bbs' === importType) {
|
||||||
|
for(let i = 0; i < importEntries.length; ++i) {
|
||||||
|
if(!validateUplinks(importEntries[i].uplinks)) {
|
||||||
|
return callback(Errors.Invalid('Invalid uplink(s)'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(!validateUplinks(uplinks)) {
|
||||||
|
return callback(Errors.Invalid('Invalid uplink(s)'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function init(callback) {
|
||||||
|
return initConfigAndDatabases(callback);
|
||||||
|
},
|
||||||
|
function validateAndCollectInput(callback) {
|
||||||
|
const msgArea = require('../../core/message_area.js');
|
||||||
|
const Config = require('../../core/config.js').config;
|
||||||
|
|
||||||
|
let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } );
|
||||||
|
if(!msgConfs) {
|
||||||
|
return callback(Errors.DoesNotExist('No conferences exist in your configuration'));
|
||||||
|
}
|
||||||
|
|
||||||
|
msgConfs = msgConfs.map(mc => {
|
||||||
|
return {
|
||||||
|
name : mc.conf.name,
|
||||||
|
value : mc.confTag,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if(confTag && !msgConfs.find(mc => {
|
||||||
|
return confTag === mc.value;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`));
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingNetworkNames = [];
|
||||||
|
if(_.has(Config, 'messageNetworks.ftn.networks')) {
|
||||||
|
existingNetworkNames = Object.keys(Config.messageNetworks.ftn.networks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(0 === existingNetworkNames.length) {
|
||||||
|
return callback(Errors.DoesNotExist('No FTN style networks exist in your configuration'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(networkName && !existingNetworkNames.find(net => networkName === net)) {
|
||||||
|
return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnswers([
|
||||||
|
{
|
||||||
|
name : 'confTag',
|
||||||
|
message : 'Message conference:',
|
||||||
|
type : 'list',
|
||||||
|
choices : msgConfs,
|
||||||
|
pageSize : 10,
|
||||||
|
when : !confTag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'networkName',
|
||||||
|
message : 'Network name:',
|
||||||
|
type : 'list',
|
||||||
|
choices : existingNetworkNames,
|
||||||
|
when : !networkName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'uplinks',
|
||||||
|
message : 'Uplink(s) (comma separated):',
|
||||||
|
type : 'input',
|
||||||
|
validate : (input) => {
|
||||||
|
const inputUplinks = input.split(/[\s,]+/);
|
||||||
|
return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)';
|
||||||
|
},
|
||||||
|
when : !uplinks && 'bbs' !== importType,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
answers => {
|
||||||
|
confTag = confTag || answers.confTag;
|
||||||
|
networkName = networkName || answers.networkName;
|
||||||
|
uplinks = uplinks || answers.uplinks;
|
||||||
|
|
||||||
|
importEntries.forEach(ie => {
|
||||||
|
ie.areaTag = ie.ftnTag.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function confirmWithUser(callback) {
|
||||||
|
const Config = require('../../core/config.js').config;
|
||||||
|
|
||||||
|
console.info(`Importing the following for "${confTag}" - (${Config.messageConferences[confTag].name} - ${Config.messageConferences[confTag].desc})`);
|
||||||
|
importEntries.forEach(ie => {
|
||||||
|
console.info(` ${ie.ftnTag} - ${ie.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('');
|
||||||
|
console.info('Importing will NOT create required FTN network configurations.');
|
||||||
|
console.info('If you have not yet done this, you will need to complete additional steps after importing.');
|
||||||
|
console.info('See docs/msg_networks.md for details.');
|
||||||
|
console.info('');
|
||||||
|
|
||||||
|
getAnswers([
|
||||||
|
{
|
||||||
|
name : 'proceed',
|
||||||
|
message : 'Proceed?',
|
||||||
|
type : 'confirm',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
answers => {
|
||||||
|
return callback(answers.proceed ? null : Errors.General('User canceled'));
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
function loadConfigHjson(callback) {
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
fs.readFile(configPath, 'utf8', (err, confData) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = hjson.parse(confData, { keepWsc : true } );
|
||||||
|
} catch(e) {
|
||||||
|
return callback(e);
|
||||||
|
}
|
||||||
|
return callback(null, config);
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function performImport(config, callback) {
|
||||||
|
const confAreas = { messageConferences : {} };
|
||||||
|
confAreas.messageConferences[confTag] = { areas : {} };
|
||||||
|
|
||||||
|
const msgNetworks = { messageNetworks : { ftn : { areas : {} } } };
|
||||||
|
|
||||||
|
importEntries.forEach(ie => {
|
||||||
|
const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area
|
||||||
|
|
||||||
|
confAreas.messageConferences[confTag].areas[ie.areaTag] = {
|
||||||
|
name : ie.name,
|
||||||
|
desc : ie.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = {
|
||||||
|
network : networkName,
|
||||||
|
tag : ie.ftnTag,
|
||||||
|
uplinks : specificUplinks
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const newConfig = _.defaultsDeep(config, confAreas, msgNetworks);
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
|
if(!writeConfig(newConfig, configPath)) {
|
||||||
|
return callback(Errors.UnexpectedState('Failed writing configuration'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
console.error(err.reason ? err.reason : err.message);
|
||||||
|
} else {
|
||||||
|
const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"';
|
||||||
|
console.info('Configuration generated.');
|
||||||
|
console.info(`You may wish to validate changes made to ${getConfigPath()}`);
|
||||||
|
console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`);
|
||||||
|
console.info('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImportEntries(importType, importData) {
|
||||||
|
let importEntries = [];
|
||||||
|
|
||||||
|
if('na' === importType) {
|
||||||
|
//
|
||||||
|
// parse out
|
||||||
|
// TAG DESC
|
||||||
|
//
|
||||||
|
const re = /^([^\s]+)\s+([^\r\n]+)/gm;
|
||||||
|
let m;
|
||||||
|
|
||||||
|
while( (m = re.exec(importData) )) {
|
||||||
|
importEntries.push({
|
||||||
|
ftnTag : m[1],
|
||||||
|
name : m[2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ('bbs' === importType) {
|
||||||
|
//
|
||||||
|
// Various formats for AREAS.BBS seem to exist. We want to support as much as possible.
|
||||||
|
//
|
||||||
|
// SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS
|
||||||
|
// CODE TAG UPLINKS
|
||||||
|
//
|
||||||
|
// VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS
|
||||||
|
// TAG UPLINKS
|
||||||
|
//
|
||||||
|
// Misc
|
||||||
|
// PATH|OTHER TAG UPLINKS
|
||||||
|
//
|
||||||
|
// Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end)
|
||||||
|
//
|
||||||
|
const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm;
|
||||||
|
let m;
|
||||||
|
while ( (m = re.exec(importData) )) {
|
||||||
|
const tag = m[1];
|
||||||
|
|
||||||
|
importEntries.push({
|
||||||
|
ftnTag : tag,
|
||||||
|
name : `Area: ${tag}`,
|
||||||
|
uplinks : m[2].split(/[\s,]+/),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return importEntries;
|
||||||
|
}
|
||||||
|
|
||||||
function handleConfigCommand() {
|
function handleConfigCommand() {
|
||||||
if(true === argv.help) {
|
if(true === argv.help) {
|
||||||
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(argv.new) {
|
const action = argv._[1];
|
||||||
askNewConfigQuestions( (err, configPath, config) => {
|
|
||||||
if(err) {
|
switch(action) {
|
||||||
return;
|
case 'new' : return buildNewConfig();
|
||||||
}
|
case 'import-areas' : return importAreas();
|
||||||
|
|
||||||
config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t' } );
|
default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
||||||
|
}
|
||||||
try {
|
|
||||||
fs.writeFileSync(configPath, config, 'utf8');
|
|
||||||
console.info('Configuration generated');
|
|
||||||
} catch(e) {
|
|
||||||
console.error('Exception attempting to create config: ' + e.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
|
||||||
return nextFile(null);
|
return nextFile(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(`* Scanning ${fullPath}... `);
|
process.stdout.write(`Scanning ${fullPath}... `);
|
||||||
|
|
||||||
fileArea.scanFile(
|
fileArea.scanFile(
|
||||||
fullPath,
|
fullPath,
|
||||||
|
@ -134,28 +134,44 @@ function dumpAreaInfo(areaInfo, areaAndStorageInfo, cb) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dumpFileInfo(shaOrFileId, cb) {
|
function getSpecificFileEntry(pattern, cb) {
|
||||||
|
// spec: FILE_ID|SHA|PARTIAL_SHA
|
||||||
const FileEntry = require('../../core/file_entry.js');
|
const FileEntry = require('../../core/file_entry.js');
|
||||||
|
|
||||||
async.waterfall(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function getBySha(callback) {
|
function getByFileId(callback) {
|
||||||
FileEntry.findFileBySha(shaOrFileId, (err, fileEntry) => {
|
const fileId = parseInt(pattern);
|
||||||
return callback(null, fileEntry);
|
if(!/^[0-9]+$/.test(pattern) || isNaN(fileId)) {
|
||||||
|
return callback(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileEntry = new FileEntry();
|
||||||
|
fileEntry.load(fileId, () => {
|
||||||
|
return callback(null, fileEntry); // try sha
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function getByFileId(fileEntry, callback) {
|
function getBySha(fileEntry, callback) {
|
||||||
if(fileEntry) {
|
if(fileEntry) {
|
||||||
return callback(null, fileEntry); // already got it by sha
|
return callback(null, fileEntry); // already got it by sha
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileId = parseInt(shaOrFileId);
|
FileEntry.findFileBySha(pattern, (err, fileEntry) => {
|
||||||
if(isNaN(fileId)) {
|
return callback(err, fileEntry);
|
||||||
return callback(Errors.DoesNotExist('Not found'));
|
});
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
(err, fileEntry) => {
|
||||||
|
return cb(err, fileEntry);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fileEntry = new FileEntry();
|
function dumpFileInfo(shaOrFileId, cb) {
|
||||||
fileEntry.load(shaOrFileId, err => {
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function getEntry(callback) {
|
||||||
|
getSpecificFileEntry(shaOrFileId, (err, fileEntry) => {
|
||||||
return callback(err, fileEntry);
|
return callback(err, fileEntry);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -164,7 +180,8 @@ function dumpFileInfo(shaOrFileId, cb) {
|
||||||
|
|
||||||
console.info(`file_id: ${fileEntry.fileId}`);
|
console.info(`file_id: ${fileEntry.fileId}`);
|
||||||
console.info(`sha_256: ${fileEntry.fileSha256}`);
|
console.info(`sha_256: ${fileEntry.fileSha256}`);
|
||||||
console.info(`area_tag: ${fileEntry.areaTag}`);
|
console.info(`area_tag: ${fileEntry.areaTag}`);
|
||||||
|
console.info(`storage_tag: ${fileEntry.storageTag}`);
|
||||||
console.info(`path: ${fullPath}`);
|
console.info(`path: ${fullPath}`);
|
||||||
console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`);
|
console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`);
|
||||||
console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`);
|
console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`);
|
||||||
|
@ -185,30 +202,6 @@ function dumpFileInfo(shaOrFileId, cb) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
/*
|
|
||||||
FileEntry.findFileBySha(sha, (err, fileEntry) => {
|
|
||||||
if(err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = paths.join(fileArea.getAreaStorageDirectoryByTag(fileEntry.storageTag), fileEntry.fileName);
|
|
||||||
|
|
||||||
console.info(`file_id: ${fileEntry.fileId}`);
|
|
||||||
console.info(`sha_256: ${fileEntry.fileSha256}`);
|
|
||||||
console.info(`area_tag: ${fileEntry.areaTag}`);
|
|
||||||
console.info(`path: ${fullPath}`);
|
|
||||||
console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`);
|
|
||||||
console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`);
|
|
||||||
|
|
||||||
_.each(fileEntry.meta, (metaValue, metaName) => {
|
|
||||||
console.info(`${metaName}: ${metaValue}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(argv['show-desc']) {
|
|
||||||
console.info(`${fileEntry.desc}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayFileAreaInfo() {
|
function displayFileAreaInfo() {
|
||||||
|
@ -298,6 +291,126 @@ function scanFileAreas() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveFiles() {
|
||||||
|
//
|
||||||
|
// oputil fb move SRC [SRC2 ...] DST
|
||||||
|
//
|
||||||
|
// SRC: PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
|
||||||
|
// DST: AREA_TAG[@STORAGE_TAG]
|
||||||
|
//
|
||||||
|
if(argv._.length < 4) {
|
||||||
|
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveArgs = argv._.slice(2);
|
||||||
|
let src = getAreaAndStorage(moveArgs.slice(0, -1));
|
||||||
|
let dst = getAreaAndStorage(moveArgs.slice(-1))[0];
|
||||||
|
let FileEntry;
|
||||||
|
|
||||||
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function init(callback) {
|
||||||
|
return initConfigAndDatabases( err => {
|
||||||
|
if(!err) {
|
||||||
|
fileArea = require('../../core/file_base_area.js');
|
||||||
|
}
|
||||||
|
return callback(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function validateAndExpandSourceAndDest(callback) {
|
||||||
|
let srcEntries = [];
|
||||||
|
|
||||||
|
const areaInfo = fileArea.getFileAreaByTag(dst.areaTag);
|
||||||
|
if(areaInfo) {
|
||||||
|
dst.areaInfo = areaInfo;
|
||||||
|
} else {
|
||||||
|
return callback(Errors.DoesNotExist('Invalid or unknown destination area'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each SRC may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
|
||||||
|
FileEntry = require('../../core/file_entry.js');
|
||||||
|
|
||||||
|
async.eachSeries(src, (areaAndStorage, next) => {
|
||||||
|
//
|
||||||
|
// If this entry represents a area tag, it means *all files* in that area
|
||||||
|
//
|
||||||
|
const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
|
||||||
|
if(areaInfo) {
|
||||||
|
src.areaInfo = areaInfo;
|
||||||
|
|
||||||
|
const findFilter = {
|
||||||
|
areaTag : areaAndStorage.areaTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(areaAndStorage.storageTag) {
|
||||||
|
findFilter.storageTag = areaAndStorage.storageTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileEntry.findFiles(findFilter, (err, fileIds) => {
|
||||||
|
if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.each(fileIds, (fileId, nextFileId) => {
|
||||||
|
const fileEntry = new FileEntry();
|
||||||
|
fileEntry.load(fileId, err => {
|
||||||
|
if(!err) {
|
||||||
|
srcEntries.push(fileEntry);
|
||||||
|
}
|
||||||
|
return nextFileId(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// PATH|FILE_ID|SHA|PARTIAL_SHA
|
||||||
|
getSpecificFileEntry(areaAndStorage.pattern, (err, fileEntry) => {
|
||||||
|
if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
srcEntries.push(fileEntry);
|
||||||
|
return next(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return callback(err, srcEntries);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function moveEntries(srcEntries, callback) {
|
||||||
|
|
||||||
|
if(!dst.storageTag) {
|
||||||
|
dst.storageTag = dst.areaInfo.storageTags[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const destDir = FileEntry.getAreaStorageDirectoryByTag(dst.storageTag);
|
||||||
|
|
||||||
|
async.eachSeries(srcEntries, (entry, nextEntry) => {
|
||||||
|
const srcPath = entry.filePath;
|
||||||
|
const dstPath = paths.join(destDir, entry.fileName);
|
||||||
|
|
||||||
|
process.stdout.write(`Moving ${srcPath} => ${dstPath}... `);
|
||||||
|
|
||||||
|
FileEntry.moveEntry(entry, dst.areaTag, dst.storageTag, err => {
|
||||||
|
if(err) {
|
||||||
|
console.info(`Failed: ${err.message}`);
|
||||||
|
} else {
|
||||||
|
console.info('Done');
|
||||||
|
}
|
||||||
|
return nextEntry(null); // always try next
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
return callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function handleFileBaseCommand() {
|
function handleFileBaseCommand() {
|
||||||
if(true === argv.help) {
|
if(true === argv.help) {
|
||||||
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
|
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
|
||||||
|
@ -308,5 +421,8 @@ function handleFileBaseCommand() {
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case 'info' : return displayFileAreaInfo();
|
case 'info' : return displayFileAreaInfo();
|
||||||
case 'scan' : return scanFileAreas();
|
case 'scan' : return scanFileAreas();
|
||||||
|
case 'move' : return moveFiles();
|
||||||
|
|
||||||
|
default : return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ const usageHelp = exports.USAGE_HELP = {
|
||||||
|
|
||||||
global args:
|
global args:
|
||||||
--config PATH : specify config path (${getDefaultConfigPath()})
|
--config PATH : specify config path (${getDefaultConfigPath()})
|
||||||
|
--no-prompt : assume defaults/don't prompt for input where possible
|
||||||
|
|
||||||
where <command> is one of:
|
where <command> is one of:
|
||||||
user : user utilities
|
user : user utilities
|
||||||
|
@ -32,28 +33,39 @@ valid args:
|
||||||
`,
|
`,
|
||||||
|
|
||||||
Config :
|
Config :
|
||||||
`usage: optutil.js config <args>
|
`usage: optutil.js config <action> [<args>]
|
||||||
|
|
||||||
valid args:
|
where <action> is one of:
|
||||||
--new : generate a new/initial configuration
|
new : generate a new/initial configuration
|
||||||
|
import-areas PATH : import areas using fidonet *.NA or AREAS.BBS file from PATH
|
||||||
|
|
||||||
|
valid import-areas <args>:
|
||||||
|
--conf CONF_TAG : specify conference tag in which to import areas
|
||||||
|
--network NETWORK : specify network name/key to associate FTN areas
|
||||||
|
--uplinks UL1,UL2,... : specify one or more comma separated uplinks
|
||||||
|
--type TYPE : specifies area import type. valid options are "bbs" and "na"
|
||||||
`,
|
`,
|
||||||
FileBase :
|
FileBase :
|
||||||
`usage: oputil.js fb <action> [<args>] <AREA_TAG|SHA|FILE_ID[@STORAGE_TAG] ...> [<args>]
|
`usage: oputil.js fb <action> [<args>] <AREA_TAG|SHA|FILE_ID[@STORAGE_TAG] ...> [<args>]
|
||||||
|
|
||||||
where <action> is one of:
|
where <action> is one of:
|
||||||
scan AREA_TAG|SHA|FILE_ID : scan specified areas
|
scan AREA_TAG : scan specified areas
|
||||||
AREA_TAG may be suffixed with @STORAGE_TAG; for example: retro@bbs
|
AREA_TAG may be suffixed with @STORAGE_TAG; for example: retro@bbs
|
||||||
|
|
||||||
info AREA_TAG|FILE_ID|SHA : display information about areas and/or files
|
info AREA_TAG|SHA|FILE_ID : display information about areas and/or files
|
||||||
SHA may be a full or partial SHA-256
|
SHA may be a full or partial SHA-256
|
||||||
|
|
||||||
|
move SRC DST : move entry(s) from SRC to DST where:
|
||||||
|
SRC may be FILE_ID|SHA|AREA_TAG
|
||||||
|
DST may be AREA_TAG, optionally suffixed with @STORAGE_TAG; for example: retro@bbs
|
||||||
|
SHA may be a full or partial SHA-256
|
||||||
|
multiple instances of SRC may exist: SRC1 SRC2 ...
|
||||||
|
|
||||||
valid scan <args>:
|
valid scan <args>:
|
||||||
--tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries
|
--tags TAG1,TAG2,... : specify tag(s) to assign to discovered entries
|
||||||
|
|
||||||
valid info <args>:
|
valid info <args>:
|
||||||
--show-desc : display short description, if any
|
--show-desc : display short description, if any
|
||||||
|
|
||||||
|
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,6 @@ module.exports = function() {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return printUsageAndSetExitCode('', ExitCodes.BAD_COMMAND);
|
return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetE
|
||||||
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
const ExitCodes = require('./oputil_common.js').ExitCodes;
|
||||||
const argv = require('./oputil_common.js').argv;
|
const argv = require('./oputil_common.js').argv;
|
||||||
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
|
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
|
||||||
|
const getHelpFor = require('./oputil_help.js').getHelpFor;
|
||||||
|
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
@ -14,7 +15,7 @@ exports.handleUserCommand = handleUserCommand;
|
||||||
|
|
||||||
function handleUserCommand() {
|
function handleUserCommand() {
|
||||||
if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) {
|
if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) {
|
||||||
return printUsageAndSetExitCode('User', ExitCodes.ERROR);
|
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_.isString(argv.password)) {
|
if(_.isString(argv.password)) {
|
||||||
|
|
Loading…
Reference in New Issue