File server yang tumbuh tanpa rencana biasanya berakhir sebagai satu folder raksasa berisi campuran dokumen kerja, foto liburan, dan salinan backup yang saling menimpa — dan permission? Semuanya 777, karena “biar tidak ribet”. Bagian ini mencegah kekacauan itu sejak awal: kita menetapkan share per fungsi, memisahkan data ke disk sendiri, lalu membangun struktur folder beserta grup dan mode permission-nya lewat skrip yang bisa dijalankan ulang. Tiga artefak hasil bagian ini — tabel rancangan, entri fstab, dan struktur /srv/share — menjadi acuan langsung bagi konfigurasi Samba dan NFS pada Bagian 2.

Merancang Struktur Share #

Lokasi baku untuk data layanan di Linux adalah /srv — jadi share kita hidup di bawah /srv/share. Sebelum mengetik perintah apa pun, seluruh rancangan kita tuangkan dulu ke satu tabel:

ShareIsiGrup pemilikMode di diskTamu
/srv/share/dokumendokumen kerja dan catatankeluarga2770ditolak
/srv/share/mediafoto dan video keluargakeluarga2775baca saja
/srv/share/backuparsip backup dari laptopbackup2770ditolak

Tiga keputusan tersembunyi di tabel itu. Pertama, pemisahan per fungsi: media tumbuh besar dan jarang diubah, dokumen kecil tapi sibuk diedit, dan backup tidak boleh tersentuh oleh pembersihan rutin — mencampurnya dalam satu folder membuat ketiganya saling mengganggu. Kedua, mode 2770 dan 2775 sama-sama memasang bit setgid (digit 2) agar berkas baru diwariskan ke grup foldernya — mekanisme ini kita buktikan hidupnya di Bagian 3; perbedaannya hanya pada kelas other: 0 menutup akses tamu sepenuhnya, 5 mengizinkan mereka membaca share media. Ketiga, share backup diberi grup sendiri (backup) karena hanya admin yang boleh membaca-menulis isinya — anggota keluarga yang butuh file lamanya meminta bantuan admin, bukan menghapus-rapikan sendiri.

Mode oktal dan kelas owner/group/other sudah kita kenal dari Chapter File Permissions ; digit keempat milik setgid dibahas di Chapter Special Permissions .

Disk Data Terpisah #

Satu keputusan struktural menyelamatkan banyak server rumahan: data hidup di disk (atau partisi) terpisah dari sistem. Saat sistem operasi diinstall ulang — yang di server rumah bisa terjadi saat eksperimen gagal — disk data tidak ikut terformat; cukup dipasang kembali ke mount point yang sama dan seluruh share kembali hidup. Pola partisinya sederhana: satu disk untuk /, satu disk data ber-filesystem ext4 yang dipasang di /srv/share.

Disk data terpisah juga mengubah ukuran masalah: ruang penyimpanan bisa ditambah dengan disk baru tanpa memperbesar partisi sistem, dan penuhnya disk data “hanya” membuat share berhenti menerima berkas — sistem tetap bisa login dan memberi peringatan.

Perintah persiapan di server dijalankan sekali dan membutuhkan root — lsblk -f untuk melihat nama disk dan UUID-nya, mkfs.ext4 untuk memformat, lalu direktori mount point dibuat:

bash
lsblk -f
sudo mkfs.ext4 /dev/sdb1
sudo mkdir -p /srv/share

Pemasangan permanen diatur /etc/fstab — cara kerja keenam field-nya sudah dibedah tuntas di Chapter fstab . Berkas berikut adalah entri kita, ditulis persis seperti yang ditempelkan ke /etc/fstab di server; versi lengkapnya tersedia sebagai config/fstab-share pada bagian Berkas Sumber di bawah halaman ini:

bash
mkdir -p config
cat > config/fstab-share <<'FSTAB'
# fstab-share — entri disk data untuk file server.
#
# Salin baris di bawah ini ke /etc/fstab di server, lalu terapkan:
#   sudo mount -a && findmnt /srv/share
#
# Ganti UUID dengan disk data Anda (lihat daftarnya: lsblk -f).
# Subnet 192.0.2.0/24 dipakai sebagai contoh blok dokumentasi.

UUID=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d  /srv/share  ext4  defaults,nofail  0  2
FSTAB
cat config/fstab-share

Output yang Dihasilkan:

