Program kita sudah bisa mencari; sekarang saatnya melaporkan temuan dengan detail yang layak. Bagian ini menambahkan pembacaan metadata — informasi tentang berkas yang bukan isinya: ukuran dalam byte, permission (hak akses), dan waktu ubah terakhir. Ketiganya disediakan std::filesystem sebagai function siap pakai, tetapi masing-masing punya karakter berbeda: ukuran aman dicetak ke output terverifikasi, permission perlu diterjemahkan dulu menjadi notasi yang terbaca, dan waktu ubah bergantung pada kapan berkas dibuat sehingga tidak pernah kita jadikan output uji. Memahami perbedaan ini adalah pelajaran terpenting bagian ini — metadata yang baik untuk ditampilkan belum tentu baik untuk dijadikan kontrak pengujian.

Metadata yang Dibaca Pengelola #

Tiga metadata kita masukkan ke struct InfoFile dan diambil di scanner:

MetadataFunction std::filesystemSifat nilainyaDicetak di output uji?
Ukuranfile_sizeditentukan isi berkas, sama di semua mesinya
Permissionstatus(...).permissions()kita atur eksplisit saat membuat berkasya
Waktu ubahlast_write_timebergantung waktu pembuatan di tiap mesintidak

Baris pertama tabel sudah kita kenal dari Bagian 2: it->file_size(ecEntri) mengembalikan std::uintmax_t — tipe unsigned terlebar pustaka standar, cukup untuk berkas sebesar apa pun. Yang baru ada dua. fs::status(jalur, ecEntri).permissions() mengembalikan nilai bertipe fs::perms, sebuah bitmask berisi bit-bit hak akses gaya Unix (baca/tulis/jalankan untuk pemilik, grup, dan lainnya). Dan fs::last_write_time mengembalikan fs::file_time_type — sebuah std::chrono::time_point pada jam khusus filesystem, bukan jam dinding biasa; kekhususan ini yang membuat penanganannya menarik, dan kita bedah terpisah di bawah.

Satu keputusan penting mendahului pembahasan permission: program kita menetapkan permission berkas contoh secara eksplisit saat membuatnya, bukan membiarkan nilai bawaan sistem. Nilai bawaan bergantung pada umask — pengaturan shell yang menarik sebagian bit hak akses setiap berkas baru — sehingga berbeda antar mesin. Dengan fs::permissions(target, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::others_read, ecHak) setelah tiap berkas dibuat, folder contoh selalu berisi berkas ber-hak 0644, dan kolom permission di output kita tetap stabil di mesin mana pun.

Permission dalam Notasi Ringkas #

Nilai fs::perms berupa angka; yang ingin kita tampilkan adalah bentuk yang akrab bagi pengguna Linux, seperti rw-r--r--. Sayangnya libstdc++ belum menyediakan operator<< untuk fs::perms, jadi kita terjemahkan sembilan bitnya sendiri:

cpp hakRingkas — sembilan bit menjadi sembilan huruf
std::string hakRingkas(fs::perms hak) {
    const unsigned bit = static_cast<unsigned>(hak) & 0777u;
    std::string huruf = "rwxrwxrwx";
    for (int i = 0; i < 9; ++i) {
        if (!(bit & (0400u >> i))) {
            huruf[static_cast<std::size_t>(i)] = '-';
        }
    }
    return huruf;
}

Mekanismenya murni operasi bit. Mask 0777u (oktal, tiga kelompok tiga bit) menyaring sembilan bit hak akses dari bit-bit lain yang mungkin menyertai nilai perms. Konstanta 0400u adalah bit pemilik boleh baca; menggesernya ke kanan i kali menelusuri sembilan bit dari kiri ke kanan — urutan yang sama dengan notasi ls. Untuk 0644, hasilnya rw-r--r--: pemilik boleh baca-tulis, grup dan lainnya hanya baca. Saat mencetak, kita format angkanya dengan {:03o} — oktal, tiga digit, diisi nol — sehingga 0644 tampil sebagai 644, bentuk yang biasa diketik ke chmod.

