UNPKG

spase-resource-tools

Version:
214 lines (184 loc) 6.14 kB
#!/usr/bin/env node "use strict"; /** * Read a resource description and place it in an appriately named file and path * based on the ResourceID * * @author Todd King **/ const fs = require('fs'); const yargs = require('yargs'); const path = require('path'); const fastXmlParser = require('fast-xml-parser'); const walk = require('./walk-tree') // Formerly walk-folder-tree var options = yargs .version('1.0.2') .usage('Read a resource description and place it in an appropriately named file and path based on the ResourceID.') .usage('$0 [args] <files...>') .example('$0 example.xml', 'Place "example.xml" in an appropriately named file and path.') .epilog('copyright 2018') .showHelpOnFail(false, "Specify --help for available options") .help('h') // version .options({ // Verbose flag 'v' : { alias: 'verbose', describe : 'show information while processing files', type: 'boolean', default: false }, // help text 'h' : { alias : 'help', description: 'show information about the app.' }, // Recursively scan for files 'r' : { alias: 'recurse', describe : 'Recursively process all files starting at path.', type: 'boolean', default: false }, // Check Resource ID references 'k' : { alias: 'check', describe : 'Check files, but do not write collated output.', type: 'boolean', default: false }, // Base path 'b' : { alias: 'base', describe : 'Base path for collated output.', type: 'string', default: '.' }, // File name extensions 'x' : { alias: 'ext', describe : 'File name extension for filtering files when processing folders.', type: 'string', default: '.xml' }, }) .argv ; var args = options._; // Unprocessed command line arguments // Global variables var fileCnt = 0; var failCnt = 0; /** * @brief Create a full path to a resource using VxO rules. * * @param [in] base the base path to prepent to the authority/path portion of the resourceID. * @param [in] resourceID The resourceID to parse and munge. * * @return A file pathname. * * @details Create a full path to a resource using VxO rules. * The protocol is striped from the ResourceID and the base path * is preprended. */ function makeResourcePath(base, resourceID) { if(base == null || resourceID == null) return null; if(resourceID.indexOf("..") != -1) return null; // Gaurd against path spoofing var n = resourceID.indexOf("://"); if(n != -1) resourceID = resourceID.substring(n+3); return base + "/" + resourceID + ".xml"; } function makePath(pathname) { var path = pathname; var n = path.lastIndexOf('/'); if(n > 0) path = path.substring(0, n); if (!fs.existsSync(path)) { var dirName = ""; var pathSplit = path.split('/'); for (var index = 0; index < pathSplit.length; index++) { dirName += pathSplit[index]+'/'; if (!fs.existsSync(dirName)) fs.mkdirSync(dirName); } } } /** * @brief Scan a parsed (XML) document and find matching elements. * * @param [in] dom The document represented as a Javascript object * @param [in] pattern The Regex pattern to search for * @param [in] exclude Elements to exclude from search * @param [in] list List of elements already found. * * @return The list of values for all matching elements */ function findAll(dom, pattern, exclude, list) { if( ! dom ) return list; var fullList = (list === undefined ? [] : list); for(var name in dom) { // console.log(name); if( ! (exclude === undefined) && name.match(exclude) ) continue; // skip if( name.match(pattern) ) { fullList.push(dom[name]); } if( Array.isArray(dom[name]) ) { // Check each element of an array var item = dom[name]; for(var i = 0; i < item.length; i++) { findAll(item[i], pattern, exclude, fullList); } } if( typeof dom[name] === 'object') { // An item with children // console.log((typeof dom[name]) + " => " + name + ":" + Object.keys(dom[name]).length); findAll(dom[name], pattern, exclude, fullList); } } return fullList; } async function collateFile(pathname) { fileCnt++; pathname = pathname.replace(/\\/g, '/'); // Normalize path var xmlDoc = fs.readFileSync(pathname, 'utf8'); var xml = fastXmlParser.parse(xmlDoc); // Check syntax var id = findAll(xml, '^ResourceID$'); var resourcePath = makeResourcePath(options.base, id[0]); if(options.verbose) console.log( "Checking: " + pathname ); if(options.check) { // Check if file is in correct location if( resourcePath.startsWith('./') ) { // Make pathname match relative if( ! pathname.startsWith('/') ) { pathname = "./" + pathname; } } if(resourcePath != pathname) { failCnt++; console.log(" ERROR: " + pathname + " should be named " + resourcePath); } } else { // Copy/rename path to destination makePath(resourcePath); fs.writeFileSync(resourcePath, fs.readFileSync(pathname)); } } /** * @brief Perform task. * * @param [in] args command line arguments after processing options. * @return nothing */ function main(args) { // If no files or options show help if (args.length == 0) { yargs.showHelp(); return; } var includeFiles = new RegExp(options.ext.replace(/\./g, '\\.') + '$'); // literal dot (.) and ends with extension var includeFolders = /(^[.]$|^[^.])/; // ignore folders starting with ., except for '.' (current directory) var root = args[0]; if(fs.statSync(root).isDirectory()) { // Walk the tree walk(root, { filterFolders: includeFolders, filterFiles: includeFiles, recurse: options.recurse }, async function(params, cb) { if( ! params.directory ) { // A file - process var pathname = path.join(root, params.path); await collateFile(pathname); } cb(); }).then(function() { console.log(" SUMMARY: scanned: " + fileCnt + " files(s); " + failCnt + " failure(s)"); }); } else { // Single file collateFile(root); } } main(options._);