Pemeriksaan menghasilkan angka, pembersihan menghasilkan daftar — dan keduanya belum berarti apa-apa sampai ada yang membacanya. Laporan adalah bagian proyek yang ditujukan untuk manusia: administrator yang punya dua menit sebelum rapat dan perlu tahu apakah ada yang perlu ditindaklanjuti. Pada bagian ini kita merangkai hasil Bagian 1 dan Bagian 2 menjadi skrip final yang menulis laporan terstruktur ke stdout sekaligus berkas, dengan rekomendasi yang berubah mengikuti temuan, lalu membuktikan seluruh janji desain lewat test suite.

Empat Seksi, Empat Pertanyaan #

Laporan yang mudah dipindai punya seksi tetap — pembaca yang sama selalu tahu ke mana melompat. Empat seksi kita menjawab empat pertanyaan berurutan:

SeksiPertanyaan yang dijawabIsi
PEMERIKSAANapa yang dicek dan bagaimana hasilnya?disk per mount dengan status, jumlah paket tertunda, service gagal
PEMBERSIHANapa yang dibersihkan?kandidat atau file terhapus per whitelist, dengan ukuran byte
HEMAT RUANGberapa untungnya?total file dan byte — potensi di dry-run, nyata setelah apply
REKOMENDASIapa selanjutnya?langkah lanjutan yang diturunkan dari temuan

Header laporan menyimpan dua hal yang menentukan cara membacanya: tanggal acuan (dari HARI_INI) dan mode — supaya tidak ada keraguan apakah baris [dry-run] berarti “sudah terhapus” atau “akan dihapus”. Karena formatnya berulang, laporan juga mudah disaring: grep WASPADA untuk temuan, grep terhapus untuk yang sudah dibersihkan, dan seterusnya.

Skrip Final dan Sesi Lengkap #

Skrip final pemeliharaan.sh menyatukan semua bagian: pembacaan argumen dan konfigurasi, tiga pemeriksaan dari Bagian 1, pembersihan ber-whitelist dari Bagian 2, lalu penyusunan laporan. Blok berikut menyiapkan ulang fixture lengkap server-kantor, menuliskan skrip dan konfigurasi, menjalankan dry-run, apply, dan satu dry-run lagi setelah pembersihan — versi finalnya juga tersedia sebagai src/pemeliharaan.sh pada bagian Berkas Sumber di bawah halaman ini:

bash
mkdir -p data/cache data/log data/tmp
cat > data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   35G   13G  73% /
/dev/sdb1       200G  170G   21G  89% /data
tmpfs           7.8G  1.1M  7.8G   1% /dev/shm
EOF
cat > data/paket.txt <<'EOF'
Reading package lists... Done
Building dependency tree... Done
Calculating upgrade... Done
Reading state information... Done
The following packages will be upgraded:
  curl libcurl4 libssl3 openssl
4 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Inst curl [7.88.1-10] (7.88.1-11)
Inst libcurl4 [7.88.1-10] (7.88.1-11)
Inst libssl3 [3.0.8-1] (3.0.8-2)
Inst openssl [3.0.8-1] (3.0.8-2)
Conf curl (7.88.1-11)
Conf libcurl4 (7.88.1-11)
Conf libssl3 (3.0.8-2)
Conf openssl (3.0.8-2)
EOF
cat > data/services.txt <<'EOF'
nginx.service failed
postgresql.service failed
sshd.service running
cron.service running
EOF
printf 'isi cache lama\n' > data/cache/lama.txt;    touch -d "2026-01-10" data/cache/lama.txt
printf 'isi cache baru\n' > data/cache/baru.txt;    touch -d "2026-02-10" data/cache/baru.txt
printf 'isi log lama terkompresi\n' > data/log/app.log.1.gz; touch -d "2026-01-25" data/log/app.log.1.gz
printf 'isi log lama tapi bukan gz\n' > data/log/app.log;     touch -d "2026-01-20" data/log/app.log
printf 'isi log baru\n' > data/log/app.log.2.gz;    touch -d "2026-02-12" data/log/app.log.2.gz
printf 'sisa instalasi lama\n' > data/tmp/sisa.tmp; touch -d "2026-02-01" data/tmp/sisa.tmp
printf 'file tmp aktif\n' > data/tmp/aktif.tmp;    touch -d "2026-02-13" data/tmp/aktif.tmp

cat > pemeliharaan.conf <<'CONF'
# pemeliharaan.conf — konfigurasi skrip pemeliharaan.
# Sunting nilai di bawah ini sesuai komputer Anda; skrip pemeliharaan.sh
# tidak perlu disentuh. Jalur whitelist dihitung dari direktori kerja
# saat skrip dijalankan; jalur absolut (mis. /var/cache/myapp) juga
# boleh dipakai.

# Whitelist folder yang BOLEH dibersihkan, dipisah spasi.
# Folder di luar tiga daftar ini tidak pernah disentuh skrip.
CACHE_DIRS="data/cache"
LOG_DIRS="data/log"
TMP_DIRS="data/tmp"

# Umur maksimum file (hari) sebelum layak dibersihkan.
CACHE_MAX_DAYS=30
LOG_MAX_DAYS=14
TMP_MAX_DAYS=7

# Ambang pemakaian disk (%) yang menyalakan status WASPADA (1-99).
DISK_WARN_PCT=80

# Berkas laporan (opsional; default: laporan-pemeliharaan.txt).
REPORT_FILE="laporan-pemeliharaan.txt"
CONF
cat > pemeliharaan.sh <<'BASH'
#!/usr/bin/env bash
# pemeliharaan.sh — pemeliharaan berkala: periksa, bersihkan, laporkan.
#
# Membaca snapshot data sistem (df.txt, paket.txt, services.txt) dari
# folder data, memeriksa tiga hal — pemakaian disk, paket update
# tertunda, service gagal — lalu membersihkan file cache, log terkompresi
# (*.gz), dan file tmp usang HANYA di dalam folder whitelist yang
# tercantum di berkas konfigurasi. Mode default DRY-RUN: tidak ada
# file yang dihapus sampai --apply diberikan, dan mode tidak bisa
# diubah dari konfigurasi. Hasil akhirnya adalah laporan terstruktur
# yang ditulis ke stdout sekaligus berkas laporan.
#
# Pemakaian:
#   bash pemeliharaan.sh [--data-dir DIR] [--config FILE] [--apply]
#
# Variabel lingkungan (berguna untuk pengujian):
#   CONF      berkas konfigurasi yang dibaca (default: pemeliharaan.conf)
#   SOURCE_DIR  folder data; kalah oleh --data-dir (default: data)
#   HARI_INI  tanggal acuan umur file, format YYYY-MM-DD (default: hari ini)
#
# Kode keluar:
#   0 sukses                        1 konfigurasi tidak valid
#   2 berkas snapshot tidak ada     3 folder whitelist tidak ada

set -u

CONF="${CONF:-pemeliharaan.conf}"
DATA_DIR="${SOURCE_DIR:-data}"
HARI_INI="${HARI_INI:-$(date +%F)}"
APPLY=0

# --- 1. Baca argumen -----------------------------------------------------
while [ $# -gt 0 ]; do
    case "$1" in
        --apply) APPLY=1; shift ;;
        --data-dir|--config)
            if [ $# -lt 2 ]; then
                printf 'pemeliharaan.sh: %s butuh nilai\n' "$1" >&2
                exit 1
            fi
            if [ "$1" = "--data-dir" ]; then
                DATA_DIR="$2"
            else
                CONF="$2"
            fi
            shift 2
            ;;
        *)
            printf 'pemeliharaan.sh: argumen tidak dikenal: %s\n' "$1" >&2
            exit 1
            ;;
    esac
done

# --- 2. Baca dan validasi konfigurasi ------------------------------------
if [ ! -f "$CONF" ]; then
    printf 'pemeliharaan.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
    exit 1
fi

# shellcheck source=/dev/null
. "$CONF"

if [ -z "${CACHE_DIRS:-}" ] && [ -z "${LOG_DIRS:-}" ] && [ -z "${TMP_DIRS:-}" ]; then
    printf 'pemeliharaan.sh: whitelist kosong (CACHE_DIRS/LOG_DIRS/TMP_DIRS) di %s\n' "$CONF" >&2
    exit 1
fi

valid_umur() {
    case $1 in
        ''|*[!0-9]*|0) return 1 ;;
    esac
    return 0
}

valid_umur "${CACHE_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: CACHE_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }
valid_umur "${LOG_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: LOG_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }
valid_umur "${TMP_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: TMP_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }

case ${DISK_WARN_PCT:-} in
    ''|*[!0-9]*|0)
        printf 'pemeliharaan.sh: DISK_WARN_PCT harus angka 1-99\n' >&2
        exit 1
        ;;
esac
if [ "$DISK_WARN_PCT" -gt 99 ]; then
    printf 'pemeliharaan.sh: DISK_WARN_PCT harus angka 1-99\n' >&2
    exit 1
fi

# Mode sengaja di set SETELAH konfigurasi dibaca: walau berkas
# konfigurasi berisi "MODE=apply", nilainya tetap ditimpa di sini.
# Hanya argumen --apply yang boleh menyalakan pembersihan sungguhan.
MODE="dry-run"
if [ "$APPLY" -eq 1 ]; then
    MODE="apply"
fi

