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.

js 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:

  1. readdir(folder, { withFileTypes: true }): opsi ini membuat setiap entri dikembalikan sebagai object Dirent yang sudah tahu dirinya berkas atau folder lewat isDirectory(), tanpa panggilan stat tambahan.
  2. localeCompare(b.name, 'id'): entri diurutkan menurut nama dengan lokal Indonesia yang ditulis eksplisit, sehingga urutannya sama di komputer mana pun.
  3. 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.
  4. padEnd dan padStart: nama diratakan ke kiri selebar LEBAR_NAMA (22 kolom) dan ukuran diratakan ke kanan selebar LEBAR_UKURAN (10 kolom), lalu jenis mengikut di belakangnya — kolom tetap sejajar walau panjang nama berbeda-beda.
  5. Baris total: penutup ringkas yang menghitung jumlah entri dengan baris.length, dipisahkan satu baris kosong dari tabel.

Sekarang kita lihat hasilnya pada folder data yang berisi dua berkas dan satu folder:

bash
node src/cli.js daftar data

Output yang Dihasilkan:

text
nama                      ukuran  jenis
catatan.txt                 29 B  berkas
gambar                         -  folder
laporan.txt                 11 B  berkas

total: 3 butir

Perhatikan 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.

js src/perintah-baca.js
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:

bash
node src/cli.js baca data/catatan.txt

Output yang Dihasilkan:

text
  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.

js src/perintah-baca.js
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:

js src/format.js
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:

bash
node src/cli.js info data/laporan.txt

Output yang Dihasilkan:

text
nama       : laporan.txt
jenis      : berkas
ukuran     : 11 byte (11 B)
diubah     : 2026-01-15 08:00 UTC

Menerjemahkan 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.

js src/galat.js
// 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.

bash
node src/cli.js baca data/hilang.txt

Output yang Dihasilkan:

text
manajer: tidak ditemukan: data/hilang.txt
kode keluar: 2
bash
node src/cli.js daftar data/berkas.txt

Output yang Dihasilkan:

text
manajer: path bukan folder: data/berkas.txt
kode keluar: 2

Tiga 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.