Skip to content

Instantly share code, notes, and snippets.

@sempostma
Created March 23, 2023 15:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sempostma/b6599b1e63591a5cb03b0df349f3a770 to your computer and use it in GitHub Desktop.
Save sempostma/b6599b1e63591a5cb03b0df349f3a770 to your computer and use it in GitHub Desktop.
Create a treemap of largest files in a folder recursively
const { exec } = require('child_process')
const fs = require('fs')
const path = require('path')
let lastDir = ''
let currDir
const recurse = async (dir, filenames, writeStream) => {
currDir = dir
const stats = filenames.map(async filename => {
const filepath = path.join(dir, filename)
try {
const stat = await fs.promises.stat(filepath)
stat.filename = filename
if (stat.isDirectory()) {
stat.filenames = await fs.promises.readdir(filepath)
}
return stat
} catch(err) {
return {
error: err.toString()
}
}
})
for (let index = 0; index < stats.length; index++) {
const stat = await stats[index];
let status
if (index !== 0) {
writeStream.write(',')
}
if (stat.error) {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename + ' (ERROR)')},
value: 1
}`)
}
else if (stat.isFile()) {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename)},
value: ${stat.size}
}`)
} else if (stat.isDirectory()) {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename)},
children: [`)
await recurse(path.join(dir, stat.filename), stat.filenames, writeStream)
status = writeStream.write(`]
}`)
} else if (stat.isSymbolicLink()) {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename + ' (LINK)')},
value: 1
}`)
} else if (stat.isFIFO()) {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename + ' (FIFO)')},
value: 1
}`)
} else if (stat.isSocket) {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename + ' (SOCKET)')},
value: 1
}`)
} else {
status = writeStream.write(`{
name: ${JSON.stringify(stat.filename + ' (UNKNOWN)')},
value: 1
}`)
}
if (status === false && writeStream.writableNeedDrain) {
await new Promise(r => writeStream.once('drain', r))
}
}
}
const analyze = async (dir) => {
console.log('Run with admin privileges!')
const outputFile = path.join(__dirname, 'index.html')
const writeStream = fs.createWriteStream(outputFile)
if (fs.existsSync(outputFile)) {
fs.unlinkSync(outputFile)
}
writeStream.write(`
<html>
<head>
<style>
.treemap-viz .tooltip {
max-width: none!important;
}
body, html {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/treemap-chart"></script>
<script>
function humanFileSize(bytes, si=false, dp=1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10**dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
function getNodeStack(d) {
const stack = [];
let curNode = d;
while (curNode) {
stack.unshift(curNode);
curNode = curNode.parent;
}
return stack;
}
function genColor(ratio) {
var color = Math.round(ratio * 255);
var red = color.toString(16);
var green = (255 - color).toString(16);
// pad any colors shorter than 6 characters with leading 0s
while(red.length < 2) {
red = '0' + red;
}
while(green.length < 2) {
green = '0' + green;
}
color = '#' + red + green + '00'
console.log(color)
return color;
}
async function run() {
const myChart = Treemap();
const mb50 = 1024 * 1024 * 50
myChart
.minBlockArea(50 * 50)
.label(d => d.name + ' ' + humanFileSize(d.__dataNode.value))
.color(d => d.__dataNode.children ? 'lightgrey' : genColor(d.__dataNode.value / Math.max(d.__dataNode.parent.value, mb50)))
.tooltipTitle(d => getNodeStack(d.__dataNode.parent)
.map(d => d.data.name)
.join('\\\\'))
.data(
{
name: ${JSON.stringify(dir)},
children: [`)
const filenames = await fs.promises.readdir(dir)
currDir = dir
lastDir = dir
const interval = setInterval(function() {
let size = Math.floor(writeStream.bytesWritten / 1048576)
if (size === 0) size = writeStream.bytesWritten + ' bytes'
else size += ' MB'
const currDirSections = currDir.split(path.sep)
const lastDirSections = lastDir.split(path.sep)
let matchingParents = currDirSections.findIndex((v, i) => v !== lastDirSections[i])
if (matchingParents === -1) matchingParents = currDirSections.length + 1
else matchingParents++
const displayDir = currDirSections.slice(0, matchingParents).join(path.sep)
console.log('Treemap size: ' + size + ' [current_dir=' + displayDir + ']')
lastDir = displayDir
}, 5000)
await recurse(dir, filenames, writeStream)
clearInterval(interval)
let size = Math.floor(writeStream.bytesWritten / 1048576)
if (size === 0) size = writeStream.bytesWritten + ' bytes'
else size += ' MB'
console.log('Treemap size: ' + size)
writeStream.end(`]
}
)
(document.getElementById('root'));
}
run();
</script>
</body>
</html>
`)
}
const run = async () => {
const root = process.argv[2]
console.log('root: ' + root)
await analyze(root)
const htmlFile = path.join(__dirname, 'index.html')
exec(`start file://${htmlFile}`)
}
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment