diff --git a/resize-img.sh b/resize-img.sh index 4820168..94de8db 100755 --- a/resize-img.sh +++ b/resize-img.sh @@ -1,88 +1,167 @@ #!/bin/bash set -e -TARGET_SIZE_MB=28672 # 28GB to fit on all "32GB" SD cards -SECTOR_SIZE=512 -PADDING_MB=4 -IMAGE="" -COMPRESSED_NAME="" +REQUIRED_TOOLS=(losetup lsblk parted sgdisk e2fsck resize2fs dumpe2fs du truncate xz 7z) -echo "==== 🔍 SD Image Shrinker + Selector ====" +echo "==== 🔍 Smart SD Image Shrinker ====" -# === Step 1: Find .img files === -mapfile -t IMAGES < <(find . -maxdepth 1 -type f -name "*.img" | sort) +# --- Cleanup handler (set early so it works on error/CTRL+C) --- +cleanup() { + [[ -n "$LOOP_DEV" ]] && sudo losetup -d "$LOOP_DEV" 2>/dev/null || true + [[ -n "$MNT_DIR" && -d "$MNT_DIR" ]] && sudo umount "$MNT_DIR" 2>/dev/null || true +} +trap cleanup EXIT -if [ ${#IMAGES[@]} -eq 0 ]; then - echo "❌ No .img files found in the current directory." - exit 1 -fi - -echo "" -echo "📂 Found the following .img files:" -for i in "${!IMAGES[@]}"; do - printf " [%d] %s\n" "$((i+1))" "${IMAGES[$i]}" +# --- Check dependencies --- +MISSING_TOOLS=() +for tool in "${REQUIRED_TOOLS[@]}"; do + if ! command -v "$tool" &>/dev/null; then + MISSING_TOOLS+=("$tool") + fi done -echo "" -read -p "👉 Enter the number of the image you want to shrink: " SELECTION +if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then + echo "âš ī¸ Missing required tools: ${MISSING_TOOLS[*]}" + echo "đŸ“Ļ Attempting to install..." + if command -v apt &>/dev/null; then + sudo apt update + sudo apt install -y util-linux parted gdisk e2fsprogs xz-utils p7zip-full + elif command -v dnf &>/dev/null; then + sudo dnf install -y util-linux parted gdisk e2fsprogs xz p7zip + elif command -v yum &>/dev/null; then + sudo yum install -y util-linux parted gdisk e2fsprogs xz p7zip + elif command -v pacman &>/dev/null; then + sudo pacman -Sy --needed util-linux parted gptfdisk e2fsprogs xz p7zip coreutils + else + echo "❌ Unsupported package manager. Please install manually." + exit 1 + fi + echo "✅ Dependencies installed." +fi +SECTOR_SIZE=512 +PADDING_MB=4 + +# --- Find images --- +mapfile -t IMAGES < <(find . -maxdepth 1 -type f -name "*.img" | sort) +if [ ${#IMAGES[@]} -eq 0 ]; then + echo "❌ No .img files found." + exit 1 +fi + +echo "📂 Found:" +for i in "${!IMAGES[@]}"; do + printf " [%d] %s\n" "$((i+1))" "${IMAGES[$i]}" +done + +read -p "👉 Enter the number of the image: " SELECTION if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || [ "$SELECTION" -lt 1 ] || [ "$SELECTION" -gt "${#IMAGES[@]}" ]; then - echo "❌ Invalid selection." - exit 1 + echo "❌ Invalid selection." + exit 1 fi IMAGE="${IMAGES[$((SELECTION-1))]}" -COMPRESSED_NAME="$IMAGE.xz" echo "✅ Selected: $IMAGE" -echo "" -# === Step 2: Attach image to loop device === -echo "📌 Attaching image to loop device..." -LOOP_DEV=$(sudo losetup --show -Pf "$IMAGE") +# --- Attach image --- +echo "📌 Attaching..." +LOOP_DEV=$(sudo losetup --find --show -Pf "$IMAGE") -# === Step 3: Detect largest partition === +# --- Find largest partition --- PARTS=$(lsblk -nrpo NAME "$LOOP_DEV" | grep -E "$LOOP_DEV"p) ROOT_PART=$(for P in $PARTS; do lsblk -bno SIZE "$P" | awk -v p="$P" '{print $1, p}'; done | sort -nr | head -n1 | awk '{print $2}') - if [ -z "$ROOT_PART" ]; then - echo "❌ Error: Couldn't detect root partition." - sudo losetup -d "$LOOP_DEV" - exit 1 + echo "❌ Could not detect root partition." + exit 1 fi -PART_NUM=$(basename "$ROOT_PART" | grep -o '[0-9]*$') -echo "🔍 Detected rootfs: $ROOT_PART (Partition #$PART_NUM)" +PART_NUM=$(basename "$ROOT_PART" | grep -o '[0-9]*$']) +echo "🔍 Rootfs: $ROOT_PART (Partition #$PART_NUM)" -# === Step 4: Unmount if mounted === +# --- Unmount if mounted --- sudo umount "$ROOT_PART" 2>/dev/null || true -# === Step 5: Shrink filesystem === -echo "🔧 Running fsck and shrinking rootfs to ${TARGET_SIZE_MB}MB..." -sudo e2fsck -fy "$ROOT_PART" -sudo resize2fs "$ROOT_PART" "${TARGET_SIZE_MB}M" +# --- Measure usage --- +MNT_DIR=$(mktemp -d) +sudo mount "$ROOT_PART" "$MNT_DIR" +USED_MB=$(sudo du -sx --block-size=1M "$MNT_DIR" | awk '{print $1}') +sudo umount "$MNT_DIR" && rmdir "$MNT_DIR" -# === Step 6: Resize partition in image === +MIN_SAFE_MB=$((USED_MB + 500)) +echo "📊 Used: ${USED_MB}MB" +echo "💡 Minimum safe size: ${MIN_SAFE_MB}MB" + +# --- Size selection --- +SD_OPTIONS=("8GB" "16GB" "32GB" "64GB" "Custom") +SD_MB_VALUES=(8192 16384 32768 65536) + +DEFAULT_CHOICE="" +for i in "${!SD_MB_VALUES[@]}"; do + if (( SD_MB_VALUES[i] >= MIN_SAFE_MB )) && [ -z "$DEFAULT_CHOICE" ]; then + DEFAULT_CHOICE=$((i+1)) + fi +done +echo "💡 Recommended: [$DEFAULT_CHOICE] ${SD_OPTIONS[$((DEFAULT_CHOICE-1))]}" + +read -p "👉 Choice (default $DEFAULT_CHOICE): " SD_CHOICE +SD_CHOICE=${SD_CHOICE:-$DEFAULT_CHOICE} + +if [[ "$SD_CHOICE" == "5" ]]; then + read -p "Enter custom size in MB: " TARGET_SIZE_MB +else + TARGET_SIZE_MB=${SD_MB_VALUES[$((SD_CHOICE-1))]} +fi +echo "✅ Target shrink size: ${TARGET_SIZE_MB}MB" + +# --- Confirm --- +read -p "âš ī¸ Proceed with shrinking? (y/N): " CONFIRM +[[ "$CONFIRM" =~ ^[Yy]$ ]] || exit 0 + +# --- Shrink FS --- +CUR_BLOCKS=$(sudo dumpe2fs "$ROOT_PART" 2>/dev/null | grep '^Block count:' | awk '{print $3}') +BLOCK_SIZE=$(sudo dumpe2fs "$ROOT_PART" 2>/dev/null | grep '^Block size:' | awk '{print $3}') +CUR_MB=$((CUR_BLOCKS * BLOCK_SIZE / 1024 / 1024)) + +if [ "$CUR_MB" -gt "$TARGET_SIZE_MB" ]; then + sudo e2fsck -fy "$ROOT_PART" + sudo resize2fs "$ROOT_PART" "${TARGET_SIZE_MB}M" +else + echo "â„šī¸ Already <= target size." +fi + +# --- Partition update --- +PART_TYPE=$(sudo parted -s "$IMAGE" print | grep "^Partition Table:" | awk '{print $3}') START_SECTOR=$(sudo parted -s "$IMAGE" unit s print | grep "^ $PART_NUM" | awk '{print $2}' | sed 's/s//') TARGET_SECTORS=$((TARGET_SIZE_MB * 1024 * 1024 / SECTOR_SIZE)) END_SECTOR=$((START_SECTOR + TARGET_SECTORS - 1)) echo "📏 Updating partition table..." -sudo sgdisk --delete=$PART_NUM "$IMAGE" -sudo sgdisk --new=$PART_NUM:$START_SECTOR:$END_SECTOR "$IMAGE" +if [[ "$PART_TYPE" == "gpt" ]]; then + sudo sgdisk --delete=$PART_NUM "$IMAGE" + sudo sgdisk --new=$PART_NUM:$START_SECTOR:$END_SECTOR "$IMAGE" + sudo sgdisk -e "$IMAGE" +else + sudo parted -s "$IMAGE" rm $PART_NUM + sudo parted -s "$IMAGE" mkpart primary ext4 ${START_SECTOR}s ${END_SECTOR}s +fi -# === Step 7: Detach + reattach loop device to refresh layout === +# --- Detach/reattach & truncate --- sudo losetup -d "$LOOP_DEV" -LOOP_DEV=$(sudo losetup --show -Pf "$IMAGE") - -# === Step 8: Truncate image === TRUNC_SIZE=$(( (END_SECTOR + (PADDING_MB * 1024 * 1024 / SECTOR_SIZE)) * SECTOR_SIZE )) -echo "âœ‚ī¸ Truncating image to $((TRUNC_SIZE / 1024 / 1024))MB..." +echo "âœ‚ī¸ Truncating to $((TRUNC_SIZE / 1024 / 1024))MB..." truncate -s $TRUNC_SIZE "$IMAGE" -# === Step 9: Cleanup + compress === -sudo losetup -d "$LOOP_DEV" -echo "đŸ“Ļ Compressing with xz -9 -T0..." -xz -T0 -9 "$IMAGE" +# --- Compression --- +echo "" +echo "đŸ“Ļ Compression:" +echo " [1] xz (max, multi-thread)" +echo " [2] 7z (ultra)" +echo " [3] None" +read -p "👉 Choose: " COMP_METHOD +case "$COMP_METHOD" in + 1) echo "âš™ī¸ Compressing with xz..."; xz -T0 -9 "$IMAGE"; COMPRESSED_NAME="$IMAGE.xz" ;; + 2) echo "âš™ī¸ Compressing with 7z..."; 7z a -t7z -mx=9 "${IMAGE}.7z" "$IMAGE"; COMPRESSED_NAME="$IMAGE.7z" ;; + *) echo "âš ī¸ Skipping compression."; COMPRESSED_NAME="$IMAGE" ;; +esac echo "" -echo "✅ All done! Compressed output: $COMPRESSED_NAME" - +echo "✅ Done! Output: $COMPRESSED_NAME"