59ed4bd1c2
- Add orphan detection: stacks not in catalog marked as "Elavult"
- Add DELETE /api/stacks/{name} endpoint with HDD data handling
- Add GET /api/stacks/{name}/hdd-data endpoint
- Add delete confirmation modal with HDD data checkbox (Hungarian UI)
- Add filebrowser to protected stacks list
- Add scripts/hdd-setup.sh and scripts/docker-setup.sh for node setup
- Hide "Frissítés" and "Részletek" buttons for orphaned stacks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1252 lines
42 KiB
Bash
1252 lines
42 KiB
Bash
#!/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 STORAGE_DIRS=(
|
|
"storage/immich"
|
|
"storage/nextcloud"
|
|
"storage/backups/local"
|
|
"storage/backups/appdata"
|
|
)
|
|
|
|
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:
|
|
<mount_point>/
|
|
├── Dokumentumok/
|
|
├── media/
|
|
│ ├── downloads/complete/
|
|
│ ├── downloads/incomplete/
|
|
│ ├── movies/
|
|
│ ├── series/
|
|
│ ├── music/
|
|
│ └── books/
|
|
├── storage/
|
|
│ ├── immich/
|
|
│ ├── nextcloud/
|
|
│ └── backups/
|
|
│ ├── local/
|
|
│ └── appdata/
|
|
└── appdata/
|
|
|
|
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:-<none>}${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:-<none>} uuid=${part_uuid:-<none>}${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 "${STORAGE_DIRS[@]}"; do
|
|
echo " ├── $dir/"
|
|
done
|
|
for dir in "${APPDATA_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[@]}" "${STORAGE_DIRS[@]}" "${APPDATA_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 "<none>")
|
|
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 "<none>")
|
|
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}/storage/immich:/usr/src/app/upload"
|
|
echo ""
|
|
echo " # Nextcloud data"
|
|
echo " volumes:"
|
|
echo " - ${mount_point}/storage/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 "$@" |