# --- 3. Validasi snapshot data --------------------------------------------
for f in df.txt paket.txt services.txt; do
    if [ ! -f "$DATA_DIR/$f" ]; then
        printf 'pemeliharaan.sh: snapshot %s/%s tidak ditemukan\n' "$DATA_DIR" "$f" >&2
        exit 2
    fi
done

# --- 4. Pemeriksaan -------------------------------------------------------
# Temuan pemeriksaan terkumpul di sini, lalu menjadi bagian laporan.
# Disk: kolom ke-5 df adalah Use%; "+0" di awk membuang karakter '%'
# sehingga "89%" terbaca sebagai angka 89.
DISK_BARIS=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 {
        pct = $5 + 0
        if (pct >= ambang)
            printf "   %-10s %3d%%  WASPADA\n", $6, pct
        else
            printf "   %-10s %3d%%  OK\n", $6, pct
    }' "$DATA_DIR/df.txt")

DISK_MELEBIHI=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 && ($5 + 0) >= ambang {
        printf "%s%s", pisah, $6
        pisah = ", "
    }' "$DATA_DIR/df.txt")
DISK_N=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 && ($5 + 0) >= ambang { n++ } END { print n + 0 }' "$DATA_DIR/df.txt")

# Paket tertunda: output simulasi apt-get -s upgrade menandai tiap paket
# dengan satu baris "Inst <nama> [versi lama] (versi baru)".
PAKET_DAFTAR=$(awk '/^Inst / {
        printf "%s%s", pisah, $2
        pisah = ", "
    }' "$DATA_DIR/paket.txt")
PAKET_N=$(awk '/^Inst / { n++ } END { print n + 0 }' "$DATA_DIR/paket.txt")

# Service gagal: snapshot berformat "unit substate" per baris.
SVC_DAFTAR=$(awk '$2 == "failed" {
        printf "%s%s", pisah, $1
        pisah = ", "
    }' "$DATA_DIR/services.txt")
SVC_N=$(awk '$2 == "failed" { n++ } END { print n + 0 }' "$DATA_DIR/services.txt")

# --- 5. Pembersihan (hanya di dalam whitelist) -----------------------------
# Hasil pembersihan terkumpul di BAG_BERSIH; penghitung JUMLAH_FILE dan
# HEMAT_BYTES dipakai seksi HEMAT RUANG di laporan.
JUMLAH_FILE=0
HEMAT_BYTES=0
BAG_BERSIH=""

# bersihkan <judul> <dirs> <umur-hari> <pola-nama>:
# kumpulkan file di folder whitelist yang lebih tua dari batas umur,
# catat ukurannya untuk laporan, dan hapus hanya bila mode apply.
# -type f menjaga folder tetap utuh; find hanya berangkat dari folder
# whitelist, jadi file di luar daftar tidak pernah tersentuh.
bersihkan() {
    judul="$1"
    dirs="$2"
    umur="$3"
    pola="${4:-*}"
    batas=$(date -d "$HARI_INI -$umur days" +%F)
    if [ "$pola" = "*" ]; then
        label="file"
    else
        label="file $pola"
    fi
    terpakai=0

    if [ -n "$BAG_BERSIH" ]; then
        BAG_BERSIH+=$'\n'
    fi
    BAG_BERSIH+="$judul — $label lebih tua dari $batas (whitelist: $dirs)"$'\n'

    # shellcheck disable=SC2086  # whitelist memang dipisah spasi
    for dir in $dirs; do
        if [ ! -d "$dir" ]; then
            printf 'pemeliharaan.sh: folder whitelist %s tidak ditemukan\n' "$dir" >&2
            exit 3
        fi
        # Process substitution < <(...) menjaga penghitung tetap hidup
        # di shell utama; pipeline biasa akan menjalankan while di
        # subshell sehingga JUMLAH_FILE hilang.
        while IFS= read -r f; do
            [ -n "$f" ] || continue
            ukuran=$(stat -c %s "$f")
            JUMLAH_FILE=$((JUMLAH_FILE + 1))
            HEMAT_BYTES=$((HEMAT_BYTES + ukuran))
            terpakai=$((terpakai + 1))
            if [ "$MODE" = "apply" ]; then
                rm -f -- "$f"
                BAG_BERSIH+="   [terhapus] $f (${ukuran} byte)"$'\n'
            else
                BAG_BERSIH+="   [dry-run] $f (${ukuran} byte)"$'\n'
            fi
        done < <(find "$dir" -type f -name "$pola" ! -newermt "$batas" | sort)
    done

    if [ "$terpakai" -eq 0 ]; then
        BAG_BERSIH+="   (tidak ada kandidat)"$'\n'
    fi
}

if [ -n "${CACHE_DIRS:-}" ]; then
    bersihkan "cache" "$CACHE_DIRS" "$CACHE_MAX_DAYS"
fi
if [ -n "${LOG_DIRS:-}" ]; then
    bersihkan "log" "$LOG_DIRS" "$LOG_MAX_DAYS" "*.gz"
fi
if [ -n "${TMP_DIRS:-}" ]; then
    bersihkan "tmp" "$TMP_DIRS" "$TMP_MAX_DAYS"
fi

# --- 6. Susun laporan -----------------------------------------------------
if [ "$MODE" = "apply" ]; then
    LABEL_MODE="APPLY (pembersihan benar-benar dijalankan)"
    LABEL_HEMAT="benar-benar dibebaskan dari disk"
else
    LABEL_MODE="DRY-RUN (tidak ada file yang dihapus)"
    LABEL_HEMAT="potensi hemat; jalankan --apply untuk benar-benar menghapus"
fi

LAPORAN=""
tambah() {
    LAPORAN+="$1"
    LAPORAN+=$'\n'
}

tambah "================================================================"
tambah "LAPORAN PEMELIHARAAN SISTEM"
tambah "Tanggal acuan : $HARI_INI"
tambah "Mode          : $LABEL_MODE"
tambah "================================================================"
tambah ""
tambah "PEMERIKSAAN"
tambah "----------------------------------------------------------------"
tambah "1. Pemakaian disk (ambang $DISK_WARN_PCT%)"
tambah "$DISK_BARIS"
tambah "2. Paket update tertunda"
if [ "$PAKET_N" -gt 0 ]; then
    tambah "   $PAKET_N paket siap di-upgrade: $PAKET_DAFTAR"
else
    tambah "   tidak ada paket tertunda"
fi
tambah "3. Service gagal"
if [ "$SVC_N" -gt 0 ]; then
    tambah "   $SVC_N service failed: $SVC_DAFTAR"
else
    tambah "   semua service berjalan normal"
fi
tambah ""
tambah "PEMBERSIHAN"
tambah "----------------------------------------------------------------"
tambah "${BAG_BERSIH%$'\n'}"
tambah ""
tambah "HEMAT RUANG"
tambah "----------------------------------------------------------------"
tambah "Total: $JUMLAH_FILE file, $HEMAT_BYTES byte ($LABEL_HEMAT)"
tambah ""
tambah "REKOMENDASI"
tambah "----------------------------------------------------------------"
nomor=1
if [ "$DISK_N" -gt 0 ]; then
    tambah "$nomor. Pemakaian disk melewati ambang $DISK_WARN_PCT% di $DISK_MELEBIHI:"
    tambah "   cari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan."
    nomor=$((nomor + 1))
fi
if [ "$PAKET_N" -gt 0 ]; then
    tambah "$nomor. Ada $PAKET_N paket update tertunda:"
    tambah "   jalankan apt upgrade pada jadwal maintenance berikutnya."
    nomor=$((nomor + 1))
fi
if [ "$SVC_N" -gt 0 ]; then
    tambah "$nomor. Ada $SVC_N service gagal ($SVC_DAFTAR):"
    tambah "   periksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman."
    nomor=$((nomor + 1))
fi
if [ "$MODE" = "dry-run" ] && [ "$JUMLAH_FILE" -gt 0 ]; then
    tambah "$nomor. DRY-RUN menemukan $JUMLAH_FILE file ($HEMAT_BYTES byte) layak dibersihkan:"
    tambah "   periksa daftar di bagian PEMBERSIHAN, lalu jalankan ulang dengan --apply."
    nomor=$((nomor + 1))
fi
if [ "$nomor" -eq 1 ]; then
    tambah "1. Tidak ada temuan: sistem dalam kondisi baik. Ulangi pemeliharaan sesuai jadwal."
fi

# --- 7. Tulis laporan ke stdout dan berkas ---------------------------------
REPORT_FILE="${REPORT_FILE:-laporan-pemeliharaan.txt}"
mkdir -p "$(dirname "$REPORT_FILE")"
printf '%s' "$LAPORAN" | tee "$REPORT_FILE"
exit 0
BASH

echo "== DRY-RUN: laporan penuh (mode default) =="
HARI_INI=2026-02-15 bash pemeliharaan.sh
echo "== APPLY: seksi akhir laporan, dibaca dari berkas =="
HARI_INI=2026-02-15 bash pemeliharaan.sh --apply > /dev/null
sed -n '/^PEMBERSIHAN/,$p' laporan-pemeliharaan.txt
echo "== folder setelah apply =="
ls -1 data/cache data/log data/tmp
echo "== dry-run berikutnya: kandidat sudah bersih =="
HARI_INI=2026-02-15 bash pemeliharaan.sh | sed -n '/^PEMBERSIHAN/,$p'

Output yang Dihasilkan:

text
== DRY-RUN: laporan penuh (mode default) ==
================================================================
LAPORAN PEMELIHARAAN SISTEM
Tanggal acuan : 2026-02-15
Mode          : DRY-RUN (tidak ada file yang dihapus)
================================================================