Waktu Ubah: Dibaca, Tidak Pernah Dicetak #

last_write_time adalah metadata yang paling diminta pengguna dan paling bermasalah untuk pengujian. Nilainya memang bisa dibaca dengan rapi:

cpp membaca last_write_time dan mengonversinya ke jam sistem
std::error_code ecWaktu;
const fs::file_time_type waktuTulis = fs::last_write_time(berkas.jalur, ecWaktu);
if (!ecWaktu) {
    const auto waktuSistem = std::chrono::time_point_cast<std::chrono::seconds>(
        fs::file_time_type::clock::to_sys(waktuTulis));
    std::println("  waktu ubah : {:%Y-%m-%d %H:%M:%S} UTC", waktuSistem);
}

Dua konversi di potongan itu wajib dipahami. Jam milik filesystem (fs::file_time_type::clock) tidak dijamin sama dengan jam dinding, jadi to_sys() mengonversinya ke std::chrono::system_clock yang mewakili waktu nyata. time_point_cast<std::chrono::seconds> lalu memotong presisinya sampai detik, dan format %Y-%m-%d %H:%M:%S mencetaknya sebagai teks tanggal standar. Kode ini benar dan berjalan — tetapi kita sengaja tidak memasukkannya ke output terverifikasi mana pun.

Perbedaan “boleh ditampilkan, tidak boleh dikontrakkan” ini bukan pengecualian sepele. Alur kerja kita selama proyek — tangkap output nyata, bandingkan byte-per-byte, jalankan dua kali untuk uji determinisme — hanya berarti jika keluaran program stabil. Metadata yang menempel pada waktu pembuatan termasuk kategori yang harus selalu dijelaskan di prosa, bukan diujikan.

Program Lengkap Bagian 3 #

Simpan program berikut sebagai pengelola-v3.cpp — inilah kondisi src/pengelola-file.cpp sesaat sebelum lapisan penanganan error dituntaskan:

cpp src/pengelola-file.cpp — versi Bagian 3
// pengelola-v3.cpp — Bagian 3: membaca metadata berkas (ukuran, permission)
// dan menampilkannya sebagai tabel rapi. Ini kondisi src/pengelola-file.cpp
// sesaat sebelum lapisan penanganan error dituntaskan di Bagian 4.

#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <print>
#include <string>
#include <system_error>
#include <utility>
#include <vector>

namespace fs = std::filesystem;

struct InfoFile {
    fs::path jalur;          // jalur lengkap berkas
    std::uintmax_t ukuran;   // ukuran dalam byte
    fs::perms hak;           // permission berkas
};

struct Kriteria {
    std::string ekstensi;    // "" = tanpa filter ekstensi
    std::string kata;        // "" = tanpa filter nama
    std::uintmax_t ukuranMin = 0;
    bool aktif() const { return !ekstensi.empty() || !kata.empty() || ukuranMin > 0; }
};

bool lolos(const InfoFile& berkas, const Kriteria& kriteria) {
    if (!kriteria.ekstensi.empty() && berkas.jalur.extension().string() != kriteria.ekstensi) {
        return false;
    }
    if (!kriteria.kata.empty() && !berkas.jalur.filename().string().contains(kriteria.kata)) {
        return false;
    }
    if (berkas.ukuran < kriteria.ukuranMin) {
        return false;
    }
    return true;
}

// Mengubah nilai fs::perms menjadi string gaya ls, misalnya "rw-r--r--".
// Operator << untuk fs::perms belum tersedia di libstdc++, jadi kita
// periksa sembilan bit hak akses satu per satu.
std::string hakRingkas(fs::perms hak) {
    const unsigned bit = static_cast<unsigned>(hak) & 0777u;
    std::string huruf = "rwxrwxrwx";
    for (int i = 0; i < 9; ++i) {
        if (!(bit & (0400u >> i))) {
            huruf[static_cast<std::size_t>(i)] = '-';
        }
    }
    return huruf;
}

