#!/bin/sh

set -eu
export LC_ALL=C

SELF=${0##*/}

usage() {
    cat <<-EOF
	Utilisation :
	  sudo $SELF

	Étend la partition qui contient la racine actuellement montée sur / jusqu'à
	la fin de l'espace libre contigu, puis agrandit son système de fichiers.

	Limitations :
	  - partition simple sur un disque GPT ou MBR ;
	  - système de fichiers ext3 ou ext4 monté en lecture-écriture ;

	Le script affiche les tailles avant modification et demande de saisir
	exactement le chemin de la partition racine pour confirmer.
	EOF
}

die() {
    printf 'Erreur : %s\n' "$*" >&2
    exit 1
}

require_commands() {
    for required_command in \
        awk blockdev cat dumpe2fs findmnt grep id lsblk mktemp parted partx \
        readlink resize2fs rm
    do
        command -v "$required_command" >/dev/null 2>&1 || \
            die "commande requise absente : $required_command"
    done
}

format_bytes() {
    awk -v bytes="$1" 'BEGIN {
        split("B KiB MiB GiB TiB PiB", units, " ")
        value = bytes + 0
        unit = 1
        while (value >= 1024 && unit < 6) {
            value /= 1024
            unit++
        }
        if (unit == 1)
            printf "%.0f %s", value, units[unit]
        else
            printf "%.1f %s", value, units[unit]
    }'
}

filesystem_bytes() {
    fb_device=$1
    fb_header=$(dumpe2fs -h "$fb_device" 2>/dev/null) || return 1
    fb_blocks=$(printf '%s\n' "$fb_header" | awk -F: \
        '$1 == "Block count" { gsub(/[[:space:]]/, "", $2); print $2; exit }')
    fb_block_size=$(printf '%s\n' "$fb_header" | awk -F: \
        '$1 == "Block size" { gsub(/[[:space:]]/, "", $2); print $2; exit }')

    [ -n "$fb_blocks" ] && [ -n "$fb_block_size" ] || return 1
    printf '%s\n' "$((fb_blocks * fb_block_size))"
}

read_parted_layout() {
    rpl_errors=$(mktemp /tmp/grow-rootfs-parted.XXXXXX)
    PARTED_STATUS=0
    PARTED_LAYOUT=$(parted -m -s "$DISK" unit s print free 2>"$rpl_errors") || \
        PARTED_STATUS=$?
    PARTED_DIAGNOSTICS=$(cat "$rpl_errors")
    rm -f -- "$rpl_errors"
}

case "${1:-}" in
    -h|--help|help)
        usage
        exit 0
        ;;
esac

[ "$#" -eq 0 ] || {
    usage >&2
    exit 2
}

[ "$(id -u)" -eq 0 ] || die "ce script doit être lancé avec sudo ou en root"
require_commands

ROOT_SOURCE=$(findmnt -n -o SOURCE -M /) || die "impossible d'identifier la source de /"
ROOT_DEVICE=$(readlink -f -- "$ROOT_SOURCE")
[ -b "$ROOT_DEVICE" ] || die "$ROOT_SOURCE ne désigne pas un périphérique bloc"
[ "$(lsblk -dnro TYPE "$ROOT_DEVICE")" = part ] || \
    die "$ROOT_DEVICE n'est pas une partition simple"

ROOT_FSTYPE=$(findmnt -n -o FSTYPE -M /) || die "impossible d'identifier le système de fichiers de /"
case "$ROOT_FSTYPE" in
    ext3|ext4) ;;
    *) die "système de fichiers racine non pris en charge : $ROOT_FSTYPE" ;;
esac

ROOT_OPTIONS=$(findmnt -n -o OPTIONS -M /) || die "impossible de lire les options de montage de /"
case ",$ROOT_OPTIONS," in
    *,rw,*) ;;
    *) die "la racine doit être montée en lecture-écriture" ;;
esac

DISK=$(lsblk -npo PKNAME "$ROOT_DEVICE") || die "impossible d'identifier le disque parent"
PARTITION_NUMBER=$(lsblk -dnro PARTN "$ROOT_DEVICE") || \
    die "impossible d'identifier le numéro de partition"
[ -b "$DISK" ] || die "$DISK n'est pas un disque accessible"
[ -n "$PARTITION_NUMBER" ] || die "numéro de partition racine vide"

PARTITION_TABLE=$(lsblk -dnro PTTYPE "$DISK")
case "$PARTITION_TABLE" in
    gpt|dos) ;;
    *) die "table de partitions non prise en charge : ${PARTITION_TABLE:-inconnue}" ;;
esac

read_parted_layout
GPT_REPAIR_REQUIRED=0

if [ "$PARTITION_TABLE" = gpt ] && \
   printf '%s\n' "$PARTED_DIAGNOSTICS" | \
       grep -Eiq 'backup GPT|not all of the space available'; then
    [ -n "$PARTED_DIAGNOSTICS" ] && printf '%s\n' "$PARTED_DIAGNOSTICS" >&2
    GPT_REPAIR_REQUIRED=1
    PARTED_DIAGNOSTICS=
fi