PEMERIKSAAN
----------------------------------------------------------------
1. Pemakaian disk (ambang 80%)
   /           73%  OK
   /data       89%  WASPADA
   /dev/shm     1%  OK
2. Paket update tertunda
   4 paket siap di-upgrade: curl, libcurl4, libssl3, openssl
3. Service gagal
   2 service failed: nginx.service, postgresql.service

PEMBERSIHAN
----------------------------------------------------------------
cache — file lebih tua dari 2026-01-16 (whitelist: data/cache)
   [dry-run] data/cache/lama.txt (15 byte)

log — file *.gz lebih tua dari 2026-02-01 (whitelist: data/log)
   [dry-run] data/log/app.log.1.gz (25 byte)

tmp — file lebih tua dari 2026-02-08 (whitelist: data/tmp)
   [dry-run] data/tmp/sisa.tmp (20 byte)

HEMAT RUANG
----------------------------------------------------------------
Total: 3 file, 60 byte (potensi hemat; jalankan --apply untuk benar-benar menghapus)

REKOMENDASI
----------------------------------------------------------------
1. Pemakaian disk melewati ambang 80% di /data:
   cari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan.
2. Ada 4 paket update tertunda:
   jalankan apt upgrade pada jadwal maintenance berikutnya.
3. Ada 2 service gagal (nginx.service, postgresql.service):
   periksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman.
4. DRY-RUN menemukan 3 file (60 byte) layak dibersihkan:
   periksa daftar di bagian PEMBERSIHAN, lalu jalankan ulang dengan --apply.
== APPLY: seksi akhir laporan, dibaca dari berkas ==
PEMBERSIHAN
----------------------------------------------------------------
cache — file lebih tua dari 2026-01-16 (whitelist: data/cache)
   [terhapus] data/cache/lama.txt (15 byte)

log — file *.gz lebih tua dari 2026-02-01 (whitelist: data/log)
   [terhapus] data/log/app.log.1.gz (25 byte)

tmp — file lebih tua dari 2026-02-08 (whitelist: data/tmp)
   [terhapus] data/tmp/sisa.tmp (20 byte)

HEMAT RUANG
----------------------------------------------------------------
Total: 3 file, 60 byte (benar-benar dibebaskan dari disk)

REKOMENDASI
----------------------------------------------------------------
1. Pemakaian disk melewati ambang 80% di /data:
   cari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan.
2. Ada 4 paket update tertunda:
   jalankan apt upgrade pada jadwal maintenance berikutnya.
3. Ada 2 service gagal (nginx.service, postgresql.service):
   periksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman.
== folder setelah apply ==
data/cache:
baru.txt

data/log:
app.log
app.log.2.gz

data/tmp:
aktif.tmp
== dry-run berikutnya: kandidat sudah bersih ==
PEMBERSIHAN
----------------------------------------------------------------
cache — file lebih tua dari 2026-01-16 (whitelist: data/cache)
   (tidak ada kandidat)

log — file *.gz lebih tua dari 2026-02-01 (whitelist: data/log)
   (tidak ada kandidat)

tmp — file lebih tua dari 2026-02-08 (whitelist: data/tmp)
   (tidak ada kandidat)

HEMAT RUANG
----------------------------------------------------------------
Total: 0 file, 0 byte (potensi hemat; jalankan --apply untuk benar-benar menghapus)

REKOMENDASI
----------------------------------------------------------------
1. Pemakaian disk melewati ambang 80% di /data:
   cari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan.
2. Ada 4 paket update tertunda:
   jalankan apt upgrade pada jadwal maintenance berikutnya.
3. Ada 2 service gagal (nginx.service, postgresql.service):
   periksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman.

Sesi itu menampilkan seluruh siklus pemeliharaan dalam satu blok. Mari kita bedah bagian-bagian yang khusus menyusun laporan:

  1. tambah(): satu-satunya cara menulis laporan — menambah satu baris ke variabel LAPORAN beserta baris barunya ($'\n'). Laporan dirakit dari potongan-potongan kecil, bukan satu echo raksasa, sehingga seksi bisa disisipkan kondisi apa pun.
  2. BAG_BERSIH: variabel ini terisi selama fungsi bersihkan() bekerja — tiap kandidat menambah satu baris [dry-run] atau [terhapus] beserta ukurannya dari stat -c %s, dan whitelist tanpa kandidat menulis (tidak ada kandidat) agar laporan tidak pernah menampilkan seksi kosong tanpa penjelasan.
  3. LABEL_MODE dan LABEL_HEMAT: dua kalimat yang berbeda mengikuti mode — di dry-run seksi hemat menulis “potensi hemat”, setelah --apply menjadi “benar-benar dibebaskan dari disk”. Satu sumber data, dua cara membaca.
  4. Rantai rekomendasi: setiap temuan menambah satu butir bernomor — nomor diinkremen setiap kali — dan butir “jalankan ulang dengan –apply” hanya muncul di dry-run yang punya kandidat. Saat tidak ada temuan sama sekali, laporan tidak kosong melainkan menulis satu kalimat “sistem dalam kondisi baik”.
  5. printf '%s' "$LAPORAN" | tee "$REPORT_FILE": tee menyalin stdin ke berkas dan stdout sekaligus — satu konten, dua tujuan, tanpa menulis laporan dua kali; perilakunya terdokumentasi di tee(1) .

Perhatikan dry-run terakhir pada sesi di atas: setelah apply, semua whitelist menulis (tidak ada kandidat), total hemat kembali ke nol, dan rekomendasi menyusut dari empat butir menjadi tiga — persis laporan yang akan dibaca administrator minggu berikutnya jika pembersihan ini dijadwalkan.

Rekomendasi Berbasis Temuan #

Rekomendasi skrip tidak dibuat dari template tetap, melainkan diturunkan dari temuan lewat rantai kondisi — setiap baris tabel berikut adalah satu if di skrip:

TemuanRekomendasi yang ditulis
disk melewati ambangcari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan
ada paket tertundajalankan apt upgrade pada jadwal maintenance berikutnya
ada service gagalperiksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman
dry-run menemukan kandidatperiksa daftar, lalu jalankan ulang dengan --apply
tidak ada temuanulangi pemeliharaan sesuai jadwal

Setiap rekomendasi menyebut perintah yang tepat untuk temuan yang tepat, bukan saran umum. Ambang disk memakai DISK_WARN_PCT dari konfigurasi; saran paket mengarah ke jadwal maintenance, bukan “segera update”, karena skrip tidak tahu kebijakan jendela layanan mesin Anda — keputusan seperti itu memang ditinggalkan untuk manusia yang membaca laporan.

Test Suite dengan Assertion #

Laporan yang bagus di sesi demo belum menjadi jaminan; jaminan datang dari pengujian yang mengulang janji-janji desain setiap kali skrip berubah. Test suite kita memeriksa: dry-run sukses dan tidak menghapus, laporan dibuat dengan empat seksi, parsing ketiga cek benar, apply hanya menghapus file yang benar sementara file sehat tetap ada, angka laporan akurat, dua run dry-run menghasilkan laporan identik, dan setiap jenis kegagalan keluar dengan kode sesuai kontrak. Blok berikut menyusun tata letak proyek persis seperti paket yang menyertai halaman ini, lalu menjalankan suitenya:

bash
mkdir -p proyek/src proyek/config proyek/tests
cat > proyek/src/pemeliharaan.sh <<'BASH'
#!/usr/bin/env bash
# pemeliharaan.sh — pemeliharaan berkala: periksa, bersihkan, laporkan.
#
# Membaca snapshot data sistem (df.txt, paket.txt, services.txt) dari
# folder data, memeriksa tiga hal — pemakaian disk, paket update
# tertunda, service gagal — lalu membersihkan file cache, log terkompresi
# (*.gz), dan file tmp usang HANYA di dalam folder whitelist yang
# tercantum di berkas konfigurasi. Mode default DRY-RUN: tidak ada
# file yang dihapus sampai --apply diberikan, dan mode tidak bisa
# diubah dari konfigurasi. Hasil akhirnya adalah laporan terstruktur
# yang ditulis ke stdout sekaligus berkas laporan.
#
# Pemakaian:
#   bash pemeliharaan.sh [--data-dir DIR] [--config FILE] [--apply]
#
# Variabel lingkungan (berguna untuk pengujian):
#   CONF      berkas konfigurasi yang dibaca (default: pemeliharaan.conf)
#   SOURCE_DIR  folder data; kalah oleh --data-dir (default: data)
#   HARI_INI  tanggal acuan umur file, format YYYY-MM-DD (default: hari ini)
#
# Kode keluar:
#   0 sukses                        1 konfigurasi tidak valid
#   2 berkas snapshot tidak ada     3 folder whitelist tidak ada

set -u

CONF="${CONF:-pemeliharaan.conf}"
DATA_DIR="${SOURCE_DIR:-data}"
HARI_INI="${HARI_INI:-$(date +%F)}"
APPLY=0

# --- 1. Baca argumen -----------------------------------------------------
while [ $# -gt 0 ]; do
    case "$1" in
        --apply) APPLY=1; shift ;;
        --data-dir|--config)
            if [ $# -lt 2 ]; then
                printf 'pemeliharaan.sh: %s butuh nilai\n' "$1" >&2
                exit 1
            fi
            if [ "$1" = "--data-dir" ]; then
                DATA_DIR="$2"
            else
                CONF="$2"
            fi
            shift 2
            ;;
        *)
            printf 'pemeliharaan.sh: argumen tidak dikenal: %s\n' "$1" >&2
            exit 1
            ;;
    esac