text
# fstab-share — entri disk data untuk file server.
#
# Salin baris di bawah ini ke /etc/fstab di server, lalu terapkan:
#   sudo mount -a && findmnt /srv/share
#
# Ganti UUID dengan disk data Anda (lihat daftarnya: lsblk -f).
# Subnet 192.0.2.0/24 dipakai sebagai contoh blok dokumentasi.

UUID=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d  /srv/share  ext4  defaults,nofail  0  2

Mari kita bedah keenam field baris aktifnya:

  1. UUID=a1b2...4c5d: identitas disk yang stabil — nama perangkat /dev/sdb1 bisa bergeser saat urutan boot berubah, UUID tidak. Nilai di atas contoh; ganti dengan output lsblk -f di server Anda.
  2. /srv/share: mount point tempat isi disk muncul di pohon direktori.
  3. ext4: jenis filesystem yang kita pilih saat mkfs.ext4.
  4. defaults,nofail: opsi standar plus nofail — bila disk data mati atau lupa dipasang, boot tidak menggantung menunggunya; sistem tetap naik dan masalah bisa diperbaiki lewat SSH.
  5. 0: urutan dump, warisan backup lawas yang sudah tidak dipakai modern systemd.
  6. 2: urutan fsck — disk data diperiksa setelah root (1), bukan bersamaan.

Setelah sudo mount -a, perintah ls -a /srv/share memperlihatkan satu folder yang tidak kita buat: lost+found. Itu bukan penyusup — folder ini dibuat mkfs.ext4 di akar filesystem sebagai tempat fsck menaruh berkas yang ditemukannya tanpa induk setelah kerusakan disk. Ia dimiliki root dengan mode 0700, ukurannya bisa beberapa megabyte, dan sebaiknya dibiarkan apa adanya; karena share kita adalah subfolder /srv/share/dokumen dan kawan-kawannya, lost+found tidak pernah muncul di dalam share mana pun.

Validasi Entri fstab #

Entri fstab yang salah salah satu field-nya bisa membuat server gagal boot atau justru mengabaikan disk data, jadi layak diperiksa dengan mesin, bukan mata. Di server, alat resminya adalah sudo findmnt --fstab --verify; di latihan kita cukup awk pada berkasnya. Blok berikut menulis ulang entri utama sebagai fixture satu baris, mendefinisikan pemeriksanya, lalu menguji empat sifat entri aktif — jumlah field, sumber UUID, kehadiran nofail, dan urutan fsck — termasuk pada entri yang sengaja dibolongi:

bash
periksa_fstab() {
    awk '
    !/^[[:space:]]*#/ && NF {
        if (NF != 6)               printf "GAGAL %d field (harapan 6): %s\n", NF, $2
        else if ($1 !~ /^UUID=/)   printf "GAGAL sumber bukan UUID: %s\n", $1
        else if ($4 !~ /nofail/)   printf "GAGAL tanpa nofail: %s\n", $2
        else if ($6 != "2")        printf "GAGAL urutan fsck: %s\n", $6
        else                       printf "OK    %s -> %s\n", $1, $2
    }' "$1"
}
printf '%s\n' \
    'UUID=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d  /srv/share  ext4  defaults,nofail  0  2' \
    > fstab-share
echo "== entri utama =="
periksa_fstab fstab-share
echo "== entri tanpa nofail =="
printf 'UUID=bebas-apa-pun /srv/share ext4 defaults 0 1\n' > fstab-rusak
periksa_fstab fstab-rusak

Output yang Dihasilkan:

text
== entri utama ==
OK    UUID=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d -> /srv/share
== entri tanpa nofail ==
GAGAL tanpa nofail: /srv/share

Bedahnya singkat:

  1. Pola !/^[[:space:]]*#/ && NF: hanya baris aktif yang diperiksa — komentar dan baris kosong dilewati, sama seperti pola audit di Chapter NFS .
  2. Rantai if/else if: entri diperiksa dari kesalahan paling fatal; pesan pertama yang cocok cukup, karena satu entri rusak diperbaiki satu kesalahan demi satu kesalahan.
  3. $4 !~ /nofail/: opsi mount diperiksa sebagai substring dari field keempat, sehingga defaults,nofail maupun nofail,defaults sama-sama lolos.

