console.time('import load time');
// local modules
import isMainESM from './isMainESM.js';
// can just import wrapper function that call the others
import { isSafeWinPath, isFile } from './utils.js';
import {
cleanGlobSyncPipe,
cleanCwdSyncPipe,
cleanPathAsyncPipe
} from './pipe.js';
import { tweakTags } from './id3-tags.js';
// npm modules
import minimist from 'minimist';
import prompts from 'prompts';
import glob from 'glob';
// glob is CommonJS module and sync cannot be name imported?
// const { sync } = glob;
// standard library modules
import { parse, join, extname } from 'path';
// import { resolve, parse, join, sep, posix, win32, extname, normalize, basename, dirname, isAbsolute } from 'path';
// import { inspect } from 'util';
// import { homedir } from 'os';
console.timeEnd('import load time');
// --------------------------------------------------------------------------- //
/**
* Initiate module functions in sequence of clean, check, get, use.
*
* Is called after sanitising the command-line arguments provided.
* Accepts a path from the command line via minimist args.
*
* @async
* @function init
*/
const init = async () => {
// console.log('init 🚦');
// sanitise and handle args and get results object back
const args = await argsHandle();
// console.log('args: ', args);
// could use destructuring/spread to pull arg props out?
// free to use --path
const files = getFiles(args.path, args.ext, args.recurse);
console.log('files:', files);
// move on to mp3 handling (functionalise)
// || extname(args.path).toLowerCase() === '.mp3'
if (files.length && args.ext === '.mp3') {
console.log('extension is mp3');
if (args.tagType && args.tagReplace) {
console.log('passing to id3-tags..');
// CLI params for tag-type, find-str, and replace-str
tweakTags(files, args.tagType, args.tagFind || undefined, args.tagReplace);
}
}
};
// --------------------------------------------------------------------------- //
/**
* Handle command line arguments given to the main.js program.
*
* sanitises and handles the command-line arguments provided.
* Accepts a path from the command line via minimist args.
*
* @async
* @function argsHandle
*/
const argsHandle = async () => {
// must flatten these conditions somewhat...
// would be good to loop them based off common checks
// but some differences remain like maybe cwd
// atm glob is not working properly idk why exactly
// could be to do with awaits? but i doubt it
// i changed args
// defaults object for minimist arguments
const argDefaults = { ext: '' }; // recurse: ''
// instantiate minimist args parser
const args = minimist(process.argv.slice(2), { default: argDefaults });
console.log('minimist args:', args);
// handle extension arg (uses minimist default empty string)
// check if --ext (extension) flag was set
if (args.hasOwnProperty('ext') && args.ext && args.ext !== true) {
console.log('ext detected:', args.ext);
// add dot to beginning of ext if needed
if (args.ext[0] !== '.') {
args.ext = '.' + args.ext;
}
} else if (args.hasOwnProperty('path') && extname(args.path)) {
// if not set --ext but path has an extension use that
// extname() always returns a dot .{ext} if an extension exists
args.ext = extname(args.path);
} else {
console.log('ext error', args.ext);
}
// normalise to lower case
// (relies on glob search being case-insensitive?)
args.ext = args.ext.toLowerCase();
// check if --recurse flag was set
// this should just use minimist default args object at top of function
// if (args.recurse) {}
args.recurse =
args.hasOwnProperty('recurse') && args.recurse === true ? '/' : '';
// handle no path arg
if (!args.hasOwnProperty('path')) {
console.log(`cmd did not include '--path'`);
console.log('trying cwd..');
let cwd = await pipeHandle(process.cwd(), cleanCwdSyncPipe);
// if path value is falsy pipeHandle rejected path
// instead of if-conditioning this...
// what if all the pipes ended with a sorting function?
// the sorting function must make existence/value/type checks etc
// and handle the different errors, close gracefully etc
if (!cwd) {
// no --path and cwd is unsafe
console.error('[cwd error]', cwd);
console.log('calling prompt..');
args.path = await promptHandle();
return args;
}
// safe to use cwd as default
console.log('no --path and cwd is a safe default search location:', cwd);
args.path = cwd;
return args;
}
// handle minimist path arg edge case
// check if '--path' in args but is missing a value
// (minimist resolves missing value to true)
if (!args.path || args.path === true) {
console.error('[--path is falsy or empty]', args.path);
console.log('calling prompt..');
args.path = await promptHandle();
return args;
}
// both conditions above resolve to using cwd or a prompt
// --> assume this space in code is safe to check and use --path
// TEST PIPE FUNCTIONS HERE - cli-args are handled, path is uncleaned
args.path = await pipeHandle(args.path, cleanPathAsyncPipe);
if (!args.path) {
// pipeHandle error --> call prompt
console.error('[path handle error]', args.path);
console.log('calling prompt..');
args.path = await promptHandle();
return args;
}
// generalised return
return args;
};
// --------------------------------------------------------------------------- //
/**
* Runs composed sync and async pipes.
*
* run path cleaning and safety checking functions.
*
* @async
* @function pipeHandle
* @param {string} _path - The path to be piped.
* @param {function} pipe - The pipe function to use. (default to cleanPathAsyncPipe?)
* @return The clean path on success, false on error.
*/
const pipeHandle = async function (_path, pipe) {
try {
// pass in path to composed path cleaning pipe and return
_path = await pipe(_path);
} catch (err) {
console.error('[pipe error]', err);
return false;
}
// isSafeWinPath uses inOS and isSystemPath which rely on unix-path input
// && isSafeWinPath(_path)
if (_path) {
// console.log(`[pipeHandle] path is clean and safe: '${_path}'`);
return _path;
}
// else {
// console.error('[path is not clean and/or safe]');
// return false;
// }
};
// --------------------------------------------------------------------------- //
/**
* Uses prompts module to ask for a new path.
* @async
* @function promptHandle
* @param {string} message - Optional custom message to use in prompt.
* @return {string} The new path from prompt response.
*/
const promptHandle = async message => {
const prompt = await prompts({
type: 'text',
name: 'path',
message:
message ||
'Please enter a path to search or press enter to default to current directory',
validate: async _path => {
return !(await pipeHandle(_path, cleanPathAsyncPipe))
? `❌ Path is forbidden, please try again`
: true;
}
});
// MORE ESSENTIAL PROBLEM:
// promptHandle basically doesn't understand relative vs absolute
// and is occasionally bailed out by bash/shell path intellisense
// a prompted user should be able to use a relative path
// if one is detected, path must be concatentated:
// safe cwd + relative path + glob pattern
// catch empty path error
// ultimately, i could add a 'detectEmpty' function to the pipe
if (prompt.path === '' || prompt.path === '.' || prompt.path === './') {
const cwd = await pipeHandle(process.cwd(), cleanCwdSyncPipe);
// if cwd is false need to re-prompt for new path instead of just returning false...
prompt.path = cwd ? cwd : false;
}
// return the new path from user
return prompt.path;
};
// --------------------------------------------------------------------------- //
/**
* Search for files and get matching absolute paths.
* If _path is ommitted the current working directory is defaulted to.
* If extension is ommitted no file extension is added.
*
* @function getFiles
* @param {string} _path - The directory path to search for matches.
* @param {string} ext - {optional} The file extension (including the dot) to search for.
* @param {string} recurse - {optional} flag for recursive glob search.
* @return {array} The list of matching paths in posix format.
*/
const getFiles = (_path, ext, recurse) => {
// use glob ignore for hiding on the fly
// use inSysPath etc for prompting user
// (later) implement some kind of db/storage for this and other data
// are these case insensitive due to glob options? (test --> no they are sensitive)
const ignoreGlobs = [
// files
'**/*NTUSER*',
'**/*ntuser*',
'**/*.DAT',
'**/*.dat',
'**/*.SYS',
'**/*.sys',
'**/*msdownld.tmp',
'**/*Recovery.txt',
// folders
'**/AppData/**',
'**/Application Data/**',
'**/$AVG/**',
'**/$RECYCLE.BIN/**',
'**/$Recycle.Bin/**',
'**/System Volume Information/**',
'**/Windows/**',
'**/Application Data/**',
'**/Local Settings/**',
'**/ProgramData/**',
'**/Program Files/**',
'**/Program Files (x86)/**',
'**/Recovery/**',
'**/PerfLogs/**',
'**/Documents and Settings/**',
// likely convienent
'**/node_modules/**',
// glob error: Error: EPERM: operation not permitted, scandir 'C:/Config.Msi'
'C:/*Config*',
// filepaths specific to personal external HDD
// convienient non c-drive folder exclusions
'F:/Utility/**',
// attempt to handle system image scan errors
'F:/UMIT/OS/**'
];
// glob object
const options = {
ignore: ignoreGlobs,
// my approach to controlling error behaviour here is:
// this is a CLI tool for users, hide failure to access and move on
strict: false,
silent: true,
// if ext is set then need to only return files and not folders
// (avoids false-positive folders that end in an ext-like string)
nodir: ext ? true : false,
nocase: true,
// potential nocase + drive-letter issues:
// https://github.com/isaacs/node-glob/issues/42
// https://github.com/isaacs/node-glob/issues/123
statCache: true
};
// fix from: https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows
let pathRoot = parse(_path).root;
let noDrivePath = _path.slice(Math.max(pathRoot.length - 1, 0));
let pattern = '**/*';
// console.log(`_path: '${_path}'`);
// console.log(`pathRoot: '${pathRoot}'`);
// console.log(`noDrivePath: '${noDrivePath}'`);
// console.log(`recurse: '${recurse}'`);
// console.log(`pattern: '${pattern}'`);
// console.log(`old glob-string: '${_path}${recurse}${pattern}${ext}'`);
if (_path === '/') {
// c root search
console.log('c root-level search detected..');
if (recurse) {
// wipe path --> recurse + pattern '/**/*'
console.log(`..recurse detected --> editing _path..`);
_path = '';
} else {
// edit pattern to '*' --> path + recurse + pattern '/*'
console.log(`..recurse not detected --> editing pattern..`);
pattern = '*';
}
}
// detect non-c letter drive path root and apply fix
if (pathRoot[0] !== '/') {
// not c drive
console.log('non-c letter drive detected..');
if (noDrivePath === '/') {
console.log('..root-level search detected');
// wipe it cos config.cwd will provide it
noDrivePath = '';
// wipe pattern if non-c-root search isnt recursive
if (!recurse) {
console.log('..recurse not detected');
// console.log('old pattern: ', pattern);
pattern = '/*';
}
}
// apply fix
options.cwd = pathRoot;
_path = noDrivePath;
// now letter drive searches should work whether they are root or not
console.log(`set '${pathRoot}' as cwd and '${noDrivePath}' as path`);
}
// seperate path into ${parse(_path).dir}*/*${filename}*${ext} ?
// if someone wants to search X dir using Y substring, that'd be useful
// catch file-path
// in case of non-c path send isFile a combination path to include root
if (isFile(join(pathRoot, noDrivePath))) {
console.log('file-path detected');
// wipe all flag values
recurse = '';
pattern = '';
ext = '';
// supply just the file-path
_path = join(pathRoot, noDrivePath);
console.log('file-path:', _path);
}
// glob-string should be ready now
console.log(`new glob-string: '${_path}${recurse}${pattern}${ext}'`);
console.time('search execution time');
try {
// todo: make this and other sync calls async
const files = glob.sync(`${_path}${recurse}${pattern}${ext}`, options);
// console.log(
// '[passed glob.sync] path:',
// `${_path}${recurse}${pattern}${ext}`
// );
console.timeEnd('search execution time');
// if file(s) exist sanitise path(s)
if (files.length) {
// return files
return files.map(file => cleanGlobSyncPipe(file));
} else {
// no files found
console.log('no files found', files);
return false;
}
} catch (err) {
console.error('glob error:', err);
return false;
}
};
// --------------------------------------------------------------------------- //
// init pre-init check to see if this module was run directly
// is it clunky to have to pass in import.meta.url?
// how can isMainESM.js be refactored so its code is executed in this context?
if (isMainESM(import.meta.url)) {
// console.log('main.js called directly');
if (process.platform === 'win32') {
// proceed with main (only works on windows)
console.time('init() execution time');
init();
console.timeEnd('init() execution time');
// some functions can be used universally (seperate + document)
} else {
console.error('[platform is not win32] platform:', process.platform);
}
}
// else module wasn't called directly, some function of it was, so don't run init
// --------------------------------------------------------------------------- //
/**
* Windows filesystem utilities.
*
* Runs pipe-utils functions in sequence.
* Clean, check, search, get, clean, use.
*
* @module Main
*
* */
export { init, pipeHandle, promptHandle, getFiles };
// --------------------------------------------------------------------------- //