done

# --- 2. Baca dan validasi konfigurasi ------------------------------------
if [ ! -f "$CONF" ]; then
    printf 'pemeliharaan.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
    exit 1
fi

# shellcheck source=/dev/null
. "$CONF"

if [ -z "${CACHE_DIRS:-}" ] && [ -z "${LOG_DIRS:-}" ] && [ -z "${TMP_DIRS:-}" ]; then
    printf 'pemeliharaan.sh: whitelist kosong (CACHE_DIRS/LOG_DIRS/TMP_DIRS) di %s\n' "$CONF" >&2
    exit 1
fi

valid_umur() {
    case $1 in
        ''|*[!0-9]*|0) return 1 ;;
    esac
    return 0
}

valid_umur "${CACHE_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: CACHE_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }
valid_umur "${LOG_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: LOG_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }
valid_umur "${TMP_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: TMP_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }

case ${DISK_WARN_PCT:-} in
    ''|*[!0-9]*|0)
        printf 'pemeliharaan.sh: DISK_WARN_PCT harus angka 1-99\n' >&2
        exit 1
        ;;
esac
if [ "$DISK_WARN_PCT" -gt 99 ]; then
    printf 'pemeliharaan.sh: DISK_WARN_PCT harus angka 1-99\n' >&2
    exit 1
fi

# Mode sengaja di set SETELAH konfigurasi dibaca: walau berkas
# konfigurasi berisi "MODE=apply", nilainya tetap ditimpa di sini.
# Hanya argumen --apply yang boleh menyalakan pembersihan sungguhan.
MODE="dry-run"
if [ "$APPLY" -eq 1 ]; then
    MODE="apply"
fi

# --- 3. Validasi snapshot data --------------------------------------------
for f in df.txt paket.txt services.txt; do
    if [ ! -f "$DATA_DIR/$f" ]; then
        printf 'pemeliharaan.sh: snapshot %s/%s tidak ditemukan\n' "$DATA_DIR" "$f" >&2
        exit 2
    fi
done

# --- 4. Pemeriksaan -------------------------------------------------------
# Temuan pemeriksaan terkumpul di sini, lalu menjadi bagian laporan.
# Disk: kolom ke-5 df adalah Use%; "+0" di awk membuang karakter '%'
# sehingga "89%" terbaca sebagai angka 89.
DISK_BARIS=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 {
        pct = $5 + 0
        if (pct >= ambang)
            printf "   %-10s %3d%%  WASPADA\n", $6, pct
        else
            printf "   %-10s %3d%%  OK\n", $6, pct
    }' "$DATA_DIR/df.txt")

DISK_MELEBIHI=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 && ($5 + 0) >= ambang {
        printf "%s%s", pisah, $6
        pisah = ", "
    }' "$DATA_DIR/df.txt")
DISK_N=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 && ($5 + 0) >= ambang { n++ } END { print n + 0 }' "$DATA_DIR/df.txt")

# Paket tertunda: output simulasi apt-get -s upgrade menandai tiap paket
# dengan satu baris "Inst <nama> [versi lama] (versi baru)".
PAKET_DAFTAR=$(awk '/^Inst / {
        printf "%s%s", pisah, $2
        pisah = ", "
    }' "$DATA_DIR/paket.txt")
PAKET_N=$(awk '/^Inst / { n++ } END { print n + 0 }' "$DATA_DIR/paket.txt")

# Service gagal: snapshot berformat "unit substate" per baris.
SVC_DAFTAR=$(awk '$2 == "failed" {
        printf "%s%s", pisah, $1
        pisah = ", "
    }' "$DATA_DIR/services.txt")
SVC_N=$(awk '$2 == "failed" { n++ } END { print n + 0 }' "$DATA_DIR/services.txt")

# --- 5. Pembersihan (hanya di dalam whitelist) -----------------------------
# Hasil pembersihan terkumpul di BAG_BERSIH; penghitung JUMLAH_FILE dan
# HEMAT_BYTES dipakai seksi HEMAT RUANG di laporan.
JUMLAH_FILE=0
HEMAT_BYTES=0
BAG_BERSIH=""

# bersihkan <judul> <dirs> <umur-hari> <pola-nama>:
# kumpulkan file di folder whitelist yang lebih tua dari batas umur,
# catat ukurannya untuk laporan, dan hapus hanya bila mode apply.
# -type f menjaga folder tetap utuh; find hanya berangkat dari folder
# whitelist, jadi file di luar daftar tidak pernah tersentuh.
bersihkan() {
    judul="$1"
    dirs="$2"
    umur="$3"
    pola="${4:-*}"
    batas=$(date -d "$HARI_INI -$umur days" +%F)
    if [ "$pola" = "*" ]; then
        label="file"
    else
        label="file $pola"
    fi
    terpakai=0

    if [ -n "$BAG_BERSIH" ]; then
        BAG_BERSIH+=$'\n'
    fi
    BAG_BERSIH+="$judul — $label lebih tua dari $batas (whitelist: $dirs)"$'\n'

    # shellcheck disable=SC2086  # whitelist memang dipisah spasi
    for dir in $dirs; do
        if [ ! -d "$dir" ]; then
            printf 'pemeliharaan.sh: folder whitelist %s tidak ditemukan\n' "$dir" >&2
            exit 3
        fi
        # Process substitution < <(...) menjaga penghitung tetap hidup
        # di shell utama; pipeline biasa akan menjalankan while di
        # subshell sehingga JUMLAH_FILE hilang.
        while IFS= read -r f; do
            [ -n "$f" ] || continue
            ukuran=$(stat -c %s "$f")
            JUMLAH_FILE=$((JUMLAH_FILE + 1))
            HEMAT_BYTES=$((HEMAT_BYTES + ukuran))
            terpakai=$((terpakai + 1))
            if [ "$MODE" = "apply" ]; then
                rm -f -- "$f"
                BAG_BERSIH+="   [terhapus] $f (${ukuran} byte)"$'\n'
            else
                BAG_BERSIH+="   [dry-run] $f (${ukuran} byte)"$'\n'
            fi
        done < <(find "$dir" -type f -name "$pola" ! -newermt "$batas" | sort)
    done

    if [ "$terpakai" -eq 0 ]; then
        BAG_BERSIH+="   (tidak ada kandidat)"$'\n'
    fi
}

if [ -n "${CACHE_DIRS:-}" ]; then
    bersihkan "cache" "$CACHE_DIRS" "$CACHE_MAX_DAYS"
fi
if [ -n "${LOG_DIRS:-}" ]; then
    bersihkan "log" "$LOG_DIRS" "$LOG_MAX_DAYS" "*.gz"
fi
if [ -n "${TMP_DIRS:-}" ]; then
    bersihkan "tmp" "$TMP_DIRS" "$TMP_MAX_DAYS"
fi

# --- 6. Susun laporan -----------------------------------------------------
if [ "$MODE" = "apply" ]; then
    LABEL_MODE="APPLY (pembersihan benar-benar dijalankan)"
    LABEL_HEMAT="benar-benar dibebaskan dari disk"
else
    LABEL_MODE="DRY-RUN (tidak ada file yang dihapus)"
    LABEL_HEMAT="potensi hemat; jalankan --apply untuk benar-benar menghapus"
fi

LAPORAN=""
tambah() {
    LAPORAN+="$1"
    LAPORAN+=$'\n'
}

tambah "================================================================"
tambah "LAPORAN PEMELIHARAAN SISTEM"
tambah "Tanggal acuan : $HARI_INI"
tambah "Mode          : $LABEL_MODE"
tambah "================================================================"
tambah ""
tambah "PEMERIKSAAN"
tambah "----------------------------------------------------------------"
tambah "1. Pemakaian disk (ambang $DISK_WARN_PCT%)"
tambah "$DISK_BARIS"
tambah "2. Paket update tertunda"
if [ "$PAKET_N" -gt 0 ]; then
    tambah "   $PAKET_N paket siap di-upgrade: $PAKET_DAFTAR"
else
    tambah "   tidak ada paket tertunda"
fi
tambah "3. Service gagal"
if [ "$SVC_N" -gt 0 ]; then
    tambah "   $SVC_N service failed: $SVC_DAFTAR"
else
    tambah "   semua service berjalan normal"
fi
tambah ""
tambah "PEMBERSIHAN"
tambah "----------------------------------------------------------------"
tambah "${BAG_BERSIH%$'\n'}"
tambah ""
tambah "HEMAT RUANG"
tambah "----------------------------------------------------------------"
tambah "Total: $JUMLAH_FILE file, $HEMAT_BYTES byte ($LABEL_HEMAT)"
tambah ""
tambah "REKOMENDASI"
tambah "----------------------------------------------------------------"
nomor=1
if [ "$DISK_N" -gt 0 ]; then
    tambah "$nomor. Pemakaian disk melewati ambang $DISK_WARN_PCT% di $DISK_MELEBIHI:"
    tambah "   cari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan."
    nomor=$((nomor + 1))
fi
if [ "$PAKET_N" -gt 0 ]; then
    tambah "$nomor. Ada $PAKET_N paket update tertunda:"
    tambah "   jalankan apt upgrade pada jadwal maintenance berikutnya."
    nomor=$((nomor + 1))