Entri kedua memang lolos dua pemeriksaan pertama hanya untuk tertangkap di nofail — dan itulah kegunaan checker: kesalahan yang paling sering dilupakan manusia adalah yang paling murah ditangkap mesin. Rincian semua field dan opsi lain ada di fstab(5) .

Membangun Struktur dengan Skrip #

Rancangan kini diterjemahkan menjadi struktur sungguhan. Alasnya skrip, bukan deretan perintah manual: struktur harus bisa dibangun ulang dengan hasil identik saat server diganti atau disk data dipasang ulang. Skrip berikut — identik dengan deploy/buat-struktur.sh — membaca rencana dari satu variabel, membuat ketiga folder, mengatur grup dan mode, lalu memverifikasi hasilnya dengan stat. Saat berjalan sebagai root ia membuat grup sungguhan lewat groupadd; sebagai user biasa, ia berjalan dalam mode simulasi: struktur dan permission tetap asli, sedangkan chgrp ke grup keluarga digantikan ke grup utama kita dan rencana grupnya dicatat sebagai fixture:

bash
cat > buat-struktur.sh <<'BASH'
#!/usr/bin/env bash
# buat-struktur.sh — bangun struktur penyimpanan file server.
#
# Dua mode eksekusi:
#   - sebagai root di server  : membuat group sungguhan (groupadd bila
#     belum ada), folder di bawah TARGET, dan chgrp ke group aslinya.
#   - sebagai user biasa      : simulasi — folder tetap dibuat, chgrp
#     digantikan ke group utama kita, dan rencana group dicatat ke
#     TARGET/etc/group sebagai fixture yang bisa dibaca ulang.
#
# Pemakaian:
#   sudo bash buat-struktur.sh /srv      # di server sungguhan
#   bash buat-struktur.sh latihan        # latihan lokal tanpa root
#
# Kode keluar:
#   0 sukses                             1 argumen tidak valid
#   2 struktur gagal dibuat/tidak sesuai

set -u

TARGET="${1:-latihan}"

# Rencana share: nama:group:mode (mode 4 digit oktal, setgid aktif).
RENCANA="dokumen:keluarga:2770 media:keluarga:2775 backup:backup:2770"

if [ -z "$TARGET" ] || [ "$TARGET" = "/" ]; then
    printf 'buat-struktur.sh: target tidak valid: %s\n' "$TARGET" >&2
    exit 1
fi
if [ "$TARGET" = "/srv" ] && [ "$(id -u)" -ne 0 ]; then
    printf 'buat-struktur.sh: /srv milik root; jalankan dengan sudo\n' >&2
    exit 1
fi

mkdir -p "$TARGET" || exit 1
SHARE_DIR="$TARGET/share"

# --- 1. Siapkan group ----------------------------------------------------
if [ "$(id -u)" -eq 0 ]; then
    printf 'mode    : server (root)\n'
    for grup in keluarga tamu backup; do
        if ! getent group "$grup" > /dev/null; then
            groupadd "$grup" || exit 2
        fi
    done
else
    printf 'mode    : simulasi tanpa root\n'
    mkdir -p "$TARGET/etc" || exit 2
    cat > "$TARGET/etc/group" <<'GRUP'
keluarga:x:1100:budi,siti
tamu:x:1101:tamu1
backup:x:1102:budi
GRUP
    printf 'group   : keluarga, tamu, backup dicatat di %s/etc/group\n' "$TARGET"
fi