// Membuat folder beserta tiga berkas contoh bila belum ada (idempoten).
// Permission ditetapkan eksplisit (0644) agar kolom hak akses di output
// selalu sama, tidak tergantung umask mesin.
bool siapkanFolderContoh(const fs::path& folder) {
    std::error_code ec;
    fs::create_directories(folder, ec);
    if (ec) {
        std::println(std::cerr, "Galat: tidak dapat membuat folder '{}': {}",
                     folder.string(), ec.message());
        return false;
    }

    const std::vector<std::pair<std::string, std::string>> contoh = {
        {"catatan.txt", "Baris pertama catatan.\nBaris kedua catatan.\n"},
        {"laporan.csv", "id;nama;nilai\n1;Budi;90\n2;Siti;85\n"},
        {"gambar.png", "\x89PNG\r\n\x1a\ndata gambar tiruan"},
    };

    for (const auto& [nama, isi] : contoh) {
        const fs::path target = folder / nama;
        std::error_code ecAda;
        if (fs::exists(target, ecAda)) {
            continue;   // berkas sudah ada — jangan ditulis ulang
        }
        std::ofstream berkas(target, std::ios::binary);
        if (!berkas) {
            std::println(std::cerr, "Galat: tidak dapat menulis '{}'",
                         target.string());
            return false;
        }
        berkas << isi;
        berkas.close();

        std::error_code ecHak;
        fs::permissions(target,
                        fs::perms::owner_read | fs::perms::owner_write |
                            fs::perms::group_read | fs::perms::others_read,
                        ecHak);
        if (ecHak) {
            std::println(std::cerr, "Galat: tidak dapat mengatur permission '{}'",
                         target.string());
            return false;
        }
    }
    return true;
}

// Memindai satu folder dan mengumpulkan metadata tiap berkas reguler.
std::vector<InfoFile> kumpulkanEntri(const fs::path& folder, std::error_code& ec) {
    std::vector<InfoFile> hasil;

    for (fs::directory_iterator it(folder, fs::directory_options::skip_permission_denied, ec),
         akhir;
         it != akhir; it.increment(ec)) {
        if (ec) {
            break;
        }
        const fs::path jalur = it->path();

        std::error_code ecEntri;
        if (!it->is_regular_file(ecEntri) || ecEntri) {
            continue;   // subfolder, symlink rusak, dan sejenisnya dilewati
        }

        InfoFile info;
        info.jalur = jalur;
        info.ukuran = it->file_size(ecEntri);
        if (ecEntri) {
            info.ukuran = 0;
        }
        info.hak = fs::status(jalur, ecEntri).permissions();
        if (ecEntri) {
            info.hak = fs::perms::none;
        }
        hasil.push_back(info);
    }

    std::sort(hasil.begin(), hasil.end(),
              [](const InfoFile& a, const InfoFile& b) {
                  return a.jalur.filename().string() < b.jalur.filename().string();
              });
    return hasil;
}

void tampilkan(const std::vector<InfoFile>& hasil, const Kriteria& kriteria) {
    if (hasil.empty()) {
        std::println("Tidak ada berkas yang cocok dengan kriteria.");
        return;
    }

    std::uintmax_t total = 0;
    for (const InfoFile& berkas : hasil) {
        const unsigned bitHak = static_cast<unsigned>(berkas.hak) & 0777u;
        std::println("{:<16}{:>8} byte  {:03o}", berkas.jalur.filename().string(),
                     berkas.ukuran, bitHak);
        total += berkas.ukuran;
    }
    std::println("total: {} byte", total);
    if (kriteria.aktif()) {
        std::println("ditemukan: {} berkas lolos filter", hasil.size());
    }
}

