Menghapus file adalah bagian pemeliharaan yang paling tidak bisa diulang: file yang salah terhapus tidak punya undo. Karena itu bagian ini justru dimulai dari aturan, bukan dari perintah — tiga pengaman yang membuat pembersihan bisa dipertanggungjawabkan bahkan saat dijadwalkan tiap minggu. Setelah aturannya jelas, kita menentukan umur file dengan tanggal acuan yang bisa dikunci, lalu menjalankan pembersihan dalam dua tahap: dry-run menampilkan daftar dulu, --apply baru benar-benar menghapus.

Tiga Pengaman Sebelum Menghapus #

Pengaman pertama adalah whitelist: daftar folder yang boleh dibersihkan, ditulis di berkas konfigurasi, dan find hanya boleh berangkat dari folder dalam daftar itu. File di luar whitelist tidak “juga ikut dibersihkan” — ia tidak pernah dicari sekalipun. Pengaman kedua adalah umur: hanya file yang lebih tua dari batas per jenisnya, jadi file yang masih aktif tidak tersentuh. Pengaman ketiga adalah dry-run sebagai default: skrip menampilkan apa yang akan dihapus, dan tidak menjalankan penghapusan sampai --apply diketik sendiri oleh manusia yang membaca daftarnya.

Whitelist dan Umur di Konfigurasi #

Seluruh keputusan pembersihan hidup di pemeliharaan.conf, bukan di dalam skrip — pengguna cukup menyunting nilai tanpa menyentuh logika:

ini
# 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"

Enam variabel pertama adalah inti whitelist: tiga jenis sumber (CACHE_DIRS, LOG_DIRS, TMP_DIRS) masing-masing dengan batas umurnya sendiri. Jenisnya berbeda perlakuan: cache dan tmp dibersihkan tanpa saringan nama, sedangkan log hanya file terkompresi *.gz — file app.log yang masih ditulis aplikasi tidak boleh dihapus, sementara file .gz justru memang buatan rotasi log yang sudah tua. Ambang disk dipakai cek dari Bagian 1, dan REPORT_FILE menentukan tujuan laporan Bagian 3.

Perhatikan juga apa yang tidak ada di berkas itu: mode. Tidak ada baris MODE="apply", dan itu disengaja. Skrip menetapkan mode setelah konfigurasi dibaca — walau seseorang menambahkan baris MODE=apply ke konfigurasi, nilainya tetap ditimpa kembali ke dry-run. Pembersihan sungguhan hanya menyala lewat argumen --apply yang harus diketik pada eksekusi itu juga, sehingga tidak ada konfigurasi yang terlupa membuat skrip terjadwal menjadi perusak diam-diam.

Umur File: -mtime atau Tanggal Acuan #

Cara klasik mencari file tua adalah find -mtime +30 — “lebih tua dari 30 hari”. Cara itu benar, tapi dihitung dari jam eksekusi: file yang sama bisa jadi kandidat hari ini dan tidak besok, sehingga hasilnya sulit diuji dan sulit dibandingkan antar mesin. Skrip kita memakai ! -newermt dengan batas tanggal yang dihitung dari tanggal acuan:

Aspek-mtime +30! -newermt "$batas"
Titik acuanjam eksekusi findtanggal acuan HARI_INI
Menentukan bataslangsung, satu teshitung dulu dengan date -d
Hasil antar eksekusibergeser mengikuti waktuidentik selama HARI_INI sama
Cocok untukpembersihan sekali jalanskrip teruji dan terjadwal

Variabel HARI_INI default mengambil tanggal hari ini, jadi di pemakaian normal keduanya setara. Saat menguji, kita menguncinya ke tanggal tetap — dan seluruh umur file menjadi deterministik. Cara kerjanya kita lihat langsung:

bash
mkdir -p data/cache
printf 'isi cache lama\n' > data/cache/lama.txt
printf 'isi cache baru\n' > data/cache/baru.txt
touch -d "2026-01-10" data/cache/lama.txt
touch -d "2026-02-10" data/cache/baru.txt

HARI_INI=2026-02-15
batas=$(date -d "$HARI_INI -30 days" +%F)
echo "tanggal acuan : $HARI_INI"
echo "batas umur    : $batas (cache 30 hari)"
echo "== semua file cache =="
ls -1 data/cache
echo "== kandidat: lebih tua dari batas =="
find data/cache -type f ! -newermt "$batas" | sort

Output yang Dihasilkan:

text
tanggal acuan : 2026-02-15
batas umur    : 2026-01-16 (cache 30 hari)
== semua file cache ==
baru.txt
lama.txt
== kandidat: lebih tua dari batas ==
data/cache/lama.txt