# --- 2. Buat folder share beserta permission-nya -------------------------
printf 'struktur: %s/{dokumen,media,backup}\n' "$SHARE_DIR"
gagal=0
for entri in $RENCANA; do
    nama=${entri%%:*}
    sisa=${entri#*:}
    grup=${sisa%%:*}
    mode=${sisa##*:}
    dir="$SHARE_DIR/$nama"

    mkdir -p "$dir" || { gagal=$((gagal + 1)); continue; }
    if [ "$(id -u)" -eq 0 ]; then
        chgrp "$grup" "$dir" || { gagal=$((gagal + 1)); continue; }
    else
        # Tanpa root kita hanya boleh memindahkan group ke group sendiri;
        # di server, baris ini menjadi: chgrp "$grup" "$dir"
        chgrp "$(id -gn)" "$dir" || { gagal=$((gagal + 1)); continue; }
    fi
    chmod "$mode" "$dir"

    # --- 3. Verifikasi hasil dengan stat -------------------------------
    nyata=$(stat -c '%a' "$dir")
    if [ "$nyata" != "$mode" ]; then
        printf 'GAGAL  %s mode %s, harapan %s\n' "$dir" "$nyata" "$mode"
        gagal=$((gagal + 1))
    else
        printf 'OK     %s %s\n' "$mode" "$dir"
    fi
done

if [ "$gagal" -gt 0 ]; then
    printf 'GAGAL: %d pemeriksaan tidak sesuai\n' "$gagal"
    exit 2
fi
printf 'selesai tanpa error\n'
BASH
bash buat-struktur.sh latihan
echo "== struktur akhir =="
tree latihan
echo "== fixture group simulasi =="
cat latihan/etc/group
echo "== mode ketiga share =="
stat -c '%a %n' latihan/share/*

Output yang Dihasilkan:

text
mode    : simulasi tanpa root
group   : keluarga, tamu, backup dicatat di latihan/etc/group
struktur: latihan/share/{dokumen,media,backup}
OK     2770 latihan/share/dokumen
OK     2775 latihan/share/media
OK     2770 latihan/share/backup
selesai tanpa error
== struktur akhir ==
latihan
├── etc
│   └── group
└── share
    ├── backup
    ├── dokumen
    └── media

6 directories, 1 file
== fixture group simulasi ==
keluarga:x:1100:budi,siti
tamu:x:1101:tamu1
backup:x:1102:budi
== mode ketiga share ==
2770 latihan/share/backup
2770 latihan/share/dokumen
2775 latihan/share/media

Mari kita bedah bagian-bagian kuncinya:

  1. Variabel RENCANA: seluruh rancangan tabel di awal bagian ini hidup sebagai satu baris data nama:group:mode. Menambah share baru berarti menambah satu entri — logika di bawahnya tidak berubah. Ini pola yang sama dengan berkas konfigurasi terpisah pada Proyek Skrip Backup Otomatis : data dipisah dari kode.
  2. Pemecahan ${entri%%:*} / ${sisa#*:}: parameter expansion memenggal tiap entri menjadi nama, grup, dan mode tanpa memanggil perintah eksternal.
  3. Cabang id -u: satu skrip untuk dua dunia. Di server, getent group memeriksa keberadaan grup sebelum groupadd supaya skrip aman dijalankan ulang. Di latihan, chgrp "$(id -gn)" adalah pengganti jujur: user biasa hanya boleh memindahkan grup ke grup tempatnya menjadi anggota — batas ini kita saksikan lagi di Bagian 3.
  4. Verifikasi stat -c '%a': skrip tidak berhenti pada “sudah menjalankan chmod”, melainkan membaca ulang mode dari disk dan membandingkannya dengan rencana. Angka 2770 dan 2775 di output adalah bukti bit setgid benar-benar terpasang.
  5. exit 2 saat verifikasi gagal: test suite nanti bisa membedakan “struktur gagal dibuat” dari “argumen salah” (exit 1) hanya lewat kode keluar.

Di server sungguhan, perintahnya sama persis dengan di atas: sudo bash buat-struktur.sh /srv — grup keluarga, tamu, dan backup dibuat jika belum ada, ketiga folder dipindahkan ke grupnya masing-masing, lalu mode dipasang. Fixture latihan/etc/group di latihan meniru baris yang nanti ditambahkan groupadd ke /etc/group sungguhan — format kolomnya sudah kita kenal dari Chapter Group Management .

Fondasi kini berdiri: disk data dengan entri fstab yang aman, dan tiga share dengan grup serta mode yang terverifikasi stat. Namun sejauh ini semuanya masih hanya terlihat dari dalam server sendiri. Pekerjaan Bagian 2: membagikannya ke jaringan — Samba untuk klien Windows dan ponsel, NFS untuk klien Linux — dengan konfigurasi yang sejalan dengan permission yang barusan kita pasang.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

deploy/buat-struktur.sh

Unduh berkas
bash
#!/usr/bin/env bash
# buat-struktur.sh — bangun struktur penyimpanan file server.
#
# Membuat tiga share (dokumen, media, backup), mengatur group dan
# mode permission-nya, lalu memverifikasi hasil akhirnya dengan stat.
#
# Dua mode eksekusi:
#   - sebagai root di server  : membuat group sungguhan (groupadd bila
#     belum ada), folder di bawah TARGET, dan chgrp ke group aslinya.
#   - sebagai user biasa      : simulasi — folder tetap dibuat, chgrp
#     digantikan ke group utama kita, dan rencana group dicatat ke
#     TARGET/etc/group sebagai fixture yang bisa dibaca ulang.
#
# Pemakaian:
#   sudo bash buat-struktur.sh /srv      # di server sungguhan
#   bash buat-struktur.sh latihan        # latihan lokal tanpa root
#
# Kode keluar:
#   0 sukses                             1 argumen tidak valid
#   2 struktur gagal dibuat/tidak sesuai

set -u

TARGET="${1:-latihan}"

# Rencana share: nama:group:mode (mode 4 digit oktal, setgid aktif).
RENCANA="dokumen:keluarga:2770 media:keluarga:2775 backup:backup:2770"

if [ -z "$TARGET" ] || [ "$TARGET" = "/" ]; then
    printf 'buat-struktur.sh: target tidak valid: %s\n' "$TARGET" >&2
    exit 1
fi
if [ "$TARGET" = "/srv" ] && [ "$(id -u)" -ne 0 ]; then
    printf 'buat-struktur.sh: /srv milik root; jalankan dengan sudo\n' >&2
    exit 1
fi

mkdir -p "$TARGET" || exit 1
SHARE_DIR="$TARGET/share"

# --- 1. Siapkan group ----------------------------------------------------
if [ "$(id -u)" -eq 0 ]; then
    printf 'mode    : server (root)\n'
    for grup in keluarga tamu backup; do
        if ! getent group "$grup" > /dev/null; then
            groupadd "$grup" || exit 2
        fi
    done
else
    printf 'mode    : simulasi tanpa root\n'
    mkdir -p "$TARGET/etc" || exit 2
    cat > "$TARGET/etc/group" <<'GRUP'
keluarga:x:1100:budi,siti
tamu:x:1101:tamu1
backup:x:1102:budi
GRUP
    printf 'group   : keluarga, tamu, backup dicatat di %s/etc/group\n' "$TARGET"
fi

# --- 2. Buat folder share beserta permission-nya -------------------------
printf 'struktur: %s/{dokumen,media,backup}\n' "$SHARE_DIR"
gagal=0
for entri in $RENCANA; do
    nama=${entri%%:*}
    sisa=${entri#*:}
    grup=${sisa%%:*}
    mode=${sisa##*:}
    dir="$SHARE_DIR/$nama"

    mkdir -p "$dir" || { gagal=$((gagal + 1)); continue; }
    if [ "$(id -u)" -eq 0 ]; then
        chgrp "$grup" "$dir" || { gagal=$((gagal + 1)); continue; }
    else
        # Tanpa root kita hanya boleh memindahkan group ke group sendiri;
        # di server, baris ini menjadi: chgrp "$grup" "$dir"
        chgrp "$(id -gn)" "$dir" || { gagal=$((gagal + 1)); continue; }
    fi
    chmod "$mode" "$dir"

    # --- 3. Verifikasi hasil dengan stat -------------------------------
    nyata=$(stat -c '%a' "$dir")
    if [ "$nyata" != "$mode" ]; then
        printf 'GAGAL  %s mode %s, harapan %s\n' "$dir" "$nyata" "$mode"
        gagal=$((gagal + 1))
    else
        printf 'OK     %s %s\n' "$mode" "$dir"
    fi
done

if [ "$gagal" -gt 0 ]; then
    printf 'GAGAL: %d pemeriksaan tidak sesuai\n' "$gagal"
    exit 2
fi
printf 'selesai tanpa error\n'

deploy/verifikasi-share.sh

Unduh berkas
bash
#!/usr/bin/env bash
# verifikasi-share.sh — periksa directive wajib smb.conf dan format exports.
#
# Pengganti testparm/exportfs di lingkungan tanpa Samba dan NFS: menerima
# folder yang memuat smb.conf dan exports, lalu memeriksa directive
# keamanan yang kita rancang satu per satu dengan grep dan awk.
#
# Pemakaian:
#   bash verifikasi-share.sh <folder-config>
#
# Kode keluar = jumlah pemeriksaan yang gagal.

set -u

if [ $# -ne 1 ]; then
    printf 'pemakaian: verifikasi-share.sh <folder-config>\n' >&2
    exit 1
fi

SMB="$1/smb.conf"
EXP="$1/exports"
gagal=0

for f in "$SMB" "$EXP"; do
    if [ ! -f "$f" ]; then
        printf 'verifikasi-share.sh: berkas tidak ditemukan: %s\n' "$f" >&2
        exit 1
    fi
done

ok()     { printf 'OK     %s\n' "$1"; }
hilang() { printf 'HILANG %s\n' "$1"; gagal=$((gagal + 1)); }

# --- smb.conf: section dan directive wajib --------------------------------
for pola in \
    '[global]' \
    '[dokumen]' \
    '[media]' \
    '[backup]' \
    'path = /srv/share/dokumen' \
    'path = /srv/share/media' \
    'path = /srv/share/backup' \
    'valid users = @keluarga' \
    'read only = no' \
    'create mask = 0660' \
    'directory mask = 2770' \
    'browseable = no' \
    'bind interfaces only = yes'
do
    if grep -qF -- "$pola" "$SMB"; then
        ok "smb.conf: $pola"
    else
        hilang "smb.conf: $pola"
    fi
done

# --- exports: format dan opsi keamanan -----------------------------------

# 1. Setiap entri aktif memuat kurung opsi di kolom klien.
n=$(awk '!/^[[:space:]]*#/ && NF && $2 !~ /\(/ { c++ } END { print c + 0 }' "$EXP")
if [ "$n" -eq 0 ]; then
    ok 'exports: semua entri memuat opsi klien'
else
    hilang 'exports: ada entri tanpa opsi klien'
fi

# 2. Setiap entri memuat sync dan no_subtree_check.
n=$(awk '
    !/^[[:space:]]*#/ && NF && ($2 !~ /sync/ || $2 !~ /no_subtree_check/) { c++ }
    END { print c + 0 }' "$EXP")
if [ "$n" -eq 0 ]; then
    ok 'exports: semua entri memuat sync + no_subtree_check'
else
    hilang 'exports: ada entri tanpa sync / no_subtree_check'
fi

# 3. root_squash tidak boleh dimatikan.
if grep -q 'no_root_squash' "$EXP"; then
    hilang 'exports: no_root_squash terdeteksi'
else
    ok 'exports: root_squash tetap aktif'
fi

# 4. dokumen rw, media ro.
mode_dokumen=$(awk '$1 == "/srv/share/dokumen" {
    print ($2 ~ /\(rw/) ? "rw" : "ro" }' "$EXP")
if [ "$mode_dokumen" = "rw" ]; then
    ok 'exports: dokumen dibagikan rw'
else
    hilang 'exports: dokumen bukan rw'
fi
mode_media=$(awk '$1 == "/srv/share/media" {
    print ($2 ~ /\(ro/) ? "ro" : "rw" }' "$EXP")
if [ "$mode_media" = "ro" ]; then
    ok 'exports: media dibagikan ro'
else
    hilang 'exports: media bukan ro'
fi

# 5. backup hanya untuk satu klien tepercaya.
klien=$(awk '$1 == "/srv/share/backup" {
    k = $2; sub(/\(.*/, "", k); print k }' "$EXP")
