Aufwärmen · Aufgabe 1 von 7
// B4.5 · ca. 45 Min. · Einstieg
Build: ein paralleler Ordnergrößen-Bericht
Sie bauen einen Befehl, der einen Ordnerbaum durchläuft, Dateien zu wenigen gleichzeitig misst und die Größe jedes obersten Ordners ausgibt, größte zuerst. Unlesbare Einträge meldet er, statt abzubrechen.
Lektion 5 von 5 in B4 Asynchrones Node
Danach können Sie
- Einen Baum mit readdir und stat durchlaufen, Größen je oberstem Ordner summieren, sortiert ausgeben
- Dateien begrenzt parallel per stat messen und jeden Fehler mit Promise.allSettled als Ergebnis behalten
- Alles mit util.parseArgs verdrahten: Bericht auf stdout, Probleme auf stderr, Exit-Codes 0, 1 und 2
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es voraus, bevor Sie weiterlesen. sample enthält docs/a.txt (1200 Bytes), docs/guide/b.txt (300 Bytes), img/logo.svg (2048 Bytes) und notes.txt (10 Bytes). Was ist die erste Zeile des fertigen Berichts?
node main.js sampleÜben · Aufgabe 3 von 7
In sumByTop ist slash die Position des ersten / im Pfad. Ergänzen Sie das Ende des slice, damit docs/guide/b.txt für die Zeile docs/ zählt, mit Schrägstrich.
const name = slash === -1 ? path : path.slice(0, ____);const name = slash === -1 ? path : path.slice(0, );Üben · Aufgabe 4 von 7
Ordnen Sie jedem Aufruf des fertigen Tools zu, was er tut.
Üben · Aufgabe 5 von 7
settleLimit ist der fertige Pool aus report.js. Was gibt dieses Programm aus?
const task = async (name) => { if (name === 'b') throw new Error('b is gone'); return name.toUpperCase(); }; const results = await settleLimit(['a', 'b', 'c'], 2, task); console.log(results.map((r) => (r.status === 'fulfilled' ? r.value : r.reason.message)));Denksport · Aufgabe 6 von 7
Knobelaufgabe. Der Pool aus B4.3, ohne Promise.allSettled, bekommt vier Aufgaben: a dauert 30 ms, bad scheitert nach 10 ms, c und d dauern je 10 ms. Was gibt das Programm aus?
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // B4.3's pool, without Promise.allSettled. async function mapLimit(items, limit, fn) { const results = []; let next = 0; async function worker() { while (next < items.length) { const index = next++; results[index] = await fn(items[index]); } } await Promise.all(Array.from({length: limit}, worker)); return results; } async function task([name, ms]) { console.log('start', name); await sleep(ms); if (name === 'bad') throw new Error('bad'); } try { await mapLimit([['a', 30], ['bad', 10], ['c', 10], ['d', 10]], 2, task); } catch (error) { console.log('caught', error.message); }Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Ergänzen Sie run() um eine Option --by: --by size, der Standard, behält die Reihenfolge von sumByTop; --by name sortiert die Zeilen nach Namen. Jeder andere Wert ist falsche Bedienung, mit der Usage-Zeile und Exit-Code 2. Probieren Sie beide Werte an einem eigenen Ordner aus und prüfen Sie, dass die total-Zeile am Ende bleibt.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Dieselben Teile, ein anderer Bericht: die größten Dateien
Bevor Sie den Ordnerbericht bauen, sehen Sie seine Teile in einem kleineren Programm, das die größten Dateien findet. Es legt einen kleinen Baum an, listet ihn mit readdir und recursive und löscht dann eine Datei, als hätte ein anderes Programm sie während des Laufs entfernt. Ein Pool aus zwei Workern misst die Einträge per stat; jeder verpackt sein stat in Promise.allSettled, also wird die fehlende Datei zu einem Ergebnis. countedStat zählt, wie viele stat-Aufrufe gleichzeitig laufen. Die größten Dateien werden nach Größe, dann nach Pfad sortiert.
main.js
import {mkdir, readdir, rm, stat, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
// A small tree for the demo.
await rm('demo', {recursive: true, force: true});
await mkdir('demo/src/lib', {recursive: true});
await writeFile('demo/README.md', '# demo\n');
await writeFile('demo/src/main.js', 'console.log(1);\n'.repeat(20));
await writeFile('demo/src/lib/util.js', 'export {};\n'.repeat(5));
await writeFile('demo/src/old.js', '// soon gone\n');
// Every entry that is not a folder, with its full path.
const entries = await readdir('demo', {recursive: true, withFileTypes: true});
const paths = entries.filter((entry) => !entry.isDirectory()).map((entry) => join(entry.parentPath, entry.name));
await rm('demo/src/old.js'); // deleted after the listing, before its stat
let active = 0; // stats running right now
let most = 0; // the most that ran at the same time
async function countedStat(path) {
active++;
most = Math.max(most, active);
try {
return await stat(path);
} finally {
active--;
}
}
// A pool of two workers; Promise.allSettled turns each stat into a result.
const results = [];
let next = 0;
async function worker() {
while (next < paths.length) {
const index = next++;
const [result] = await Promise.allSettled([countedStat(paths[index])]);
results[index] = result;
}
}
await Promise.all([worker(), worker()]);
const found = [];
results.forEach((result, i) => {
if (result.status === 'fulfilled') found.push({path: paths[i], size: result.value.size});
else console.log('skipped', paths[i] + ':', result.reason.code);
});
found.sort((a, b) => b.size - a.size || (a.path < b.path ? -1 : 1));
for (const {path, size} of found.slice(0, 2)) console.log(size, 'bytes', path);
console.log(paths.length, 'entries listed,', found.length, 'measured, at most', most, 'stats at once');
Ausführen mit
node main.jsAusgabe
skipped demo/src/old.js: ENOENT
320 bytes demo/src/main.js
55 bytes demo/src/lib/util.js
4 entries listed, 3 measured, at most 2 stats at once- Die gelöschte Datei wird einmal gemeldet und übersprungen; die anderen drei werden trotzdem gemessen.
- Auch die Ordner src und lib wurden gelistet, und der Filter mit isDirectory() hat sie entfernt.
- Höchstens 2 stat-Aufrufe liefen gleichzeitig, so viele wie Worker, egal wie lang die Liste ist.
- Die Sortierung macht die Ausgabe bei jedem Lauf gleich, in welcher Reihenfolge readdir auch lieferte.
Übungen
Übung 1 von 4
Schritt 1: Größen je oberstem Ordner
Schreiben Sie sumByTop(records). Jeder Datensatz ist {path, size}, mit / zwischen den Teilen des Pfads. Geben Sie eine Zeile {name, size, files} pro oberstem Eintrag zurück: docs/guide/b.txt zählt für die Zeile docs/ (mit Schrägstrich), und ein Pfad ohne Schrägstrich wie notes.txt ist eine eigene Zeile. size ist die Summe, files die Anzahl. Sortieren Sie nach Größe absteigend, gleiche Größen nach Name, wie < Strings vergleicht. Die Funktion ist rein und läuft daher auch im Browser.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
const slash = path.indexOf('/'); ist -1 für eine Datei ganz oben. Sonst ist path.slice(0, slash + 1) der Ordnername mit Schrägstrich.
Hinweis 2
Halten Sie die Zeilen in einer Map nach Name: totals.get(name) ?? {name, size: 0, files: 0}, dann addieren Sie die Größe und 1 zu files.
Hinweis 3
Sortieren Sie [...totals.values()] mit (a, b) => b.size - a.size || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0).
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// Sums the sizes per top-level entry: a folder such as docs/, or a file at the top.
// Largest first; equal sizes by name, so the report is the same on every run.
export function sumByTop(records) {
const totals = new Map();
for (const {path, size} of records) {
const slash = path.indexOf('/');
const name = slash === -1 ? path : path.slice(0, slash + 1);
const row = totals.get(name) ?? {name, size: 0, files: 0};
row.size += size;
row.files += 1;
totals.set(name, row);
}
return [...totals.values()].sort((a, b) => b.size - a.size || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
console.log(sumByTop([
{path: 'docs/a.txt', size: 1200},
{path: 'img/logo.svg', size: 2048},
{path: 'docs/guide/b.txt', size: 300},
{path: 'notes.txt', size: 10}
]));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// Sums the sizes per top-level entry: a folder such as docs/, or a file at the top.
// Largest first; equal sizes by name, so the report is the same on every run.
export function sumByTop(records) {
return records.map(({path, size}) => ({name: path, size, files: 1}));
}
console.log(sumByTop([
{path: 'docs/a.txt', size: 1200},
{path: 'img/logo.svg', size: 2048},
{path: 'docs/guide/b.txt', size: 300},
{path: 'notes.txt', size: 10}
]));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {sumByTop} from './main.js';
const records = [
{path: 'docs/a.txt', size: 1200},
{path: 'img/logo.svg', size: 2048},
{path: 'docs/guide/b.txt', size: 300},
{path: 'notes.txt', size: 10}
];
test('Dateien unter demselben obersten Ordner werden addiert, größte Zeile zuerst', () => {
const got = sumByTop(records);
const want = [
{name: 'img/', size: 2048, files: 1},
{name: 'docs/', size: 1500, files: 2},
{name: 'notes.txt', size: 10, files: 1}
];
assert.deepEqual(got, want, `sumByTop lieferte ${JSON.stringify(got)}`);
});
test('Gleiche Größen werden nach Name sortiert', () => {
const got = sumByTop([{path: 'b.txt', size: 5}, {path: 'c/x.txt', size: 9}, {path: 'a/y.txt', size: 5}]).map((row) => row.name);
assert.deepEqual(got, ['c/', 'a/', 'b.txt'], `die Zeilen kamen in der Reihenfolge ${JSON.stringify(got)}`);
});
test('Keine Datensätze ergeben keine Zeilen', () => {
assert.deepEqual(sumByTop([]), [], `sumByTop([]) lieferte ${JSON.stringify(sumByTop([]))}`);
});
test('Die übergebenen Datensätze bleiben unverändert', () => {
const copy = records.map((record) => ({...record}));
sumByTop(copy);
assert.deepEqual(copy, records, 'sumByTop hat die übergebenen Datensätze verändert');
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 2 von 4
Schritt 2: lesbare Größen und der Bericht
Schreiben Sie formatSize(bytes): unter 1024 die Zahl und B, etwa 512 B; unter 1024 × 1024 KiB mit einer Nachkommastelle, etwa 1.5 KiB; darüber MiB mit einer Nachkommastelle. Dann formatReport(rows): eine Zeile pro Eintrag, Name, zwei Leerzeichen, die Größe, zwei Leerzeichen und die Anzahl in Klammern, (1 file) oder (2 files); danach eine Zeile total mit den Summen. Jede Zeile endet mit einem Zeilenumbruch. Beide sind rein und laufen daher auch im Browser.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
(bytes / 1024).toFixed(1) + ' KiB' ergibt eine Nachkommastelle; prüfen Sie zuerst bytes < 1024, dann bytes < 1024 * 1024.
Hinweis 2
Schreiben Sie eine Hilfsfunktion line(name, size, files), die name + ' ' + formatSize(size) + ' (' + files + ' files)' liefert, mit ' file)' bei files gleich 1.
Hinweis 3
Addieren Sie size und files über alle Zeilen mit reduce, hängen Sie line("total", …) an, verbinden Sie mit Zeilenumbrüchen und fügen Sie am Ende einen an.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// 512 B, 1.5 KiB, 2.0 MiB: steps of 1024, one decimal from KiB on.
export function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KiB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MiB';
}
// One line per row, then a total line.
export function formatReport(rows) {
const line = (name, size, files) => name + ' ' + formatSize(size) + ' (' + files + (files === 1 ? ' file)' : ' files)');
const size = rows.reduce((sum, row) => sum + row.size, 0);
const files = rows.reduce((sum, row) => sum + row.files, 0);
return [...rows.map((row) => line(row.name, row.size, row.files)), line('total', size, files)].join('\n') + '\n';
}
console.log(formatReport([
{name: 'img/', size: 2048, files: 1},
{name: 'docs/', size: 1500, files: 2},
{name: 'notes.txt', size: 10, files: 1}
]));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// 512 B, 1.5 KiB, 2.0 MiB: steps of 1024, one decimal from KiB on.
export function formatSize(bytes) {
return bytes + ' B';
}
// One line per row, then a total line.
export function formatReport(rows) {
return rows.map((row) => row.name + ' ' + formatSize(row.size)).join('\n');
}
console.log(formatReport([
{name: 'img/', size: 2048, files: 1},
{name: 'docs/', size: 1500, files: 2},
{name: 'notes.txt', size: 10, files: 1}
]));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatReport, formatSize} from './main.js';
test('Unter 1024 Bytes: die Zahl und B', () => {
for (const [bytes, want] of [[0, '0 B'], [10, '10 B'], [1023, '1023 B']]) {
assert.equal(formatSize(bytes), want, `formatSize(${bytes}) lieferte ${JSON.stringify(formatSize(bytes))}`);
}
});
test('KiB und MiB mit einer Nachkommastelle', () => {
for (const [bytes, want] of [[1024, '1.0 KiB'], [1536, '1.5 KiB'], [1048576, '1.0 MiB'], [3407872, '3.3 MiB']]) {
assert.equal(formatSize(bytes), want, `formatSize(${bytes}) lieferte ${JSON.stringify(formatSize(bytes))}`);
}
});
test('Eine Zeile pro Eintrag, dann die Summe', () => {
const got = formatReport([{name: 'img/', size: 2048, files: 1}, {name: 'docs/', size: 1500, files: 2}]);
assert.equal(got, 'img/ 2.0 KiB (1 file)\ndocs/ 1.5 KiB (2 files)\ntotal 3.5 KiB (3 files)\n', `formatReport lieferte ${JSON.stringify(got)}`);
});
test('Ohne Einträge bleibt nur die Summe', () => {
assert.equal(formatReport([]), 'total 0 B (0 files)\n', `formatReport([]) lieferte ${JSON.stringify(formatReport([]))}`);
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 3 von 4
Schritt 3: ein Pool, der abwartet
settleLimit(items, limit, fn) soll fn(item) für jedes Element aufrufen, mit höchstens limit Aufrufen gleichzeitig, und pro Element ein {status, value}- oder {status, reason}-Objekt liefern, in der Reihenfolge der Elemente, wie Promise.allSettled. Ein scheiternder Aufruf darf seinen Worker nicht anhalten. Der Starter wartet alles ab, startet aber jeden Aufruf sofort. Starten Sie limit Worker (oder weniger, bei einer kurzen Liste), die den nächsten Index nehmen, bis keiner übrig ist. fakeStat steht für stat.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Halten Sie let next = 0; und ein Array results außerhalb der Worker. Ein Worker läuft while (next < items.length) und nimmt const index = next++.
Hinweis 2
In der Schleife: const [result] = await Promise.allSettled([fn(items[index])]); dann results[index] = result. Eine Ablehnung ist jetzt ein Ergebnis, kein throw.
Hinweis 3
Starten Sie Math.min(limit, items.length) Worker in einem Array, warten Sie mit await Promise.all(workers) und geben Sie results zurück.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// Calls fn(item) for every item, with at most limit calls running at once.
// Never rejects: one {status, value} or {status, reason} per item, in the
// order of items, as Promise.allSettled gives them.
export async function settleLimit(items, limit, fn) {
const results = [];
let next = 0; // the index of the next item to start
async function worker() {
while (next < items.length) {
const index = next++;
const [result] = await Promise.allSettled([fn(items[index])]);
results[index] = result;
}
}
const workers = [];
for (let i = 0; i < Math.min(limit, items.length); i++) workers.push(worker());
await Promise.all(workers);
return results;
}
// A stand-in for stat: waits, then gives a size, or fails for a file that is gone.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const fakeStat = async (name) => {
await sleep(10);
if (name === 'gone.txt') throw new Error('ENOENT: ' + name);
return {size: name.length};
};
const results = await settleLimit(['a.txt', 'gone.txt', 'bb.txt'], 2, fakeStat);
console.log(results.map((r) => (r.status === 'fulfilled' ? r.value.size : r.reason.message)));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// Calls fn(item) for every item, with at most limit calls running at once.
// Never rejects: one {status, value} or {status, reason} per item, in the
// order of items, as Promise.allSettled gives them.
export async function settleLimit(items, limit, fn) {
return Promise.allSettled(items.map(fn)); // starts every call at once
}
// A stand-in for stat: waits, then gives a size, or fails for a file that is gone.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const fakeStat = async (name) => {
await sleep(10);
if (name === 'gone.txt') throw new Error('ENOENT: ' + name);
return {size: name.length};
};
const results = await settleLimit(['a.txt', 'gone.txt', 'bb.txt'], 2, fakeStat);
console.log(results.map((r) => (r.status === 'fulfilled' ? r.value.size : r.reason.message)));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {settleLimit} from './main.js';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// fn for the tests: counts the calls running at once; fails for negative numbers.
function counter() {
const seen = {active: 0, most: 0, calls: 0};
seen.fn = async (ms) => {
seen.calls++;
seen.active++;
seen.most = Math.max(seen.most, seen.active);
await sleep(Math.abs(ms));
seen.active--;
if (ms < 0) throw new Error('failed ' + ms);
return ms * 2;
};
return seen;
}
test('Ein Ergebnis pro Element, in der Reihenfolge der Elemente', async () => {
const got = await settleLimit([30, 10, 20], 2, counter().fn);
const want = [{status: 'fulfilled', value: 60}, {status: 'fulfilled', value: 20}, {status: 'fulfilled', value: 40}];
assert.deepEqual(got, want, `settleLimit lieferte ${JSON.stringify(got)}`);
});
test('Nie mehr als limit Aufrufe gleichzeitig', async () => {
const seen = counter();
await settleLimit([20, 10, 30, 10, 20, 10], 2, seen.fn);
assert.equal(seen.most, 2, `höchstens ${seen.most} Aufrufe liefen gleichzeitig; erwartet waren 2`);
});
test('Ein scheiternder Aufruf ist ein abgelehntes Ergebnis, und jedes Element wird trotzdem bearbeitet', async () => {
const seen = counter();
const got = await settleLimit([10, -10, 10, 10], 2, seen.fn);
assert.deepEqual(got.map((r) => r.status), ['fulfilled', 'rejected', 'fulfilled', 'fulfilled'], `die Status waren ${JSON.stringify(got.map((r) => r.status))}`);
assert.equal(got[1].reason.message, 'failed -10', 'das abgelehnte Ergebnis sollte den Fehler als reason behalten');
assert.equal(seen.calls, 4, `fn wurde ${seen.calls}-mal aufgerufen; erwartet war 4-mal`);
});
test('Eine leere Liste ergibt ein leeres Array', async () => {
const got = await settleLimit([], 3, counter().fn);
assert.deepEqual(got, [], `settleLimit lieferte ${JSON.stringify(got)}`);
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 4 von 4
Schritt 4: auf der Festplatte durchlaufen und messen
report.js enthält die Schritte 1 bis 3, main.js hat run() und die Verdrahtung. Schreiben Sie den Dateisystem-Teil. listFiles(root) listet den ganzen Baum mit readdir, recursive und withFileTypes, lässt Ordner weg und liefert jeden Pfad relativ zu root mit / zwischen den Teilen. statAll(root, paths, limit) misst sie mit settleLimit und liefert {records, problems}: {path, size} für jeden Erfolg und eine Zeile cannot read <path>: <code> plus Zeilenumbruch für jeden Fehler. Prüfen Sie mit node --test.
Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Hinweise
Hinweis 1
readdir(root, {recursive: true, withFileTypes: true}) liefert Dirent-Objekte für den ganzen Baum; behalten Sie die mit !entry.isDirectory().
Hinweis 2
relative(root, join(entry.parentPath, entry.name)).split(sep).join('/') ergibt auf jedem System docs/guide/b.txt.
Hinweis 3
In statAll: await settleLimit(paths, limit, (path) => stat(join(root, path))), dann jedes Ergebnis nach result.status in records oder problems einsortieren.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import {readdir, stat} from 'node:fs/promises';
import {join, relative, sep} from 'node:path';
import {parseArgs} from 'node:util';
import {formatReport, settleLimit, sumByTop} from './report.js';
const USAGE = 'Usage: node main.js [folder] [--limit <n>]';
// Every entry below root that is not a folder, as a path relative to root with / between parts.
async function listFiles(root) {
const entries = await readdir(root, {recursive: true, withFileTypes: true});
return entries
.filter((entry) => !entry.isDirectory())
.map((entry) => relative(root, join(entry.parentPath, entry.name)).split(sep).join('/'));
}
// Stats every path, at most limit at once. A path that fails is a problem, not the end of the run.
async function statAll(root, paths, limit) {
const results = await settleLimit(paths, limit, (path) => stat(join(root, path)));
const records = [];
const problems = [];
results.forEach((result, i) => {
if (result.status === 'fulfilled') records.push({path: paths[i], size: result.value.size});
else problems.push('cannot read ' + paths[i] + ': ' + result.reason.code + '\n');
});
return {records, problems};
}
// What to print, and the exit code: 0 = all read, 1 = something could not be read, 2 = wrong usage.
async function run(args) {
let values, positionals;
try {
({values, positionals} = parseArgs({
args,
options: {limit: {type: 'string', default: '8'}},
allowPositionals: true
}));
} catch (error) {
return {out: '', err: error.message + '\n' + USAGE + '\n', code: 2};
}
const limit = Number(values.limit);
if (positionals.length > 1 || !Number.isInteger(limit) || limit < 1) return {out: '', err: USAGE + '\n', code: 2};
const root = positionals[0] ?? '.';
let paths;
try {
paths = await listFiles(root);
} catch (error) {
return {out: '', err: 'cannot read ' + root + ': ' + error.code + '\n', code: 1};
}
const {records, problems} = await statAll(root, paths, limit);
return {out: formatReport(sumByTop(records)), err: problems.sort().join(''), code: problems.length > 0 ? 1 : 0};
}
// The only lines that touch the real process.
const {out, err, code} = await run(process.argv.slice(2));
process.stdout.write(out);
process.stderr.write(err);
process.exitCode = code;
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
import {readdir, stat} from 'node:fs/promises';
import {join, relative, sep} from 'node:path';
import {parseArgs} from 'node:util';
import {formatReport, settleLimit, sumByTop} from './report.js';
const USAGE = 'Usage: node main.js [folder] [--limit <n>]';
// Every entry below root that is not a folder, as a path relative to root with / between parts.
async function listFiles(root) {
return readdir(root); // only the top level, and folders too
}
// Stats every path, at most limit at once. A path that fails is a problem, not the end of the run.
async function statAll(root, paths, limit) {
const infos = await Promise.all(paths.map((path) => stat(join(root, path))));
return {records: paths.map((path, i) => ({path, size: infos[i].size})), problems: []};
}
// What to print, and the exit code: 0 = all read, 1 = something could not be read, 2 = wrong usage.
async function run(args) {
let values, positionals;
try {
({values, positionals} = parseArgs({
args,
options: {limit: {type: 'string', default: '8'}},
allowPositionals: true
}));
} catch (error) {
return {out: '', err: error.message + '\n' + USAGE + '\n', code: 2};
}
const limit = Number(values.limit);
if (positionals.length > 1 || !Number.isInteger(limit) || limit < 1) return {out: '', err: USAGE + '\n', code: 2};
const root = positionals[0] ?? '.';
let paths;
try {
paths = await listFiles(root);
} catch (error) {
return {out: '', err: 'cannot read ' + root + ': ' + error.code + '\n', code: 1};
}
const {records, problems} = await statAll(root, paths, limit);
return {out: formatReport(sumByTop(records)), err: problems.sort().join(''), code: problems.length > 0 ? 1 : 0};
}
// The only lines that touch the real process.
const {out, err, code} = await run(process.argv.slice(2));
process.stdout.write(out);
process.stderr.write(err);
process.exitCode = code;
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
test('Der Ordner sample ergibt eine Zeile pro oberstem Eintrag, größte zuerst', async () => {
const got = await runMain({args: ['sample']});
assert.equal(got, "img/ 2.0 KiB (1 file)\ndocs/ 1.5 KiB (2 files)\nnotes.txt 10 B (1 file)\ntotal 3.5 KiB (4 files)\n", `der Bericht war ${JSON.stringify(got)}`);
});
test('Ein Limit von 1 ergibt denselben Bericht', async () => {
const got = await runMain({args: ['sample', '--limit', '1']});
assert.equal(got, "img/ 2.0 KiB (1 file)\ndocs/ 1.5 KiB (2 files)\nnotes.txt 10 B (1 file)\ntotal 3.5 KiB (4 files)\n", `mit --limit 1 war der Bericht ${JSON.stringify(got)}`);
});
test('Die Pfade sind relativ zum angegebenen Ordner', async () => {
const got = await runMain({args: ['sample/docs']});
assert.equal(got, 'a.txt 1.2 KiB (1 file)\nguide/ 300 B (1 file)\ntotal 1.5 KiB (2 files)\n', `der Bericht für sample/docs war ${JSON.stringify(got)}`);
});
test('Ein Ordner, den es nicht gibt, endet mit einem Fehlercode', async () => {
await assert.rejects(runMain({args: ['nowhere']}), Error, 'node main.js nowhere sollte mit einem anderen Code als 0 enden');
});
test('Ein Limit von 0 ist falsche Bedienung', async () => {
await assert.rejects(runMain({args: ['sample', '--limit', '0']}), Error, 'node main.js sample --limit 0 sollte mit einem anderen Code als 0 enden');
});
report.js
// Pure functions: no fs, no process, so they run anywhere.
// Calls fn(item) for every item, with at most limit calls running at once.
// Never rejects: one {status, value} or {status, reason} per item, in the
// order of items, as Promise.allSettled gives them.
export async function settleLimit(items, limit, fn) {
const results = [];
let next = 0; // the index of the next item to start
async function worker() {
while (next < items.length) {
const index = next++;
const [result] = await Promise.allSettled([fn(items[index])]);
results[index] = result;
}
}
const workers = [];
for (let i = 0; i < Math.min(limit, items.length); i++) workers.push(worker());
await Promise.all(workers);
return results;
}
// Sums the sizes per top-level entry: a folder such as docs/, or a file at the top.
// Largest first; equal sizes by name, so the report is the same on every run.
export function sumByTop(records) {
const totals = new Map();
for (const {path, size} of records) {
const slash = path.indexOf('/');
const name = slash === -1 ? path : path.slice(0, slash + 1);
const row = totals.get(name) ?? {name, size: 0, files: 0};
row.size += size;
row.files += 1;
totals.set(name, row);
}
return [...totals.values()].sort((a, b) => b.size - a.size || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
}
// 512 B, 1.5 KiB, 2.0 MiB: steps of 1024, one decimal from KiB on.
export function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KiB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MiB';
}
// One line per row, then a total line.
export function formatReport(rows) {
const line = (name, size, files) => name + ' ' + formatSize(size) + ' (' + files + (files === 1 ? ' file)' : ' files)');
const size = rows.reduce((sum, row) => sum + row.size, 0);
const files = rows.reduce((sum, row) => sum + row.files, 0);
return [...rows.map((row) => line(row.name, row.size, row.files)), line('total', size, files)].join('\n') + '\n';
}
sample/docs/a.txt
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
sample/docs/guide/b.txt
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
sample/img/logo.svg
ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
sample/notes.txt
ten bytes
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenHäufige Fehler
parentPath in einer rekursiven Liste vergessen
import {mkdir, readdir, stat, writeFile} from 'node:fs/promises';
import {join} from 'node:path';
await mkdir('sample/docs', {recursive: true});
await writeFile('sample/docs/a.txt', 'alpha\n');
for (const entry of await readdir('sample', {recursive: true, withFileTypes: true})) {
if (entry.isFile()) console.log(entry.name, (await stat(join('sample', entry.name))).size);
}
Was Node.js ausgibt
Error: ENOENT: no such file or directory, stat 'sample/a.txt'Warum, und die Lösung
Mit recursive ist entry.name nur der letzte Teil, a.txt, während die Datei in sample/docs liegt. join("sample", entry.name) zeigt auf eine Datei, die es nicht gibt. entry.parentPath ist der Ordner, in dem der Eintrag gefunden wurde, also ist join(entry.parentPath, entry.name) der volle Pfad: sample/docs/a.txt.
value aus einem abgelehnten Ergebnis lesen
const results = await Promise.allSettled([Promise.resolve(1), Promise.reject(new Error('gone'))]);
console.log(results.map((r) => r.status));
const failed = results.filter((r) => r.status === 'rejected');
console.log(failed[0].value.code);
Was Node.js ausgibt
TypeError: Cannot read properties of undefined (reading 'code')Warum, und die Lösung
Ein erfülltes Ergebnis hat value; ein abgelehntes hat stattdessen reason, und value ist undefined. Prüfen Sie zuerst result.status und lesen Sie dann result.value bei 'fulfilled' und result.reason bei 'rejected', hier failed[0].reason.message.
Dirent-Methoden ohne withFileTypes
import {mkdir, readdir, writeFile} from 'node:fs/promises';
await mkdir('sample/docs', {recursive: true});
await writeFile('sample/docs/a.txt', 'alpha\n');
const entries = await readdir('sample', {recursive: true});
const files = entries.filter((entry) => !entry.isDirectory());
console.log(files);
Was Node.js ausgibt
TypeError: entry.isDirectory is not a functionWarum, und die Lösung
Ohne withFileTypes liefert readdir einfache Strings wie docs/a.txt, und ein String hat keine Methode isDirectory. Übergeben Sie {recursive: true, withFileTypes: true}, um Dirent-Objekte zu bekommen, die ihren Typ, ihren Namen und ihren parentPath kennen.
JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.