Disk tidak rusak setiap hari — dan justru itulah masalahnya. Selama berbulan-bulan semuanya berjalan normal, niat “nanti saya backup” terus tertunda, sampai suatu hari disk berhenti berputar dan semua yang tertunda ikut lenyap: dokumen kerja, foto keluarga, konfigurasi yang sudah bertahun-tahun dirapikan. Proyek ini membangun alat yang membuat penundaan semacam itu kehilangan dayanya: skrip backup otomatis berbasis bash yang membaca daftar folder dari berkas konfigurasi, membungkus setiap folder menjadi arsip tar.gz dengan checksum SHA-256, menjaga mirror rsync, merotasi arsip lama, dan mencatat semuanya ke berkas log. Seluruhnya berjalan sebagai user biasa — tanpa root — di laptop, desktop, maupun server kecil.
Skrip yang kita bangun bekerja dalam satu eksekusi:
- membaca daftar folder sumber dari berkas konfigurasi terpisah — pengguna menyunting nilai, bukan kode,
- membuat satu folder run
backup-YYYYMMDDberisi arsiptar.gzper folder sumber plus berkas checksum SHA-256, - memperbarui mirror
rsyncyang selalu berisi kondisi terkini, - merotasi retensi: hanya sejumlah
KEEP_ARCHIVESrun terbaru yang dipertahankan, - menolak berjalan ganda lewat
flockdan mencatat setiap langkah ke berkas log, - keluar dengan exit code berbeda untuk setiap jenis kegagalan sehingga cron atau systemd timer bisa membacanya.
Tampilan Program Akhir #
Sesi berikut menunjukkan hasil akhir proyek pada data contoh. Awalan [20260115] pada tiap baris log adalah identitas run; skrip mengisinya otomatis dari tanggal eksekusi:
$ bash backup.sh
[20260115] mulai backup: sumber = dokumen proyek
[20260115] arsip: dokumen -> arsip/backup-20260115/dokumen.tar.gz
[20260115] arsip: proyek -> arsip/backup-20260115/proyek.tar.gz
[20260115] mirror diperbarui: arsip/mirror
[20260115] rotasi: maksimal 3 run tersimpan
[20260115] selesai tanpa error
$ ls -1 arsip/backup-20260115
dokumen.tar.gz
dokumen.tar.gz.sha256
proyek.tar.gz
proyek.tar.gz.sha256
$ (cd arsip/backup-20260115 && sha256sum -c dokumen.tar.gz.sha256)
dokumen.tar.gz: OK
Tampilannya sederhana, tetapi di baliknya tersimpan keputusan desain yang menjadi inti proyek ini: konfigurasi dipisah dari kode, dua strategi penyimpanan digabung (arsip sebagai riwayat per titik waktu, mirror sebagai kondisi terkini), retensi dirotasi otomatis, dan setiap arsip diverifikasi checksumnya sebelum dianggap selesai.
Tujuan Pembelajaran #
Setelah menyelesaikan proyek ini, kita mampu:
- menerjemahkan kebutuhan backup menjadi keputusan desain sebelum menulis satu baris kode pun,
- memisahkan konfigurasi dari skrip supaya pengguna cukup menyunting nilai, bukan logika,
- membuat arsip
taryang stabil beserta checksumsha256sumyang bisa diverifikasi, - mengombinasikan arsip per titik waktu dengan mirror
rsynckondisi terkini, - menerapkan rotasi retensi dengan pipeline
ls,tail, danxargs, - menggunakan
flockuntuk mencegah eksekusi ganda dan exit code berbeda untuk setiap jenis kegagalan, - membuktikan bahwa backup benar-benar bisa dipulihkan lewat uji restore dan test suite bash.
Prasyarat #
Proyek ini tidak memperkenalkan alat baru, melainkan merangkai materi yang sudah dibahas. Chapter yang paling sering dirujuk:
- Rsync
— opsi
-a,--delete, dan perilaku penyalinan delta, - Tar dan Compression — membuat, mendaftar, dan mengekstrak arsip terkompresi,
- Exit Status dan Error Handling
—
$?, keluar dengan kode tertentu, dan pengecekan hasil perintah, - Backup Strategies — aturan 3-2-1, skema retensi, dan kebiasaan uji restore.
Tahapan Pengerjaan #
Proyek dikerjakan dalam tiga bagian berurutan. Setiap bagian diakhiri dengan keadaan yang bisa dijalankan dan diamati:
- Bagian 1: Perencanaan Backup — menentukan sumber data, jadwal, retensi, dan tujuan; merancang struktur direktori tujuan serta draf berkas konfigurasi.
- Bagian 2: Implementasi Script — menulis skrip bertahap: pembacaan konfigurasi, arsip tar.gz, mirror rsync, rotasi, log, dan kunci anti-dobel.
- Bagian 3: Pengujian Restore — membuktikan backup bisa dipulihkan lewat simulasi bencana mini dan test suite dengan assertion.
Mulailah dari Bagian 1 karena setiap keputusan di sana — format arsip, struktur tujuan, isi konfigurasi — menjadi dasar langsung untuk kode pada dua bagian berikutnya. Berkas sumber proyek (backup.sh, backup.conf, dan run-tests.sh) tersedia pada bagian Berkas Sumber di bawah halaman setiap bagian, sehingga Anda bisa membandingkan hasil pekerjaan dengan versi referensi kapan saja.
Tentukan sumber data, jadwal, jenis backup, dan struktur direktori sebelum menulis kode.
Buka → # Bagian 2: Implementasi ScriptTulis skrip backup dengan konfigurasi terpisah, logging, dan penanganan error.
Buka → # Bagian 3: Pengujian RestoreUji restore data dari arsip backup dan buktikan backup benar-benar bisa dipulihkan.
Buka →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 ]