if [ "$klien" = "192.0.2.10" ]; then
    ok 'exports: backup terbatas ke 192.0.2.10'
else
    hilang 'exports: klien backup bukan 192.0.2.10'
fi

exit "$gagal"

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk proyek server file lokal.
# Suite menyiapkan folder kerja terisolasi (mktemp), lalu menguji:
# struktur share hasil deploy/buat-struktur.sh (folder, mode, fixture
# group), directive wajib pada config/smb.conf dan config/exports
# lewat deploy/verifikasi-share.sh, entri config/fstab-share, pewarisan
# setgid pada folder share, serta penolakan tulis pada file read-only.
# 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
WORK=""

# 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
}

bersihkan() {
    if [ -n "$WORK" ]; then
        rm -rf "$WORK"
    fi
}
trap bersihkan EXIT

WORK=$(mktemp -d)
cd "$WORK" || exit 1

# --- uji 1: buat-struktur.sh membangun struktur yang sesuai ---------------
bash "$PROJ_DIR/deploy/buat-struktur.sh" latihan > /dev/null 2>&1
assert_eq 0 "$?" "buat-struktur.sh keluar 0"
for nama in dokumen media backup; do
    [ -d "latihan/share/$nama" ]
    assert_eq 0 "$?" "share $nama dibuat"
