Created
August 26, 2025 10:43
-
-
Save joelbarba/95f5ec95dec6695642e143f5333833df to your computer and use it in GitHub Desktop.
Node.js terminal tool for linux folder assisted copy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'fs'; | |
import path from 'path'; | |
import process from 'process'; | |
import { exec } from 'child_process'; | |
import readline from 'readline'; | |
// const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); | |
// function ask(text = '') { return new Promise((resolve) => rl.question(text, (answer) => resolve(answer))); } | |
readline.emitKeypressEvents(process.stdin); | |
if (process.stdin.setRawMode != null) { process.stdin.setRawMode(true); } | |
const excludeDirs = [ | |
{ name: 'node_modules', isExcluded: true }, | |
{ name: '.git', isExcluded: true }, | |
{ name: '.env', isExcluded: true }, | |
{ name: '.angular', isExcluded: true }, | |
{ name: '.next', isExcluded: true }, | |
{ name: 'bin', isExcluded: false }, | |
{ name: 'cache', isExcluded: true }, | |
]; | |
/******************************************************************************************* | |
Param1: Target folder to copy | |
Param2: Base destination path where to copy | |
A new folder with the name of the original folder will be created on "Param2" path. | |
Add to .bashrc: alias copy="node ~/joel_scripts/copy.js $1 $2" | |
Example: | |
copy /home/barba/DEV/JB-PIANO /media/DISK12/PROGRAMES_PROPIS --run | |
This will create a new folder ------> /media/DISK12/PROGRAMES_PROPIS/JB-PIANO and copy *.* within | |
*******************************************************************************************/ | |
let fromPath = process.argv[2] || process.cwd(); // Full path of the directory to copy | |
let toPath = process.argv[3] || process.cwd(); // Path of the destination where the copy is going to be created | |
let autoRun = process.argv[4] === '--run'; // If present, it automatically starts the copy | |
// If not present, the menu is shown and you can navigate the options | |
if (fromPath.at(-1) === '/') { fromPath = fromPath.slice(0, -1); } // If present, remove the last '/' | |
if (toPath.at(-1) === '/') { toPath = toPath.slice(0, -1); } // If present, remove the last '/' | |
if (fromPath[0] !== '/') { fromPath = '/' + fromPath; } | |
if (!dirExist(fromPath)) { | |
fromPath = process.cwd() + fromPath; | |
if (!dirExist(fromPath)) { fromPath = process.cwd(); } | |
} | |
// ------------------------------------------------------------------------------------------------------------ | |
process.stdin.on('keypress', (str, key) => { | |
if (key.name === 'c' && key.ctrl) { | |
// console.clear(); | |
move(1, height + 14); | |
process.exit(0); | |
} | |
// if (isBusy) { return; } | |
// print(key.name, 120, 0); | |
// print('op = ' + op + ', status = ' + status, 30, 60); | |
// if (key.name === 'return') { updateStatus(status + 1); } | |
// if (key.name === 'escape') { updateStatus(status - 1); } | |
if (status === 1 || status === 2 || status === 3) { | |
if (key.name === 'right') { | |
if (status === 1) { status = 2; printAll(); return; } | |
if (status === 2) { status = 3; printAll(); return; } | |
} | |
if (key.name === 'left') { | |
if (status === 2) { status = 1; printAll(); return; } | |
if (status === 3) { status = 2; printAll(); return; } | |
} | |
} | |
if (status === 1) { // Options | |
if (key.name === 'up') { printOptions(op - 1); } | |
if (key.name === 'down') { printOptions(op + 1); } | |
if (key.name === 'space' || key.name === 'return') { | |
if (op === 1) { return preCopyScan(); } // Start Copy | |
if (op === 2) { analyseDir(fromPath, 100); } // Analyze Dir | |
if (op === 3) { | |
showHidden = !showHidden; | |
cdDirA(fromPath); | |
cdDirB(toPath); | |
printOptions(); | |
} | |
if (op > 3) { excludeDirs[op - 4].isExcluded = !excludeDirs[op - 4].isExcluded; printOptions(); } | |
} | |
} | |
if (status === 2) { | |
if (key.name === 'up') { moveCursorUp( 1, 'A'); } | |
if (key.name === 'down') { moveCursorDown(1, 'A'); } | |
if (key.name === 'pageup') { moveCursorUp( 15, 'A'); } | |
if (key.name === 'pagedown') { moveCursorDown(15, 'A'); } | |
if (key.name === 'home') { moveCursorUp( fileListA.length, 'A'); } | |
if (key.name === 'end') { moveCursorDown(fileListA.length, 'A'); } | |
if (key.name === 'return') { | |
if (selA?.type === 'dir') { cdDirA(fromPath + '/' + selA.name); } | |
else if (selA?.type === 'back') { moveDirUp(fromPath, cdDirA); } | |
} | |
} | |
if (status === 3) { | |
if (key.name === 'up') { moveCursorUp( 1, 'B'); } | |
if (key.name === 'down') { moveCursorDown(1, 'B'); } | |
if (key.name === 'pageup') { moveCursorUp( 15, 'B'); } | |
if (key.name === 'pagedown') { moveCursorDown(15, 'B'); } | |
if (key.name === 'home') { moveCursorUp( fileListB.length, 'B'); } | |
if (key.name === 'end') { moveCursorDown(fileListB.length, 'B'); } | |
if (key.name === 'return') { | |
if (selB?.type === 'dir') { cdDirB(toPath + '/' + selB.name); } | |
else if (selB?.type === 'back') { moveDirUp(toPath, cdDirB); } | |
} | |
} | |
if (status === 5) { // Confirm option | |
if (key.name === 'right') { op = 2; printConfirm(); } | |
if (key.name === 'left') { op = 1; printConfirm(); } | |
if (key.name === 'return') { | |
if (op === 2) { return backToStart(); } // cancel | |
if (op === 1) { return startCopy(); } | |
} | |
} | |
if (status === 7) { // Copy file error confirm | |
if (key.name === 'return') { errorFile = null; return copyFiles(); } | |
if (key.name === 's') { errorFile.copied = true; return copyFiles(); } | |
if (key.name === 'y') { errorFile.copied = true; return copyFiles('ignore'); } | |
if (key.name === 'escape') { return backToStart(); } | |
} | |
}); | |
function getColParams(colName) { | |
const selInd = colName === 'A' ? selIndA : selIndB; | |
const scroll = colName === 'A' ? scrollA : scrollB; | |
const hoverFile = colName === 'A' ? hoverFileA : hoverFileB; | |
const scrollList = colName === 'A' ? scrollListA : scrollListB; | |
const printDirFrom = colName === 'A' ? printDirFromA : printDirFromB; | |
const fileList = colName === 'A' ? fileListA : fileListB; | |
return { selInd, scroll, hoverFile, scrollList, printDirFrom, fileList }; | |
} | |
function moveCursorUp(jump = 1, colName) { | |
const { selInd, scroll, hoverFile, scrollList, printDirFrom } = getColParams(colName); | |
const maxJump = selInd; | |
if (jump > maxJump) { jump = maxJump; } | |
if (jump > 0) { | |
const diff = scroll - (selInd - jump); | |
if (diff > 0) { scrollList(-diff); } // scroll up | |
hoverFile(selInd - jump); | |
printDirFrom(); | |
} | |
} | |
function moveCursorDown(jump = 1, colName) { | |
const { selInd, scroll, hoverFile, scrollList, printDirFrom, fileList } = getColParams(colName); | |
const maxJump = fileList.length - selInd - 1; | |
// print(`selInd=${selInd}, scroll=${scroll}, height=${height}, fileList[].lenght=${fileList.length}, jump=${jump}, maxJump=${maxJump} `, 0, 41); | |
if (jump > maxJump) { jump = maxJump; } | |
if (jump > 0) { | |
const diff = (selInd + jump) - scroll - height; | |
if (diff > 0) { scrollList(diff); } // scroll down | |
hoverFile(selInd + jump); | |
printDirFrom(); | |
// print(`diff=${diff} `, 0, 43); | |
} | |
} | |
function moveDirUp(path, cdDir) { // Select the parent directory | |
const pArr = path.split('/'); | |
pArr.pop(); | |
cdDir(pArr.join('/')); | |
} | |
function hoverFileA(index) { selIndA = index; selA = fileListA[selIndA]; } | |
function hoverFileB(index) { selIndB = index; selB = fileListB[selIndB]; } | |
function scrollListA(diff) { scrollA += diff; } | |
function scrollListB(diff) { scrollB += diff; } | |
// ------------------------------------------------------------------------------------------------------------ | |
let fileListA = []; // List of files/dirs of the current selected path | |
let selIndA = 0; // Index of currFileList for the current selected file | |
let selA = null; // Current selected item from currFileList ---> currSel = currFileList[selInd] | |
let scrollA = 0; // Scroll of the box list | |
let fileListB = []; | |
let selIndB = 0; | |
let selB = null; | |
let scrollB = 0; | |
let showHidden = false; | |
let op = 1; // current option from status 1 | |
let status = 1; // 1 = options, 2 = from files, 3 = to files, | |
// 4 = scanning (pre copy), | |
// 5 = scan and confirm (pre copy), | |
// 6 = copy progress, | |
// 7 = copy error stop, | |
// 8 = finished | |
const height = 30; | |
const width1 = 40, width2 = 60, width3 = 60; | |
const width = width1 + width2 + width3; | |
function printScreen() { | |
const x = 0, y = 3; | |
setColor('white', 'dim'); | |
print(`Directory to duplicate -------> `, 1, 1); | |
print(`Path where to duplicate it ---> `, 1, 2); | |
print(`┌${repeat(width1, '─')}┬${repeat(width2, '─')}┬${repeat(width3, '─')}┐`, x, y + 1); | |
print(`│ Options:`, x, y + 2); | |
print(`│ Copy:`, x + width1 + 1, y + 2); | |
print(`│ To:`, x + width1 + width2 + 2, y + 2); | |
print(`│`, x + width1 + width2 + width3 + 3, y + 2); | |
for (let t = 0; t <= height + 1; t++) { | |
print(`│`, x, y + t + 2); | |
print(`│`, x + width1 + 1, y + t + 2); | |
print(`│`, x + width1 + width2 + 2, y + t + 2); | |
print(`│`, x + width1 + width2 + width3 + 3, y + t + 2); | |
} | |
print(`└${repeat(width1, '─')}┴${repeat(width2, '─')}┴${repeat(width3, '─')}┘`, x, y + height + 4); | |
print(`├${repeat(width1, '─')}┤`, 0, y + height - 3); | |
// print(`├┤`, 0, hOffset + 2); | |
print(`- Folders to exclude:`, x + 2, y + 10); | |
resetColor(); | |
print(white(`Copy:`), x + width1 + 3, y + 2); | |
print(white(`To:`), x + width1 + width2 + 4, y + 2); | |
// print(`├${hLine}┤`, 0, hOffset + 2); | |
// print(` ${cyan('↑↓')} Move, ${cyan('←')} Directory Up, ${cyan('→')} Directory In, ${cyan('Enter')}: Select Directory, ${cyan('h')}: Show Hidden, ${cyan('c')}: Get Directory Info`, 0, hOffset + height + 3); | |
// printDirFromA(); | |
} | |
function printAll() { | |
printMain(); | |
print(repeat(width1 + width2 + width3, ' '), 0, height + 10); | |
if (status === 1) { printDirFromA(); printDirFromB(); printOptions(); } | |
if (status === 2) { printOptions(); printDirFromB(); printDirFromA(); } | |
if (status === 3) { printOptions(); printDirFromA(); printDirFromB(); } | |
} | |
function printMain() { | |
const maxLength = width1 + width2 + width3 - 30; | |
print(repeat(maxLength, ' '), 33, 1); // Clear previous values | |
print(repeat(maxLength, ' '), 33, 2); // Clear previous values | |
let pArr = fromPath.split('/'); | |
let lastName = pArr.pop(); | |
let basePath = pArr.join('/'); | |
if (fromPath.length > maxLength) { // in case the path is too long | |
pArr = fromPath.slice(-maxLength).split('/'); | |
basePath = '..' + pArr.join('/') || '/'; | |
} | |
print(`${basePath}/${color(lastName, 'yellow')}`, 33, 1); | |
if (basePath === toPath) { lastName += '_copy'; } | |
print(`${color(toPath + '/', 'white')}${color(lastName, 'green')}`, 33, 2); | |
print(gray(`A new directory "`) + green(lastName) + gray(`" will be created on ${toPath}/`), 1, height + 8); | |
} | |
function printOptions(newOp = op) { | |
if (newOp > 0 && newOp < excludeDirs.length + 4) { op = newOp; } | |
function opEffect(sel) { return 'bright' + (status === 1 && sel === op ? ' reverse' : ''); } | |
const x = 0, y = 3; | |
print(color(`- Start Copy `, 'cyan', opEffect(1)), x + 2, y + 4); | |
print(color(`- Analyse Directory `, 'cyan', opEffect(2)), x + 2, y + 6); | |
print(color(`[${showHidden ? 'X':' '}] Show Hidden Files `, 'cyan', opEffect(3)), x + 2, y + 8); | |
excludeDirs.forEach((dir, ind) => { | |
print(color(`[${dir.isExcluded ? 'X':' '}] ${dir.name} `, 'cyan', opEffect(4 + ind)), x + 4, y + ind + 12); | |
}); | |
if (status === 1) { | |
if (op === 1) { move(x + 2, y + 4); } | |
if (op === 2) { move(x + 2, y + 6); } | |
if (op === 3) { move(x + 3, y + 8); } | |
if (op > 3) { move(x + 5, y + op + 8); } | |
} | |
} | |
function printDirFromA() { | |
const x = width1 + 3, y = 6; | |
// print('op = ' + op + ', status = ' + status + ', selIndA = ' + selIndA, 30, 60); | |
for (let t = 0; t <= height; t++) { | |
const row = y + t; | |
const ind = scrollA + t; | |
print(repeat(width2 - 1, ' '), x, row); // Delete previous content | |
if (ind < fileListA.length) { | |
const file = fileListA[ind]; | |
const prefix = ind === fileListA.length - 1 ? '└─ ' : '├─ '; | |
printFile(file, prefix, x, row, status === 2 && selIndA === ind, width2); | |
} | |
} | |
if (scrollA > 0) { print(`↑`, x + width2 - 2, y); } | |
if (fileListA.length > scrollA + height) { print(`↓`, x + width2 - 2, y + height); } | |
if (status === 2) { move(x + 2, y + selIndA - scrollA); } | |
} | |
function printDirFromB() { | |
const x = width1 + width2 + 4, y = 6; | |
for (let t = 0; t <= height; t++) { | |
const row = y + t; | |
const ind = scrollB + t; | |
print(repeat(width3 - 1, ' '), x, row); // Delete previous content | |
if (ind < fileListB.length) { | |
const file = fileListB[ind]; | |
const prefix = ind === fileListB.length - 1 ? '└─ ' : '├─ '; | |
printFile(file, prefix, x, row, status === 3 && selIndB === ind); | |
} | |
} | |
if (scrollB > 0) { print(`↑`, x + width3 - 2, y); } | |
if (fileListB.length > scrollB + height) { print(`↓`, x + width3 - 2, y + height); } | |
if (status === 3) { move(x + 2, y + selIndB - scrollB); } | |
} | |
function printFile(file, prefix, x, row, isSel, width = 60) { | |
const size = ' ' + formatSize(file.size); | |
const name = file.name.slice(0, width - (file.type === 'dir' ? 6 : 15)); | |
if (file.size) { print(color(repeat(width - 2, '.'), 'gray', ''), x, row); } // dotted line | |
if (isSel) { // If this is the current selection | |
if (file.type === 'dir') { print(prefix + color(`/${name}`, 'black', '', 'white'), x, row); } | |
if (file.type === 'back') { print(prefix + color(`/${name}`, 'black', '', 'white'), x, row); } | |
if (file.type === 'file') { print(prefix + color(`${name}`, 'white', '', 'gray'), x, row); } | |
if (file.size) { print(color(`${size}`, 'black', '', 'white'), x + width - size.length - 2, row); } | |
} else { // Print for non selected | |
if (file.type === 'back') { print(prefix + color(`/${name}`, 'white', 'dim'), x, row); } | |
if (file.type === 'dir') { print(prefix + color(`/${name}`, 'white', 'bright'), x, row); } | |
if (file.type === 'file') { print(prefix + color(`${name}`, 'white', 'dim'), x, row); } | |
if (file.size) { print(color(`${size}`, 'white', ''), x + width - size.length - 2, row); } | |
} | |
} | |
function printConfirm() { | |
const x = 1, y = height + 10; | |
if (status === 5) { | |
print(repeat(50, ' '), x, y); | |
print(red('Confirm: '), x, y); | |
if (op === 1) { | |
print(color(` OK `, 'white', 'reverse'), x + 10, y); | |
print(color(` Cancel `, 'white'), x + 15, y); | |
move(x + 14, y); | |
} else { | |
print(color(` OK `, 'white'), x + 10, y); | |
print(color(` Cancel `, 'white', 'reverse'), x + 15, y); | |
move(x + 23, y); | |
} | |
} | |
} | |
function getPrintableDirList(dir) { | |
const list = getDirList(dir || '/', showHidden).sort((a, b) => { | |
if (a.type === 'dir' && b.type !== 'dir') { return -1; } | |
if (a.type !== 'dir' && b.type === 'dir') { return 1; } | |
return a.name.toUpperCase() > b.name.toUpperCase() ? 1: -1; | |
}); | |
if (dir !== '') { | |
list.unshift({ type: 'back', name: '..', path: dir, size: 0 }); | |
} | |
return list; | |
} | |
function cdDirA(dir) { | |
fileListA = getPrintableDirList(dir); | |
fromPath = dir; | |
scrollA = 0; | |
hoverFileA(0); | |
print(repeat(width2 - 7, ' '), width1 + 9, 5); | |
let dirName = fromPath.split('/').reverse()[0]; | |
if (dirName) { dirName = `.../${dirName}`; } | |
print(yellow(dirName || '/'), width1 + 9, 5); | |
// Match directories with scanned ones to add their sizes (if there are) | |
const dirs = allFiles.filter(f => f.type === 'dir'); | |
fileListA.filter(d => d.type === 'dir').forEach(d => d.size = dirs.find(dir => dir.path === d.path)?.size); | |
printMain(); | |
printDirFromA(); | |
} | |
function cdDirB(dir) { | |
fileListB = getPrintableDirList(dir); | |
toPath = dir; | |
scrollB = 0; | |
hoverFileB(0); | |
print(repeat(width3 - 7, ' '), width1 + width2 + 9, 5); | |
let dirName = toPath.split('/').reverse()[0]; | |
if (dirName) { dirName = `.../${dirName}/`; } | |
print(color(dirName || '/', 'white') + color(' ←', 'green'), width1 + width2 + 9, 5); | |
printMain(); | |
printDirFromB(); | |
} | |
function backToStart() { | |
print(repeat(width, ' '), 0, height + 8); | |
print(repeat(width, ' '), 0, height + 9); | |
print(repeat(width, ' '), 0, height + 10); | |
print(repeat(width, ' '), 0, height + 11); | |
print(repeat(width, ' '), 0, height + 12); | |
status = 1; | |
op = 1; | |
printAll(); | |
} | |
(async function run() { | |
console.clear(); | |
// print('process.argv[0] = ' + process.argv[0], 1, 45); | |
// print('process.argv[1] = ' + process.argv[1], 1, 46); | |
// print('process.argv[2] = ' + process.argv[2], 1, 47); | |
// print('process.argv[3] = ' + process.argv[3], 1, 48); | |
// print('process.argv[4] = ' + process.argv[4], 1, 49); | |
// print('fromPath = ' + fromPath, 1, 50); | |
// print('process.cwd() = ' + process.cwd(), 1, 51); | |
exec('resize -s 50 170', (err, stdout, stderr) => { // Set terminal size 70 rows, 220 cols | |
// print('Terminal size: ' + process.stdout.columns + 'x' + process.stdout.rows, 1, 51); | |
printScreen(); | |
cdDirA(fromPath); | |
cdDirB(toPath); | |
printAll(); | |
// fromPath = '/home/barba/AAA.tmp/jb-icomoon'; | |
// toPath = '/home/barba/AA2.tmp'; | |
// analyseDir(fromPath); | |
// preCopyScan(); | |
// startCopy(); | |
if (autoRun) { preCopyScan(); } | |
}); | |
// print(`\n\nProcess completed`); | |
// process.exit(0); | |
}()); | |
// ------------------------------------- Backend Ops --------------------------------------------- | |
function getDirList(dir, addHidden = true) { // dir should be the full path | |
try { | |
const list = []; | |
const files = fs.readdirSync(dir); | |
for (let file of files) { | |
const fullPath = path.join(dir, file); | |
if (!fs.existsSync(fullPath)) { // In case of broken Symlink | |
// console.log('Broken Symlink (skipping) --> ', fullPath); | |
} else { | |
const fileStat = fs.statSync(fullPath); | |
if (addHidden || file[0] !== '.') { | |
if (fileStat.isFile()) { | |
list.push({ type: 'file', name: file, path: fullPath, size: fileStat.size }); | |
} else if (fileStat.isDirectory()) { | |
list.push({ type: 'dir', name: file, path: fullPath, size: 0 }); | |
} | |
} | |
} | |
} | |
return list; | |
} catch(err) { console.log('ERROR', err); process.exit(1); } | |
} | |
function getNewFolderName() { | |
const dirName = fromPath.split('/').reverse()[0]; // Single name of the directory to duplicate | |
let newFolder = toPath + '/' + dirName; | |
if (dirExist(newFolder)) { newFolder += '_copy'; } // If it exists, try to extend the name "copy" | |
let copyNum = 2; | |
while (dirExist(newFolder) || copyNum > 1000) { // If still exists, try to add a number | |
newFolder = `${toPath}/${dirName}_copy_${copyNum++}`; | |
} | |
const lastName = newFolder.split('/').reverse()[0]; | |
print(gray(`A new directory "`) + green(lastName) + gray(`" will be created on ${toPath}/`), 1, height + 8); | |
return newFolder; | |
} | |
let allFiles = []; // Global list with all recursively scanned files and subdirectories | |
function analyseDir(rootDir, delay = 0) { | |
print(repeat(width1, ' '), 1, 31); | |
print(repeat(width1, ' '), 1, 33); | |
print(repeat(width1, ' '), 1, 34); | |
print(repeat(width1, ' '), 1, 35); | |
print(`Directory: ${yellow(rootDir.split('/').reverse()[0])}`, 2, 31); | |
print(color(` Analyzing...`, 'red', 'bright blink'), 2, 33); move(17, 33); | |
allFiles = []; | |
function checkDir(dir, deep = 0) { | |
if (deep > 20) { return 0; } | |
let size = 0; | |
getDirList(dir).forEach(file => { | |
file.relativePath = file.path.split(fromPath).join(''); | |
allFiles.push(file); | |
if (file.type === 'dir' && !excludeDirs.find(d => d.isExcluded && d.name === file.name)) { | |
file.size = checkDir(file.path, deep + 1); // recursive check to subdirectory | |
}; | |
size += file.size; | |
}); | |
return size; | |
} | |
const totalSize = checkDir(rootDir); | |
const files = allFiles.filter(f => f.type === 'file'); | |
const dirs = allFiles.filter(f => f.type === 'dir'); | |
// const totalSize = files.reduce((acc, file) => file.size + acc, 0); | |
function result() { | |
print(repeat(width1, ' '), 1, 33); | |
print(` - Size : ${yellow(formatSize(totalSize))}`, 2, 33); | |
print(` - Files : ${yellow(files.length)}`, 2, 34); | |
print(` - Subdirs : ${yellow(dirs.length)}`, 2, 35); | |
cdDirA(fromPath); | |
printOptions(); | |
} | |
if (delay === 0) { result(); } | |
else { setTimeout(() => result(), delay); } | |
} | |
// copy /home/barba/AAA.tmp/jb-icomoon /home/barba/AA2.tmp | |
// fromPath = '/home/barba/AAA.tmp/jb-icomoon'; | |
// toPath = '/home/barba/AA2.tmp'; | |
function preCopyScan() { | |
status = 4; | |
print(repeat(width1 + width2 + width3, ' '), 2, height + 10); | |
print(green(`Scanning original content...`), 2, height + 10); | |
analyseDir(fromPath); | |
getNewFolderName(); // Find the new folder name (with renames if alredy exists) | |
status = 5; | |
op = 1; | |
printConfirm(); | |
if (autoRun) { startCopy(); } | |
} | |
async function startCopy() { | |
const x = 1, y = height + 10; | |
const progressBarWidth = 50; | |
let isErr = false; | |
status = 6; | |
const dirName = fromPath.split('/').reverse()[0]; // Single name of the directory to duplicate | |
const newFolder = getNewFolderName(); | |
allFiles.forEach(file => file.newPath = newFolder + file.relativePath); // shortcut to new path | |
const dirs = allFiles.filter(f => f.type === 'dir'); | |
// ---- Create the new directory and all its subdirectories ---- | |
print(repeat(width, ' '), x, y); | |
print(green(`Creating new directory structure: `), 2, y); | |
print(green(repeat(progressBarWidth, `░`)), 36, y); | |
try { fs.mkdirSync(newFolder); } | |
catch(err) { error(`Error, "${dirName}" directory cannot be created`); } | |
for (let t = 0; t < dirs.length; t++) { | |
const path = dirs[t].newPath; | |
try { fs.mkdirSync(path); } | |
catch(err) { | |
print(red(`Error: "${dirName}" directory cannot be created`.padEnd(width - 50, ' ')), 2, y + 2); | |
isErr = true; break; | |
} | |
let progress = Math.round(progressBarWidth * t / (dirs.length - 1)); | |
let progressPer = Math.round(100 * t / (dirs.length - 1)); | |
print(green(repeat(progress, `█`)), 36, y); | |
print(green(`${progressPer} %`), 36 + progressBarWidth + 2, y); | |
move(36 + progress, y); | |
// await sleep(200); | |
} | |
if (isErr) { return backToStart(); } // Safe stop point in case of error | |
copyFiles(); | |
} | |
let errorFile; // pointer to the last file that failed when copying | |
async function copyFiles(errOp = '') { | |
const x = 1, y = height + 10; | |
const progressBarWidth = 50; | |
let isErr = false; | |
status = 6; | |
const files = allFiles.filter(f => f.type === 'file'); | |
const totalSize = files.reduce((acc, file) => file.size + acc, 0); | |
// files[4].path += 'X'; // force copy file error to test | |
// files[12].path += 'XXXX'; // force copy file error to test | |
// ------------------------- Copy files ------------------------- | |
print(repeat(width, ' '), x, y - 2); | |
print(repeat(width, ' '), x, y - 1); | |
print(repeat(width, ' '), x, y); | |
print(repeat(width, ' '), x, y + 1); | |
print(repeat(width, ' '), x, y + 2); | |
print(green(`Copying files: `), 2, y); | |
print(green(repeat(progressBarWidth, `░`)), 36, y); | |
getNewFolderName(); // Leave this here to print the action | |
let sizeCount = 0; | |
let errCount = 0; | |
const totalFiles = files.length; | |
for (let t = 0; t < files.length; t++) { | |
const file = files[t]; | |
print(green(`${t+1} of ${totalFiles}`), 18, y); | |
print(`File: ${file.path.padEnd(width - 10, ' ').slice(0, width - 10)}`, 2, y + 2); | |
if (!file.copied) { | |
try { fs.copyFileSync(file.path, file.newPath); } | |
catch(err) { | |
print(repeat(width, ' '), x, y + 3); | |
print(red(`Error: Cannot copy file: ${file.path}`), 2, y + 2); | |
if (errOp === 'ignore') { | |
errCount++; | |
} else { // Stop and ask what to do with the error | |
print(`Press "${cyan('Enter')}" to retry`, 100, y - 2); | |
print(`Press "${cyan('s')}" to skip and continue`, 100, y - 1); | |
print(`Press "${cyan('y')}" to skip and continue and ignore future errors`, 100, y); | |
print(`Press "${cyan('Esc')}" to cancel`, 100, y + 1); | |
errorFile = file; | |
status = 7; | |
isErr = true; | |
break; | |
} | |
} | |
file.copied = true; // mark it as successfully copied | |
} | |
sizeCount += file.size; | |
let pRef = sizeCount / totalSize; | |
let progress = Math.round(progressBarWidth * pRef); | |
let progressPer = Math.round(100 * pRef); | |
print(green(repeat(progress, `█`)), 36, y); | |
print(green(`${progressPer} %`), 36 + progressBarWidth + 2, y); | |
move(36 + progress, y); | |
// await sleep(100); | |
} | |
if (isErr) { return; } // Safe stop point in case of error | |
// Successfully finished | |
status = 8; | |
print(repeat(width, ' '), x, y + 2); | |
print(green(`Copy successfully finised ✓`), 2, y + 2); | |
if (errorFile) { print(red(`(but with ${errCount} errors)`), 30, y + 2); } | |
move(1, height + 14); | |
process.exit(0); | |
} | |
// ------------------------------------- Functions --------------------------------------------- | |
function dirExist(fullPath) { // Check if a directory exists | |
try { | |
if (fs.existsSync(fullPath)) { return true; } | |
} catch(err) { return false; } | |
} | |
function formatTime(ms) { | |
if (ms < 1000) { return `00:00`; } | |
if (ms < 60000) { | |
const sec = Math.round(ms / 1000); | |
return `00:${pad(sec, 2, '0')}`; | |
} | |
const min = Math.floor(ms / 60000); | |
const sec = Math.round((ms - (min * 60000)) / 1000); | |
return `${pad(min, 2, '0')}:${pad(sec, 2, '0')}`; | |
} | |
const KB = Math.pow(1024, 1); | |
const MB = Math.pow(1024, 2); | |
const GB = Math.pow(1024, 3); | |
function formatSize(size = 0) { | |
if (size < KB) { return `${size} B`; } | |
if (size < MB) { return `${Math.floor(10 * size / KB) / 10} KB`; } | |
if (size < GB) { return `${Math.floor(10 * size / MB) / 10} MB`; } | |
return `${Math.floor(10 * size / GB) / 10} GB`; | |
} | |
function pad(number, width = 10, placeholder = '0') { // lPad | |
const n = number + ''; | |
return n.length >= width ? n : new Array(width - n.length + 1).join(placeholder) + n; | |
} | |
function move(x = 1, y = 1) { readline.cursorTo(process.stdout, x, y); } | |
function print(text = '', x = null, y = null) { | |
if (x !== null && y === null) { readline.cursorTo(process.stdout, x); } | |
if (x !== null && y !== null) { readline.cursorTo(process.stdout, x, y); } | |
let output = ''; | |
for (let t = 0; t < text.length; t++) { | |
let nextChar = text.at(t); | |
if (text.slice(t, t + 3) === '[R_') { nextChar = `\x1b[31m\x1b[1m`; t += 2; } | |
if (text.slice(t, t + 3) === '[G_') { nextChar = `\x1b[32m\x1b[1m`; t += 2; } | |
if (text.slice(t, t + 3) === '[B_') { nextChar = `\x1b[34m\x1b[1m`; t += 2; } | |
if (text.slice(t, t + 3) === '[Y_') { nextChar = `\x1b[33m\x1b[1m`; t += 2; } | |
if (text.slice(t, t + 3) === '[C_') { nextChar = `\x1b[36m\x1b[1m`; t += 2; } | |
if (text.slice(t, t + 3) === '[M_') { nextChar = `\x1b[35m\x1b[1m`; t += 2; } | |
if (text.slice(t, t + 2) === '_]') { nextChar = `\x1b[0m`; t += 1; } | |
output += nextChar; | |
} | |
process.stdout.write(output + `\n`); | |
} | |
const reset = `\x1b[0m`; | |
const allEffects = { | |
bright : `\x1b[1m`, | |
dim : `\x1b[2m`, | |
underscore : `\x1b[4m`, | |
blink : `\x1b[5m`, | |
reverse : `\x1b[7m`, | |
hidden : `\x1b[8m`, | |
} | |
const allColors = { | |
black : `\x1b[30m`, bg_black: '\x1b[40m', | |
red : `\x1b[31m`, bg_red: '\x1b[41m', | |
green : `\x1b[32m`, bg_green: '\x1b[42m', | |
yellow : `\x1b[33m`, bg_yellow: '\x1b[43m', | |
blue : `\x1b[34m`, bg_blue: '\x1b[44m', | |
magenta : `\x1b[35m`, bg_magenta: '\x1b[45m', | |
cyan : `\x1b[36m`, bg_cyan: '\x1b[46m', | |
white : `\x1b[37m`, bg_white: '\x1b[47m', | |
gray : `\x1b[90m`, bg_gray: '\x1b[100m', | |
}; | |
function color(text, color = 'green', effect = 'bright', bgColor = '') { | |
const effectCodes = effect.split(' ').map(eff => allEffects[eff] || '').join(''); | |
return `${effectCodes}${allColors[color] || ''}${allColors['bg_' + bgColor] || ''}${text}${reset}`; | |
} | |
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } | |
function red(text) { return color(text, 'red'); } | |
function green(text) { return color(text, 'green'); } | |
function yellow(text) { return color(text, 'yellow'); } | |
function blue(text) { return color(text, 'blue'); } | |
function gray(text) { return color(text, 'white', 'dim'); } | |
function gray2(text) { return color(text, 'gray'); } | |
function cyan(text) { return color(text, 'cyan'); } | |
function black(text) { return color(text, 'black', ''); } | |
function brown(text) { return color(text, 'yellow', ''); } | |
function white(text) { return color(text, 'white'); } | |
function repeat(num, char) { return Array.from(Array(num)).map(_ => char).join(''); } | |
// function cap(text = '', width = maxWidth) { return text.slice(0, width).padEnd(width, '.'); } | |
function setColor(color = 'green', effect = 'bright', bgColor = '') { | |
const effectCodes = effect.split(' ').map(eff => allEffects[eff] || '').join(''); | |
let code = ''; | |
if (effect) { code += (effectCodes || ''); } | |
if (color) { code += (allColors[color] || ''); } | |
if (bgColor) { code += (allColors['bg_' + bgColor] || ''); } | |
process.stdout.write(code); | |
} | |
function resetColor() { | |
process.stdout.write(reset); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment