diff --git a/scripts/hdd-setup.sh b/scripts/hdd-setup.sh deleted file mode 100644 index 935fa8b..0000000 --- a/scripts/hdd-setup.sh +++ /dev/null @@ -1,1244 +0,0 @@ -#!/bin/bash - -#=============================================================================== -# HDD Detection & Setup Script v1.0 -# Detects unmounted HDDs, checks state, formats, mounts, creates folder structure -# -# This script safely prepares external/additional drives for Felhom homeserver: -# - Detects unmounted block devices (HDDs and SSDs) -# - Shows SMART health, partition tables, existing filesystems -# - Formats with ext4 (only after explicit confirmation + safety checks) -# - Mounts by UUID in /etc/fstab with proper options -# - Creates standard Felhom media + storage folder structure -# -# Usage: -# sudo ./hdd-setup.sh [OPTIONS] -# -# Options: -# --scan Scan and report only (no changes) -# --mount-point PATH Pre-set mount point (skip prompt) -# --skip-smart Skip SMART health check -# --skip-folders Skip folder structure creation -# --dry-run Show what would be done without making changes -# --debug Enable bash debug tracing (set -x) -# -h, --help Show this help message -# -# Safety: -# - Never formats a drive with existing filesystems without triple confirmation -# - Shows all existing data/partitions before any destructive action -# - Writes fstab backup before modifications -# - Uses nofail mount option to prevent boot failures -# - All destructive operations require typing "YES" (not just y/n) -# -#=============================================================================== - -set -euo pipefail - -#------------------------------------------------------------------------------- -# Configuration -#------------------------------------------------------------------------------- -SCRIPT_VERSION="1.0.1" - -# Default values -SCAN_ONLY=false -PRESET_MOUNT_POINT="" -SKIP_SMART=false -SKIP_FOLDERS=false -DRY_RUN=false -DEBUG_MODE=false - -# Folder structure owner (UID:GID for Docker containers, typically 1000:1000) -FOLDER_UID=1000 -FOLDER_GID=1000 - -# Standard Felhom folder structure -declare -a MEDIA_DIRS=( - "media/downloads/complete" - "media/downloads/incomplete" - "media/movies" - "media/series" - "media/music" - "media/books" -) - -declare -a BACKUP_DIRS=( - "backups" -) - -declare -a USER_DIRS=( - "Dokumentumok" -) - -declare -a APPDATA_DIRS=( - "appdata" -) - -#------------------------------------------------------------------------------- -# Colors and logging (matching docker-setup.sh conventions) -#------------------------------------------------------------------------------- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } -log_step() { echo -e "${BLUE}[STEP]${NC} $1"; } -log_success() { echo -e "${GREEN}[OK]${NC} $1"; } -log_skip() { echo -e "${CYAN}[SKIP]${NC} $1"; } -log_debug() { if [[ "$DEBUG_MODE" == true ]]; then echo -e "${CYAN}[DEBUG]${NC} $1"; fi; } -log_danger() { echo -e "${RED}${BOLD}[DANGER]${NC} $1"; } - -#------------------------------------------------------------------------------- -# Error handling -#------------------------------------------------------------------------------- -on_error() { - echo "" - log_error "Script failed at line $1. Collecting diagnostics..." - echo "--- Block Devices ---" - lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT 2>/dev/null || true - echo "--- /etc/fstab ---" - cat /etc/fstab 2>/dev/null || true - echo "--- Mount status ---" - mount 2>/dev/null | grep -E "^/dev/" || true -} -trap 'on_error $LINENO' ERR - -# Cleanup trap for temp files -TEMP_FILES=() -LOCK_FILE="/tmp/.hdd_setup.lock" -cleanup() { - rm -f /tmp/.hdd_setup_part_dev 2>/dev/null || true - rm -f "$LOCK_FILE" 2>/dev/null || true - for f in "${TEMP_FILES[@]}"; do - rm -f "$f" 2>/dev/null || true - done -} -trap cleanup EXIT - -#------------------------------------------------------------------------------- -# Helpers -#------------------------------------------------------------------------------- -print_banner() { - echo "" - echo -e "${BOLD}${BLUE}-==================================================================¬${NC}" - echo -e "${BOLD}${BLUE}¦ HDD Detection & Setup Script v${SCRIPT_VERSION} ¦${NC}" - echo -e "${BOLD}${BLUE}L==================================================================-${NC}" - echo "" -} - -print_help() { - cat << 'EOF' -HDD Detection & Setup Script v1.0 - -Safely detects, formats, mounts, and prepares external drives for Felhom homeserver. - -USAGE: - sudo ./hdd-setup.sh [OPTIONS] - -OPTIONS: - --scan Scan and report only (no changes made) - --mount-point PATH Pre-set mount point (e.g., /mnt/hdd_1) - --skip-smart Skip SMART health check - --skip-folders Skip folder structure creation - --dry-run Show what would be done without making changes - --debug Enable verbose debug output - -h, --help Show this help - -WHAT THIS SCRIPT DOES: - 1. Detects unmounted block devices - 2. Shows disk info (size, partitions, SMART health) - 3. Lets you select a disk to set up - 4. Formats with ext4 (with safety confirmations) - 5. Mounts via UUID in /etc/fstab (with nofail) - 6. Creates Felhom folder structure - -FOLDER STRUCTURE CREATED: - / - ├── Dokumentumok/ - ├── media/ - │ ├── downloads/complete/ - │ ├── downloads/incomplete/ - │ ├── movies/ - │ ├── series/ - │ ├── music/ - │ └── books/ - ├── appdata/ (app data — created per-app on deploy) - └── backups/ (managed by felhom-controller) - -SAFETY: - - Drives with existing data require typing "YES" to format - - fstab is backed up before any modification - - Uses 'nofail' mount option (system boots even if drive missing) - - --scan mode for non-destructive inspection - -EOF - exit 0 -} - -parse_args() { - while [[ $# -gt 0 ]]; do - case "$1" in - --scan) SCAN_ONLY=true; shift ;; - --mount-point) PRESET_MOUNT_POINT="$2"; shift 2 ;; - --skip-smart) SKIP_SMART=true; shift ;; - --skip-folders) SKIP_FOLDERS=true; shift ;; - --dry-run) DRY_RUN=true; shift ;; - --debug) DEBUG_MODE=true; shift ;; - -h|--help) print_help ;; - *) log_error "Unknown option: $1"; print_help ;; - esac - done -} - -check_root() { - if [[ $EUID -ne 0 ]]; then - log_error "This script must be run as root (use sudo)" - exit 1 - fi -} - -install_dependencies() { - local missing=() - - # smartmontools for SMART data - if ! command -v smartctl &> /dev/null && [[ "$SKIP_SMART" == false ]]; then - missing+=("smartmontools") - fi - - # parted for partition inspection - if ! command -v parted &> /dev/null; then - missing+=("parted") - fi - - # util-linux should be present, but check for blkid/lsblk - if ! command -v blkid &> /dev/null; then - missing+=("util-linux") - fi - - if [[ ${#missing[@]} -gt 0 ]]; then - log_info "Installing missing dependencies: ${missing[*]}" - if [[ "$DRY_RUN" == true ]]; then - log_info "[DRY RUN] Would install: ${missing[*]}" - return - fi - apt-get update -qq - apt-get install -qq -y "${missing[@]}" > /dev/null 2>&1 - log_success "Dependencies installed" - fi -} - -#------------------------------------------------------------------------------- -# Disk detection -#------------------------------------------------------------------------------- - -# Get the device name of the root filesystem (e.g., "sda") -get_root_disk() { - local root_dev - root_dev=$(findmnt -n -o SOURCE / 2>/dev/null | head -1) - - # Handle LVM: /dev/mapper/vg-root -> find underlying PV - if [[ "$root_dev" == /dev/mapper/* ]]; then - if command -v pvs &> /dev/null; then - root_dev=$(pvs --noheadings -o pv_name 2>/dev/null | head -1 | tr -d ' ') - fi - fi - - # Handle partitions: /dev/sda2 -> sda, /dev/nvme0n1p2 -> nvme0n1 - local disk_name - disk_name=$(lsblk -no PKNAME "$root_dev" 2>/dev/null | head -1) - - if [[ -z "$disk_name" ]]; then - # Fallback: strip partition number - disk_name=$(echo "$root_dev" | sed 's|/dev/||' | sed 's/[0-9]*$//' | sed 's/p$//') - fi - - echo "$disk_name" -} - -# Get list of disks that are NOT the system disk and NOT mounted -get_candidate_disks() { - local root_disk - root_disk=$(get_root_disk) - log_debug "Root disk detected as: $root_disk" - - local candidates=() - - # Iterate over whole-disk block devices (not partitions) - while IFS= read -r line; do - local name size type - name=$(echo "$line" | awk '{print $1}') - size=$(echo "$line" | awk '{print $2}') - type=$(echo "$line" | awk '{print $3}') - - # Skip if this IS the root disk - if [[ "$name" == "$root_disk" ]]; then - log_debug "Skipping $name (root disk)" - continue - fi - - # Skip if type is not "disk" - if [[ "$type" != "disk" ]]; then - log_debug "Skipping $name (type: $type)" - continue - fi - - # Skip very small devices (<1GB) - likely USB sticks with boot media, etc. - local size_bytes - size_bytes=$(lsblk -bno SIZE "/dev/$name" 2>/dev/null | head -1) - if [[ -n "$size_bytes" ]] && [[ "$size_bytes" -lt 1073741824 ]]; then - log_debug "Skipping $name (too small: $size)" - continue - fi - - # Skip loop, ram, zram devices - if [[ "$name" =~ ^(loop|ram|zram) ]]; then - log_debug "Skipping $name (virtual device)" - continue - fi - - # Check if ANY partition of this disk is mounted - local any_mounted=false - while IFS= read -r part_line; do - local part_name part_mount - part_name=$(echo "$part_line" | awk '{print $1}') - part_mount=$(echo "$part_line" | awk '{print $7}') - if [[ -n "$part_mount" ]]; then - any_mounted=true - log_debug "Skipping $name (partition $part_name mounted at $part_mount)" - break - fi - done < <(lsblk -lno NAME,MAJ:MIN,RM,SIZE,RO,TYPE,MOUNTPOINT "/dev/$name" 2>/dev/null | tail -n +2) - - # Also check if the disk itself is mounted (whole-disk filesystem) - local disk_mount - disk_mount=$(lsblk -lno MOUNTPOINT "/dev/$name" 2>/dev/null | head -1) - if [[ -n "$disk_mount" ]]; then - any_mounted=true - log_debug "Skipping $name (disk mounted at $disk_mount)" - fi - - if [[ "$any_mounted" == true ]]; then - continue - fi - - candidates+=("$name") - - done < <(lsblk -lno NAME,SIZE,TYPE 2>/dev/null | grep ' disk$') - - echo "${candidates[@]}" -} - -#------------------------------------------------------------------------------- -# Disk inspection -#------------------------------------------------------------------------------- - -print_disk_info() { - local disk="$1" - local dev="/dev/$disk" - - echo "" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BOLD} Disk: /dev/$disk${NC}" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - - # Basic info - local size model serial rotational transport - size=$(lsblk -dno SIZE "$dev" 2>/dev/null | tr -d ' ') - model=$(lsblk -dno MODEL "$dev" 2>/dev/null | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - serial=$(lsblk -dno SERIAL "$dev" 2>/dev/null | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - rotational=$(cat /sys/block/"$disk"/queue/rotational 2>/dev/null || echo "?") - transport=$(lsblk -dno TRAN "$dev" 2>/dev/null | tr -d ' ') - - local disk_type="Unknown" - if [[ "$rotational" == "1" ]]; then - disk_type="HDD (rotational)" - elif [[ "$rotational" == "0" ]]; then - disk_type="SSD (non-rotational)" - fi - - echo -e " ${BOLD}Size:${NC} $size" - echo -e " ${BOLD}Model:${NC} ${model:-Unknown}" - echo -e " ${BOLD}Serial:${NC} ${serial:-Unknown}" - echo -e " ${BOLD}Type:${NC} $disk_type" - echo -e " ${BOLD}Transport:${NC} ${transport:-Unknown}" - - # Partition table - echo "" - echo -e " ${BOLD}Partition layout:${NC}" - local part_output - part_output=$(lsblk -o NAME,SIZE,TYPE,FSTYPE,LABEL,MOUNTPOINT "$dev" 2>/dev/null) - echo "$part_output" | sed 's/^/ /' - - # Check for existing filesystems on whole disk and partitions - echo "" - echo -e " ${BOLD}Filesystem detection:${NC}" - local has_data=false - - # Check whole disk - local whole_fs - whole_fs=$(blkid -o value -s TYPE "$dev" 2>/dev/null || echo "") - if [[ -n "$whole_fs" ]]; then - local whole_label - whole_label=$(blkid -o value -s LABEL "$dev" 2>/dev/null || echo "") - echo -e " ${YELLOW}$dev: filesystem=$whole_fs label=${whole_label:-}${NC}" - has_data=true - fi - - # Check each partition - while IFS= read -r part; do - [[ -z "$part" ]] && continue - local part_dev="/dev/$part" - local part_fs part_label part_uuid - part_fs=$(blkid -o value -s TYPE "$part_dev" 2>/dev/null || echo "") - part_label=$(blkid -o value -s LABEL "$part_dev" 2>/dev/null || echo "") - part_uuid=$(blkid -o value -s UUID "$part_dev" 2>/dev/null || echo "") - if [[ -n "$part_fs" ]]; then - echo -e " ${YELLOW}$part_dev: filesystem=$part_fs label=${part_label:-} uuid=${part_uuid:-}${NC}" - has_data=true - else - echo -e " $part_dev: ${GREEN}no filesystem detected${NC}" - fi - done < <(lsblk -lno NAME "$dev" 2>/dev/null | tail -n +2) - - if [[ "$has_data" == false ]]; then - echo -e " ${GREEN}No filesystems detected (clean disk)${NC}" - fi - - # SMART health - if [[ "$SKIP_SMART" == false ]] && command -v smartctl &> /dev/null; then - echo "" - echo -e " ${BOLD}SMART Health:${NC}" - local smart_output - smart_output=$(smartctl -H "$dev" 2>/dev/null || echo "SMART not available") - - if echo "$smart_output" | grep -q "PASSED"; then - echo -e " ${GREEN}SMART overall: PASSED${NC}" - elif echo "$smart_output" | grep -q "FAILED"; then - echo -e " ${RED}${BOLD}SMART overall: FAILED - DO NOT USE THIS DISK${NC}" - else - echo -e " ${YELLOW}SMART: Not available (USB enclosures often don't support SMART)${NC}" - fi - - # Show key SMART attributes if available - local reallocated power_on temp - reallocated=$(smartctl -A "$dev" 2>/dev/null | grep -i "Reallocated_Sector" | awk '{print $NF}' || echo "") - power_on=$(smartctl -A "$dev" 2>/dev/null | grep -i "Power_On_Hours" | awk '{print $NF}' || echo "") - temp=$(smartctl -A "$dev" 2>/dev/null | grep -i "Temperature_Celsius" | awk '{print $NF}' || echo "") - - if [[ -n "$reallocated" ]]; then - if [[ "$reallocated" -gt 0 ]] 2>/dev/null; then - echo -e " ${YELLOW}Reallocated sectors: $reallocated (some wear detected)${NC}" - else - echo -e " Reallocated sectors: $reallocated" - fi - fi - [[ -n "$power_on" ]] && echo " Power-on hours: $power_on" - [[ -n "$temp" ]] && echo " Temperature: ${temp}°C" - fi - - # Return whether disk has data (for caller to use) - if [[ "$has_data" == true ]]; then - return 1 - else - return 0 - fi -} - -#------------------------------------------------------------------------------- -# Disk formatting -#------------------------------------------------------------------------------- - -# Check if a disk has any filesystem signatures anywhere -disk_has_data() { - local disk="$1" - local dev="/dev/$disk" - - # Check whole disk - if blkid "$dev" &>/dev/null; then - return 0 # has data - fi - - # Check all partitions - while IFS= read -r part; do - [[ -z "$part" ]] && continue - if blkid "/dev/$part" &>/dev/null; then - return 0 # has data - fi - done < <(lsblk -lno NAME "$dev" 2>/dev/null | tail -n +2) - - return 1 # no data -} - -# Check if disk is referenced anywhere in fstab -disk_in_fstab() { - local disk="$1" - local dev="/dev/$disk" - - # Check by device path - if grep -q "$dev" /etc/fstab 2>/dev/null; then - return 0 - fi - - # Check by UUID of any partition - while IFS= read -r part; do - [[ -z "$part" ]] && continue - local part_uuid - part_uuid=$(blkid -o value -s UUID "/dev/$part" 2>/dev/null || echo "") - if [[ -n "$part_uuid" ]] && grep -q "$part_uuid" /etc/fstab 2>/dev/null; then - return 0 - fi - done < <(lsblk -lno NAME "$dev" 2>/dev/null | tail -n +2) - - # Check whole disk UUID - local disk_uuid - disk_uuid=$(blkid -o value -s UUID "$dev" 2>/dev/null || echo "") - if [[ -n "$disk_uuid" ]] && grep -q "$disk_uuid" /etc/fstab 2>/dev/null; then - return 0 - fi - - return 1 -} - -format_disk() { - local disk="$1" - local dev="/dev/$disk" - - echo "" - log_step "Preparing to format /dev/$disk" - - # Safety check: Is the disk part of a RAID array? - if command -v mdadm &>/dev/null; then - if mdadm --examine "$dev" &>/dev/null || mdadm --examine "${dev}"* &>/dev/null 2>&1; then - log_error "Disk /dev/$disk appears to be part of a RAID array!" - log_error "Remove it from the array first with: mdadm --zero-superblock $dev" - return 1 - fi - fi - # Check for RAID signatures via blkid - if blkid "$dev" 2>/dev/null | grep -qi "linux_raid_member"; then - log_error "Disk /dev/$disk has a RAID superblock! Cannot format." - return 1 - fi - - # Safety check: Is the disk used by LVM? - if command -v pvs &>/dev/null; then - if pvs "$dev" &>/dev/null 2>&1 || pvs "${dev}"* &>/dev/null 2>&1; then - log_error "Disk /dev/$disk is an LVM physical volume!" - log_error "Remove it first with: pvremove $dev" - return 1 - fi - fi - - # Safety check: Is the disk or any partition currently in use? - if lsof "$dev"* 2>/dev/null | grep -q .; then - log_error "Disk /dev/$disk has open file handles! Cannot proceed." - log_error "Another process is using this disk." - return 1 - fi - - # Safety check: Is it in fstab? - if disk_in_fstab "$disk"; then - log_warn "This disk is referenced in /etc/fstab!" - grep -E "(${dev}|$(blkid -o value -s UUID "$dev" 2>/dev/null || echo 'NOUUID'))" /etc/fstab 2>/dev/null | sed 's/^/ /' - echo "" - log_warn "Proceeding will remove the old fstab entry." - fi - - local has_existing_data=false - if disk_has_data "$disk"; then - has_existing_data=true - fi - - if [[ "$has_existing_data" == true ]]; then - echo "" - log_danger "═══════════════════════════════════════════════════════════════" - log_danger " THIS DISK CONTAINS DATA! Formatting will DESTROY everything." - log_danger "═══════════════════════════════════════════════════════════════" - echo "" - echo -e " Disk: ${BOLD}/dev/$disk${NC}" - echo -e " Existing filesystems:" - blkid "$dev"* 2>/dev/null | sed 's/^/ /' || true - echo "" - - # First confirmation - echo -e "${RED}${BOLD}Type the disk name (e.g., sdb) to confirm you want to DESTROY ALL DATA:${NC}" - read -r confirm_name - if [[ "$confirm_name" != "$disk" ]]; then - log_info "Disk name did not match. Aborting format." - return 1 - fi - - # Second confirmation - echo -e "${RED}${BOLD}Type YES (uppercase) to confirm formatting /dev/$disk:${NC}" - read -r confirm_yes - if [[ "$confirm_yes" != "YES" ]]; then - log_info "Confirmation not received. Aborting format." - return 1 - fi - else - echo "" - echo "This disk appears empty (no filesystems detected)." - read -p "Format /dev/$disk with ext4? [y/N] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - log_info "Skipping format." - return 1 - fi - fi - - if [[ "$DRY_RUN" == true ]]; then - log_info "[DRY RUN] Would format /dev/$disk with ext4" - return 0 - fi - - log_info "Wiping existing partition table on /dev/$disk..." - wipefs -a "$dev" 2>/dev/null || true - - log_info "Creating GPT partition table..." - parted -s "$dev" mklabel gpt - - log_info "Creating single ext4 partition spanning entire disk..." - parted -s "$dev" mkpart primary ext4 0% 100% - - # Wait for kernel to pick up partition - sleep 2 - partprobe "$dev" 2>/dev/null || true - sleep 1 - - # Determine partition device name (sdb1 or nvme0n1p1) - local part_dev="" - # Try common naming patterns - if [[ -b "${dev}1" ]]; then - part_dev="${dev}1" - elif [[ -b "${dev}p1" ]]; then - part_dev="${dev}p1" - else - # Wait a bit more and retry - sleep 3 - partprobe "$dev" 2>/dev/null || true - if [[ -b "${dev}1" ]]; then - part_dev="${dev}1" - elif [[ -b "${dev}p1" ]]; then - part_dev="${dev}p1" - else - log_error "Could not find partition device after partitioning!" - log_error "Expected ${dev}1 or ${dev}p1" - lsblk "$dev" - return 1 - fi - fi - - log_info "Formatting ${part_dev} as ext4..." - mkfs.ext4 -F -L "felhom_data" "$part_dev" - - log_success "Disk formatted successfully" - echo " Partition: $part_dev" - echo " Filesystem: ext4" - echo " Label: felhom_data" - - # Return the partition device for mounting - echo "$part_dev" > /tmp/.hdd_setup_part_dev - return 0 -} - -#------------------------------------------------------------------------------- -# Mount point selection and mounting -#------------------------------------------------------------------------------- - -suggest_mount_point() { - # Find next available /mnt/hdd_N - local n=1 - while [[ -d "/mnt/hdd_${n}" ]] && mountpoint -q "/mnt/hdd_${n}" 2>/dev/null; do - ((n++)) - done - - # If dir exists but not mounted, it might be a leftover — still suggest it - if [[ -d "/mnt/hdd_${n}" ]] && [[ -z "$(ls -A /mnt/hdd_${n} 2>/dev/null)" ]]; then - echo "/mnt/hdd_${n}" - elif [[ ! -d "/mnt/hdd_${n}" ]]; then - echo "/mnt/hdd_${n}" - else - # Directory exists and has content, try next - ((n++)) - echo "/mnt/hdd_${n}" - fi -} - -select_mount_point() { - local mount_point="" - - if [[ -n "$PRESET_MOUNT_POINT" ]]; then - mount_point="$PRESET_MOUNT_POINT" - log_info "Using pre-set mount point: $mount_point" >&2 - else - local suggested - suggested=$(suggest_mount_point) - - echo "" >&2 - log_step "Select mount point" >&2 - echo "" >&2 - echo " Suggested: $suggested" >&2 - echo "" >&2 - read -rp " Enter mount point [${suggested}]: " user_mount - mount_point="${user_mount:-$suggested}" - fi - - # Validate mount point - if [[ ! "$mount_point" =~ ^/ ]]; then - log_error "Mount point must be an absolute path (start with /)" >&2 - return 1 - fi - - # Check if something is already mounted there - if mountpoint -q "$mount_point" 2>/dev/null; then - log_error "$mount_point is already a mount point!" >&2 - mount | grep "$mount_point" >&2 - return 1 - fi - - # Check if directory exists and has content - if [[ -d "$mount_point" ]] && [[ -n "$(ls -A "$mount_point" 2>/dev/null)" ]]; then - log_warn "$mount_point exists and is not empty!" >&2 - ls -la "$mount_point" >&2 - echo "" >&2 - log_warn "Mounting here will HIDE existing content (it won't be deleted, but inaccessible while mounted)." >&2 - read -p "Continue with this mount point? [y/N] " -n 1 -r - echo >&2 - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - return 1 - fi - fi - - echo "$mount_point" -} - -mount_disk() { - local part_dev="$1" - local mount_point="$2" - - echo "" - log_step "Mounting $part_dev at $mount_point" - - # Get UUID - local uuid - uuid=$(blkid -o value -s UUID "$part_dev" 2>/dev/null) - if [[ -z "$uuid" ]]; then - log_error "Could not determine UUID for $part_dev" - return 1 - fi - - log_info "Partition UUID: $uuid" - - if [[ "$DRY_RUN" == true ]]; then - log_info "[DRY RUN] Would create $mount_point" - log_info "[DRY RUN] Would add to /etc/fstab: UUID=$uuid $mount_point ext4 defaults,noatime,nofail,x-systemd.device-timeout=10 0 2" - log_info "[DRY RUN] Would mount $part_dev at $mount_point" - return 0 - fi - - # Create mount point - mkdir -p "$mount_point" - - # Backup fstab - local fstab_backup - fstab_backup="/etc/fstab.backup.$(date +%Y%m%d_%H%M%S)" - cp /etc/fstab "$fstab_backup" - log_info "fstab backed up to: $fstab_backup" - - # Remove any existing entry for this UUID (in case of re-setup) - if grep -q "$uuid" /etc/fstab 2>/dev/null; then - log_warn "Removing existing fstab entry for UUID=$uuid" - sed -i "\|$uuid|d" /etc/fstab - fi - - # Add fstab entry - # nofail: system boots even if drive is missing - # x-systemd.device-timeout=10: don't wait forever during boot - # noatime: skip updating access timestamps (better performance) - local fstab_entry="UUID=$uuid $mount_point ext4 defaults,noatime,nofail,x-systemd.device-timeout=10 0 2" - - { - echo "" - echo "# Felhom data drive - added by hdd-setup.sh on $(date '+%Y-%m-%d %H:%M:%S')" - echo "$fstab_entry" - } >> /etc/fstab - - log_info "Added to /etc/fstab:" - echo " $fstab_entry" - - # Verify fstab syntax (multiple methods - findmnt segfaults on some Debian 13 builds) - local fstab_ok=false - - # Method 1: findmnt --verify (preferred, but may segfault on trixie) - local verify_exit=0 - findmnt --verify --tab-file /etc/fstab &>/dev/null || verify_exit=$? - if [[ $verify_exit -eq 0 ]]; then - fstab_ok=true - log_debug "fstab verified via findmnt" - elif [[ $verify_exit -eq 139 ]] || [[ $verify_exit -gt 128 ]]; then - log_debug "findmnt segfaulted (exit $verify_exit), falling back to mount --fake" - fi - - # Method 2: mount --fake -a (dry-run mount test) - if [[ "$fstab_ok" == false ]]; then - if mount --fake -a &>/dev/null; then - fstab_ok=true - log_debug "fstab verified via mount --fake" - fi - fi - - # Method 3: Basic sanity check on the entry we just wrote - if [[ "$fstab_ok" == false ]]; then - # Verify our entry has all required fields (UUID, mountpoint, fstype, options, dump, pass) - local field_count - field_count=$(echo "$fstab_entry" | awk '{print NF}') - if [[ "$field_count" -eq 6 ]] && [[ "$fstab_entry" == UUID=* ]]; then - fstab_ok=true - log_debug "fstab verified via field count sanity check" - fi - fi - - if [[ "$fstab_ok" == false ]]; then - log_error "fstab verification failed! Restoring backup..." - cp "$fstab_backup" /etc/fstab - log_info "fstab restored from backup" - return 1 - fi - log_success "fstab syntax verified" - - # Mount - mount "$mount_point" - - if mountpoint -q "$mount_point"; then - log_success "Disk mounted at $mount_point" - - # Show mounted disk info - df -h "$mount_point" | sed 's/^/ /' - else - log_error "Mount failed! Check dmesg for errors." - dmesg | tail -5 - return 1 - fi -} - -#------------------------------------------------------------------------------- -# Folder structure creation -#------------------------------------------------------------------------------- - -create_folder_structure() { - local mount_point="$1" - - echo "" - log_step "Creating Felhom folder structure at $mount_point" - echo "" - echo -e " ${BOLD}Planned structure:${NC}" - echo " $mount_point/" - - for dir in "${USER_DIRS[@]}"; do - echo " ├── $dir/" - done - for dir in "${MEDIA_DIRS[@]}"; do - echo " ├── $dir/" - done - for dir in "${APPDATA_DIRS[@]}"; do - echo " ├── $dir/" - done - for dir in "${BACKUP_DIRS[@]}"; do - echo " └── $dir/" - done - - echo "" - echo " Owner: ${FOLDER_UID}:${FOLDER_GID} (standard Docker user)" - echo "" - - if [[ "$DRY_RUN" == true ]]; then - log_info "[DRY RUN] Would create the above folder structure" - return 0 - fi - - read -p "Create this folder structure? [Y/n] " -n 1 -r - echo - if [[ $REPLY =~ ^[Nn]$ ]]; then - log_skip "Folder structure creation skipped" - return 0 - fi - - for dir in "${USER_DIRS[@]}" "${MEDIA_DIRS[@]}" "${APPDATA_DIRS[@]}" "${BACKUP_DIRS[@]}"; do - mkdir -p "${mount_point}/${dir}" - log_debug "Created: ${mount_point}/${dir}" - done - - # Set ownership recursively - chown -R "${FOLDER_UID}:${FOLDER_GID}" "$mount_point" - - # Set permissions: owner rwx, group rwx, others r-x - # This allows Docker containers running as 1000:1000 to read/write - chmod -R 775 "$mount_point" - - # Verify - local created_count - created_count=$(find "$mount_point" -type d | wc -l) - log_success "Folder structure created ($created_count directories)" - - echo "" - echo -e " ${BOLD}Result:${NC}" - # Use find to show the tree structure (works without 'tree' command) - (cd "$mount_point" && find . -type d -maxdepth 3 | sort | sed 's|^\.||' | sed 's|/| |g' | sed 's/^/ /') -} - -#------------------------------------------------------------------------------- -# Existing partition handling -#------------------------------------------------------------------------------- - -# Handle a disk that already has a usable ext4 partition -handle_existing_partition() { - local disk="$1" - local dev="/dev/$disk" - - echo "" - log_step "Checking for usable existing partitions on /dev/$disk" - - local usable_parts=() - local part_info=() - - # Check whole disk for ext4 - local whole_fs - whole_fs=$(blkid -o value -s TYPE "$dev" 2>/dev/null || echo "") - if [[ "$whole_fs" == "ext4" ]]; then - usable_parts+=("$dev") - local whole_label whole_uuid - whole_label=$(blkid -o value -s LABEL "$dev" 2>/dev/null || echo "") - whole_uuid=$(blkid -o value -s UUID "$dev" 2>/dev/null || echo "") - part_info+=("$dev fs=ext4 label=$whole_label uuid=$whole_uuid") - fi - - # Check partitions - while IFS= read -r part; do - [[ -z "$part" ]] && continue - local part_dev="/dev/$part" - local part_fs - part_fs=$(blkid -o value -s TYPE "$part_dev" 2>/dev/null || echo "") - if [[ "$part_fs" == "ext4" ]]; then - usable_parts+=("$part_dev") - local p_label p_uuid - p_label=$(blkid -o value -s LABEL "$part_dev" 2>/dev/null || echo "") - p_uuid=$(blkid -o value -s UUID "$part_dev" 2>/dev/null || echo "") - part_info+=("$part_dev fs=ext4 label=$p_label uuid=$p_uuid") - fi - done < <(lsblk -lno NAME "$dev" 2>/dev/null | tail -n +2) - - if [[ ${#usable_parts[@]} -eq 0 ]]; then - return 1 # No usable ext4 partitions - fi - - echo "" - echo -e " ${GREEN}Found usable ext4 partition(s):${NC}" - for info in "${part_info[@]}"; do - echo " $info" - done - echo "" - - echo "Options:" - echo " 1) Use existing ext4 partition (no format, no data loss)" - echo " 2) Wipe and reformat the entire disk" - echo " 3) Cancel" - echo "" - read -rp "Choose [1/2/3]: " choice - - case "$choice" in - 1) - # Use the first (or only) usable ext4 partition - local selected_part="${usable_parts[0]}" - if [[ ${#usable_parts[@]} -gt 1 ]]; then - echo "" - echo "Multiple ext4 partitions found. Select one:" - for i in "${!usable_parts[@]}"; do - echo " $((i+1))) ${part_info[$i]}" - done - read -rp "Choose [1-${#usable_parts[@]}]: " part_choice - local idx=$((part_choice - 1)) - if [[ $idx -ge 0 && $idx -lt ${#usable_parts[@]} ]]; then - selected_part="${usable_parts[$idx]}" - else - log_error "Invalid selection" - return 1 - fi - fi - - log_info "Using existing partition: $selected_part" - - # Run fsck before mounting - log_info "Running filesystem check on $selected_part..." - if ! fsck.ext4 -n "$selected_part" 2>/dev/null; then - log_warn "Filesystem has issues. Run 'fsck.ext4 -y $selected_part' manually to fix." - read -p "Try auto-fix now? [y/N] " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - fsck.ext4 -y "$selected_part" || true - fi - else - log_success "Filesystem check passed" - fi - - echo "$selected_part" > /tmp/.hdd_setup_part_dev - return 0 - ;; - 2) - # Will proceed to format - return 1 - ;; - 3) - log_info "Cancelled." - exit 0 - ;; - *) - log_error "Invalid choice" - return 1 - ;; - esac -} - -#------------------------------------------------------------------------------- -# Main flow -#------------------------------------------------------------------------------- - -main() { - parse_args "$@" - check_root - - # Logging - LOG_FILE="/var/log/hdd-setup.log" - mkdir -p /var/log 2>/dev/null || true - exec > >(tee -a "$LOG_FILE") 2>&1 - - if [[ "$DEBUG_MODE" == true ]]; then - set -x - fi - - print_banner - - # Prevent concurrent runs - if [[ -f "$LOCK_FILE" ]]; then - local lock_pid - lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "") - if [[ -n "$lock_pid" ]] && kill -0 "$lock_pid" 2>/dev/null; then - log_error "Another instance is already running (PID: $lock_pid)" - log_error "If this is incorrect, remove $LOCK_FILE" - exit 1 - fi - fi - echo $$ > "$LOCK_FILE" - - if [[ "$DRY_RUN" == true ]]; then - echo -e "${YELLOW}DRY RUN MODE - no changes will be made${NC}" - echo "" - fi - - install_dependencies - - #--------------------------------------------------------------------------- - # Phase 1: Detect candidate disks - #--------------------------------------------------------------------------- - log_step "Phase 1: Scanning for unmounted disks..." - - local root_disk - root_disk=$(get_root_disk) - log_info "System disk: /dev/$root_disk (will be excluded)" - - local candidates_str - candidates_str=$(get_candidate_disks) - - if [[ -z "$candidates_str" ]]; then - echo "" - log_warn "No unmounted disks found!" - echo "" - echo " All detected disks:" - lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT | sed 's/^/ /' - echo "" - echo " Possible reasons:" - echo " - All disks are already mounted" - echo " - External drive not connected/detected" - echo " - Drive may need a different driver" - echo "" - echo " Troubleshooting:" - echo " dmesg | tail -20 # Check kernel messages" - echo " lsusb # Check USB devices" - echo " lsblk # Full block device list" - exit 0 - fi - - # Convert to array - read -ra candidates <<< "$candidates_str" - - echo "" - log_info "Found ${#candidates[@]} unmounted disk(s)" - - #--------------------------------------------------------------------------- - # Phase 2: Show disk information - #--------------------------------------------------------------------------- - log_step "Phase 2: Inspecting disks..." - - local disk_has_data_map=() - for disk in "${candidates[@]}"; do - local has_data=false - print_disk_info "$disk" || has_data=true - disk_has_data_map+=("$has_data") - done - - if [[ "$SCAN_ONLY" == true ]]; then - echo "" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BOLD} Scan complete. Use without --scan to set up a disk.${NC}" - echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - exit 0 - fi - - #--------------------------------------------------------------------------- - # Phase 3: Select disk - #--------------------------------------------------------------------------- - echo "" - log_step "Phase 3: Select a disk to set up" - echo "" - - if [[ ${#candidates[@]} -eq 1 ]]; then - echo " Only one unmounted disk found: /dev/${candidates[0]}" - read -p " Set up this disk? [Y/n] " -n 1 -r - echo - if [[ $REPLY =~ ^[Nn]$ ]]; then - log_info "Cancelled." - exit 0 - fi - selected_disk="${candidates[0]}" - else - echo " Available disks:" - for i in "${!candidates[@]}"; do - local d="${candidates[$i]}" - local s - s=$(lsblk -dno SIZE "/dev/$d" 2>/dev/null | tr -d ' ') - local m - m=$(lsblk -dno MODEL "/dev/$d" 2>/dev/null | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - echo " $((i+1))) /dev/$d ($s, ${m:-Unknown})" - done - echo "" - read -rp " Select disk [1-${#candidates[@]}]: " disk_choice - local idx=$((disk_choice - 1)) - if [[ $idx -lt 0 || $idx -ge ${#candidates[@]} ]]; then - log_error "Invalid selection" - exit 1 - fi - selected_disk="${candidates[$idx]}" - fi - - log_info "Selected: /dev/$selected_disk" - - #--------------------------------------------------------------------------- - # Phase 4: Format or use existing - #--------------------------------------------------------------------------- - log_step "Phase 4: Prepare filesystem" - - local part_dev="" - - # Check if disk already has a usable ext4 partition - if handle_existing_partition "$selected_disk"; then - # User chose to use existing partition - part_dev=$(cat /tmp/.hdd_setup_part_dev 2>/dev/null) - rm -f /tmp/.hdd_setup_part_dev - else - # Need to format - if format_disk "$selected_disk"; then - part_dev=$(cat /tmp/.hdd_setup_part_dev 2>/dev/null) - rm -f /tmp/.hdd_setup_part_dev - else - log_error "Format failed or was cancelled." - exit 1 - fi - fi - - if [[ -z "$part_dev" ]] && [[ "$DRY_RUN" == false ]]; then - log_error "No partition device determined. Cannot proceed." - exit 1 - fi - - # For dry run, simulate a partition device - if [[ "$DRY_RUN" == true ]] && [[ -z "$part_dev" ]]; then - part_dev="/dev/${selected_disk}1" - fi - - #--------------------------------------------------------------------------- - # Phase 5: Mount - #--------------------------------------------------------------------------- - log_step "Phase 5: Mount disk" - - local mount_point - mount_point=$(select_mount_point) - if [[ -z "$mount_point" ]]; then - log_error "No mount point selected. Aborting." - exit 1 - fi - - mount_disk "$part_dev" "$mount_point" - - #--------------------------------------------------------------------------- - # Phase 6: Create folder structure - #--------------------------------------------------------------------------- - if [[ "$SKIP_FOLDERS" == false ]]; then - log_step "Phase 6: Create folder structure" - create_folder_structure "$mount_point" - fi - - #--------------------------------------------------------------------------- - # Summary - #--------------------------------------------------------------------------- - echo "" - echo -e "${BOLD}${GREEN}-==================================================================¬${NC}" - echo -e "${BOLD}${GREEN}¦ HDD Setup Complete! ¦${NC}" - echo -e "${BOLD}${GREEN}L==================================================================-${NC}" - echo "" - echo -e " ${BOLD}Disk:${NC} /dev/$selected_disk" - echo -e " ${BOLD}Partition:${NC} $part_dev" - if [[ "$DRY_RUN" == false ]]; then - local final_uuid - final_uuid=$(blkid -o value -s UUID "$part_dev" 2>/dev/null || echo "N/A") - echo -e " ${BOLD}UUID:${NC} $final_uuid" - fi - echo -e " ${BOLD}Mount point:${NC} $mount_point" - echo -e " ${BOLD}Filesystem:${NC} ext4" - echo -e " ${BOLD}fstab:${NC} Configured (nofail)" - echo "" - - if [[ "$DRY_RUN" == false ]]; then - echo -e " ${BOLD}Disk usage:${NC}" - df -h "$mount_point" | sed 's/^/ /' - echo "" - fi - - echo -e " ${BOLD}Docker volume mount examples:${NC}" - echo " # Media stack (Sonarr/Radarr/Plex)" - echo " volumes:" - echo " - ${mount_point}/media:/data/media" - echo "" - echo " # Immich photo storage" - echo " volumes:" - echo " - ${mount_point}/appdata/immich:/usr/src/app/upload" - echo "" - echo " # Nextcloud data" - echo " volumes:" - echo " - ${mount_point}/appdata/nextcloud:/var/www/html/data" - echo "" - echo -e " ${BOLD}Verify mount survives reboot:${NC}" - echo " sudo mount -a # Test fstab now" - echo " sudo reboot # Full test (when convenient)" - echo "" - echo -e " ${BOLD}Quick check commands:${NC}" - echo " lsblk # See all block devices" - echo " df -h ${mount_point} # Disk usage" - echo " ls -la ${mount_point} # Folder structure" - echo "" -} - -main "$@" \ No newline at end of file