Desain dari Bagian 1 sekarang diterjemahkan menjadi kode. Kita menulis skrip dalam dua tahap: tahap pertama membangun kerangka yang membaca dan memvalidasi konfigurasi, tahap kedua menambahkan seluruh pekerjaan backup — arsip, mirror, rotasi, log, dan kunci anti-dobel. Setiap tahap dijalankan sungguhan di folder latihan, bukan sekadar dibaca, sehingga kesalahan ketik ketahuan seketika. Versi lengkap skrip akhir juga tersedia sebagai src/backup.sh pada bagian Berkas Sumber di bawah halaman ini.
Tahap 1: Kerangka Konfigurasi dan Validasi #
Kerangka tahap pertama punya satu tugas: memastikan segala sesuatu yang skrip butuhkan sudah tersedia, dan menolak jalan terus bila ada yang salah. Konfigurasi dibaca dengan perintah . (source), validasi variabel memakai ${VAR:-} agar aman di bawah set -u, dan setiap jenis kegagalan keluar dengan exit code berbeda. Simpan blok berikut dan jalankan — tiga eksekusi terakhirnya sengaja menampilkan jalur sukses, konfigurasi hilang, dan sumber hilang:
mkdir -p latihan/{dokumen/laporan,proyek/catatan}
printf 'daftar tugas minggu ini\n' > latihan/dokumen/tugas.txt
printf 'laporan penjualan kuartal pertama\n' > latihan/dokumen/laporan/penjualan.txt
printf 'ide aplikasi cuaca\n' > latihan/proyek/catatan/ide.txt
cat > backup.conf <<'CONF'
BACKUP_DIRS="latihan/dokumen latihan/proyek"
BACKUP_DEST="latihan/arsip"
KEEP_ARCHIVES=3
LOG_FILE="latihan/arsip/backup.log"
CONF
cat > backup.sh <<'BASH'
#!/usr/bin/env bash
# backup.sh — tahap 1: baca dan validasi konfigurasi.
set -u
CONF="${CONF:-backup.conf}"
STAMP="${STAMP:-$(date +%Y%m%d)}"
# log(): tahap ini cukup ke stdout; menulis ke berkas log
# menyusul setelah letak log diketahui dari konfigurasi.
log() {
printf '[%s] %s\n' "$STAMP" "$1"
}
die() {
log "GAGAL: $1 (kode $2)"
exit "$2"
}
if [ ! -f "$CONF" ]; then
printf 'backup.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 1
fi
. "$CONF"
if [ -z "${BACKUP_DIRS:-}" ]; then
printf 'backup.sh: BACKUP_DIRS belum diisi di %s\n' "$CONF" >&2
exit 1
fi
if [ -z "${BACKUP_DEST:-}" ]; then
printf 'backup.sh: BACKUP_DEST belum diisi di %s\n' "$CONF" >&2
exit 1
fi
case ${KEEP_ARCHIVES:-} in
''|*[!0-9]*|0)
printf 'backup.sh: KEEP_ARCHIVES harus angka 1 atau lebih\n' >&2
exit 1
;;
esac
hilang=""
# shellcheck disable=SC2086 # daftar sumber memang dipisah spasi
for src in $BACKUP_DIRS; do
[ -d "$src" ] || hilang="$hilang $src"
done
if [ -n "$hilang" ]; then
die "folder sumber tidak ditemukan:$hilang" 2
fi
# shellcheck disable=SC2086
jumlah=$(printf '%s\n' $BACKUP_DIRS | wc -w)
log "konfigurasi valid: $jumlah folder sumber siap"
BASH
STAMP=20260115 bash backup.sh; echo "exit=$?"
CONF=halang.conf bash backup.sh 2>&1; echo "exit=$?"
printf 'BACKUP_DIRS="latihan/dokumen foto"\nBACKUP_DEST="latihan/arsip"\nKEEP_ARCHIVES=3\n' > tanpa-sumber.conf
CONF=tanpa-sumber.conf STAMP=20260115 bash backup.sh; echo "exit=$?"Output yang Dihasilkan:
[20260115] konfigurasi valid: 2 folder sumber siap
exit=0
backup.sh: berkas konfigurasi halang.conf tidak ditemukan
exit=1
[20260115] GAGAL: folder sumber tidak ditemukan: foto (kode 2)
exit=2Mari kita bedah bagian-bagian kuncinya:
- Baris
set -u: referensi ke variabel yang belum diisi langsung menjadi error, bukan string kosong yang merembet jadi kejutan di baris ke-50. Seluruh skrip jadi lebih defensif tanpa menulis pemeriksaan tambahan. - Baris
CONF="${CONF:-backup.conf}"danSTAMP="${STAMP:-...}": pola parameter expansion memberi nilai bawaan sekaligus mengizinkan override dari lingkungan — inilah yang membuatSTAMP=20260115 bash backup.shbekerja tanpa mengubah skrip. - Baris
. "$CONF": perintah.membaca berkas konfigurasi dan mengeksekusinya di shell yang sama, sehingga keempat variabel langsung tersedia. Formatnya memang harus penugasan shell yang sah — itulah sebabnya draf di Bagian 1 kita tulis dalam bentukKEY="nilai". - Pemeriksaan
case ${KEEP_ARCHIVES:-} in ''|*[!0-9]*|0): pola*[!0-9]*menolak isi yang memuat karakter selain angka, dan0ditolak terpisah karena rotasi dengan “simpan 0 arsip” berarti menghapus semuanya. Validasi seperti ini lebih murah daripada men-debugtail -n +1yang tiba-tiba menghapus semua backup. - Fungsi
die(): kegagalan dicatat lewatloglalu keluar dengan kode tertentu. Konvensi kita: 1 untuk konfigurasi tidak valid, 2 untuk sumber hilang — daftar lengkapnya nanti tertulis di komentar skrip final. - Dua jalur pesan yang berbeda: kegagalan tahap awal (berkas konfigurasi, variabel kosong) keluar ke stderr lewat
printf ... >&2, sedangkan kegagalan sesudahnya lewatlog. Letak berkas log sendiri baru diketahui setelah konfigurasi terbaca, jadi tahap sebelum itu memang belum bisa menulis log.
Rotasi dalam Tiga Perintah #
Sebelum menuliskan rotasi ke skrip, kita pahami dulu pipelinenya secara terpisah. Tugasnya: dari semua folder backup-*, sisakan KEEP_ARCHIVES yang terbaru. Perintah touch -d di bawah ini hanya menandai waktu folder agar urutannya pasti — di skrip asli, urutan itu datang dari waktu folder dibuat:
mkdir -p arsip/backup-{20260112,20260113,20260114,20260115,20260116}
touch -d 2026-01-12 arsip/backup-20260112
touch -d 2026-01-13 arsip/backup-20260113
touch -d 2026-01-14 arsip/backup-20260114
touch -d 2026-01-15 arsip/backup-20260115
touch -d 2026-01-16 arsip/backup-20260116
ls -1dt arsip/backup-*
echo "== di luar 3 terbaru (kandidat dihapus) =="
ls -1dt arsip/backup-* | tail -n +4Output yang Dihasilkan:
arsip/backup-20260116
arsip/backup-20260115
arsip/backup-20260114
arsip/backup-20260113
arsip/backup-20260112
== di luar 3 terbaru (kandidat dihapus) ==
arsip/backup-20260113
arsip/backup-20260112Bedahnya ringkas tapi penting:
ls -1dt: opsi-tmengurutkan dari yang termuda,-dmembuatlsmelaporkan folder itu sendiri alih-alih isinya, dan-1menjamin satu entri per baris. Urutan itulah fondasi rotasi.tail -n +4: angka+4berarti “mulai cetak dari baris ke-4” — tiga baris pertama (tiga run terbaru) dilewati. DenganKEEP_ARCHIVES=3, rumusnya menjaditail -n +"$((KEEP_ARCHIVES + 1))".xargs -r rm -rf: setiap baris yang lolos menjadi argumenrm -rf. Opsi-rmenghentikanxargssaat tidak ada input — penting saat jumlah run belum melebihi retensi, karenarm -rftanpa argumen akan error. Di skrip final,2> /dev/nulljuga ditambahkan padalskarena di run pertama folderbackup-*memang belum ada.
Tahap 2: Skrip Lengkap #
Sekarang kita tambahkan seluruh pekerjaan backup ke kerangka tadi. Blok berikut berisi skrip final — identik dengan src/backup.sh — diikuti eksekusi sungguhan pada folder latihan, pemeriksaan isi run, uji restore kecil, dan pembuktian bahwa backup ganda ditolak:
mkdir -p latihan/{dokumen/laporan,proyek/catatan}
printf 'daftar tugas minggu ini\n' > latihan/dokumen/tugas.txt
printf 'laporan penjualan kuartal pertama\n' > latihan/dokumen/laporan/penjualan.txt
printf 'ide aplikasi cuaca\n' > latihan/proyek/catatan/ide.txt
cat > backup.conf <<'CONF'
BACKUP_DIRS="latihan/dokumen latihan/proyek"
BACKUP_DEST="latihan/arsip"
KEEP_ARCHIVES=3
LOG_FILE="latihan/arsip/backup.log"
CONF
cat > backup.sh <<'BASH'
#!/usr/bin/env bash
# backup.sh — backup otomatis untuk pengguna desktop dan server kecil.
#
# Membaca daftar folder sumber dari berkas konfigurasi (backup.conf),
# lalu pada setiap eksekusi: membuat arsip tar.gz beserta checksum
# SHA-256, memperbarui mirror rsync, merotasi run lama, dan mencatat
# log. Berjalan sebagai user biasa — tidak butuh root.
#
# Variabel lingkungan (berguna untuk pengujian):
# CONF berkas konfigurasi yang dibaca (default: backup.conf)
# STAMP identitas run (default: tanggal hari ini, format YYYYMMDD)
#
# Kode keluar:
# 0 sukses 1 konfigurasi tidak valid
# 2 folder sumber tidak ada 3 arsip gagal dibuat/dibaca
# 4 mirror rsync gagal 5 backup lain sedang berjalan
set -u
CONF="${CONF:-backup.conf}"
STAMP="${STAMP:-$(date +%Y%m%d)}"
# log(): tulis satu baris ke stdout sekaligus ke berkas log.
log() {
printf '[%s] %s\n' "$STAMP" "$1" | tee -a "$LOG_FILE"
}
# die(): catat kegagalan lewat log, lalu keluar dengan kode tertentu.
die() {
log "GAGAL: $1 (kode $2)"
exit "$2"
}
# --- 1. Baca dan validasi konfigurasi ---------------------------------
# Pesan tahap ini keluar lewat stderr: letak berkas log sendiri baru
# diketahui setelah konfigurasi terbaca, jadi log belum bisa dipakai.
if [ ! -f "$CONF" ]; then
printf 'backup.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 1
fi
# shellcheck source=/dev/null
. "$CONF"
if [ -z "${BACKUP_DIRS:-}" ]; then
printf 'backup.sh: BACKUP_DIRS belum diisi di %s\n' "$CONF" >&2
exit 1
fi
if [ -z "${BACKUP_DEST:-}" ]; then
printf 'backup.sh: BACKUP_DEST belum diisi di %s\n' "$CONF" >&2
exit 1
fi
case ${KEEP_ARCHIVES:-} in
''|*[!0-9]*|0)
printf 'backup.sh: KEEP_ARCHIVES harus angka 1 atau lebih\n' >&2
exit 1
;;
esac
# --- 2. Siapkan tujuan, log, dan kunci anti-dobel ----------------------
mkdir -p "$BACKUP_DEST" || exit 1
LOG_FILE="${LOG_FILE:-$BACKUP_DEST/backup.log}"
# flock -n: bila kunci sibuk, ada backup lain yang masih berjalan.
exec 9>>"$BACKUP_DEST/.backup.lock"
if ! flock -n 9; then
die "backup lain sedang berjalan; kunci .backup.lock sibuk" 5
fi
log "mulai backup: sumber = $BACKUP_DIRS"
# --- 3. Periksa semua folder sumber tersedia ---------------------------
hilang=""
# shellcheck disable=SC2086 # daftar sumber memang dipisah spasi
for src in $BACKUP_DIRS; do
[ -d "$src" ] || hilang="$hilang $src"
done
if [ -n "$hilang" ]; then
die "folder sumber tidak ditemukan:$hilang" 2
fi
# --- 4. Arsip tar.gz + checksum SHA-256 per folder sumber --------------
RUNDIR="$BACKUP_DEST/backup-$STAMP"
mkdir -p "$RUNDIR"
# shellcheck disable=SC2086
for src in $BACKUP_DIRS; do
nama=$(basename "$src")
arsip="$RUNDIR/$nama.tar.gz"
# -C + basename: jalur di dalam arsip dimulai dari nama folder
# sumber, bukan seluruh jalur relatifnya; --sort=name membuat
# daftar isi arsip stabil dan terurut.
if ! tar --sort=name -C "$(dirname "$src")" -czf "$arsip" "$nama"; then
die "tar gagal membungkus $src" 3
fi
if ! tar -tzf "$arsip" > /dev/null; then
die "arsip $arsip tidak terbaca" 3
fi
# Checksum disimpan di samping arsip agar restore bisa diverifikasi.
( cd "$RUNDIR" && sha256sum "$nama.tar.gz" > "$nama.tar.gz.sha256" )
log "arsip: $src -> $arsip"
done
# --- 5. Mirror rsync: kondisi terkini tanpa kompresi -------------------
mkdir -p "$BACKUP_DEST/mirror"
# shellcheck disable=SC2086
for src in $BACKUP_DIRS; do
rsync -a --delete "$src" "$BACKUP_DEST/mirror/" ||
die "rsync gagal untuk $src" 4
done
log "mirror diperbarui: $BACKUP_DEST/mirror"
# --- 6. Rotasi: simpan hanya KEEP_ARCHIVES run terbaru ------------------
ls -1dt "$BACKUP_DEST"/backup-* 2> /dev/null |
tail -n +"$((KEEP_ARCHIVES + 1))" |
xargs -r rm -rf
log "rotasi: maksimal $KEEP_ARCHIVES run tersimpan"
log "selesai tanpa error"
exit 0
BASH
STAMP=20260115 bash backup.sh
echo "== isi run =="
ls -1 latihan/arsip/backup-20260115
(cd latihan/arsip/backup-20260115 && sha256sum -c dokumen.tar.gz.sha256)
echo "== restore uji kecil =="
mkdir -p uji-restore
tar -xzf latihan/arsip/backup-20260115/dokumen.tar.gz -C uji-restore
diff -r latihan/dokumen uji-restore/dokumen && echo "arsip dokumen identik dengan sumber"
diff -r latihan/proyek latihan/arsip/mirror/proyek && echo "mirror proyek identik dengan sumber"
echo "== backup ganda ditolak flock =="
flock latihan/arsip/.backup.lock sleep 5 &
sleep 1
STAMP=20260115 bash backup.sh; echo "exit=$?"
waitOutput yang Dihasilkan:
[20260115] mulai backup: sumber = latihan/dokumen latihan/proyek
[20260115] arsip: latihan/dokumen -> latihan/arsip/backup-20260115/dokumen.tar.gz
[20260115] arsip: latihan/proyek -> latihan/arsip/backup-20260115/proyek.tar.gz
[20260115] mirror diperbarui: latihan/arsip/mirror
[20260115] rotasi: maksimal 3 run tersimpan
[20260115] selesai tanpa error
== isi run ==
dokumen.tar.gz
dokumen.tar.gz.sha256
proyek.tar.gz
proyek.tar.gz.sha256
dokumen.tar.gz: OK
== restore uji kecil ==
arsip dokumen identik dengan sumber
mirror proyek identik dengan sumber
== backup ganda ditolak flock ==
[20260115] GAGAL: backup lain sedang berjalan; kunci .backup.lock sibuk (kode 5)
exit=5Bagian-bagian baru yang ditambahkan pada tahap ini:
log()versi berkas: keluaran kini diteruskan lewattee -a "$LOG_FILE"— satu baris tampil di layar sekaligus menempel di ujung berkas log. Setiap entri diawali identitas run[$STAMP], sehingga log mudah disaring per hari backup dengangrep.tar --sort=name -C "$(dirname "$src")" ... "$(basename "$src")": kombinasi-Cdanbasenamemembuat jalur di dalam arsip dimulai dari nama folder sumber (dokumen/...), bukan seluruh jalur relatifnya (latihan/dokumen/...) — hasil restore jadi rapi di mana pun arsipnya diekstrak. Opsi--sort=namemengurutkan isi arsip sehinggatar -tselalu menampilkan daftar yang sama untuk isi yang sama; daftar lengkap opsinya ada di tar(1) .( cd "$RUNDIR" && sha256sum ... ): checksum ditulis dari dalam folder run dalam subshell, sehingga berkas.sha256hanya memuat nama berkas arsip, bukan jalur panjangnya. Konsekuensi baiknya:sha256sum -cbisa dijalankan dari folder mana pun asal berada di dalam folder run.tar -tzf "$arsip" > /dev/null: setelah dibuat, arsip langsung dibaca ulang untuk memastikan kompresi dan strukturnya utuh. Backup yang gagal setengah jalan ketahuan di sini, bukan saat dibutuhkan tiga minggu kemudian.rsync -a --delete "$src" "$BACKUP_DEST/mirror/": mirror diperbarui per folder sumber; rsync hanya menyalin bedanya, sehingga run kedua dan seterusnya cepat. Perilaku delta ini dibahas tuntas di Chapter Rsync; rincian semua opsinya terdokumentasi di rsync(1) .exec 9>>"$BACKUP_DEST/.backup.lock"danflock -n 9: skrip membuka berkas kunci pada deskriptor 9, lalu mencoba menguncinya tanpa menunggu. Bila kunci sibuk, berarti backup lain sedang berjalan — misalnya run malam yang belum selesai bertemu run pagi — dan skrip keluar kode 5 alih-alih mengacak-acak folder run yang sama. Mekanisme kunci berkas ini milik util-linux dan terdokumentasi di flock(1) .
Perhatikan urutan langkah pada akhir blok: setelah backup sukses, diff -r membuktikan arsip dan mirror sama persis dengan sumbernya, lalu demo flock menahan kunci selama dua detik dari proses lain dan eksekusi kedua ditolak dengan kode 5. Tiga bukti itu — arsip terbentuk, isi identik, ganda ditolak — adalah fondasi yang perlu kita ketahui sebelum melangkah lebih jauh.
Skrip kini berdiri penuh: konfigurasi tervalidasi, arsip dan checksum terbentuk, mirror mengikuti sumber, rotasi menjaga retensi, log mencatat semuanya, dan kunci mencegah tabrakan. Namun sesi di atas masih berupa pemeriksaan manual satu per satu — pada Bagian 3 kita menyulapnya menjadi simulasi bencana dan test suite otomatis yang membuktikan backup benar-benar bisa dipulihkan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
src/backup.sh
Unduh berkas#!/usr/bin/env bash
# backup.sh — backup otomatis untuk pengguna desktop dan server kecil.
#
# Membaca daftar folder sumber dari berkas konfigurasi (backup.conf),
# lalu pada setiap eksekusi: membuat arsip tar.gz beserta checksum
# SHA-256, memperbarui mirror rsync, merotasi run lama, dan mencatat
# log. Berjalan sebagai user biasa — tidak butuh root.
#
# Variabel lingkungan (berguna untuk pengujian):
# CONF berkas konfigurasi yang dibaca (default: backup.conf)
# STAMP identitas run (default: tanggal hari ini, format YYYYMMDD)
#
# Kode keluar:
# 0 sukses 1 konfigurasi tidak valid
# 2 folder sumber tidak ada 3 arsip gagal dibuat/dibaca
# 4 mirror rsync gagal 5 backup lain sedang berjalan
set -u
CONF="${CONF:-backup.conf}"
STAMP="${STAMP:-$(date +%Y%m%d)}"
# log(): tulis satu baris ke stdout sekaligus ke berkas log.
log() {
printf '[%s] %s\n' "$STAMP" "$1" | tee -a "$LOG_FILE"
}
# die(): catat kegagalan lewat log, lalu keluar dengan kode tertentu.
die() {
log "GAGAL: $1 (kode $2)"
exit "$2"
}
# --- 1. Baca dan validasi konfigurasi ---------------------------------
# Pesan tahap ini keluar lewat stderr: letak berkas log sendiri baru
# diketahui setelah konfigurasi terbaca, jadi log belum bisa dipakai.
if [ ! -f "$CONF" ]; then
printf 'backup.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 1
fi
# shellcheck source=/dev/null
. "$CONF"
if [ -z "${BACKUP_DIRS:-}" ]; then
printf 'backup.sh: BACKUP_DIRS belum diisi di %s\n' "$CONF" >&2
exit 1
fi
if [ -z "${BACKUP_DEST:-}" ]; then
printf 'backup.sh: BACKUP_DEST belum diisi di %s\n' "$CONF" >&2
exit 1
fi
case ${KEEP_ARCHIVES:-} in
''|*[!0-9]*|0)
printf 'backup.sh: KEEP_ARCHIVES harus angka 1 atau lebih\n' >&2
exit 1
;;
esac
# --- 2. Siapkan tujuan, log, dan kunci anti-dobel ----------------------
mkdir -p "$BACKUP_DEST" || exit 1
LOG_FILE="${LOG_FILE:-$BACKUP_DEST/backup.log}"
# flock -n: bila kunci sibuk, ada backup lain yang masih berjalan.
exec 9>>"$BACKUP_DEST/.backup.lock"
if ! flock -n 9; then
die "backup lain sedang berjalan; kunci .backup.lock sibuk" 5
fi
log "mulai backup: sumber = $BACKUP_DIRS"
# --- 3. Periksa semua folder sumber tersedia ---------------------------
hilang=""
# shellcheck disable=SC2086 # daftar sumber memang dipisah spasi
for src in $BACKUP_DIRS; do
[ -d "$src" ] || hilang="$hilang $src"
done
if [ -n "$hilang" ]; then
die "folder sumber tidak ditemukan:$hilang" 2
fi
# --- 4. Arsip tar.gz + checksum SHA-256 per folder sumber --------------
RUNDIR="$BACKUP_DEST/backup-$STAMP"
mkdir -p "$RUNDIR"
# shellcheck disable=SC2086
for src in $BACKUP_DIRS; do
nama=$(basename "$src")
arsip="$RUNDIR/$nama.tar.gz"
# -C + basename: jalur di dalam arsip dimulai dari nama folder
# sumber, bukan seluruh jalur relatifnya; --sort=name membuat
# daftar isi arsip stabil dan terurut.
if ! tar --sort=name -C "$(dirname "$src")" -czf "$arsip" "$nama"; then
die "tar gagal membungkus $src" 3
fi
if ! tar -tzf "$arsip" > /dev/null; then
die "arsip $arsip tidak terbaca" 3
fi
# Checksum disimpan di samping arsip agar restore bisa diverifikasi.
( cd "$RUNDIR" && sha256sum "$nama.tar.gz" > "$nama.tar.gz.sha256" )
log "arsip: $src -> $arsip"
done
# --- 5. Mirror rsync: kondisi terkini tanpa kompresi -------------------
mkdir -p "$BACKUP_DEST/mirror"
# shellcheck disable=SC2086
for src in $BACKUP_DIRS; do
rsync -a --delete "$src" "$BACKUP_DEST/mirror/" ||
die "rsync gagal untuk $src" 4
done
log "mirror diperbarui: $BACKUP_DEST/mirror"
# --- 6. Rotasi: simpan hanya KEEP_ARCHIVES run terbaru ------------------
ls -1dt "$BACKUP_DEST"/backup-* 2> /dev/null |
tail -n +"$((KEEP_ARCHIVES + 1))" |
xargs -r rm -rf
log "rotasi: maksimal $KEEP_ARCHIVES run tersimpan"
log "selesai tanpa error"
exit 0
tests/run-tests.sh
Unduh berkas#!/usr/bin/env bash
# run-tests.sh — test suite untuk backup.sh.
# Suite menyiapkan sendiri folder kerja terisolasi (mktemp) beserta
# data contohnya, menyalin src/backup.sh dan config/backup.conf,
# 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/backup.sh" "$PROJ_DIR/config/backup.conf" "$WORK/"
cd "$WORK" || exit 1
# Data contoh (fixture) mengikuti config/backup.conf bawaan.
mkdir -p dokumen/laporan proyek/catatan
printf 'daftar tugas minggu ini\n' > dokumen/tugas.txt
printf 'laporan penjualan kuartal pertama\n' > dokumen/laporan/penjualan.txt
printf 'ide aplikasi cuaca\n' > proyek/catatan/ide.txt
# --- uji 1: eksekusi sukses keluar 0 ------------------------------------
STAMP=20260115 bash backup.sh > /dev/null 2>&1
assert_eq 0 "$?" "backup sukses keluar 0"
touch -d "2026-01-15 09:00" arsip/backup-20260115
# --- uji 2: arsip dan checksum dibuat -----------------------------------
[ -f arsip/backup-20260115/dokumen.tar.gz ]
assert_eq 0 "$?" "arsip dokumen.tar.gz dibuat"
[ -f arsip/backup-20260115/proyek.tar.gz ]
assert_eq 0 "$?" "arsip proyek.tar.gz dibuat"
[ -f arsip/backup-20260115/dokumen.tar.gz.sha256 ]
assert_eq 0 "$?" "checksum dokumen.tar.gz.sha256 dibuat"
# --- uji 3: isi arsip benar ----------------------------------------------
isi=$(tar -tzf arsip/backup-20260115/dokumen.tar.gz |
grep -c '^dokumen/tugas.txt$')
assert_eq 1 "$isi" "tugas.txt tercantum di dalam arsip"
# --- uji 4: checksum arsip konsisten -------------------------------------
( cd arsip/backup-20260115 && sha256sum -c dokumen.tar.gz.sha256 \
> /dev/null 2>&1 )
assert_eq 0 "$?" "checksum arsip cocok"
# --- uji 5: restore penuh identik dengan sumber ---------------------------
mkdir -p pulih
tar -xzf arsip/backup-20260115/dokumen.tar.gz -C pulih
tar -xzf arsip/backup-20260115/proyek.tar.gz -C pulih
diff -r dokumen pulih/dokumen > /dev/null 2>&1
assert_eq 0 "$?" "restore dokumen identik"
diff -r proyek pulih/proyek > /dev/null 2>&1
assert_eq 0 "$?" "restore proyek identik"
# --- uji 6: mirror mengikuti keadaan terkini -------------------------------
printf 'daftar tugas minggu ini (revisi)\n' > dokumen/tugas.txt
STAMP=20260116 bash backup.sh > /dev/null 2>&1
assert_eq 0 "$?" "backup kedua sukses keluar 0"
touch -d "2026-01-16 09:00" arsip/backup-20260116
diff -r dokumen arsip/mirror/dokumen > /dev/null 2>&1
assert_eq 0 "$?" "mirror memuat revisi terbaru"
# --- uji 7: rotasi menyisakan 3 run ---------------------------------------
# Waktu tiap folder ditandai agar urutan ls -t pasti deterministik.
for s in 20260117 20260118 20260119 20260120; do
STAMP=$s bash backup.sh > /dev/null 2>&1
touch -d "${s:0:4}-${s:4:2}-${s:6:2} 09:00" "arsip/backup-$s"
done
jumlah=$(ls -1d arsip/backup-* | wc -l)
assert_eq 3 "$jumlah" "rotasi menyisakan 3 run"
[ -d arsip/backup-20260120 ]
assert_eq 0 "$?" "run terbaru tetap ada"
[ ! -d arsip/backup-20260115 ]
assert_eq 0 "$?" "run tertua sudah dirotasi"
# --- uji 8: exit code untuk masalah konfigurasi dan sumber -----------------
printf 'BACKUP_DIRS=""\nBACKUP_DEST="arsip"\nKEEP_ARCHIVES=3\n' > kosong.conf
CONF=kosong.conf bash backup.sh > /dev/null 2>&1
assert_eq 1 "$?" "konfigurasi kosong keluar 1"
printf 'BACKUP_DIRS="dokumen foto"\nBACKUP_DEST="arsip"\nKEEP_ARCHIVES=3\n' > rusak.conf
CONF=rusak.conf bash backup.sh > /dev/null 2>&1
assert_eq 2 "$?" "sumber hilang keluar 2"
# --- uji 9: kunci flock menolak backup ganda -------------------------------
flock arsip/.backup.lock sleep 3 &
sleep 0.5
STAMP=20260121 bash backup.sh > /dev/null 2>&1
assert_eq 5 "$?" "backup ganda ditolak keluar 5"
wait
# --- 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 ]