done
assert_eq 2770 "$(stat -c '%a' latihan/share/dokumen)" \
    "mode dokumen 2770"
assert_eq 2775 "$(stat -c '%a' latihan/share/media)" \
    "mode media 2775"
assert_eq 2770 "$(stat -c '%a' latihan/share/backup)" \
    "mode backup 2770"

# --- uji 2: fixture group tercatat, skrip idempoten ------------------------
grep -q '^keluarga:x:1100:budi,siti$' latihan/etc/group
assert_eq 0 "$?" "fixture group memuat keluarga:budi,siti"
grep -q '^tamu:x:1101:tamu1$' latihan/etc/group
assert_eq 0 "$?" "fixture group memuat tamu"
bash "$PROJ_DIR/deploy/buat-struktur.sh" latihan > /dev/null 2>&1
assert_eq 0 "$?" "buat-struktur.sh idempoten (dijalankan dua kali)"

# --- uji 3: konfigurasi bawaan lolos verifikasi-share.sh -------------------
bash "$PROJ_DIR/deploy/verifikasi-share.sh" "$PROJ_DIR/config" \
    > /dev/null 2>&1
assert_eq 0 "$?" "config/ lolos verifikasi-share.sh"

# --- uji 4: konfigurasi rusak tertangkap -----------------------------------
mkdir -p rusak
printf '[global]\n[dokumen]\npath = /srv/share/dokumen\nread only = no\n' \
    > rusak/smb.conf
