Kerangka dari Bagian 1 belum menyentuh disk sama sekali — perintahnya hanya menolak pemakaian yang salah. Pada bagian ini kita mengisi tiga perintah baca: daftar, baca, dan info. Ketiganya sengaja didahulukan karena hanya membaca data tanpa mengubah apa pun, sehingga kita bisa berlatih pola dasar node:fs/promises tanpa risiko kehilangan isi. Di sepanjang jalan kita juga akan menata format tampilan agar rapi dibaca, lalu menerjemahkan galat sistem menjadi pesan Indonesia yang sesuai kontrak kode keluar 2.
Menampilkan Isi Folder #
Perintah daftar membaca satu folder lalu menyusun tabel berisi nama, ukuran, dan jenis setiap entri. Seluruh logikanya berada di fungsi daftar pada src/perintah-baca.js.
export async function daftar(folder) {
const entri = await readdir(folder, { withFileTypes: true });
entri.sort((a, b) => a.name.localeCompare(b.name, 'id'));
const baris = [];
for (const satu of entri) {
const jenis = satu.isDirectory() ? 'folder' : 'berkas';
let ukuran = '-';
if (jenis === 'berkas') {
const rincian = await stat(join(folder, satu.name));
ukuran = formatUkuran(rincian.size);
}
baris.push(
satu.name.padEnd(LEBAR_NAMA) + ukuran.padStart(LEBAR_UKURAN) + ' ' + jenis,
);
}
const kepala = 'nama'.padEnd(LEBAR_NAMA) + 'ukuran'.padStart(LEBAR_UKURAN) + ' jenis';
return [kepala, ...baris, '', `total: ${baris.length} butir`].join('\n');
}Mari kita bedah bagian-bagian pentingnya:
readdir(folder, { withFileTypes: true }): opsi ini membuat setiap entri dikembalikan sebagai objectDirentyang sudah tahu dirinya berkas atau folder lewatisDirectory(), tanpa panggilanstattambahan.localeCompare(b.name, 'id'): entri diurutkan menurut nama dengan lokal Indonesia yang ditulis eksplisit, sehingga urutannya sama di komputer mana pun.stat(join(folder, satu.name)): ukuran hanya diambil untuk berkas; folder cukup menampilkan tanda-karena ukuran folder di level sistem berkas tidak bermakna bagi pembaca.padEnddanpadStart: nama diratakan ke kiri selebarLEBAR_NAMA(22 kolom) dan ukuran diratakan ke kanan selebarLEBAR_UKURAN(10 kolom), lalu jenis mengikut di belakangnya — kolom tetap sejajar walau panjang nama berbeda-beda.- Baris
total: penutup ringkas yang menghitung jumlah entri denganbaris.length, dipisahkan satu baris kosong dari tabel.
Sekarang kita lihat hasilnya pada folder data yang berisi dua berkas dan satu folder:
node src/cli.js daftar dataOutput yang Dihasilkan:
nama ukuran jenis
catatan.txt 29 B berkas
gambar - folder
laporan.txt 11 B berkas
total: 3 butirPerhatikan bahwa gambar tampil di antara kedua berkas karena urutan mengikuti nama, bukan jenisnya. Subfolder gambar tidak dibedah isinya — daftar memang hanya membaca satu tingkat, dan membaca rekursif bisa kita tambahkan sebagai latihan mandiri.
Membaca Berkas dengan Nomor Baris #
Perintah baca menampilkan isi berkas teks dengan nomor baris. Penomoran membuat baris mudah dirujuk saat kita mendiskusikan isi berkas, misalnya saat membaca log atau catatan panjang.
export async function baca(berkas) {
const rincian = await stat(berkas);
if (rincian.isDirectory()) {
throw new GalatBerkas(`path menuju folder, bukan berkas: ${berkas}`);
}
const isi = await readFile(berkas, 'utf8');
if (isi === '') return '(berkas kosong)';
const bersih = isi.endsWith('\n') ? isi.slice(0, -1) : isi;
return bersih
.split('\n')
.map((teks, i) => `${String(i + 1).padStart(3)} | ${teks}`)
.join('\n');
}Ada tiga keputusan yang perlu dijelaskan di sini. Pertama, fungsi memanggil stat sebelum readFile demi menghasilkan pesan galat yang benar — kalau tidak, readFile pada folder melempar galat EISDIR yang tidak menyertakan path, sehingga pesan terjemahannya jadi tidak informatif. Kedua, satu baris baru terakhir dibuang dulu supaya berkas yang berakhir rapi tidak menghasilkan baris kosong bernomor di ujung. Ketiga, nomor baris diratakan ke kanan tiga kolom dengan padStart(3), sehingga berkas dengan lebih dari sembilan baris tetap sejajar.
Berikut contoh pembacaan berkas dua baris:
node src/cli.js baca data/catatan.txtOutput yang Dihasilkan:
1 | Belajar Node.js di terminal.
2 | Saat ini belajar fs/promises.Berkas kosong ditangani khusus dengan teks (berkas kosong) alih-alih tidak mencetak apa pun, supaya pembaca yakin perintahnya tetap berhasil dan memang isinya kosong.
Metadata Berkas dengan info #
Perintah info merangkum metadata satu path: nama, jenis, ukuran, dan waktu terakhir diubah. Seluruh data datanya dari satu panggilan stat.
export async function info(berkas) {
const rincian = await stat(berkas);
const jenis = rincian.isDirectory() ? 'folder' : 'berkas';
const ukuran = rincian.isDirectory()
? '-'
: `${rincian.size} byte (${formatUkuran(rincian.size)})`;
return [
`nama : ${basename(berkas)}`,
`jenis : ${jenis}`,
`ukuran : ${ukuran}`,
`diubah : ${formatWaktu(rincian.mtime)}`,
].join('\n');
}Dua fungsi format tingkat rendah dipisah ke src/format.js agar bisa diuji sendiri di Bagian 4. Fungsi formatUkuran mengubah jumlah byte menjadi bentuk yang mudah dibaca:
export function formatUkuran(jumlahByte) {
if (jumlahByte < 1024) return `${jumlahByte} B`;
if (jumlahByte < 1024 * 1024) {
return `${(jumlahByte / 1024).toFixed(1).replace('.', ',')} KB`;
}
return `${(jumlahByte / (1024 * 1024)).toFixed(1).replace('.', ',')} MB`;
}Bentuk 1,5 KB memakai koma desimal sesuai tulisan angka Indonesia. Adapun formatWaktu mencetak mtime dalam zona UTC dengan format tetap — 2026-01-15 08:00 UTC — sehingga hasilnya sama di komputer dengan zona waktu mana pun. Contoh berikut sengaja membekukan waktu ubah berkas memakai touch agar keluarannya dapat direproduksi:
node src/cli.js info data/laporan.txtOutput yang Dihasilkan:
nama : laporan.txt
jenis : berkas
ukuran : 11 byte (11 B)
diubah : 2026-01-15 08:00 UTCMenerjemahkan Galat Sistem #
Hingga di sini semua contoh berhasil, tetapi kontrak kode keluar 2 baru terlihat saat path yang diminta tidak ada. Fungsi perintah sengaja membiarkan galat node:fs/promises menggelembung naik; penerjemahannya terjadi sekali saja di src/galat.js lewat tabel PETA_PESAN yang memetakan kode galat sistem ke pesan Indonesia.
// Pesan Indonesia untuk kode galat sistem yang paling sering muncul.
const PETA_PESAN = {
ENOENT: (g) => `tidak ditemukan: ${g.path}`,
EISDIR: (g) => `path menuju folder, bukan berkas: ${g.path}`,
ENOTDIR: (g) => `path bukan folder: ${g.path}`,
EACCES: (g) => `tidak ada izin untuk: ${g.path}`,
};
// Menyeragamkan galat apa pun menjadi galat proyek ini beserta kode keluarnya.
export function ubahGalat(galat) {
if (galat instanceof GalatPemakaian || galat instanceof GalatBerkas) {
return galat;
}
const susun = PETA_PESAN[galat.code];
const pesan = susun ? susun(galat) : `galat sistem: ${galat.message}`;
return new GalatBerkas(pesan);
}Galat sistem dari node:fs/promises membawa dua properti penting: code (misalnya ENOENT untuk path yang tidak ada) dan path (path yang sedang diproses). Keduanya cukup untuk menyusun pesan yang jelas tanpa harus membaca pesan Inggris bawaan. Galat yang kodenya tidak ada di tabel tetap dibungkus GalatBerkas dengan pesan aslinya, jadi tidak ada galat yang lolos tanpa kode keluar. Sekarang kita buktikan kontraknya pada dua keadaan: berkas hilang dan daftar yang diberi path berkas.
node src/cli.js baca data/hilang.txtOutput yang Dihasilkan:
manajer: tidak ditemukan: data/hilang.txt
kode keluar: 2node src/cli.js daftar data/berkas.txtOutput yang Dihasilkan:
manajer: path bukan folder: data/berkas.txt
kode keluar: 2Tiga perintah baca kini lengkap beserta jalur galatnya. Kita sudah bisa menjelajahi isi disk dengan aman karena sejauh ini tidak ada satu pun perintah yang mengubah berkas. Kontrak berikutnya justru sebaliknya: Bagian 3 membangun tulis, salin, dan hapus — operasi yang benar-benar mengubah disk, lengkap dengan pengaman konfirmasi sebelum menghapus.