fi
if [ "$SVC_N" -gt 0 ]; then
    tambah "$nomor. Ada $SVC_N service gagal ($SVC_DAFTAR):"
    tambah "   periksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman."
    nomor=$((nomor + 1))
fi
if [ "$MODE" = "dry-run" ] && [ "$JUMLAH_FILE" -gt 0 ]; then
    tambah "$nomor. DRY-RUN menemukan $JUMLAH_FILE file ($HEMAT_BYTES byte) layak dibersihkan:"
    tambah "   periksa daftar di bagian PEMBERSIHAN, lalu jalankan ulang dengan --apply."
    nomor=$((nomor + 1))
fi
if [ "$nomor" -eq 1 ]; then
    tambah "1. Tidak ada temuan: sistem dalam kondisi baik. Ulangi pemeliharaan sesuai jadwal."
fi

# --- 7. Tulis laporan ke stdout dan berkas ---------------------------------
REPORT_FILE="${REPORT_FILE:-laporan-pemeliharaan.txt}"
mkdir -p "$(dirname "$REPORT_FILE")"
printf '%s' "$LAPORAN" | tee "$REPORT_FILE"
exit 0
BASH
cat > proyek/config/pemeliharaan.conf <<'CONF'
# pemeliharaan.conf — konfigurasi skrip pemeliharaan.
# Sunting nilai di bawah ini sesuai komputer Anda; skrip pemeliharaan.sh
# tidak perlu disentuh. Jalur whitelist dihitung dari direktori kerja
# saat skrip dijalankan; jalur absolut (mis. /var/cache/myapp) juga
# boleh dipakai.

# Whitelist folder yang BOLEH dibersihkan, dipisah spasi.
# Folder di luar tiga daftar ini tidak pernah disentuh skrip.
CACHE_DIRS="data/cache"
LOG_DIRS="data/log"
TMP_DIRS="data/tmp"

# Umur maksimum file (hari) sebelum layak dibersihkan.
CACHE_MAX_DAYS=30
LOG_MAX_DAYS=14
TMP_MAX_DAYS=7

# Ambang pemakaian disk (%) yang menyalakan status WASPADA (1-99).
DISK_WARN_PCT=80

# Berkas laporan (opsional; default: laporan-pemeliharaan.txt).
REPORT_FILE="laporan-pemeliharaan.txt"
CONF
cat > proyek/tests/run-tests.sh <<'TESTS'
#!/usr/bin/env bash
# run-tests.sh — test suite untuk pemeliharaan.sh.
# Suite menyiapkan sendiri folder kerja terisolasi (mktemp) beserta
# data contohnya — snapshot beku (df.txt, paket.txt, services.txt) dan
# file cache/log/tmp dengan mtime beku lewat touch -d — lalu menjalankan
# rangkaian pengujian dengan assertion sederhana. Jalankan dari mana
# saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/src/pemeliharaan.sh" "$PROJ_DIR/config/pemeliharaan.conf" "$WORK/"
cd "$WORK" || exit 1

# --- fixture: snapshot data beku ----------------------------------------
mkdir -p data/cache data/log data/tmp
cat > data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   35G   13G  73% /
/dev/sdb1       200G  170G   21G  89% /data
tmpfs           7.8G  1.1M  7.8G   1% /dev/shm
EOF
cat > data/paket.txt <<'EOF'
Reading package lists... Done
Building dependency tree... Done
Calculating upgrade... Done
Reading state information... Done
The following packages will be upgraded:
  curl libcurl4 libssl3 openssl
4 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Inst curl [7.88.1-10] (7.88.1-11)
Inst libcurl4 [7.88.1-10] (7.88.1-11)
Inst libssl3 [3.0.8-1] (3.0.8-2)
Inst openssl [3.0.8-1] (3.0.8-2)
Conf curl (7.88.1-11)
Conf libcurl4 (7.88.1-11)
Conf libssl3 (3.0.8-2)
Conf openssl (3.0.8-2)
EOF
cat > data/services.txt <<'EOF'
nginx.service failed
postgresql.service failed
sshd.service running
cron.service running
EOF

# --- fixture: file dengan mtime beku -------------------------------------
# Tanggal acuan pengujian: 2026-02-15. Batas umur menjadi
# cache 30 hari -> 2026-01-16, log 14 hari -> 2026-02-01,
# tmp 7 hari -> 2026-02-08.
printf 'isi cache lama\n' > data/cache/lama.txt
touch -d "2026-01-10" data/cache/lama.txt
printf 'isi cache baru\n' > data/cache/baru.txt
touch -d "2026-02-10" data/cache/baru.txt
printf 'isi log lama terkompresi\n' > data/log/app.log.1.gz
touch -d "2026-01-25" data/log/app.log.1.gz
printf 'isi log lama tapi bukan gz\n' > data/log/app.log
touch -d "2026-01-20" data/log/app.log
printf 'isi log baru\n' > data/log/app.log.2.gz
touch -d "2026-02-12" data/log/app.log.2.gz
printf 'sisa instalasi lama\n' > data/tmp/sisa.tmp
touch -d "2026-02-01" data/tmp/sisa.tmp
printf 'file tmp aktif\n' > data/tmp/aktif.tmp
touch -d "2026-02-13" data/tmp/aktif.tmp

# Ukuran total yang harus dibebaskan mode apply: tiga file tua.
total_hemat=$(stat -c %s data/cache/lama.txt data/log/app.log.1.gz \
    data/tmp/sisa.tmp | awk '{ s += $1 } END { print s }')

# --- uji 1: dry-run sukses dan tidak menghapus ---------------------------
HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 0 "$?" "dry-run sukses keluar 0"
[ -f data/cache/lama.txt ]
assert_eq 0 "$?" "dry-run tidak menghapus cache lama"
[ -f data/log/app.log.1.gz ]
assert_eq 0 "$?" "dry-run tidak menghapus log lama"

# --- uji 2: laporan dibuat dengan empat seksi ----------------------------
[ -f laporan-pemeliharaan.txt ]
assert_eq 0 "$?" "berkas laporan dibuat"
grep -q '^PEMERIKSAAN$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi PEMERIKSAAN"
grep -q '^PEMBERSIHAN$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi PEMBERSIHAN"
grep -q '^HEMAT RUANG$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi HEMAT RUANG"
grep -q '^REKOMENDASI$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi REKOMENDASI"

# --- uji 3: parsing ketiga pemeriksaan benar ------------------------------
grep -q '89%  WASPADA' laporan-pemeliharaan.txt
assert_eq 0 "$?" "disk 89% ditandai WASPADA"
grep -q '73%  OK' laporan-pemeliharaan.txt
assert_eq 0 "$?" "disk 73% ditandai OK"
grep -q '4 paket siap di-upgrade: curl, libcurl4, libssl3, openssl' \
    laporan-pemeliharaan.txt
assert_eq 0 "$?" "jumlah dan daftar paket tertunda benar"
grep -q '2 service failed: nginx.service, postgresql.service' \
    laporan-pemeliharaan.txt
assert_eq 0 "$?" "jumlah dan daftar service gagal benar"

# --- uji 4: daftar kandidat dan rekomendasi dry-run ------------------------
grep -Fq '[dry-run] data/cache/lama.txt' laporan-pemeliharaan.txt
assert_eq 0 "$?" "dry-run menampilkan kandidat cache"
grep -Fq '[dry-run] data/log/app.log.1.gz' laporan-pemeliharaan.txt
assert_eq 0 "$?" "dry-run menampilkan kandidat log"
grep -Fq '[dry-run] data/tmp/sisa.tmp' laporan-pemeliharaan.txt
assert_eq 0 "$?" "dry-run menampilkan kandidat tmp"
grep -q 'jalankan ulang dengan --apply' laporan-pemeliharaan.txt
assert_eq 0 "$?" "rekomendasi menyarankan --apply"

# --- uji 5: dua run dry-run menghasilkan laporan identik -------------------
cp laporan-pemeliharaan.txt laporan-run1.txt
HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 0 "$?" "dry-run kedua sukses keluar 0"
diff laporan-run1.txt laporan-pemeliharaan.txt > /dev/null 2>&1
assert_eq 0 "$?" "dua laporan dry-run identik"

# --- uji 6: apply menghapus yang benar saja -------------------------------
HARI_INI=2026-02-15 bash pemeliharaan.sh --apply > /dev/null 2>&1
assert_eq 0 "$?" "apply sukses keluar 0"
[ ! -f data/cache/lama.txt ]
assert_eq 0 "$?" "apply menghapus cache lama"
[ ! -f data/log/app.log.1.gz ]
assert_eq 0 "$?" "apply menghapus log gz lama"
[ ! -f data/tmp/sisa.tmp ]
assert_eq 0 "$?" "apply menghapus tmp lama"
[ -f data/cache/baru.txt ]
assert_eq 0 "$?" "cache baru tetap ada"
[ -f data/log/app.log ]
assert_eq 0 "$?" "log lama non-gz tetap ada (pola hanya *.gz)"
[ -f data/log/app.log.2.gz ]
assert_eq 0 "$?" "log gz baru tetap ada"
[ -f data/tmp/aktif.tmp ]
assert_eq 0 "$?" "tmp baru tetap ada"

# --- uji 7: laporan apply akurat -------------------------------------------
terhapus=$(grep -Fc '[terhapus]' laporan-pemeliharaan.txt)
assert_eq 3 "$terhapus" "laporan apply mencatat 3 file terhapus"
grep -q "Total: 3 file, $total_hemat byte (benar-benar dibebaskan dari disk)" \
    laporan-pemeliharaan.txt