printf '/srv/share/dokumen  192.0.2.0/24(rw,async)\n' > rusak/exports
bash "$PROJ_DIR/deploy/verifikasi-share.sh" rusak > /dev/null 2>&1
if [ "$?" -gt 0 ]; then
    PASS=$((PASS + 1))
    printf 'OK    konfigurasi rusak tertangkap (exit bukan 0)\n'
else
    FAIL=$((FAIL + 1))
    printf 'GAGAL konfigurasi rusak tertangkap: verifikasi-share.sh keluar 0\n'
fi

# --- uji 5: entri fstab-share memuat nofail dan enam field -----------------
grep -q 'nofail' "$PROJ_DIR/config/fstab-share"
assert_eq 0 "$?" "fstab-share memuat nofail"
jumlah=$(awk '!/^[[:space:]]*#/ && NF { print NF }' \
    "$PROJ_DIR/config/fstab-share" | sort -u)
assert_eq 6 "$jumlah" "entri fstab-share berisi enam field"
mnt=$(awk '$2 == "/srv/share" { print $2 }' "$PROJ_DIR/config/fstab-share")
assert_eq "/srv/share" "$mnt" "mountpoint fstab-share /srv/share"

# --- uji 6: setgid diwarisi oleh isi baru folder share ---------------------
mkdir -p latihan/share/dokumen/laporan
touch latihan/share/dokumen/laporan/catatan.txt
assert_eq "$(stat -c '%G' latihan/share/dokumen)" \
    "$(stat -c '%G' latihan/share/dokumen/laporan/catatan.txt)" \
    "file baru mewarisi group folder share"
mode_sub=$(stat -c '%a' latihan/share/dokumen/laporan)
if [ "$(( 8#$mode_sub & 8#2000 ))" -ne 0 ]; then
    PASS=$((PASS + 1))
    printf 'OK    folder baru mewarisi bit setgid (%s)\n' "$mode_sub"
else
    FAIL=$((FAIL + 1))
    printf 'GAGAL folder baru mewarisi bit setgid: mode %s tanpa bit 2000\n' \
        "$mode_sub"
fi

# --- uji 7: tulis ditolak pada file read-only ------------------------------
printf 'isi awal\n' > latihan/kunci.txt
chmod 444 latihan/kunci.txt
if (echo 'tambahan' >> latihan/kunci.txt) 2> /dev/null; then
    FAIL=$((FAIL + 1))
    printf 'GAGAL tulis ke file ro ditolak: append justru berhasil\n'
else
    PASS=$((PASS + 1))
    printf 'OK    tulis ke file ro ditolak\n'
fi
assert_eq 'isi awal' "$(cat latihan/kunci.txt)" "isi file ro tidak berubah"
chmod 644 latihan/kunci.txt
echo 'tambahan' >> latihan/kunci.txt
assert_eq 0 "$?" "tulis diizinkan kembali setelah mode rw"

# --- 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
[ "$FAIL" -eq 0 ]