if [ "$PARTED_STATUS" -ne 0 ] && [ -z "$PARTED_LAYOUT" ]; then
    [ -n "$PARTED_DIAGNOSTICS" ] && printf '%s\n' "$PARTED_DIAGNOSTICS" >&2
    die "parted ne peut pas lire la table de partitions de $DISK"
fi
[ -z "$PARTED_DIAGNOSTICS" ] || printf '%s\n' "$PARTED_DIAGNOSTICS" >&2

ROOT_START=$(printf '%s\n' "$PARTED_LAYOUT" | awk -F: -v number="$PARTITION_NUMBER" '
    $1 == number && $5 != "free;" {
        value = $2
        sub(/s$/, "", value)
        print value
        exit
    }
')
ROOT_END=$(printf '%s\n' "$PARTED_LAYOUT" | awk -F: -v number="$PARTITION_NUMBER" '
    $1 == number && $5 != "free;" {
        value = $3
        sub(/s$/, "", value)
        print value
        exit
    }
')
[ -n "$ROOT_START" ] && [ -n "$ROOT_END" ] || \
    die "partition $PARTITION_NUMBER absente de la table lue par parted"

NEW_END=$(printf '%s\n' "$PARTED_LAYOUT" | awk -F: -v root_end="$ROOT_END" '
    $5 == "free;" {
        start = $2
        end = $3
        sub(/s$/, "", start)
        sub(/s$/, "", end)
        if (start == root_end + 1) {
            print end
            exit
        }
    }
')
[ -n "$NEW_END" ] || die "aucun espace libre contigu après $ROOT_DEVICE"
[ "$NEW_END" -gt "$ROOT_END" ] || die "$ROOT_DEVICE occupe déjà tout l'espace disponible"

SECTOR_SIZE=$(blockdev --getss "$DISK")
OLD_PARTITION_BYTES=$(blockdev --getsize64 "$ROOT_DEVICE")
NEW_PARTITION_BYTES=$(((NEW_END - ROOT_START + 1) * SECTOR_SIZE))
OLD_FILESYSTEM_BYTES=$(filesystem_bytes "$ROOT_DEVICE") || \
    die "impossible de déterminer la taille du système de fichiers racine"

printf 'Racine             : %s (%s)\n' "$ROOT_DEVICE" "$ROOT_FSTYPE"
printf 'Disque              : %s (%s)\n' "$DISK" "$PARTITION_TABLE"
printf 'Partition actuelle  : %s\n' "$(format_bytes "$OLD_PARTITION_BYTES")"
printf 'Partition proposée  : %s\n' "$(format_bytes "$NEW_PARTITION_BYTES")"
printf 'Système de fichiers : %s\n' "$(format_bytes "$OLD_FILESYSTEM_BYTES")"
if [ "$GPT_REPAIR_REQUIRED" -eq 1 ]; then
    printf 'GPT secondaire      : sera réparée et replacée en fin de disque\n'
fi
printf '\n'
printf 'ATTENTION : la fin de la partition %s va être modifiée.\n' "$ROOT_DEVICE"
printf 'Une sauvegarde des données importantes est recommandée.\n'
printf 'Pour confirmer, saisis exactement %s : ' "$ROOT_DEVICE"
[ -r /dev/tty ] || die "aucun terminal disponible pour lire la confirmation"
IFS= read -r confirmation </dev/tty
[ "$confirmation" = "$ROOT_DEVICE" ] || die "confirmation incorrecte ; aucune modification effectuée"

if [ "$GPT_REPAIR_REQUIRED" -eq 1 ]; then
    printf '\nRéparation de la table GPT secondaire...\n'
    parted -s -f "$DISK" unit s print >/dev/null
fi

printf "\nExtension de la partition jusqu'au secteur %s...\n" "$NEW_END"
printf 'Yes\n' | parted "$DISK" unit s resizepart "$PARTITION_NUMBER" "${NEW_END}s"

printf '\nMise à jour de la taille connue par le noyau...\n'
partx --update --nr "$PARTITION_NUMBER" "$DISK" || :
LIVE_PARTITION_BYTES=$(blockdev --getsize64 "$ROOT_DEVICE")
if [ "$LIVE_PARTITION_BYTES" -le "$OLD_PARTITION_BYTES" ]; then
    printf "La table de partitions a été modifiée, mais le noyau utilise encore l'ancienne taille.\n" >&2
    printf 'Redémarre la machine, puis exécute :\n' >&2
    printf '  sudo resize2fs "%s"\n' "$ROOT_DEVICE" >&2
    exit 1
fi

printf 'Partition agrandie : %s -> %s\n' \
    "$(format_bytes "$OLD_PARTITION_BYTES")" \
    "$(format_bytes "$LIVE_PARTITION_BYTES")"

printf '\nExtension du système de fichiers...\n'
resize2fs "$ROOT_DEVICE"
NEW_FILESYSTEM_BYTES=$(filesystem_bytes "$ROOT_DEVICE") || \
    die "agrandissement terminé, mais nouvelle taille illisible"

printf 'Système de fichiers agrandi : %s -> %s\n' \
    "$(format_bytes "$OLD_FILESYSTEM_BYTES")" \
    "$(format_bytes "$NEW_FILESYSTEM_BYTES")"
printf '\nOpération terminée.\n'