assert_eq 0 "$?" "total hemat ruang cocok dengan jumlah byte fixture"
! grep -q 'jalankan ulang dengan --apply' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan apply tidak menyarankan --apply lagi"

# --- uji 8: exit code untuk tiap jenis kegagalan ----------------------------
CONF=nihil.conf HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 1 "$?" "konfigurasi hilang keluar 1"
printf 'CACHE_DIRS="data/cache"\nLOG_DIRS=""\nTMP_DIRS=""\nCACHE_MAX_DAYS=abc\nLOG_MAX_DAYS=14\nTMP_MAX_DAYS=7\nDISK_WARN_PCT=80\n' > umur-rusak.conf
CONF=umur-rusak.conf HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 1 "$?" "umur tidak angka keluar 1"
mkdir -p data-kosong
SOURCE_DIR=data-kosong HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 2 "$?" "snapshot hilang keluar 2"
printf 'CACHE_DIRS="data/cache"\nLOG_DIRS="data/log"\nTMP_DIRS="data/tidak-ada"\nCACHE_MAX_DAYS=30\nLOG_MAX_DAYS=14\nTMP_MAX_DAYS=7\nDISK_WARN_PCT=80\n' > whitelist-rusak.conf
CONF=whitelist-rusak.conf HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 3 "$?" "folder whitelist hilang keluar 3"
HARI_INI=2026-02-15 bash pemeliharaan.sh --bogus > /dev/null 2>&1
assert_eq 1 "$?" "argumen tak dikenal keluar 1"

# --- ringkasan --------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]
TESTS
bash proyek/tests/run-tests.sh

Output yang Dihasilkan:

text
OK    dry-run sukses keluar 0
OK    dry-run tidak menghapus cache lama
OK    dry-run tidak menghapus log lama
OK    berkas laporan dibuat
OK    laporan memuat seksi PEMERIKSAAN
OK    laporan memuat seksi PEMBERSIHAN
OK    laporan memuat seksi HEMAT RUANG
OK    laporan memuat seksi REKOMENDASI
OK    disk 89% ditandai WASPADA
OK    disk 73% ditandai OK
OK    jumlah dan daftar paket tertunda benar
OK    jumlah dan daftar service gagal benar
OK    dry-run menampilkan kandidat cache
OK    dry-run menampilkan kandidat log
OK    dry-run menampilkan kandidat tmp
OK    rekomendasi menyarankan --apply
OK    dry-run kedua sukses keluar 0
OK    dua laporan dry-run identik
OK    apply sukses keluar 0
OK    apply menghapus cache lama
OK    apply menghapus log gz lama
OK    apply menghapus tmp lama
OK    cache baru tetap ada
OK    log lama non-gz tetap ada (pola hanya *.gz)
OK    log gz baru tetap ada
OK    tmp baru tetap ada
OK    laporan apply mencatat 3 file terhapus
OK    total hemat ruang cocok dengan jumlah byte fixture
OK    laporan apply tidak menyarankan --apply lagi
OK    konfigurasi hilang keluar 1
OK    umur tidak angka keluar 1
OK    snapshot hilang keluar 2
OK    folder whitelist hilang keluar 3
OK    argumen tak dikenal keluar 1
PASSED: 34/34 test lulus

Pola-pola penting di dalam suite:

  1. assert_eq <harapan> <nyata> <pesan>: pengujian sebagai perbandingan nilai; $? ditangkap tepat setelah perintah yang diperiksa, dan pesan kegagalan menyebut kedua nilai sehingga penyebabnya terbaca tanpa menjalankan ulang.
  2. Fixture beku: snapshot ditulis dengan heredoc, umur file dibekukan dengan touch -d, dan HARI_INI=2026-02-15 dikunci di setiap pemanggilan — tiga lapis yang membuat suite memberi hasil identik di mesin mana pun, kapan pun.
  3. Uji dry-run yang tegas: setelah eksekusi dry-run, suite memeriksa file lama masih ada — bukan hanya memeriksa exit code. Mode default diuji lewat akibatnya, bukan lewat klaimnya.
  4. Uji apply yang selektif: tiga file tua dinyatakan hilang, empat file sehat dinyatakan masih ada — termasuk app.log yang sama tua dengan app.log.1.gz tapi bukan *.gz. Penghapusan yang “terlalu rajin” akan tertangkap di sini.
  5. Akurasi angka: total hemat diharapkan sama dengan jumlah byte ketiga file yang dihitung ulang lewat stat — laporan tidak boleh mengarang angka, dan pengujian memastikannya.
  6. Determinisme: dua run dry-run dibandingkan dengan diff — kalau laporan memuat sesuatu yang berubah antar eksekusi (jam, PID, urutan acak), suite gagal.
  7. Kontrak exit code: 0 sukses, 1 konfigurasi tidak valid atau argumen salah, 2 snapshot hilang, 3 folder whitelist hilang — masing-masing dipicu dengan sengaja dan diperiksa kodenya; konvensi kode keluar sendiri sudah dibahas di Chapter Exit Status dan Error Handling .

Dari Skrip ke Jadwal #

Skrip yang teruji tinggal dijadwalkan. Satu baris cron seperti 30 7 * * 1 cd /home/budi/pemeliharaan && bash pemeliharaan.sh menjalankan pemeliharaan tiap Senin pagi dan meninggalkan laporan di berkas yang bisa dibaca kapan saja — pola penjadwalannya dibahas tuntas di Chapter Scheduling Scripts . Dua kebiasaan penutup yang layak dipertahankan: biarkan cron menjalankan dry-run dan jalankan --apply sendiri setelah membaca laporan, setidaknya sampai whitelist Anda terbukti benar dalam beberapa minggu; dan sisihkan waktu membaca rekomendasi — skrip menemukan gejalanya, manusia yang memutuskan prioritasnya. Panduan ringkas pemasangan, pembacaan laporan, dan tabel kode keluar tersedia di docs/README.txt pada paket berkas halaman ini.

Proyek pemeliharaan selesai: data dibekukan lalu diperiksa, pembersihan dibatasi whitelist-umur-dry-run, laporan menyusun temuan menjadi keputusan, dan test suite menjaga semuanya. Pola rancangannya — kumpulkan, periksa, bertindak dengan aman, laporkan — akan terpakai lagi dalam skala yang lebih besar pada proyek berikutnya, Server Web Linux , tempat pemeliharaan bukan lagi skrip mingguan melainkan bagian dari merawat layanan yang berjalan terus.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

src/pemeliharaan.sh

Unduh berkas
bash
#!/usr/bin/env bash
# pemeliharaan.sh — pemeliharaan berkala: periksa, bersihkan, laporkan.
#
# Membaca snapshot data sistem (df.txt, paket.txt, services.txt) dari
# folder data, memeriksa tiga hal — pemakaian disk, paket update
# tertunda, service gagal — lalu membersihkan file cache, log terkompresi
# (*.gz), dan file tmp usang HANYA di dalam folder whitelist yang
# tercantum di berkas konfigurasi. Mode default DRY-RUN: tidak ada
# file yang dihapus sampai --apply diberikan, dan mode tidak bisa
# diubah dari konfigurasi. Hasil akhirnya adalah laporan terstruktur
# yang ditulis ke stdout sekaligus berkas laporan.
#
# Pemakaian:
#   bash pemeliharaan.sh [--data-dir DIR] [--config FILE] [--apply]
#
# Variabel lingkungan (berguna untuk pengujian):
#   CONF      berkas konfigurasi yang dibaca (default: pemeliharaan.conf)
#   SOURCE_DIR  folder data; kalah oleh --data-dir (default: data)
#   HARI_INI  tanggal acuan umur file, format YYYY-MM-DD (default: hari ini)
#
# Kode keluar:
#   0 sukses                        1 konfigurasi tidak valid
#   2 berkas snapshot tidak ada     3 folder whitelist tidak ada

set -u

CONF="${CONF:-pemeliharaan.conf}"
DATA_DIR="${SOURCE_DIR:-data}"
HARI_INI="${HARI_INI:-$(date +%F)}"
APPLY=0

# --- 1. Baca argumen -----------------------------------------------------
while [ $# -gt 0 ]; do
    case "$1" in
        --apply) APPLY=1; shift ;;
        --data-dir|--config)
            if [ $# -lt 2 ]; then
                printf 'pemeliharaan.sh: %s butuh nilai\n' "$1" >&2
                exit 1
            fi
            if [ "$1" = "--data-dir" ]; then
                DATA_DIR="$2"
            else
                CONF="$2"
            fi
            shift 2
            ;;
        *)
            printf 'pemeliharaan.sh: argumen tidak dikenal: %s\n' "$1" >&2
            exit 1
            ;;
    esac
done

# --- 2. Baca dan validasi konfigurasi ------------------------------------
if [ ! -f "$CONF" ]; then
    printf 'pemeliharaan.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
    exit 1
fi

# shellcheck source=/dev/null
. "$CONF"

if [ -z "${CACHE_DIRS:-}" ] && [ -z "${LOG_DIRS:-}" ] && [ -z "${TMP_DIRS:-}" ]; then
    printf 'pemeliharaan.sh: whitelist kosong (CACHE_DIRS/LOG_DIRS/TMP_DIRS) di %s\n' "$CONF" >&2
    exit 1
fi

valid_umur() {
    case $1 in
        ''|*[!0-9]*|0) return 1 ;;
    esac
    return 0
}

