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:

bash
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:

text
[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 mirror

Tiga 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:

bash
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.sh

Output yang Dihasilkan:

text
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 lulus

Enam belas pengujian itu menutup seluruh janji desain dari Bagian 1. Mari kita bedah pola-pola pentingnya:

  1. 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.
  2. 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.
  3. touch -d pada 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 urutan ls -1dt pasti deterministik.
  4. 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.
  5. Pengujian exit code per jenis gagal: konfigurasi kosong harus keluar 1, sumber hilang keluar 2, dan backup ganda ditolak keluar 5 lewat flock yang 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
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

tests/run-tests.sh

Unduh berkas
bash
#!/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 ]