Ada satu kalimat yang menjadi filosofi seluruh proyek ini: backup yang belum pernah diuji restore-nya bukan backup, melainkan harapan. Arsip bisa terbentuk setiap malam, log bisa penuh baris sukses, tetapi jika tidak ada satu pun proses yang pernah membuktikan arsip itu bisa dibuka dan isinya kembali utuh, maka yang kita miliki cuma keyakinan. Pada bagian ini kita meruntuhkan keyakinan menjadi bukti: pertama lewat simulasi bencana mini yang dijalankan manual, lalu lewat test suite bash yang mengulang pemeriksaan yang sama secara otomatis setiap kali skrip berubah.
Simulasi Bencana Mini #
Skenarionya sesederhana mungkin dan seburuk mungkin: backup berhasil pada pagi hari, folder sumber terhapus pada sore harinya, dan kita harus pulih sepenuhnya dari folder tujuan. Blok berikut menjalankan backup, menghapus kedua folder sumber, lalu membangkitkannya kembali dari arsip:
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 "== simulasikan bencana: folder sumber terhapus =="
rm -rf latihan/dokumen latihan/proyek
ls latihan
echo "== pulihkan dari arsip =="
mkdir -p pulih
tar -xzf latihan/arsip/backup-20260115/dokumen.tar.gz -C pulih
tar -xzf latihan/arsip/backup-20260115/proyek.tar.gz -C pulih
cat pulih/dokumen/tugas.txt
diff -r latihan/arsip/mirror/dokumen pulih/dokumen && echo "pulihan cocok dengan mirror"Output 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
== simulasikan bencana: folder sumber terhapus ==
arsip
== pulihkan dari arsip ==
daftar tugas minggu ini
pulihan cocok dengan mirrorTiga baris terakhir output itu adalah intinya. ls latihan setelah penghapusan hanya menyisakan arsip — data sungguhan sudah tiada. Perintah cat memperlihatkan isi salah satu berkas hasil restore utuh terbaca, dan diff -r membandingkan seluruh pohon direktori hasil ekstraksi dengan mirror, lalu diam — diff yang tidak mencetak apa pun berarti tidak ada satu byte pun berbeda. Perhatikan juga bahwa pembandingnya adalah mirror, bukan sumber: sumber sudah terhapus, jadi mirror berperan sebagai salinan pembanding dari titik waktu yang sama.
Rangkaian langkah manual di atas layak dijalankan sekali untuk merasakan prosesnya. Namun pemeriksaan yang mengandalkan kesabaran manusia tidak akan diulang — dan tepat di situlah test suite masuk.
Test Suite dengan Assertion #
Test suite kita adalah skrip bash biasa dengan satu fungsi kecil bernama assert_eq yang membandingkan dua nilai: bila sama, pengujian dicatat lulus; bila beda, dicatat gagal beserta kedua nilainya. Di sekeliling fungsi itu, suite menyiapkan folder kerja terisolasi lewat mktemp -d, membuat data contoh sendiri, lalu memanggil skrip backup dan memeriksa akibatnya. Blok berikut menyusun tata letak proyek (src/, config/, tests/) persis seperti paket yang menyertai halaman ini, lalu menjalankan suitenya:
mkdir -p proyek/src proyek/config proyek/tests
cat > proyek/src/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
cat > proyek/config/backup.conf <<'CONF'
# backup.conf — konfigurasi skrip backup.
# Sunting nilai di bawah ini sesuai komputer Anda; skrip backup.sh
# tidak perlu disentuh. Jalur relatif dihitung dari direktori kerja
# saat skrip dijalankan; jalur absolut (mis. /home/budi/dokumen)
# juga boleh dipakai.
# Daftar folder sumber, dipisah spasi.
BACKUP_DIRS="dokumen proyek"
# Folder tujuan backup: arsip per-run, mirror, dan log disimpan di sini.
# Contoh untuk harddisk eksternal: /media/budi/eksternal/backup
BACKUP_DEST="arsip"
# Berapa run backup-YYYYMMDD yang dipertahankan sebelum dirotasi.
KEEP_ARCHIVES=3
# Berkas log (opsional; default: $BACKUP_DEST/backup.log).
LOG_FILE="arsip/backup.log"
CONF
cat > proyek/tests/run-tests.sh <<'TESTS'
#!/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 ]
TESTS
bash proyek/tests/run-tests.shOutput yang Dihasilkan:
OK backup sukses keluar 0
OK arsip dokumen.tar.gz dibuat
OK arsip proyek.tar.gz dibuat
OK checksum dokumen.tar.gz.sha256 dibuat
OK tugas.txt tercantum di dalam arsip
OK checksum arsip cocok
OK restore dokumen identik
OK restore proyek identik
OK backup kedua sukses keluar 0
OK mirror memuat revisi terbaru
OK rotasi menyisakan 3 run
OK run terbaru tetap ada
OK run tertua sudah dirotasi
OK konfigurasi kosong keluar 1
OK sumber hilang keluar 2
OK backup ganda ditolak keluar 5
PASSED: 16/16 test lulusEnam belas pengujian itu menutup seluruh janji desain dari Bagian 1. Mari kita bedah pola-pola pentingnya:
assert_eq <harapan> <nyata> <pesan>: pengujian ditulis sebagai perbandingan nilai, dan$?ditangkap segera setelah perintah yang diperiksa. Pola ini membuat kegagalan terbaca jelas — pesannya menyebut harapan dan kenyataan — tanpa butuh kerangka test eksternal.- Folder kerja
mktemp -d: setiap eksekusi suite mendapat folder sementara baru, dan seluruh data contoh dibuat di sana. Suite dengan demikian bisa dijalankan dua kali berturut-turut dengan hasil identik — syarat penting agar pengujian sendiri bisa dipercaya. touch -dpada folder run: pengujian rotasi menjalankan lima run berturut-turuk dalam hitungan detik, sehingga waktu folder bisa saja sama persis. Menandai setiap folder dengan tanggal tiruan membuat urutanls -1dtpasti deterministik.- Pengujian rotasi berlapis: suite tidak berhenti pada jumlah (
wc -l= 3), tetapi juga memastikan run terbaru masih ada dan run tertua sudah tidak — jumlah yang benar dengan isi yang salah tetap tertangkap. - Pengujian exit code per jenis gagal: konfigurasi kosong harus keluar 1, sumber hilang keluar 2, dan backup ganda ditolak keluar 5 lewat
flockyang sengaja ditahan proses lain selama satu detik. Nomor-nomor ini adalah kontrak skrip dengan cron dan systemd timer kita di masa depan.
Suite ini juga menjadi contoh nyata bagaimana pengujian menjaga perubahan: setiap kali skrip diedit, satu perintah bash tests/run-tests.sh menjawab dalam hitungan detik apakah keenam belas janji masih ditepati.
Dari Sekali Jalan Menjadi Kebiasaan #
Skrip yang sudah teruji tinggal dijadwalkan agar tidak bergantung pada ingatan. Cara paling ringkas adalah cron — satu baris 0 21 * * * cd /home/budi/backup && bash backup.sh pada crontab -e menjalankan backup setiap malam jam 21:00; pola lengkapnya dibahas pada Chapter Scheduling Scripts
. Kebiasaan kedua yang tidak bisa digantikan jadwal: uji restore berkala, misalnya sebulan sekali ekstrak satu arsip di mesin lain dan buka beberapa berkasnya. Jadwal menguji bahwa backup berjalan; latihan restore menguji bahwa backup berguna. Panduan ringkat keduanya tersedia di docs/README.txt pada paket berkas halaman ini.
Skrip backup otomatis kita selesai: kebutuhan dirancang dulu, kode ditulis bertahap dan dijalankan sungguhan, dan restore dibuktikan lewat simulasi bencana serta test suite yang lulus penuh. Kemampuan yang sama — membaca kondisi sistem, mencatat ke log, keluar dengan kode bermakna — akan kita pakai lagi dengan bahan yang berbeda pada proyek berikutnya, System Monitor Sederhana , tempat skrip kita berjalan terus-menerus alih-alih sekali semalam.
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 ]