valid_umur "${CACHE_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: CACHE_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }
valid_umur "${LOG_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: LOG_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }
valid_umur "${TMP_MAX_DAYS:-}" || {
    printf 'pemeliharaan.sh: TMP_MAX_DAYS harus angka 1 atau lebih\n' >&2; exit 1; }

case ${DISK_WARN_PCT:-} in
    ''|*[!0-9]*|0)
        printf 'pemeliharaan.sh: DISK_WARN_PCT harus angka 1-99\n' >&2
        exit 1
        ;;
esac
if [ "$DISK_WARN_PCT" -gt 99 ]; then
    printf 'pemeliharaan.sh: DISK_WARN_PCT harus angka 1-99\n' >&2
    exit 1
fi

# Mode sengaja di set SETELAH konfigurasi dibaca: walau berkas
# konfigurasi berisi "MODE=apply", nilainya tetap ditimpa di sini.
# Hanya argumen --apply yang boleh menyalakan pembersihan sungguhan.
MODE="dry-run"
if [ "$APPLY" -eq 1 ]; then
    MODE="apply"
fi

# --- 3. Validasi snapshot data --------------------------------------------
for f in df.txt paket.txt services.txt; do
    if [ ! -f "$DATA_DIR/$f" ]; then
        printf 'pemeliharaan.sh: snapshot %s/%s tidak ditemukan\n' "$DATA_DIR" "$f" >&2
        exit 2
    fi
done

# --- 4. Pemeriksaan -------------------------------------------------------
# Temuan pemeriksaan terkumpul di sini, lalu menjadi bagian laporan.
# Disk: kolom ke-5 df adalah Use%; "+0" di awk membuang karakter '%'
# sehingga "89%" terbaca sebagai angka 89.
DISK_BARIS=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 {
        pct = $5 + 0
        if (pct >= ambang)
            printf "   %-10s %3d%%  WASPADA\n", $6, pct
        else
            printf "   %-10s %3d%%  OK\n", $6, pct
    }' "$DATA_DIR/df.txt")

DISK_MELEBIHI=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 && ($5 + 0) >= ambang {
        printf "%s%s", pisah, $6
        pisah = ", "
    }' "$DATA_DIR/df.txt")
