Skrip backup yang ditulis begitu saja tanpa perencanaan biasanya berakhir sebagai satu berkas panjang yang sulit diubah: folder sumber tertanam di dalam kode, format arsip dipilih asal, dan saat kebutuhan berubah pengguna terpaksa menyunting skripnya sendiri. Pada bagian ini kita menahan diri untuk tidak menulis kode lebih dulu, dan menjawab empat pertanyaan perancangan dulu: data apa yang dibackup, seberapa sering, berapa lama disimpan, dan disimpan ke mana. Jawaban-jawaban itu kemudian kita tuangkan menjadi struktur direktori tujuan dan draf berkas konfigurasi — dua artefak yang akan dipakai langsung oleh skrip pada Bagian 2.
Kebutuhan: Apa, Seberapa Sering, Berapa Lama #
Agar keputusan tidak mengambang, kita bekerja untuk satu profil pengguna yang konkret: Budi, pengguna desktop Linux yang menyimpan pekerjaannya di dua folder — dokumen berisi catatan dan laporan, serta proyek berisi berkas kerja aplikasi yang sedang dikembangkan. Ukuran totalnya ratusan megabyte, berubah sedikit setiap hari, dan Budi tidak ingin mengingat backup sama sekali: sekali dipasang, lupakan.
Dari profil itu kita menurunkan tiga keputusan:
- Sumber data: folder
dokumendanproyek— bukan seluruh home directory, karena cache dan unduhan tidak bernilai untuk dibackup setiap hari. - Jadwal: sekali sehari, malam hari lewat cron. Jarak antar-backup menentukan seberapa banyak data yang boleh hilang — untuk dokumen pribadi, batas 24 jam adalah kompromi yang wajar.
- Retensi: tiga run terakhir disimpan; lebih tua dihapus otomatis supaya ruang tujuan tidak membengkak.
Aturan 3-2-1 memberi kita kerangka lebih besar: tiga salinan data, di dua media berbeda, satu di luar lokasi. Skrip ini menangani salinan kedua pada media kedua — misalnya folder tujuan di harddisk eksternal atau di server kecil di jaringan yang sama. Salinan ketiga di luar lokasi (cloud terenkripsi, disk di rumah kedua) berada di luar cakupan skrip, tetapi arsip tar.gz yang dihasilkannya siap diunggah untuk kebutuhan itu.
Arsip atau Mirror? Kita Pakai Keduanya #
Dua cara menyimpan backup yang sudah kita kenal punya kekuatan yang berlawanan, dan tabel berikut memperlihatkan mengapa memilih salah satu saja tidak cukup:
| Aspek | Arsip tar.gz | Mirror rsync |
|---|---|---|
| Isi yang disimpan | potret pada satu titik waktu | selalu kondisi terkini |
| Riwayat perubahan | ya — satu arsip per run | tidak — hanya versi terakhir |
| Ruang penyimpanan | membesar tiap run, tapi terkompresi | kira-kira sebesar sumber |
| Mengambil satu file | harus ekstrak dulu | langsung dibuka |
| Peran dalam desain kita | riwayat yang bisa di-restore | salinan cepat dan pembanding |
Arsip menjawab pertanyaan “bagaimana keadaan data kamis lalu?”, sedangkan mirror menjawab “bagaimana keadaan data sekarang?”. File yang terhapus keliru kemarin masih hidup di arsip kemarin meskipun mirror sudah tidak memuatnya — tetapi mirror menyediakan salinan yang bisa langsung disalin kembali tanpa ekstrak. Karena keduanya murah untuk data berukuran ratusan megabyte, skrip kita membuat keduanya pada setiap run.
Struktur Direktori Tujuan #
Setiap eksekusi skrip menghasilkan satu folder run bernama backup-YYYYMMDD — tanggal menjadi identitas alami karena jadwal kita harian. Folder tujuan arsip menampung semua run, mirror, dan log. Setelah run pertama, bentuknya seperti ini:
latihan/
├── dokumen/ ← folder sumber
│ ├── laporan/
│ │ └── penjualan.txt
│ └── tugas.txt
├── proyek/ ← folder sumber
│ └── catatan/
│ └── ide.txt
└── arsip/ ← folder tujuan
├── backup-20260115/ ← satu run = satu folder bertanggal
│ ├── dokumen.tar.gz
│ ├── dokumen.tar.gz.sha256
│ ├── proyek.tar.gz
│ └── proyek.tar.gz.sha256
├── mirror/ ← salinan terkini, tanpa kompresi
│ ├── dokumen/
│ └── proyek/
└── backup.log ← catatan setiap eksekusiSebelum menulis skrip, kita bangun dulu kerangka latihannya di terminal. Ekspansi kurung kurawal {a,b} membuat beberapa folder sekaligus dalam satu perintah — trik yang sudah kita kenal dari Chapter Wildcard dan Pattern Matching:
mkdir -p latihan/{dokumen/laporan,proyek/catatan,arsip}
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
tree latihanOutput yang Dihasilkan:
latihan
├── arsip
├── dokumen
│ ├── laporan
│ │ └── penjualan.txt
│ └── tugas.txt
└── proyek
└── catatan
└── ide.txt
6 directories, 3 filesStruktur inilah yang nantinya dipakai skrip: folder dokumen dan proyek menjadi sumber, folder arsip menjadi tujuan, dan seluruh isi di dalamnya dibuat serta dirotasi otomatis. Di komputer Anda, nama foldernya boleh berbeda — yang penting pemisahan sumber dan tujuan tetap jelas.
Konfigurasi Terpisah, Bukan Bagian Skrip #
Keputusan terakhir pada bagian ini: daftar folder, tujuan, dan jumlah retensi ditulis di berkas terpisah backup.conf, bukan ditahan sebagai variabel di dalam skrip. Alasannya praktis. Pengguna yang ingin menambah satu folder sumber cukup menyunting satu baris nilai tanpa menyentuh logika — mengerjakan perubahan kecil pada kode jauh lebih rentan salah ketik dan lupa tanda kutip. Skrip juga bisa diperbarui (misalnya menambah fitur) tanpa mengubah apa pun yang sudah dikonfigurasi pengguna.
Draf berkas konfigurasinya kita tulis dalam format penugasan shell biasa, karena nantinya cukup dibaca dengan satu perintah source:
# backup.conf — konfigurasi skrip backup.
# Sunting nilai di bawah ini; skrip backup.sh tidak perlu disentuh.
# Daftar folder sumber, dipisah spasi.
BACKUP_DIRS="dokumen proyek"
# Folder tujuan: arsip per-run, mirror, dan log disimpan di sini.
BACKUP_DEST="arsip"
# Berapa run backup-YYYYMMDD yang dipertahankan.
KEEP_ARCHIVES=3
# Berkas log (opsional; default: $BACKUP_DEST/backup.log).
LOG_FILE="arsip/backup.log"Empat variabel itu mencakup seluruh keputusan yang sudah kita ambil: sumbernya dua folder, tujuannya folder arsip, retensinya tiga run, dan lognya ikut tersimpan di tujuan. Versi lengkap berkas ini tersedia sebagai config/backup.conf pada bagian Berkas Sumber di bawah halaman ini.
Rencana Uji Sejak Awal #
Perencanaan belum lengkap tanpa definisi “berhasil”. Pada Bagian 3 nanti kita akan membuktikan hal-hal berikut secara otomatis lewat test suite: arsip benar-benar dibuat, isi arsip memuat berkas yang diharapkan, checksum arsip cocok, rotasi menyisakan tepat tiga run, restore menghasilkan salinan yang identik dengan sumber, dan setiap jenis kegagalan keluar dengan exit code yang berbeda. Menyusun daftar ini sekarang memastikan skrip pada Bagian 2 ditulis untuk lulus uji, bukan sekadar jalan.
Desain sudah lengkap: sumber dua folder, jadwal harian, retensi tiga run, arsip tar.gz plus mirror rsync, konfigurasi di berkas terpisah, dan tujuan berstruktur backup-YYYYMMDD. Semua keputusan itu tinggal diterjemahkan menjadi kode — itu pekerjaan Bagian 2, tempat kita menulis skripnya bertahap mulai dari pembacaan konfigurasi.
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 ]