enigma-bbs/core/oputil/oputil_file_base.js

1209 lines
48 KiB
JavaScript

/* jslint node: true */
/* eslint-disable no-console */
'use strict';
const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode;
const ExitCodes = require('./oputil_common.js').ExitCodes;
const argv = require('./oputil_common.js').argv;
const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases;
const getHelpFor = require('./oputil_help.js').getHelpFor;
const {
getAreaAndStorage,
looksLikePattern,
getConfigPath,
getAnswers,
writeConfig,
} = require('./oputil_common.js');
const Errors = require('../enig_error.js').Errors;
const async = require('async');
const fs = require('graceful-fs');
const paths = require('path');
const _ = require('lodash');
const moment = require('moment');
const inq = require('inquirer');
const glob = require('glob');
const sanatizeFilename = require('sanitize-filename');
const hjson = require('hjson');
const { mkdirs } = require('fs-extra');
exports.handleFileBaseCommand = handleFileBaseCommand;
/*
:TODO:
Global options:
--yes: assume yes
--no-prompt: try to avoid user input
Prompt for import and description before scan
* Only after finding duplicate-by-path
* Default to filename -> desc if auto import
*/
let fileArea; // required during init
function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) {
async.series(
[
function getDescFromHandlerIfNeeded(callback) {
if (
fileEntry.desc &&
fileEntry.descSrc != 'fileName' &&
fileEntry.desc.length > 0 &&
!argv['desc-file']
) {
return callback(null); // we have a desc already and are NOT overriding with desc file
}
if (!descHandler) {
return callback(null); // not much we can do!
}
const desc = descHandler.getDescription(fileEntry.fileName);
if (desc) {
fileEntry.desc = desc;
}
return callback(null);
},
function getDescFromUserIfNeeded(callback) {
if (fileEntry.desc && fileEntry.desc.length > 0) {
return callback(null);
}
const getDescFromFileName =
require('../../core/file_base_area.js').getDescFromFileName;
const descFromFile = getDescFromFileName(fileEntry.fileName);
if (false === argv.prompt) {
fileEntry.desc = descFromFile;
return callback(null);
}
const questions = [
{
name: 'desc',
message: `Description for ${fileEntry.fileName}:`,
type: 'input',
default: descFromFile,
},
];
inq.prompt(questions).then(answers => {
fileEntry.desc = answers.desc;
return callback(null);
});
},
function persist(callback) {
fileEntry.persist(isUpdate, err => {
return callback(err);
});
},
],
err => {
return cb(err);
}
);
}
const SCAN_EXCLUDE_FILENAMES = ['DESCRIPT.ION', 'FILES.BBS', 'ALLFILES.TXT'];
function loadDescHandler(path, cb) {
const handlerClassFromFileName = {
'descript.ion': require('../../core/descript_ion_file.js'),
'files.bbs': require('../../core/files_bbs_file.js'),
}[paths.basename(path).toLowerCase()];
if (!handlerClassFromFileName) {
return cb(
Errors.DoesNotExist(`No handlers registered for ${paths.basename(path)}`)
);
}
handlerClassFromFileName.createFromFile(path, (err, descHandler) => {
return cb(err, descHandler);
});
}
//
// Try to find a suitable description handler by
// checking for common filenames.
//
function findSuitableDescHandler(basePath, cb) {
const commonFiles = ['FILES.BBS', 'DESCRIPT.ION'];
async.eachSeries(
commonFiles,
(fileName, nextFileName) => {
loadDescHandler(paths.join(basePath, fileName), (err, handler) => {
if (!err && handler) {
return cb(null, handler);
}
return nextFileName(null);
});
},
() => {
return cb(Errors.DoesNotExist('No suitable description handler available'));
}
);
}
function scanFileAreaForChanges(areaInfo, options, cb) {
const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => {
return options.areaAndStorageInfo.find(asi => {
return !asi.storageTag || sl.storageTag === asi.storageTag;
});
});
function updateTags(fe) {
if (Array.isArray(options.tags)) {
fe.hashTags = new Set(options.tags);
} else if (areaInfo.hashTags) {
// no explicit tags; merge in defaults, if any
fe.hashTags = areaInfo.hashTags;
}
}
const FileEntry = require('../file_entry.js');
const readDir = options.glob
? (dir, next) => {
return glob(options.glob, { cwd: dir, nodir: true }, next);
}
: (dir, next) => {
return fs.readdir(dir, next);
};
async.eachSeries(
storageLocations,
(storageLoc, nextLocation) => {
async.waterfall(
[
function initDescFile(callback) {
if (options.descFileHandler) {
return callback(null, options.descFileHandler); // we're going to use the global handler
}
findSuitableDescHandler(storageLoc.dir, (err, descHandler) => {
return callback(null, descHandler);
});
},
function scanPhysFiles(descHandler, callback) {
const physDir = storageLoc.dir;
readDir(physDir, (err, files) => {
if (err) {
return callback(err);
}
async.eachSeries(
files,
(fileName, nextFile) => {
const fullPath = paths.join(physDir, fileName);
if (
SCAN_EXCLUDE_FILENAMES.includes(
fileName.toUpperCase()
)
) {
console.info(`Excluding ${fullPath}`);
return nextFile(null);
}
fs.stat(fullPath, (err, stats) => {
if (err) {
// :TODO: Log me!
return nextFile(null); // always try next file
}
if (!stats.isFile()) {
return nextFile(null);
}
process.stdout.write(`Scanning ${fullPath}... `);
async.series([
function quickCheck(next) {
if (options['full']) {
return next(null);
}
FileEntry.quickCheckExistsByPath(
fullPath,
(err, exists) => {
if (exists) {
console.info('Dupe');
return nextFile(null);
}
return next(null);
}
);
},
function fullScan() {
fileArea.scanFile(
fullPath,
{
areaTag: areaInfo.areaTag,
storageTag: storageLoc.storageTag,
hashTags: areaInfo.hashTags,
},
(stepInfo, next) => {
if (argv.verbose) {
if (stepInfo.error) {
console.error(
` error: ${stepInfo.error}`
);
} else {
console.info(
` processing: ${stepInfo.step}`
);
}
}
return next(null);
},
(err, fileEntry, dupeEntries) => {
if (err) {
console.info(
`Error: ${err.message}`
);
return nextFile(null); // try next anyway
}
//
// We'll update the entry if the following conditions are met:
// * We have a single duplicate, and:
// * --update was passed or the existing entry's desc,
// longDesc, or est_release_year meta are blank/empty
//
if (
argv.update &&
1 === dupeEntries.length
) {
const FileEntry = require('../../core/file_entry.js');
const existingEntry =
new FileEntry();
return existingEntry.load(
dupeEntries[0].fileId,
err => {
if (err) {
console.info(
'Dupe (cannot update)'
);
return nextFile(
null
);
}
//
// Update only if tags or desc changed
//
const optTags =
Array.isArray(
options.tags
)
? new Set(
options.tags
)
: existingEntry.hashTags;
const tagsEq =
_.isEqual(
optTags,
existingEntry.hashTags
);
let descSauceCompare;
if (
existingEntry.meta
.desc_sauce
) {
descSauceCompare =
JSON.stringify(
existingEntry
.meta
.desc_sauce
);
}
if (
tagsEq &&
fileEntry.desc ===
existingEntry.desc &&
fileEntry.descLong ===
existingEntry.descLong &&
fileEntry.meta
.est_release_year ===
existingEntry
.meta
.est_release_year &&
fileEntry.meta
.desc_sauce ===
descSauceCompare
) {
console.info(
'Dupe'
);
return nextFile(
null
);
}
console.info(
'Dupe (updating)'
);
// don't allow overwrite of values if new version is blank
existingEntry.desc =
fileEntry.desc ||
existingEntry.desc;
existingEntry.descLong =
fileEntry.descLong ||
existingEntry.descLong;
if (
fileEntry.meta
.est_release_year
) {
existingEntry.meta.est_release_year =
fileEntry.meta.est_release_year;
}
if (
fileEntry.meta
.desc_sauce
) {
existingEntry.meta.desc_sauce =
fileEntry.meta.desc_sauce;
}
updateTags(
existingEntry
);
finalizeEntryAndPersist(
true,
existingEntry,
descHandler,
err => {
return nextFile(
err
);
}
);
}
);
} else if (
dupeEntries.length > 0
) {
console.info('Dupe');
return nextFile(null);
}
console.info('Done!');
updateTags(fileEntry);
finalizeEntryAndPersist(
false,
fileEntry,
descHandler,
err => {
return nextFile(err);
}
);
}
);
},
]);
});
},
err => {
return callback(err);
}
);
});
},
function scanDbEntries(callback) {
// :TODO: Look @ db entries for area that were *not* processed above
return callback(null);
},
],
err => {
return nextLocation(err);
}
);
},
err => {
return cb(err);
}
);
}
function dumpAreaInfo(areaInfo, areaAndStorageInfo, cb) {
console.info(`areaTag: ${areaInfo.areaTag}`);
console.info(`name: ${areaInfo.name}`);
console.info(`desc: ${areaInfo.desc}`);
areaInfo.storage.forEach(si => {
console.info(`storageTag: ${si.storageTag} => ${si.dir}`);
});
console.info('');
return cb(null);
}
function getFileEntries(pattern, cb) {
// spec: FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA
const FileEntry = require('../../core/file_entry.js');
async.waterfall(
[
function tryByFileId(callback) {
const fileId = parseInt(pattern);
if (!/^[0-9]+$/.test(pattern) || isNaN(fileId)) {
return callback(null, null); // try SHA
}
const fileEntry = new FileEntry();
fileEntry.load(fileId, err => {
return callback(null, err ? null : [fileEntry]);
});
},
function tryByShaOrPartialSha(entries, callback) {
if (entries) {
return callback(null, entries); // already got it by FILE_ID
}
FileEntry.findBySha(pattern, (err, fileEntry) => {
return callback(null, fileEntry ? [fileEntry] : null);
});
},
function tryByFileNameWildcard(entries, callback) {
if (entries) {
return callback(null, entries); // already got by FILE_ID|SHA
}
return FileEntry.findByFileNameWildcard(pattern, callback);
},
],
(err, entries) => {
return cb(err, entries);
}
);
}
function dumpFileInfo(shaOrFileId, cb) {
async.waterfall(
[
function getEntry(callback) {
getFileEntries(shaOrFileId, (err, entries) => {
if (err) {
return callback(err);
}
return callback(null, entries[0]);
});
},
function dumpInfo(fileEntry, callback) {
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(`storage_tag: ${fileEntry.storageTag}`);
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}`);
}
console.info('');
return callback(null);
},
],
err => {
return cb(err);
}
);
}
function displayFileOrAreaInfo() {
// AREA_TAG[@STORAGE_TAG]
// SHA256|PARTIAL|FILE_ID|FILENAME_WILDCARD
// if sha: dump file info
// if area/storage dump area(s) +
async.series(
[
function init(callback) {
return initConfigAndDatabases(callback);
},
function dumpInfo(callback) {
const sysConfig = require('../../core/config.js').get();
let suppliedAreas = argv._.slice(2);
if (!suppliedAreas || 0 === suppliedAreas.length) {
suppliedAreas = _.map(
sysConfig.fileBase.areas,
(areaInfo, areaTag) => areaTag
);
}
const areaAndStorageInfo = getAreaAndStorage(suppliedAreas);
fileArea = require('../../core/file_base_area.js');
async.eachSeries(
areaAndStorageInfo,
(areaAndStorage, nextArea) => {
const areaInfo = fileArea.getFileAreaByTag(
areaAndStorage.areaTag
);
if (areaInfo) {
return dumpAreaInfo(areaInfo, areaAndStorageInfo, nextArea);
} else {
return dumpFileInfo(areaAndStorage.areaTag, nextArea);
}
},
err => {
return callback(err);
}
);
},
],
err => {
if (err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
}
}
);
}
function scanFileAreas() {
const options = {};
const tags = argv.tags;
if (tags) {
options.tags = tags.split(',');
}
options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH
options['full'] = argv.full;
options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2));
const last = argv._[argv._.length - 1];
if (options.areaAndStorageInfo.length > 1 && looksLikePattern(last)) {
options.glob = last;
options.areaAndStorageInfo.length -= 1;
}
async.series(
[
function init(callback) {
return initConfigAndDatabases(callback);
},
function initMime(callback) {
return require('../../core/mime_util.js').startup(callback);
},
function initGlobalDescHandler(callback) {
//
// If options.descFile is a String, it represents a FILE|PATH. We'll init
// the description handler now. Else, we'll attempt to look for a description
// file in each storage location.
//
if (!_.isString(options.descFile)) {
return callback(null);
}
loadDescHandler(options.descFile, (err, descHandler) => {
options.descFileHandler = descHandler;
return callback(null);
});
},
function scanAreas(callback) {
fileArea = require('../../core/file_base_area');
// Further expand any wildcards
let areaAndStorageInfoExpanded = [];
options.areaAndStorageInfo.forEach(info => {
if (info.areaTag.indexOf('*') > -1) {
const areas = fileArea.getFileAreasByTagWildcardRule(
info.areaTag
);
areas.forEach(area => {
areaAndStorageInfoExpanded.push(
Object.assign({}, info, {
areaTag: area.areaTag,
})
);
});
} else {
areaAndStorageInfoExpanded.push(info);
}
});
options.areaAndStorageInfo = areaAndStorageInfoExpanded;
async.eachSeries(
options.areaAndStorageInfo,
(areaAndStorage, nextAreaTag) => {
const areaInfo = fileArea.getFileAreaByTag(
areaAndStorage.areaTag
);
if (!areaInfo) {
return nextAreaTag(
new Error(
`Invalid file base area tag: ${areaAndStorage.areaTag}`
)
);
}
console.info(`Processing area "${areaInfo.name}":`);
scanFileAreaForChanges(areaInfo, options, err => {
return nextAreaTag(err);
});
},
err => {
return callback(err);
}
);
},
],
err => {
if (err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
}
}
);
}
function expandFileTargets(targets, cb) {
let entries = [];
// Each entry may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
const FileEntry = require('../../core/file_entry.js');
async.eachSeries(
targets,
(areaAndStorage, next) => {
const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag);
if (areaInfo) {
// AREA_TAG[@STORAGE_TAG] - all files in area@tag
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) {
entries.push(fileEntry);
}
return nextFileId(err);
});
},
err => {
return next(err);
}
);
});
} else {
// FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA
// :TODO: FULL_PATH -> entries
getFileEntries(areaAndStorage.pattern, (err, fileEntries) => {
if (err) {
return next(err);
}
entries = entries.concat(fileEntries);
return next(null);
});
}
},
err => {
return cb(err, entries);
}
);
}
function moveFiles() {
//
// oputil fb move SRC [SRC2 ...] DST
//
// SRC: FILENAME_WC|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);
const src = getAreaAndStorage(moveArgs.slice(0, -1));
const 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) {
const areaInfo = fileArea.getFileAreaByTag(dst.areaTag);
if (areaInfo) {
dst.areaInfo = areaInfo;
} else {
return callback(
Errors.DoesNotExist('Invalid or unknown destination area')
);
}
FileEntry = require('../../core/file_entry.js');
expandFileTargets(src, (err, srcEntries) => {
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);
}
);
},
],
err => {
if (err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
}
}
);
}
function removeFiles() {
//
// oputil fb rm|remove|del|delete SRC [SRC2 ...]
//
// SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG]
//
// AREA_TAG[@STORAGE_TAG] remove all entries matching
// supplied area/storage tags
//
// --phys-file removes backing physical file(s)
//
if (argv._.length < 3) {
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
}
const removePhysFile = argv['phys-file'];
const src = getAreaAndStorage(argv._.slice(2));
async.waterfall(
[
function init(callback) {
return initConfigAndDatabases(err => {
if (!err) {
fileArea = require('../../core/file_base_area.js');
}
return callback(err);
});
},
function expandSources(callback) {
expandFileTargets(src, (err, srcEntries) => {
return callback(err, srcEntries);
});
},
function removeEntries(srcEntries, callback) {
const FileEntry = require('../../core/file_entry.js');
const extraOutput = removePhysFile ? ' (including physical file)' : '';
async.eachSeries(
srcEntries,
(entry, nextEntry) => {
process.stdout.write(
`Removing ${entry.filePath}${extraOutput}... `
);
FileEntry.removeEntry(entry, { removePhysFile }, err => {
if (err) {
console.info(`Failed: ${err.message}`);
} else {
console.info('Done');
}
return nextEntry(err);
});
},
err => {
return callback(err);
}
);
},
],
err => {
if (err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
}
}
);
}
function getFileBaseImportType(path) {
if (argv.type) {
return argv.type.toLowerCase();
}
return paths.extname(path).substr(1).toLowerCase(); // zxx, ...
}
function importFileAreas() {
//
// FILEGATE.ZXX "RAID" format currently the only supported format.
//
// See http://www.filegate.net/info/filegate.zxx
// ...same format as FILEBONE.NA:
// http://wiki.mysticbbs.com/doku.php?id=mutil_import_filebone_na
//
const importPath = argv._[argv._.length - 1];
if (argv._.length < 3 || !importPath || 0 === importPath.length) {
return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR);
}
const importType = getFileBaseImportType(importPath);
if (!['zxx', 'na'].includes(importType)) {
return console.error(`"${importType}" is not a recognized import file type`);
}
const createDirs = argv['create-dirs'];
// :TODO: --base-dir (override config base/relative dir; use full paths)
async.waterfall(
[
callback => {
fs.readFile(importPath, 'utf8', (err, importData) => {
if (err) {
return callback(err);
}
const importInfo = {
storageTags: {},
areas: {},
count: 0,
};
const re = /Area\s+([^\s]+)\s+[0-9]\s+(?:!|\*&)\s+([^\r\n]+)/gm;
let m;
while ((m = re.exec(importData))) {
const dir = m[1].trim();
const name = m[2].trim();
const safeName = sanatizeFilename(name);
const stPrefix = _.snakeCase(sanatizeFilename(safeName));
const storageTag = `${stPrefix}__${_.snakeCase(
sanatizeFilename(dir)
)}`;
const areaTag = _.snakeCase(safeName);
if (!dir || !name || !storageTag || !areaTag) {
console.info(`Skipping entry: ${m[0]}`);
continue;
}
importInfo.storageTags[storageTag] = dir;
importInfo.areas[areaTag] = {
name: name,
desc: name,
storageTags: [storageTag],
};
++importInfo.count;
}
if (0 === importInfo.count) {
return callback(new Error('Nothing to import'));
}
return callback(null, importInfo);
});
},
(importInfo, callback) => {
return initConfigAndDatabases(err => {
return callback(err, importInfo);
});
},
(importInfo, callback) => {
console.info(`Read to import the following ${importInfo.count} areas:`);
console.info('');
_.each(importInfo.areas, (area, areaTag) => {
console.info(`${area.name} (${areaTag}):`);
const dir = importInfo.storageTags[area.storageTags[0]];
console.info(` storage: ${area.storageTags[0]} => ${dir}`);
});
getAnswers(
[
{
name: 'proceed',
message: 'Proceed?',
type: 'confirm',
},
],
answers => {
if (answers.proceed) {
return callback(null, importInfo);
}
return callback(Errors.General('User canceled'));
}
);
},
(importInfo, callback) => {
fs.readFile(getConfigPath(), 'utf8', (err, configData) => {
if (err) {
return callback(err);
}
let config;
try {
config = hjson.rt.parse(configData);
} catch (e) {
return callback(e);
}
return callback(null, importInfo, config);
});
},
(importInfo, config, callback) => {
const newStorageTagDirs = [];
_.each(importInfo.areas, (area, areaTag) => {
const existingArea = _.get(config, ['fileBase', 'areas', areaTag]);
if (existingArea) {
return console.info(
`Skipping ${area.name}. Area tag "${areaTag}" already exists.`
);
}
const storageTag = area.storageTags[0];
const existingStorageTag = _.get(config, [
'fileBase',
'storageTags',
storageTag,
]);
if (existingStorageTag) {
return console.info(
`Skipping ${area.name} (${areaTag}). Storage tag "${storageTag}" already exists`
);
}
const dir = importInfo.storageTags[storageTag];
newStorageTagDirs.push(dir);
config.fileBase.storageTags[storageTag] = dir;
config.fileBase.areas[areaTag] = area;
});
return callback(null, newStorageTagDirs, config);
},
(newStorageTagDirs, config, callback) => {
if (!createDirs) {
return callback(null, config);
}
//
// Create all directories
//
const prefixDir = config.fileBase.areaStoragePrefix;
async.eachSeries(
newStorageTagDirs,
(dir, nextDir) => {
const isAbs = paths.isAbsolute(dir);
if (!isAbs) {
dir = paths.join(prefixDir, dir);
}
mkdirs(dir, err => {
if (!err) {
console.log(`Created ${dir}`);
}
return nextDir(err);
});
},
err => {
return callback(err, config);
}
);
},
(config, callback) => {
const written = writeConfig(config, getConfigPath());
return callback(written ? null : new Error('Failed to write config!'));
},
],
err => {
if (err) {
return console.error(err.reason ? err.reason : err.message);
}
console.info('Import complete.');
console.info(`You may wish to validate changes made to ${getConfigPath()}`);
}
);
}
function setFileDescription() {
//
// ./oputil.js fb set-desc CRITERIA # will prompt
// ./oputil.js fb set-desc CRITERIA "The new description"
//
let fileCriteria;
let desc;
if (argv._.length > 3) {
fileCriteria = argv._[argv._.length - 2];
desc = argv._[argv._.length - 1];
} else {
fileCriteria = argv._[argv._.length - 1];
}
async.waterfall(
[
callback => {
return initConfigAndDatabases(callback);
},
callback => {
getFileEntries(fileCriteria, (err, entries) => {
if (err) {
return callback(err);
}
if (entries.length > 1) {
return callback(Errors.General('Criteria not specific enough.'));
}
return callback(null, entries[0]);
});
},
(fileEntry, callback) => {
if (desc) {
return callback(null, fileEntry, desc);
}
getAnswers(
[
{
name: 'userDesc',
message: 'Description:',
type: 'editor',
},
],
answers => {
if (!answers.userDesc) {
return callback(Errors.General('User canceled'));
}
return callback(null, fileEntry, answers.userDesc);
}
);
},
(fileEntry, newDesc, callback) => {
fileEntry.desc = newDesc;
fileEntry.persist(true, err => {
// true=isUpdate
return callback(err);
});
},
],
err => {
if (err) {
process.exitCode = ExitCodes.ERROR;
console.error(err.message);
} else {
console.info('Description updated.');
}
}
);
}
function handleFileBaseCommand() {
function errUsage() {
return printUsageAndSetExitCode(
getHelpFor('FileBase') + getHelpFor('FileOpsInfo'),
ExitCodes.ERROR
);
}
if (true === argv.help) {
return errUsage();
}
const action = argv._[1];
return (
{
info: displayFileOrAreaInfo,
scan: scanFileAreas,
mv: moveFiles,
move: moveFiles,
rm: removeFiles,
remove: removeFiles,
del: removeFiles,
delete: removeFiles,
'import-areas': importFileAreas,
desc: setFileDescription,
description: setFileDescription,
}[action] || errUsage
)();
}