DISK_N=$(awk -v ambang="$DISK_WARN_PCT" '
    NR > 1 && ($5 + 0) >= ambang { n++ } END { print n + 0 }' "$DATA_DIR/df.txt")

# Paket tertunda: output simulasi apt-get -s upgrade menandai tiap paket
# dengan satu baris "Inst <nama> [versi lama] (versi baru)".
PAKET_DAFTAR=$(awk '/^Inst / {
        printf "%s%s", pisah, $2
        pisah = ", "
    }' "$DATA_DIR/paket.txt")
PAKET_N=$(awk '/^Inst / { n++ } END { print n + 0 }' "$DATA_DIR/paket.txt")

# Service gagal: snapshot berformat "unit substate" per baris.
SVC_DAFTAR=$(awk '$2 == "failed" {
        printf "%s%s", pisah, $1
        pisah = ", "
    }' "$DATA_DIR/services.txt")
SVC_N=$(awk '$2 == "failed" { n++ } END { print n + 0 }' "$DATA_DIR/services.txt")

# --- 5. Pembersihan (hanya di dalam whitelist) -----------------------------
# Hasil pembersihan terkumpul di BAG_BERSIH; penghitung JUMLAH_FILE dan
# HEMAT_BYTES dipakai seksi HEMAT RUANG di laporan.
JUMLAH_FILE=0
HEMAT_BYTES=0
BAG_BERSIH=""

# bersihkan <judul> <dirs> <umur-hari> <pola-nama>:
# kumpulkan file di folder whitelist yang lebih tua dari batas umur,
# catat ukurannya untuk laporan, dan hapus hanya bila mode apply.
# -type f menjaga folder tetap utuh; find hanya berangkat dari folder
# whitelist, jadi file di luar daftar tidak pernah tersentuh.
bersihkan() {
    judul="$1"
    dirs="$2"
    umur="$3"
    pola="${4:-*}"
    batas=$(date -d "$HARI_INI -$umur days" +%F)
    if [ "$pola" = "*" ]; then
        label="file"
    else
        label="file $pola"
    fi
    terpakai=0

    if [ -n "$BAG_BERSIH" ]; then
        BAG_BERSIH+=$'\n'
    fi
    BAG_BERSIH+="$judul — $label lebih tua dari $batas (whitelist: $dirs)"$'\n'

    # shellcheck disable=SC2086  # whitelist memang dipisah spasi
    for dir in $dirs; do
        if [ ! -d "$dir" ]; then
            printf 'pemeliharaan.sh: folder whitelist %s tidak ditemukan\n' "$dir" >&2
            exit 3
        fi
        # Process substitution < <(...) menjaga penghitung tetap hidup
        # di shell utama; pipeline biasa akan menjalankan while di
        # subshell sehingga JUMLAH_FILE hilang.
        while IFS= read -r f; do
            [ -n "$f" ] || continue
            ukuran=$(stat -c %s "$f")
            JUMLAH_FILE=$((JUMLAH_FILE + 1))
            HEMAT_BYTES=$((HEMAT_BYTES + ukuran))
            terpakai=$((terpakai + 1))
            if [ "$MODE" = "apply" ]; then
                rm -f -- "$f"
                BAG_BERSIH+="   [terhapus] $f (${ukuran} byte)"$'\n'
            else
                BAG_BERSIH+="   [dry-run] $f (${ukuran} byte)"$'\n'
            fi
        done < <(find "$dir" -type f -name "$pola" ! -newermt "$batas" | sort)
    done

    if [ "$terpakai" -eq 0 ]; then
        BAG_BERSIH+="   (tidak ada kandidat)"$'\n'
    fi
}

if [ -n "${CACHE_DIRS:-}" ]; then
    bersihkan "cache" "$CACHE_DIRS" "$CACHE_MAX_DAYS"
fi
if [ -n "${LOG_DIRS:-}" ]; then
    bersihkan "log" "$LOG_DIRS" "$LOG_MAX_DAYS" "*.gz"
fi
if [ -n "${TMP_DIRS:-}" ]; then
    bersihkan "tmp" "$TMP_DIRS" "$TMP_MAX_DAYS"
fi

# --- 6. Susun laporan -----------------------------------------------------
if [ "$MODE" = "apply" ]; then
    LABEL_MODE="APPLY (pembersihan benar-benar dijalankan)"
    LABEL_HEMAT="benar-benar dibebaskan dari disk"
else
    LABEL_MODE="DRY-RUN (tidak ada file yang dihapus)"
    LABEL_HEMAT="potensi hemat; jalankan --apply untuk benar-benar menghapus"
fi

LAPORAN=""
tambah() {
    LAPORAN+="$1"
    LAPORAN+=$'\n'
}

tambah "================================================================"
tambah "LAPORAN PEMELIHARAAN SISTEM"
tambah "Tanggal acuan : $HARI_INI"
tambah "Mode          : $LABEL_MODE"
tambah "================================================================"
tambah ""
tambah "PEMERIKSAAN"
tambah "----------------------------------------------------------------"
tambah "1. Pemakaian disk (ambang $DISK_WARN_PCT%)"
tambah "$DISK_BARIS"
tambah "2. Paket update tertunda"
if [ "$PAKET_N" -gt 0 ]; then
    tambah "   $PAKET_N paket siap di-upgrade: $PAKET_DAFTAR"
else
    tambah "   tidak ada paket tertunda"
fi
tambah "3. Service gagal"
if [ "$SVC_N" -gt 0 ]; then
    tambah "   $SVC_N service failed: $SVC_DAFTAR"
else
    tambah "   semua service berjalan normal"
fi
tambah ""
tambah "PEMBERSIHAN"
tambah "----------------------------------------------------------------"
tambah "${BAG_BERSIH%$'\n'}"
tambah ""
tambah "HEMAT RUANG"
tambah "----------------------------------------------------------------"
tambah "Total: $JUMLAH_FILE file, $HEMAT_BYTES byte ($LABEL_HEMAT)"
tambah ""
tambah "REKOMENDASI"
tambah "----------------------------------------------------------------"
nomor=1
if [ "$DISK_N" -gt 0 ]; then
    tambah "$nomor. Pemakaian disk melewati ambang $DISK_WARN_PCT% di $DISK_MELEBIHI:"
    tambah "   cari penyumbang terbesar dengan du -h, lalu bersihkan atau pindahkan."
    nomor=$((nomor + 1))
fi
if [ "$PAKET_N" -gt 0 ]; then
    tambah "$nomor. Ada $PAKET_N paket update tertunda:"
    tambah "   jalankan apt upgrade pada jadwal maintenance berikutnya."
    nomor=$((nomor + 1))
fi
if [ "$SVC_N" -gt 0 ]; then
    tambah "$nomor. Ada $SVC_N service gagal ($SVC_DAFTAR):"
    tambah "   periksa journalctl -u <unit> untuk penyebabnya, lalu restart bila aman."
    nomor=$((nomor + 1))
fi
if [ "$MODE" = "dry-run" ] && [ "$JUMLAH_FILE" -gt 0 ]; then
    tambah "$nomor. DRY-RUN menemukan $JUMLAH_FILE file ($HEMAT_BYTES byte) layak dibersihkan:"
    tambah "   periksa daftar di bagian PEMBERSIHAN, lalu jalankan ulang dengan --apply."
    nomor=$((nomor + 1))
fi
if [ "$nomor" -eq 1 ]; then
    tambah "1. Tidak ada temuan: sistem dalam kondisi baik. Ulangi pemeliharaan sesuai jadwal."
fi

# --- 7. Tulis laporan ke stdout dan berkas ---------------------------------
REPORT_FILE="${REPORT_FILE:-laporan-pemeliharaan.txt}"
mkdir -p "$(dirname "$REPORT_FILE")"
printf '%s' "$LAPORAN" | tee "$REPORT_FILE"
exit 0

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk pemeliharaan.sh.
# Suite menyiapkan sendiri folder kerja terisolasi (mktemp) beserta
# data contohnya — snapshot beku (df.txt, paket.txt, services.txt) dan
# file cache/log/tmp dengan mtime beku lewat touch -d — lalu menjalankan
# rangkaian pengujian dengan assertion sederhana. Jalankan dari mana
# saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/src/pemeliharaan.sh" "$PROJ_DIR/config/pemeliharaan.conf" "$WORK/"
cd "$WORK" || exit 1

# --- fixture: snapshot data beku ----------------------------------------
mkdir -p data/cache data/log data/tmp
cat > data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   35G   13G  73% /
/dev/sdb1       200G  170G   21G  89% /data
tmpfs           7.8G  1.1M  7.8G   1% /dev/shm
EOF
cat > data/paket.txt <<'EOF'
Reading package lists... Done
Building dependency tree... Done
Calculating upgrade... Done
Reading state information... Done
The following packages will be upgraded:
  curl libcurl4 libssl3 openssl
4 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Inst curl [7.88.1-10] (7.88.1-11)
Inst libcurl4 [7.88.1-10] (7.88.1-11)
Inst libssl3 [3.0.8-1] (3.0.8-2)
Inst openssl [3.0.8-1] (3.0.8-2)
Conf curl (7.88.1-11)
Conf libcurl4 (7.88.1-11)
Conf libssl3 (3.0.8-2)
Conf openssl (3.0.8-2)
EOF
cat > data/services.txt <<'EOF'
nginx.service failed
postgresql.service failed
sshd.service running
cron.service running
EOF

# --- fixture: file dengan mtime beku -------------------------------------
# Tanggal acuan pengujian: 2026-02-15. Batas umur menjadi
# cache 30 hari -> 2026-01-16, log 14 hari -> 2026-02-01,
# tmp 7 hari -> 2026-02-08.
printf 'isi cache lama\n' > data/cache/lama.txt
touch -d "2026-01-10" data/cache/lama.txt
printf 'isi cache baru\n' > data/cache/baru.txt
touch -d "2026-02-10" data/cache/baru.txt
printf 'isi log lama terkompresi\n' > data/log/app.log.1.gz
touch -d "2026-01-25" data/log/app.log.1.gz
printf 'isi log lama tapi bukan gz\n' > data/log/app.log
touch -d "2026-01-20" data/log/app.log
printf 'isi log baru\n' > data/log/app.log.2.gz
touch -d "2026-02-12" data/log/app.log.2.gz
printf 'sisa instalasi lama\n' > data/tmp/sisa.tmp
touch -d "2026-02-01" data/tmp/sisa.tmp
printf 'file tmp aktif\n' > data/tmp/aktif.tmp
touch -d "2026-02-13" data/tmp/aktif.tmp

# Ukuran total yang harus dibebaskan mode apply: tiga file tua.
total_hemat=$(stat -c %s data/cache/lama.txt data/log/app.log.1.gz \
    data/tmp/sisa.tmp | awk '{ s += $1 } END { print s }')

# --- uji 1: dry-run sukses dan tidak menghapus ---------------------------
HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 0 "$?" "dry-run sukses keluar 0"
[ -f data/cache/lama.txt ]
assert_eq 0 "$?" "dry-run tidak menghapus cache lama"
[ -f data/log/app.log.1.gz ]
assert_eq 0 "$?" "dry-run tidak menghapus log lama"

# --- uji 2: laporan dibuat dengan empat seksi ----------------------------
[ -f laporan-pemeliharaan.txt ]
assert_eq 0 "$?" "berkas laporan dibuat"
grep -q '^PEMERIKSAAN$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi PEMERIKSAAN"
grep -q '^PEMBERSIHAN$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi PEMBERSIHAN"
grep -q '^HEMAT RUANG$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi HEMAT RUANG"
grep -q '^REKOMENDASI$' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan memuat seksi REKOMENDASI"

# --- uji 3: parsing ketiga pemeriksaan benar ------------------------------
grep -q '89%  WASPADA' laporan-pemeliharaan.txt
assert_eq 0 "$?" "disk 89% ditandai WASPADA"
grep -q '73%  OK' laporan-pemeliharaan.txt
assert_eq 0 "$?" "disk 73% ditandai OK"
grep -q '4 paket siap di-upgrade: curl, libcurl4, libssl3, openssl' \
    laporan-pemeliharaan.txt
assert_eq 0 "$?" "jumlah dan daftar paket tertunda benar"
grep -q '2 service failed: nginx.service, postgresql.service' \
    laporan-pemeliharaan.txt
assert_eq 0 "$?" "jumlah dan daftar service gagal benar"

# --- uji 4: daftar kandidat dan rekomendasi dry-run ------------------------
grep -Fq '[dry-run] data/cache/lama.txt' laporan-pemeliharaan.txt
assert_eq 0 "$?" "dry-run menampilkan kandidat cache"
grep -Fq '[dry-run] data/log/app.log.1.gz' laporan-pemeliharaan.txt
assert_eq 0 "$?" "dry-run menampilkan kandidat log"
grep -Fq '[dry-run] data/tmp/sisa.tmp' laporan-pemeliharaan.txt
assert_eq 0 "$?" "dry-run menampilkan kandidat tmp"
grep -q 'jalankan ulang dengan --apply' laporan-pemeliharaan.txt
assert_eq 0 "$?" "rekomendasi menyarankan --apply"

# --- uji 5: dua run dry-run menghasilkan laporan identik -------------------
cp laporan-pemeliharaan.txt laporan-run1.txt
HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 0 "$?" "dry-run kedua sukses keluar 0"
diff laporan-run1.txt laporan-pemeliharaan.txt > /dev/null 2>&1
assert_eq 0 "$?" "dua laporan dry-run identik"

# --- uji 6: apply menghapus yang benar saja -------------------------------
HARI_INI=2026-02-15 bash pemeliharaan.sh --apply > /dev/null 2>&1
assert_eq 0 "$?" "apply sukses keluar 0"
[ ! -f data/cache/lama.txt ]
assert_eq 0 "$?" "apply menghapus cache lama"
[ ! -f data/log/app.log.1.gz ]
assert_eq 0 "$?" "apply menghapus log gz lama"
[ ! -f data/tmp/sisa.tmp ]
assert_eq 0 "$?" "apply menghapus tmp lama"
[ -f data/cache/baru.txt ]
assert_eq 0 "$?" "cache baru tetap ada"
[ -f data/log/app.log ]
assert_eq 0 "$?" "log lama non-gz tetap ada (pola hanya *.gz)"
[ -f data/log/app.log.2.gz ]
assert_eq 0 "$?" "log gz baru tetap ada"
[ -f data/tmp/aktif.tmp ]
assert_eq 0 "$?" "tmp baru tetap ada"

# --- uji 7: laporan apply akurat -------------------------------------------
terhapus=$(grep -Fc '[terhapus]' laporan-pemeliharaan.txt)
assert_eq 3 "$terhapus" "laporan apply mencatat 3 file terhapus"
grep -q "Total: 3 file, $total_hemat byte (benar-benar dibebaskan dari disk)" \
    laporan-pemeliharaan.txt
assert_eq 0 "$?" "total hemat ruang cocok dengan jumlah byte fixture"
! grep -q 'jalankan ulang dengan --apply' laporan-pemeliharaan.txt
assert_eq 0 "$?" "laporan apply tidak menyarankan --apply lagi"

# --- uji 8: exit code untuk tiap jenis kegagalan ----------------------------
CONF=nihil.conf HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 1 "$?" "konfigurasi hilang keluar 1"
printf 'CACHE_DIRS="data/cache"\nLOG_DIRS=""\nTMP_DIRS=""\nCACHE_MAX_DAYS=abc\nLOG_MAX_DAYS=14\nTMP_MAX_DAYS=7\nDISK_WARN_PCT=80\n' > umur-rusak.conf
CONF=umur-rusak.conf HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 1 "$?" "umur tidak angka keluar 1"
mkdir -p data-kosong
SOURCE_DIR=data-kosong HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 2 "$?" "snapshot hilang keluar 2"
printf 'CACHE_DIRS="data/cache"\nLOG_DIRS="data/log"\nTMP_DIRS="data/tidak-ada"\nCACHE_MAX_DAYS=30\nLOG_MAX_DAYS=14\nTMP_MAX_DAYS=7\nDISK_WARN_PCT=80\n' > whitelist-rusak.conf
CONF=whitelist-rusak.conf HARI_INI=2026-02-15 bash pemeliharaan.sh > /dev/null 2>&1
assert_eq 3 "$?" "folder whitelist hilang keluar 3"
HARI_INI=2026-02-15 bash pemeliharaan.sh --bogus > /dev/null 2>&1
assert_eq 1 "$?" "argumen tak dikenal keluar 1"

# --- ringkasan --------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]