├── .nvmrc ├── .taprc ├── .npmignore ├── .gitignore ├── .prettierrc ├── sync.d.ts ├── .gitattributes ├── cli.js ├── .github ├── dependabot.yml └── workflows │ ├── notify-release.yml │ ├── ci.yml │ └── release.yml ├── .eslintrc ├── types ├── index.d.ts └── index.test-d.ts ├── sync.js ├── index.js ├── os.js ├── LICENSE.md ├── package.json ├── test.js ├── README.md ├── get-sysinternals-du.test.js └── get-sysinternals-du.js /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | check-coverage: false 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .nyc_output/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | bin/ 4 | .nyc_output/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /sync.d.ts: -------------------------------------------------------------------------------- 1 | declare function fastFolderSizeSync(path: string): number | undefined 2 | export = fastFolderSizeSync 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # Force the following filetypes to have unix eols, so Windows does not break them 4 | *.* text eol=lf 5 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const fastFolderSize = require('.') 6 | 7 | fastFolderSize(process.argv.slice(2)[0], (err, bytes) => { 8 | if (err) { 9 | throw err 10 | } 11 | 12 | console.log(bytes) 13 | }) 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | - package-ecosystem: 'github-actions' 8 | directory: '/' 9 | schedule: 10 | interval: 'daily' 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": "latest" 9 | }, 10 | "rules": { 11 | "prefer-const": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/notify-release.yml: -------------------------------------------------------------------------------- 1 | name: Notify release 2 | 'on': 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 8 * * * 6 | release: 7 | types: 8 | - published 9 | issues: 10 | types: 11 | - closed 12 | jobs: 13 | setup: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | contents: read 18 | steps: 19 | - uses: nearform-actions/github-action-notify-release@v1 20 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ExecException, ChildProcess } from 'child_process' 2 | 3 | declare function fastFolderSize( 4 | path: string, 5 | options: { signal: AbortSignal }, 6 | callback: (err: ExecException | null, bytes?: number) => void 7 | ): ChildProcess 8 | 9 | declare function fastFolderSize( 10 | path: string, 11 | callback: (err: ExecException | null, bytes?: number) => void 12 | ): ChildProcess 13 | 14 | export default fastFolderSize 15 | -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { execSync } = require('child_process') 4 | const { commands, processOutput } = require('./os.js') 5 | 6 | function fastFolderSize(target) { 7 | const command = commands[process.platform] || commands['linux'] 8 | const stdout = execSync(command, { cwd: target }).toString() 9 | 10 | const processFn = processOutput[process.platform] || processOutput['linux'] 11 | const bytes = processFn(stdout) 12 | 13 | return bytes 14 | } 15 | 16 | module.exports = fastFolderSize 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | 7 | strategy: 8 | matrix: 9 | node-version: [16, 18, 20, 22] 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - run: npm i -g . 19 | - run: fast-folder-size . 20 | - run: npm run lint 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd' 2 | import { ExecException, ChildProcess } from 'child_process' 3 | import fastFolderSize from '.' 4 | import fastFolderSizeSync from '../sync' 5 | 6 | expectType( 7 | fastFolderSize('.', (err, bytes) => { 8 | expectType(err) 9 | expectType(bytes) 10 | }) 11 | ) 12 | 13 | const signal = new AbortController().signal 14 | 15 | expectType( 16 | fastFolderSize('.', { signal }, (err, bytes) => { 17 | expectType(err) 18 | expectType(bytes) 19 | }) 20 | ) 21 | 22 | expectType(fastFolderSizeSync('.')) 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { exec } = require('child_process') 4 | const { commands, processOutput } = require('./os.js') 5 | 6 | function fastFolderSize(target, options, cb) { 7 | if (typeof options === 'function') { 8 | cb = options 9 | options = {} 10 | } 11 | 12 | const command = commands[process.platform] || commands['linux'] 13 | 14 | return exec( 15 | command, 16 | { cwd: target, signal: options.signal }, 17 | (err, stdout) => { 18 | if (err) return cb(err) 19 | 20 | const processFn = 21 | processOutput[process.platform] || processOutput['linux'] 22 | const bytes = processFn(stdout) 23 | 24 | cb(null, bytes) 25 | } 26 | ) 27 | } 28 | 29 | module.exports = fastFolderSize 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | semver: 7 | description: 'The semver to use' 8 | required: true 9 | default: 'patch' 10 | type: choice 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | pull_request: 16 | types: [closed] 17 | 18 | jobs: 19 | release: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: nearform/optic-release-automation-action@v4 23 | with: 24 | npm-token: ${{ secrets[format('NPM_TOKEN_{0}', github.actor)] || secrets.NPM_TOKEN }} 25 | optic-token: ${{ secrets[format('OPTIC_TOKEN_{0}', github.actor)] || secrets.OPTIC_TOKEN }} 26 | semver: ${{ github.event.inputs.semver }} 27 | build-command: npm ci 28 | -------------------------------------------------------------------------------- /os.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const commands = { 4 | // windows 5 | win32: `"${path.join( 6 | __dirname, 7 | 'bin', 8 | `du${process.arch === 'x64' ? '64' : ''}.exe` 9 | )}" -nobanner -accepteula -q -c .`, 10 | 11 | // macos 12 | darwin: `du -sk .`, 13 | 14 | // any linux 15 | linux: `du -sb .`, 16 | } 17 | 18 | const processOutput = { 19 | // windows 20 | win32(stdout) { 21 | // query stats indexes from the end since path can contain commas as well 22 | const stats = stdout.split('\n')[1].split(',') 23 | 24 | const bytes = +stats.slice(-2)[0] 25 | 26 | return bytes 27 | }, 28 | 29 | // macos 30 | darwin(stdout) { 31 | const match = /^(\d+)/.exec(stdout) 32 | 33 | const bytes = Number(match[1]) * 1024 34 | 35 | return bytes 36 | }, 37 | 38 | // any linux 39 | linux(stdout) { 40 | const match = /^(\d+)/.exec(stdout) 41 | 42 | const bytes = Number(match[1]) 43 | 44 | return bytes 45 | }, 46 | } 47 | 48 | module.exports = { commands, processOutput } 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | AWISC License - Anti War ISC 2 | 3 | Copyright 2015-present Simone Busoli 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | 9 | ANTI WAR CLAUSE 10 | 11 | This license is a modification of the original ISC license. It is identical to ISC but explicitly forbids use by any businesses, institutions or individuals residing or financing countries whose governments engage in wars against other countries. 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-folder-size", 3 | "version": "2.4.0", 4 | "description": "Node CLI or module to calculate folder size", 5 | "main": "index.js", 6 | "bin": { 7 | "fast-folder-size": "cli.js" 8 | }, 9 | "types": "types/index.d.ts", 10 | "author": "Simone Busoli ", 11 | "scripts": { 12 | "test": "tap && tsd", 13 | "postinstall": "node get-sysinternals-du.js", 14 | "lint": "eslint ." 15 | }, 16 | "keywords": [ 17 | "fast", 18 | "folder", 19 | "size" 20 | ], 21 | "repository": { 22 | "url": "https://github.com/simoneb/fast-folder-size.git" 23 | }, 24 | "license": "AWISC", 25 | "dependencies": { 26 | "decompress": "^4.2.1", 27 | "https-proxy-agent": "^7.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^22.7.4", 31 | "@types/tap": "^15.0.8", 32 | "eslint": "^8.41.0", 33 | "eslint-config-prettier": "^10.1.2", 34 | "eslint-plugin-prettier": "^4.2.1", 35 | "prettier": "^2.8.8", 36 | "tap": "^16.3.4", 37 | "tsd": "^0.31.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const crypto = require('crypto') 3 | 4 | const fastFolderSize = require('.') 5 | const fastFolderSizeSync = require('./sync') 6 | 7 | test('callback', async t => { 8 | await t.test('folder size is larger than 0', t => { 9 | fastFolderSize('.', (err, bytes) => { 10 | t.error(err) 11 | t.ok(Number.isFinite(bytes)) 12 | t.ok(bytes > 0) 13 | t.end() 14 | }) 15 | }) 16 | 17 | await t.test('folder size is correct', t => { 18 | const writtenBytes = 8 * 1024 19 | 20 | const testdirName = t.testdir({ 21 | whatever: crypto.randomBytes(writtenBytes), 22 | }) 23 | 24 | fastFolderSize(testdirName, (err, bytes) => { 25 | t.error(err) 26 | console.log('real size:', writtenBytes, 'found size:', bytes) 27 | t.ok(bytes >= writtenBytes) 28 | t.ok(bytes <= writtenBytes * 1.5) 29 | t.end() 30 | }) 31 | }) 32 | 33 | await t.test('should be able to cancel the operation', t => { 34 | const controller = new AbortController() 35 | 36 | fastFolderSize('.', { signal: controller.signal }, (err, bytes) => { 37 | t.ok(err) 38 | t.equal(err.name, 'AbortError') 39 | t.notOk(bytes) 40 | t.end() 41 | }) 42 | 43 | controller.abort() 44 | }) 45 | }) 46 | 47 | test('sync', async t => { 48 | await t.test('sync: folder size is larger than 0', t => { 49 | const bytes = fastFolderSizeSync('.') 50 | t.ok(Number.isFinite(bytes)) 51 | t.ok(bytes > 0) 52 | t.end() 53 | }) 54 | 55 | await t.test('sync: folder size is correct', t => { 56 | const writtenBytes = 8 * 1024 57 | 58 | const testdirName = t.testdir({ 59 | whatever: crypto.randomBytes(writtenBytes), 60 | }) 61 | 62 | const bytes = fastFolderSizeSync(testdirName) 63 | console.log('real size:', writtenBytes, 'found size:', bytes) 64 | t.ok(bytes >= writtenBytes) 65 | t.ok(bytes <= writtenBytes * 1.5) 66 | t.end() 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > The license of this software has changed to AWISC - Anti War ISC License 2 | 3 | # fast-folder-size 4 | 5 | [![ci](https://github.com/simoneb/fast-folder-size/actions/workflows/ci.yml/badge.svg)](https://github.com/simoneb/fast-folder-size/actions/workflows/ci.yml) 6 | 7 | Node CLI or module to calculate folder size. 8 | 9 | It uses: 10 | 11 | - [Sysinternals DU](https://docs.microsoft.com/en-us/sysinternals/downloads/du) on Windows, automatically downloaded at 12 | installation time because the license does not allow redistribution. See below about specifying the download location. 13 | - native `du` on other platforms 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm i fast-folder-size 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Programmatically 24 | 25 | ```js 26 | const { promisify } = require('util') 27 | const fastFolderSize = require('fast-folder-size') 28 | const fastFolderSizeSync = require('fast-folder-size/sync') 29 | 30 | // callback 31 | fastFolderSize('.', (err, bytes) => { 32 | if (err) { 33 | throw err 34 | } 35 | 36 | console.log(bytes) 37 | }) 38 | 39 | // promise 40 | const fastFolderSizeAsync = promisify(fastFolderSize) 41 | const bytes = await fastFolderSizeAsync('.') 42 | 43 | console.log(bytes) 44 | 45 | // sync 46 | const bytes = fastFolderSizeSync('.') 47 | 48 | console.log(bytes) 49 | ``` 50 | 51 | #### Aborting 52 | 53 | The non-sync version of the API also supports `AbortSignal`, to abort the operation while in progress. 54 | 55 | ```js 56 | const fastFolderSize = require('fast-folder-size') 57 | 58 | const controller = new AbortController() 59 | 60 | fastFolderSize('.', { signal: controller.signal }, (err, bytes) => { 61 | if (err) { 62 | throw err 63 | } 64 | 65 | console.log(bytes) 66 | }) 67 | 68 | controller.abort() 69 | ``` 70 | 71 | ### Command line 72 | 73 | ```bash 74 | fast-folder-size . 75 | ``` 76 | 77 | ### Downloading the Sysinternals DU.zip 78 | 79 | By default the Sysinternals DU.zip is downloaded from https://download.sysinternals.com/files/DU.zip. 80 | 81 | If you need to change this, e.g. to download from an internal package repository 82 | or re-use an existing du.zip, you can set the **FAST_FOLDER_SIZE_DU_ZIP_LOCATION** environment variable. 83 | 84 | For example: 85 | 86 | ```shell 87 | export FAST_FOLDER_SIZE_DU_ZIP_LOCATION="https://your.internal.repository/DU.zip" 88 | ``` 89 | 90 | or 91 | 92 | ```shell 93 | export FAST_FOLDER_SIZE_DU_ZIP_LOCATION="D://download/du.zip" 94 | ``` 95 | -------------------------------------------------------------------------------- /get-sysinternals-du.test.js: -------------------------------------------------------------------------------- 1 | if (process.platform !== 'win32') { 2 | // these test cases are only for win32 3 | return 4 | } 5 | 6 | const { test, beforeEach } = require('tap') 7 | const path = require('path') 8 | const os = require('os') 9 | const fs = require('fs') 10 | const subject = require('./get-sysinternals-du.js') 11 | 12 | const workspace = path.join(os.tmpdir(), 'fast-folder-size-playground') 13 | beforeEach(() => { 14 | if (fs.existsSync(workspace)) fs.rmSync(workspace, { recursive: true }) 15 | fs.mkdirSync(workspace) 16 | }) 17 | 18 | test('it can use local file path as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION', t => { 19 | t.test('C:\\**\\du.zip', t => { 20 | const dummyDuZipPath = path.join(workspace, 'dummy-du.zip') 21 | fs.writeFileSync(dummyDuZipPath, '') 22 | process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyDuZipPath 23 | 24 | subject.onDuZipDownloaded = function (tempFilePath) { 25 | t.equal(tempFilePath, dummyDuZipPath) 26 | t.end() 27 | } 28 | 29 | subject.default(workspace) 30 | }) 31 | 32 | t.test('C://**/du.zip', t => { 33 | const dummyDuZipPath = path 34 | .join(workspace, 'dummy-du.zip') 35 | .replaceAll('\\', '/') 36 | .replace(':/', '://') 37 | fs.writeFileSync(dummyDuZipPath, '') 38 | process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyDuZipPath 39 | 40 | subject.onDuZipDownloaded = function (tempFilePath) { 41 | t.equal(tempFilePath, dummyDuZipPath) 42 | t.end() 43 | } 44 | 45 | subject.default(workspace) 46 | }) 47 | 48 | t.end() 49 | }) 50 | 51 | test('it cannot use non-exists local file path as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION', t => { 52 | process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = path.join( 53 | workspace, 54 | 'non-exists-dummy-du.zip' 55 | ) 56 | 57 | t.throws( 58 | () => subject.default(workspace), 59 | error => { 60 | return error.message.startsWith('du.zip not found at') 61 | } 62 | ) 63 | 64 | t.end() 65 | }) 66 | 67 | test('it can use http(s) url as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION', t => { 68 | const dummyUrl = 'https://non-exists.localhost/du.zip' 69 | process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyUrl 70 | 71 | subject.downloadDuZip = function (mirror) { 72 | t.equal(mirror, dummyUrl) 73 | t.end() 74 | } 75 | 76 | subject.default(workspace) 77 | }) 78 | 79 | test('when process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION not found, then download it directly', t => { 80 | delete process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION 81 | 82 | subject.downloadDuZip = function (mirror) { 83 | t.equal(mirror, undefined) 84 | t.end() 85 | } 86 | 87 | subject.default(workspace) 88 | }) 89 | -------------------------------------------------------------------------------- /get-sysinternals-du.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const os = require('os') 3 | const https = require('https') 4 | const path = require('path') 5 | const decompress = require('decompress') 6 | const { HttpsProxyAgent } = require('https-proxy-agent') 7 | 8 | exports.onDuZipDownloaded = function (tempFilePath, workspace) { 9 | decompress(tempFilePath, path.join(workspace, 'bin')) 10 | } 11 | 12 | exports.downloadDuZip = function (mirror, workspace) { 13 | const duZipLocation = 14 | mirror || 'https://download.sysinternals.com/files/DU.zip' 15 | 16 | // checks for proxy variables in user environment 17 | const proxyAddress = 18 | process.env.HTTPS_PROXY || 19 | process.env.https_proxy || 20 | process.env.HTTP_PROXY || 21 | process.env.http_proxy 22 | 23 | if (proxyAddress) { 24 | https.globalAgent = new HttpsProxyAgent(proxyAddress) 25 | } 26 | 27 | console.log(`downloading du.zip from ${duZipLocation}`) 28 | if (!mirror) { 29 | console.log( 30 | `if you have trouble while downloading, try set process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION to a proper mirror or local file path` 31 | ) 32 | } 33 | 34 | https.get(duZipLocation, function (res) { 35 | const tempFilePath = path.join(os.tmpdir(), 'du.zip') 36 | 37 | const fileStream = fs.createWriteStream(tempFilePath) 38 | res.pipe(fileStream) 39 | 40 | fileStream.on('finish', function () { 41 | fileStream.close() 42 | exports.onDuZipDownloaded(tempFilePath, workspace) 43 | }) 44 | }) 45 | } 46 | 47 | exports.default = function (workspace) { 48 | // Only run for Windows 49 | if (process.platform !== 'win32') { 50 | return 51 | } 52 | 53 | // check if du is already installed 54 | const duBinFilename = `du${process.arch === 'x64' ? '64' : ''}.exe` 55 | const defaultDuBinPath = path.join(workspace, 'bin', duBinFilename) 56 | 57 | if (fs.existsSync(defaultDuBinPath)) { 58 | console.log(`${duBinFilename} found at ${defaultDuBinPath}`) 59 | return 60 | } 61 | console.log(`${duBinFilename} not found at ${defaultDuBinPath}`) 62 | 63 | const mirrorOrCache = process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION 64 | 65 | if ( 66 | !mirrorOrCache || 67 | mirrorOrCache.startsWith('http://') || 68 | mirrorOrCache.startsWith('https://') 69 | ) { 70 | exports.downloadDuZip(mirrorOrCache, workspace) 71 | return 72 | } 73 | 74 | if (fs.existsSync(mirrorOrCache)) { 75 | exports.onDuZipDownloaded(mirrorOrCache, workspace) 76 | return 77 | } 78 | 79 | const message = `du.zip not found at ${mirrorOrCache}` 80 | // this will result the process to exit with code 1 81 | throw Error(message) 82 | } 83 | 84 | // only auto execute default() function when its invoked directly 85 | if (require.main === module) { 86 | exports.default(__dirname) 87 | } 88 | --------------------------------------------------------------------------------