int main(int argc, char* argv[]) {
    const std::vector<std::string> argumen(argv + 1, argv + argc);

    fs::path folder = "demo-folder";
    Kriteria kriteria;

    for (std::size_t i = 0; i < argumen.size(); ++i) {
        const std::string& arg = argumen[i];
        if (arg == "--cari" && i + 1 < argumen.size()) {
            kriteria.kata = argumen[++i];
        } else if (arg == "--ekstensi" && i + 1 < argumen.size()) {
            kriteria.ekstensi = argumen[++i];
        } else if (arg == "--min-ukuran" && i + 1 < argumen.size()) {
            kriteria.ukuranMin = std::stoull(argumen[++i]);
        } else {
            folder = arg;
        }
    }

    if (folder == "demo-folder") {
        if (!siapkanFolderContoh(folder)) {
            return 1;
        }
    } else {
        std::error_code ecAda;
        if (!fs::exists(folder, ecAda) || ecAda) {
            std::println(std::cerr, "Galat: folder '{}' tidak ada.", folder.string());
            return 1;
        }
    }

    std::error_code ec;
    const std::vector<InfoFile> semua = kumpulkanEntri(folder, ec);
    if (ec) {
        std::println(std::cerr, "Galat membaca folder '{}': {}", folder.string(),
                     ec.message());
        return 1;
    }

    std::vector<InfoFile> cocok;
    cocok.reserve(semua.size());
    for (const InfoFile& berkas : semua) {
        if (lolos(berkas, kriteria)) {
            cocok.push_back(berkas);
        }
    }

    tampilkan(cocok, kriteria);
    return 0;
}

Perubahan versi ini menumpuk di tiga titik: InfoFile mendapat field hak, scanner mengisinya lewat fs::status(...).permissions(), dan tampilkan kini mencetak tabel dengan format {:<16}{:>8} byte {:03o} — nama rata kiri selebar 16 kolom, ukuran rata kanan selebar 8, lalu permission oktal — plus baris rekap total. Function hakRingkas kita simpan untuk mode detail program final di Bagian 4; tabel ringkas cukup memakai angka oktalnya.

Mencoba Tabel Metadata #

Folder contoh dari Bagian 1-2 dibuat tanpa pengaturan permission, jadi kita mulai dari keadaan segar agar jalur fs::permissions ikut bekerja:

bash
g++ -std=c++23 -Wall -Wextra pengelola-v3.cpp -o pengelola-v3
rm -rf demo-folder   # keadaan segar: permission diatur ulang oleh v3
./pengelola-v3
./pengelola-v3 demo-folder --ekstensi .csv --min-ukuran 30

Output yang Dihasilkan:

text
catatan.txt           44 byte  644
gambar.png            26 byte  644
laporan.csv           34 byte  644
total: 104 byte
laporan.csv           34 byte  644
total: 34 byte
ditemukan: 1 berkas lolos filter

Tabelnya kini informatif: ukuran eksak per berkas, permission oktal 644 yang kita tetapkan sendiri, dan baris total yang menjumlahkan kolom ukuran — 44 + 26 + 34 = 104. Pada pemanggilan kedua, dua filter digabung: .csv menyisakan satu kandidat, lalu ambang 30 byte tetap melewatinya karena laporan.csv berukuran 34. Perhatikan bahwa baris ditemukan hanya muncul saat filter aktif — persis peran method aktif() yang kita bangun di Bagian 2.

Dengan tabel metadata tampil, program kita sudah lengkap secara fitur — tetapi belum tahan benturan: masukan yang salah, opsi yang ejaannya meleset, atau folder yang tidak ada masih bisa membuatnya berperilaku aneh. Bagian 4 membangun perisainya sekaligus mengunci seluruh perilaku program dengan harness pengujian.

Rujukan resmi: std::filesystem::file_size (cppreference) , std::filesystem::perms (cppreference) , std::filesystem::last_write_time (cppreference) .