Bedah baris-per-baris:

  1. touch -d "2026-01-10": menandai mtime file ke tanggal itu — inilah cara membekukan umur file di fixture, sama seperti snapshot membekukan output perintah di Bagian 1.
  2. date -d "$HARI_INI -30 days" +%F: date menerima tanggal lalu aritmetika hari, dan %F mencetak hasilnya sebagai YYYY-MM-DD — dari 2026-02-15 dan umur 30 hari lahir batas 2026-01-16.
  3. find ... ! -newermt "$batas": file yang mtime-nya tidak lebih baru dari batas — artinya sama tua atau lebih tua — masuk daftar kandidat. File baru.txt (2026-02-10) lebih baru dari 2026-01-16, jadi tersingkir; lama.txt (2026-01-10) lolos.
  4. | sort: urutan find mengikuti isi direktori yang bisa berbeda antar mesin — sort membuat daftar kandidat selalu terurut sama.

Dry-Run Dulu, Baru Apply #

Pola lengkapnya kini bisa kita jalankan pada tiga whitelist sekaligus. Bagian pertama blok berikut adalah dry-run versi panjang: find dengan -printf menampilkan kandidat beserta ukurannya tanpa menghapus apa pun. Baris terakhir menguji betapa pentingnya pola *.gz: tanpa pola itu, app.log yang sama-sama tua akan ikut terdaftar. Bagian kedua adalah apply dengan -delete, ditutup bukti ls sebelum dan sesudah:

bash
mkdir -p data/cache data/log data/tmp
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

batas_cache=$(date -d "2026-02-15 -30 days" +%F)
batas_log=$(date -d "2026-02-15 -14 days" +%F)
batas_tmp=$(date -d "2026-02-15 -7 days" +%F)

echo "== DRY-RUN: kandidat cache (lebih tua dari $batas_cache) =="
find data/cache -type f ! -newermt "$batas_cache" -printf '   akan dihapus: %p (%s byte)\n' | sort
echo "== DRY-RUN: kandidat log *.gz (lebih tua dari $batas_log) =="
find data/log -type f -name '*.gz' ! -newermt "$batas_log" -printf '   akan dihapus: %p (%s byte)\n' | sort
echo "== DRY-RUN: kandidat tmp (lebih tua dari $batas_tmp) =="
find data/tmp -type f ! -newermt "$batas_tmp" -printf '   akan dihapus: %p (%s byte)\n' | sort
echo "== semua file log tua (tanpa pola *.gz) =="
find data/log -type f ! -newermt "$batas_log" -printf '%p\n' | sort
echo "== APPLY: hapus kandidat =="
find data/cache -type f ! -newermt "$batas_cache" -delete
find data/log -type f -name '*.gz' ! -newermt "$batas_log" -delete
find data/tmp -type f ! -newermt "$batas_tmp" -delete
echo "== kondisi akhir =="
ls -1 data/cache data/log data/tmp

Output yang Dihasilkan:

text
== DRY-RUN: kandidat cache (lebih tua dari 2026-01-16) ==
   akan dihapus: data/cache/lama.txt (15 byte)
== DRY-RUN: kandidat log *.gz (lebih tua dari 2026-02-01) ==
   akan dihapus: data/log/app.log.1.gz (25 byte)
== DRY-RUN: kandidat tmp (lebih tua dari 2026-02-08) ==
   akan dihapus: data/tmp/sisa.tmp (20 byte)
== semua file log tua (tanpa pola *.gz) ==
data/log/app.log
data/log/app.log.1.gz
== APPLY: hapus kandidat ==
== kondisi akhir ==
data/cache:
baru.txt

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

data/tmp:
aktif.tmp

Hasil akhirnya adalah bukti yang ingin kita lihat setiap kali mode apply dijalankan:

  1. -printf ' akan dihapus: %p (%s byte)\n': find sendiri sanggup mencetak jalur (%p) dan ukuran (%s) — daftar dry-run jadi informatif tanpa loop tambahan.
  2. -name '*.gz' sebelum ! -newermt: urutan tes tidak mengubah hasil, tapi pola nama mempersempit ruang cari lebih dulu — app.log yang sama-sama tua dengan app.log.1.gz tidak pernah jadi kandidat.
  3. -delete: varian penghapusan langsung find — aman karena mewarisi seluruh saringan tes yang sama (whitelist, -type f, umur, pola) dalam satu perintah; rincian opsinya di find(1) .
  4. ls pembuktian: cache menyisakan baru.txt, log menyisakan app.log dan app.log.2.gz, tmp menyisakan aktif.tmp — tepat tiga file tua yang hilang, nol file sehat yang ikut.

Kalau pola seperti ini terasa familiar, memang begitu: find dengan xargs dan ekspresi majemuknya sudah kita bedah di Chapter find dan xargs , dan file .gz yang kita bersihkan adalah sisa rotasi log dari Chapter Logs dan Log Rotation . Bedanya, di sini seluruh keputusan datang dari konfigurasi dan seluruh penghapusan tercatat ukurannya — dua hal yang kita butuhkan untuk laporan pada bagian penutup proyek.

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 ]