floppy-utils/src/lib/common.sh

1019 lines
33 KiB
Bash
Raw Normal View History

# Shared helpers for floppy-utils. Source from src/floppy only.
[[ -n "${_FLOPPY_COMMON_LOADED:-}" ]] && return 0
_FLOPPY_COMMON_LOADED=1
FLOPPY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FLOPPY_SCRIPT_DIR="$(cd "$FLOPPY_LIB_DIR/.." && pwd)"
floppy_load_config() {
local cfg="${XDG_CONFIG_HOME:-$HOME/.config}/floppy-utils/config"
if [[ -f "$cfg" ]]; then
# shellcheck source=/dev/null
source "$cfg"
fi
: "${FLOPPY_DISKDIR:=$HOME/Retro/BLANKS}"
: "${FLOPPY_MEDIADIR:=/media/${USER:-root}}"
: "${FLOPPY_DEFAULT_SIZE_KB:=1440}"
# USB floppy drives (e.g. Mitsumi UFDD) often appear as /dev/sda with 0B until media is inserted.
: "${FLOPPY_DEVICE_MATCH:=mitsumi|ufdd|fdd|floppy}"
}
floppy_die() {
echo "floppy: $*" >&2
exit 1
}
floppy_info() {
# Must use stderr: this is called from functions whose stdout is captured (e.g. pick_device).
echo ">>> $*" >&2
}
floppy_warn() {
echo "floppy: warning: $*" >&2
}
floppy_require_cmd() {
local cmd
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || floppy_die "required command not found: $cmd"
done
}
floppy_require_root() {
[[ "$(id -u)" -eq 0 ]] && return 0
command -v sudo >/dev/null 2>&1 || floppy_die "root privileges required (install sudo or run as root)"
}
floppy_run_root() {
floppy_require_root
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
else
sudo -n "$@" 2>/dev/null || sudo "$@"
fi
}
floppy_ensure_diskdir() {
if [[ ! -d "$FLOPPY_DISKDIR" ]]; then
mkdir -p "$FLOPPY_DISKDIR" || floppy_die "cannot create disk directory: $FLOPPY_DISKDIR"
fi
}
floppy_validate_size_kb() {
local size="$1"
case "$size" in
360|720|1440) return 0 ;;
*) floppy_die "size must be one of: 360, 720, 1440 (got: $size)" ;;
esac
}
floppy_random_name() {
local rando
rando="$(od -An -N4 -tu4 < /dev/urandom | tr -d ' ')"
echo "floppy${rando}"
}
# Sanitize volume label / user text for use as a filename.
floppy_sanitize_name() {
local raw="$1"
local clean
clean="$(echo "$raw" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '_' | sed 's/^_//;s/_$//')"
[[ -n "$clean" ]] || clean="floppy"
echo "${clean:0:32}"
}
# Resolve argument to an absolute .img path.
# Like floppy_resolve_image but returns 1 instead of exiting.
floppy_try_resolve_image() {
local arg="${1:-}"
local path base
[[ -n "$arg" ]] || return 1
if [[ -f "$arg" ]]; then
printf '%s\n' "$(readlink -f "$arg")"
return 0
fi
base="${arg%.img}"
path="$FLOPPY_DISKDIR/${base}.img"
if [[ -f "$path" ]]; then
printf '%s\n' "$(readlink -f "$path")"
return 0
fi
return 1
}
floppy_resolve_image() {
local arg="${1:-}"
local path base
[[ -n "$arg" ]] || floppy_die "image name or path required"
if [[ -f "$arg" ]]; then
printf '%s\n' "$(readlink -f "$arg")"
return 0
fi
base="${arg%.img}"
path="$FLOPPY_DISKDIR/${base}.img"
if [[ -f "$path" ]]; then
printf '%s\n' "$(readlink -f "$path")"
return 0
fi
floppy_die "image not found: $arg (also tried $path)"
}
floppy_image_basename() {
local image="$1"
basename "${image%.img}"
}
floppy_image_path_in_diskdir() {
local name="${1%.img}"
printf '%s/%s.img\n' "$FLOPPY_DISKDIR" "$name"
}
floppy_create_blank() {
local name="$1"
local size_kb="$2"
local image
floppy_validate_size_kb "$size_kb"
floppy_ensure_diskdir
image="$(floppy_image_path_in_diskdir "$name")"
if [[ -f "$image" ]]; then
floppy_warn "image already exists: $image"
printf '%s\n' "$image"
return 0
fi
floppy_info "creating ${size_kb}K image: $image"
dd if=/dev/zero of="$image" bs=1K count="$size_kb" status=progress 2>/dev/null \
|| dd if=/dev/zero of="$image" bs=1K count="$size_kb"
printf '%s\n' "$(readlink -f "$image")"
}
# Per-device lsblk field (avoids broken parsing when LABEL is empty).
floppy_device_field() {
local dev="$1"
local field="$2"
lsblk -dno "$field" "$dev" 2>/dev/null | head -1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
# Volume label, or empty if unknown / invalid.
floppy_device_label() {
local dev="$1"
local label
label="$(floppy_device_field "$dev" LABEL)"
if [[ -z "$label" || "$label" == "/"* ]]; then
label="$(blkid -s LABEL -o value "$dev" 2>/dev/null | head -1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
fi
if [[ -n "$label" && "$label" != "/"* ]]; then
printf '%s\n' "$label"
return 0
fi
return 1
}
# Score how likely a block device is a USB floppy drive (higher = better match).
floppy_device_score() {
local dev="$1"
local model size tran label
local score=0
model="$(floppy_device_field "$dev" MODEL | tr '[:upper:]' '[:lower:]')"
size="$(floppy_device_field "$dev" SIZE)"
tran="$(floppy_device_field "$dev" TRAN | tr '[:lower:]' '[:upper:]')"
label="$(floppy_device_label "$dev" 2>/dev/null || true)"
if [[ "$model" =~ ${FLOPPY_DEVICE_MATCH} ]]; then
score=$((score + 100))
fi
if [[ "$tran" == "USB" ]]; then
score=$((score + 40))
fi
case "$size" in
0|0B) score=$((score + 5)) ;;
360K|720K|1.0M|1.2M|1.4M|1.44M) score=$((score + 30)) ;;
esac
[[ -n "$label" ]] && score=$((score + 10))
printf '%s\n' "$score"
}
# List whole-disk block devices likely to be USB/removable drives (floppy, USB FDD, etc.)
floppy_list_devices() {
local verbose="${1:-0}"
local name rm type size tran model label fstype mnt
floppy_require_cmd lsblk
while IFS= read -r name; do
[[ -z "$name" ]] && continue
rm="$(floppy_device_field "$name" RM)"
type="$(floppy_device_field "$name" TYPE)"
[[ "$type" == "disk" && "$rm" == "1" ]] || continue
[[ "$name" =~ ^/dev/(sd|fd) ]] || continue
size="$(floppy_device_field "$name" SIZE)"
tran="$(floppy_device_field "$name" TRAN)"
model="$(floppy_device_field "$name" MODEL)"
label="$(floppy_device_label "$name" 2>/dev/null || echo "")"
fstype="$(floppy_device_field "$name" FSTYPE)"
mnt="$(floppy_device_field "$name" MOUNTPOINT)"
if [[ "$verbose" -eq 1 ]]; then
printf '%s rm=%s size=%s tran=%s model=%s label=%s fstype=%s mount=%s score=%s\n' \
"$name" "$rm" "$size" "$tran" "$model" "${label:-}" "${fstype:-}" "${mnt:-}" \
"$(floppy_device_score "$name")"
else
local extra=""
[[ -n "$label" ]] && extra=" label=$label"
[[ -n "$mnt" ]] && extra="$extra mount=$mnt"
printf '%s %s %s %s%s\n' "$name" "$size" "${tran:-?}" "${model:-unknown}" "$extra"
fi
done < <(lsblk -dpno NAME,RM,TYPE 2>/dev/null | awk '$2==1 && $3=="disk" && $1 ~ /^\/dev\/(sd|fd)/ {print $1}')
}
floppy_device_has_media() {
local dev="$1"
local size
size="$(floppy_device_field "$dev" SIZE)"
[[ -n "$size" && "$size" != "0" && "$size" != "0B" ]]
}
floppy_device_show() {
local dev="$1"
lsblk -d -o NAME,SIZE,MODEL,TRAN,LABEL,FSTYPE,MOUNTPOINT "$dev" 2>/dev/null || true
}
floppy_device_sectors() {
local dev="$1"
local sectors
floppy_require_cmd blockdev
sectors="$(floppy_run_root blockdev --getsz "$dev" 2>/dev/null)" || floppy_die "cannot read size of $dev"
if [[ -z "$sectors" || "$sectors" -eq 0 ]]; then
if ! floppy_device_has_media "$dev"; then
floppy_die "no media detected in $dev (insert a floppy and retry)"
fi
floppy_die "device $dev reports 0 sectors"
fi
printf '%s\n' "$sectors"
}
floppy_device_default_name() {
local dev="$1"
local label mnt base
if label="$(floppy_device_label "$dev" 2>/dev/null)"; then
floppy_sanitize_name "$label"
return 0
fi
# Unlabeled FAT volumes: udisks often mounts as $FLOPPY_MEDIADIR/<name>
mnt="$(floppy_device_field "$dev" MOUNTPOINT)"
if [[ -n "$mnt" ]]; then
base="$(basename "$mnt")"
if [[ -n "$base" && "$base" != "/"* ]]; then
floppy_sanitize_name "$base"
return 0
fi
fi
floppy_random_name
}
floppy_unmount_device() {
local dev="$1"
local mp unmounted=0
if command -v udisksctl >/dev/null 2>&1 && floppy_device_mounted "$dev"; then
if udisksctl unmount -b "$dev" 2>/dev/null; then
floppy_info "unmounted $dev via udisks"
return 0
fi
fi
while read -r mp; do
[[ -n "$mp" ]] || continue
floppy_info "unmounting $mp"
floppy_run_root umount "$mp" && unmounted=1
done < <(lsblk -rn -o MOUNTPOINT "$dev" 2>/dev/null | sed '/^$/d')
[[ "$unmounted" -eq 1 ]] || floppy_warn "$dev does not appear to be mounted"
}
# After swapping floppies in the same USB drive: drop stale mounts and re-probe media.
floppy_cmd_refresh() {
local device="${1:-}"
local do_eject="${2:-0}"
local do_mount="${3:-0}"
local wait_secs="${4:-2}"
floppy_require_cmd lsblk blockdev
device="$(floppy_resolve_burn_device "$device")"
floppy_info "refreshing $device (unmount stale state, re-probe media)"
echo "Before:"
floppy_device_show "$device"
echo ""
if floppy_device_mounted "$device"; then
floppy_unmount_device "$device"
fi
floppy_run_root blockdev --flushbufs "$device" 2>/dev/null || true
if [[ "$do_eject" -eq 1 ]]; then
if command -v eject >/dev/null 2>&1; then
floppy_info "ejecting $device (helps some USB FDD firmware notice media change)"
floppy_run_root eject "$device" 2>/dev/null \
|| floppy_warn "eject failed — swap the disk and continue anyway"
else
floppy_warn "eject command not found; install eject(1) or omit --eject"
fi
fi
if command -v udevadm >/dev/null 2>&1; then
udevadm settle --timeout=5 2>/dev/null || true
fi
if [[ "$wait_secs" -gt 0 ]]; then
floppy_info "waiting ${wait_secs}s for drive/kernel to settle"
sleep "$wait_secs"
fi
# Reading size nudges usb-storage to re-check capacity on many drives.
floppy_run_root blockdev --getsz "$device" >/dev/null 2>&1 || true
echo "After:"
floppy_device_show "$device"
echo ""
if floppy_device_has_media "$device"; then
local label mnt
label="$(floppy_device_label "$device" 2>/dev/null || true)"
mnt="$(floppy_device_field "$device" MOUNTPOINT)"
floppy_info "media detected${label:+ (label: $label)}"
if [[ "$do_mount" -eq 1 ]] && [[ -z "$mnt" ]]; then
if command -v udisksctl >/dev/null 2>&1; then
if udisksctl mount -b "$device" 2>/dev/null; then
mnt="$(lsblk -dno MOUNTPOINT "$device" 2>/dev/null)"
floppy_info "mounted at ${mnt:-?}"
else
floppy_warn "udisks mount failed — run: udisksctl mount -b $device"
fi
else
floppy_warn "udisksctl not found; mount manually or use a file manager"
fi
elif [[ -n "$mnt" ]]; then
floppy_info "already mounted at $mnt"
fi
else
floppy_warn "no media detected (size 0B) — insert a disk and run: floppy refresh"
fi
}
# Unmount, flush, and power off USB floppy — safe to unplug the cable afterward.
floppy_cmd_disconnect() {
local device="${1:-}"
local powered_off=0
floppy_require_cmd blockdev
device="$(floppy_resolve_burn_device "$device")"
floppy_info "preparing $device for safe USB removal"
floppy_device_show "$device"
echo ""
if floppy_device_mounted "$device"; then
floppy_unmount_device "$device"
else
floppy_info "no filesystem mounted on $device"
fi
floppy_run_root blockdev --flushbufs "$device" 2>/dev/null || true
sync
if command -v udisksctl >/dev/null 2>&1; then
if udisksctl power-off -b "$device" 2>/dev/null; then
powered_off=1
floppy_info "powered off $device via udisks"
else
floppy_warn "udisks power-off failed for $device"
fi
else
floppy_warn "udisksctl not found — install udisks2 for USB power-off"
fi
if [[ "$powered_off" -eq 0 ]] && command -v eject >/dev/null 2>&1; then
if floppy_run_root eject "$device" 2>/dev/null; then
powered_off=1
floppy_info "ejected $device (fallback)"
fi
fi
if command -v udevadm >/dev/null 2>&1; then
udevadm settle --timeout=5 2>/dev/null || true
fi
echo ""
if [[ "$powered_off" -eq 1 ]]; then
echo "Safe to unplug the USB floppy drive now."
else
echo "Unmount and cache flush completed."
echo "Wait a few seconds, then unplug the USB floppy drive."
echo "If the device is busy, close file managers and retry: floppy disconnect"
fi
if [[ -b "$device" ]]; then
floppy_info "($device still visible until the cable is removed)"
fi
}
floppy_pick_device() {
local -a entries=()
local line dev score best_dev="" best_score=-1
while IFS= read -r line; do
[[ -n "$line" ]] || continue
dev="${line%% *}"
score="$(floppy_device_score "$dev")"
entries+=("$score $dev")
if (( score > best_score )); then
best_score=$score
best_dev="$dev"
fi
done < <(floppy_list_devices 0)
if [[ ${#entries[@]} -eq 0 ]]; then
floppy_die "no removable block devices found (is the USB floppy connected?)"
fi
# Auto-select a clear floppy match (e.g. Mitsumi UFDD at /dev/sda).
if [[ ${#entries[@]} -eq 1 ]]; then
floppy_info "using removable device: ${entries[0]#* }"
printf '%s\n' "${entries[0]#* }"
return 0
fi
if (( best_score >= 100 )); then
local floppy_like=0 d s
for entry in "${entries[@]}"; do
s="${entry%% *}"
if (( s >= 100 )); then
floppy_like=$((floppy_like + 1))
best_dev="${entry#* }"
fi
done
if (( floppy_like == 1 )); then
floppy_info "using USB floppy drive: $best_dev"
printf '%s\n' "$best_dev"
return 0
fi
fi
echo "Multiple removable devices found:"
local i=1 entry s d
for entry in "${entries[@]}"; do
s="${entry%% *}"
d="${entry#* }"
echo " $i) $d $(lsblk -dno SIZE,MODEL,LABEL,TRAN "$d" 2>/dev/null | tr -s ' ') (score=$s)"
((i++)) || true
done
echo -n "Select device number [1-${#entries[@]}]: "
read -r choice
if [[ ! "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#entries[@]} )); then
floppy_die "invalid selection"
fi
entry="${entries[$((choice - 1))]}"
printf '%s\n' "${entry#* }"
}
floppy_device_mounted() {
local dev="$1"
lsblk -n -o MOUNTPOINT "$dev" 2>/dev/null | grep -q .
}
floppy_resolve_burn_device() {
local device="${1:-}"
if [[ -n "$device" ]]; then
[[ -b "$device" ]] || floppy_die "not a block device: $device"
printf '%s\n' "$device"
return 0
fi
if [[ -n "${FLOPPY_DEVICE:-}" ]]; then
[[ -b "$FLOPPY_DEVICE" ]] || floppy_die "FLOPPY_DEVICE is not a block device: $FLOPPY_DEVICE"
printf '%s\n' "$FLOPPY_DEVICE"
return 0
fi
floppy_pick_device
}
floppy_confirm_burn() {
local image="$1"
local device="$2"
local yes="${3:-0}"
echo ""
echo " image: $image ($(du -h "$image" | cut -f1))"
echo " target: $device"
lsblk -d -o NAME,SIZE,MODEL,TRAN,MOUNTPOINT "$device" 2>/dev/null || true
echo ""
if [[ "$yes" -eq 1 ]]; then
return 0
fi
if floppy_device_mounted "$device"; then
floppy_warn "$device is mounted — it will be unmounted before writing"
fi
echo -n "Write image to $device? This will ERASE the floppy. [y/N] "
read -r ans
[[ "$ans" =~ ^[Yy]$ ]] || floppy_die "aborted"
}
# Find loop device backing an image, if any.
floppy_loop_for_image() {
local image="$1"
losetup -j "$image" 2>/dev/null | awk -F: '{print $1; exit}'
}
floppy_mountpoint_for_loop() {
local loop="$1"
lsblk -n -o MOUNTPOINT "$loop" 2>/dev/null | grep -m1 .
}
floppy_cmd_attach() {
local image_arg="${1:-}"
local size_kb="${2:-$FLOPPY_DEFAULT_SIZE_KB}"
local do_format="${3:-auto}"
local image name mountpoint loop need_format
floppy_require_cmd losetup mount blkid
floppy_ensure_diskdir
if [[ -z "$image_arg" ]]; then
name="$(floppy_random_name)"
image="$(floppy_create_blank "$name" "$size_kb")"
need_format=1
else
if [[ -f "$image_arg" ]]; then
image="$(readlink -f "$image_arg")"
name="$(floppy_image_basename "$image")"
elif [[ -f "$(floppy_image_path_in_diskdir "$image_arg")" ]]; then
image="$(floppy_resolve_image "$image_arg")"
name="$(floppy_image_basename "$image")"
else
name="${image_arg%.img}"
image="$(floppy_create_blank "$name" "$size_kb")"
need_format=1
fi
fi
if [[ "$do_format" == "yes" ]]; then
need_format=1
elif [[ "$do_format" == "no" ]]; then
need_format=0
fi
loop="$(floppy_loop_for_image "$image")"
if [[ -n "$loop" ]]; then
floppy_warn "already attached on $loop"
else
floppy_info "attaching loop device for $image"
loop="$(floppy_run_root losetup --find --show "$image")" || floppy_die "losetup failed"
floppy_info "loop device: $loop"
fi
if [[ "${need_format:-0}" -eq 1 ]] || ! floppy_run_root blkid "$loop" &>/dev/null; then
if [[ "$do_format" == "no" ]]; then
floppy_warn "no filesystem on $loop; mount may fail (use --format to create vfat)"
else
floppy_info "formatting $loop as vfat"
floppy_run_root mkfs -t vfat -n "${name:0:11}" "$loop" || floppy_die "mkfs failed"
need_format=1
fi
fi
mountpoint="$FLOPPY_MEDIADIR/$name"
if [[ ! -d "$mountpoint" ]]; then
floppy_info "creating mount point: $mountpoint"
floppy_run_root mkdir -p "$mountpoint"
fi
if mountpoint -q "$mountpoint" 2>/dev/null; then
floppy_info "already mounted at $mountpoint"
else
floppy_info "mounting $loop at $mountpoint"
floppy_run_root mount "$loop" "$mountpoint" || floppy_die "mount failed"
fi
if [[ "${need_format:-0}" -eq 1 ]]; then
local info_file="$mountpoint/${name}.txt"
floppy_info "saving partition info to $info_file"
floppy_run_root fdisk -l "$loop" >"${info_file}.tmp" 2>/dev/null \
&& floppy_run_root chown "${USER:-root}:${USER:-root}" "${info_file}.tmp" 2>/dev/null \
&& mv "${info_file}.tmp" "$info_file" \
|| rm -f "${info_file}.tmp"
fi
echo ""
echo " image: $image"
echo " loop: $loop"
echo " mountpoint: $mountpoint"
}
floppy_cmd_detach() {
local target="${1:-}"
local image loop mountpoint
[[ -n "$target" ]] || floppy_die "specify image name, mount point, or loop device"
if [[ -b "$target" && "$target" =~ ^/dev/loop ]]; then
loop="$target"
mountpoint="$(floppy_mountpoint_for_loop "$loop")"
elif mountpoint -q "$target" 2>/dev/null; then
mountpoint="$target"
loop="$(findmnt -n -o SOURCE "$mountpoint" 2>/dev/null)"
elif [[ -f "$target" ]] || [[ -f "$(floppy_image_path_in_diskdir "$target")" ]]; then
image="$(floppy_resolve_image "$target")"
loop="$(floppy_loop_for_image "$image")"
[[ -n "$loop" ]] || floppy_die "image is not attached: $image"
mountpoint="$(floppy_mountpoint_for_loop "$loop")"
else
# Physical floppy: udisks often mounts at $FLOPPY_MEDIADIR/<LABEL> (e.g. BOOTDISK)
if mountpoint -q "$FLOPPY_MEDIADIR/${target}" 2>/dev/null; then
mountpoint="$FLOPPY_MEDIADIR/${target}"
loop="$(findmnt -n -o SOURCE "$mountpoint" 2>/dev/null)"
elif mountpoint -q "$FLOPPY_MEDIADIR/${target%.img}" 2>/dev/null; then
mountpoint="$FLOPPY_MEDIADIR/${target%.img}"
loop="$(findmnt -n -o SOURCE "$mountpoint" 2>/dev/null)"
elif image="$(floppy_try_resolve_image "$target")"; then
loop="$(floppy_loop_for_image "$image")"
[[ -n "$loop" ]] || floppy_die "image is not attached: $image"
mountpoint="$(floppy_mountpoint_for_loop "$loop")"
else
floppy_die "cannot resolve: $target"
fi
fi
if [[ -n "${mountpoint:-}" ]] && mountpoint -q "$mountpoint" 2>/dev/null; then
floppy_info "unmounting $mountpoint"
floppy_run_root umount "$mountpoint" || floppy_die "umount failed"
fi
if [[ -n "${loop:-}" && "$loop" =~ ^/dev/loop ]]; then
floppy_info "detaching $loop"
floppy_run_root losetup -d "$loop" || floppy_die "losetup -d failed"
elif [[ -n "${loop:-}" && "$loop" =~ ^/dev/sd ]]; then
floppy_info "physical drive $loop left attached (no loop device)"
fi
floppy_info "done"
}
floppy_copy_to_image() {
local dev="$1"
local image="$2"
local sectors="$3"
local strict="${4:-0}"
local conv="noerror,sync,fsync"
local dd_status=0 expected_bytes=$((sectors * 512)) actual_bytes=0
[[ "$strict" -eq 1 ]] && conv="fsync"
floppy_info "reading $sectors sectors from $dev into $image (conv=$conv)"
set +e
floppy_run_root dd if="$dev" of="$image" bs=512 count="$sectors" conv="$conv" status=progress 2>/dev/null
dd_status=$?
if [[ "$dd_status" -ne 0 ]]; then
floppy_run_root dd if="$dev" of="$image" bs=512 count="$sectors" conv="$conv"
dd_status=$?
fi
set -e
sync
if [[ -f "$image" ]]; then
actual_bytes="$(wc -c < "$image" | tr -d ' ')"
fi
if [[ "$dd_status" -ne 0 ]]; then
floppy_warn "dd exited with status $dd_status ($actual_bytes / $expected_bytes bytes)"
if [[ "$strict" -eq 1 ]]; then
floppy_die "read failed (use default noerror,sync mode, or retry after: floppy refresh)"
fi
floppy_warn "image may be incomplete — bad sectors are common on aging floppies"
elif [[ "$actual_bytes" -lt "$expected_bytes" ]]; then
floppy_warn "short read: $actual_bytes / $expected_bytes bytes — disk may have bad sectors"
fi
}
floppy_copy_to_device() {
local image="$1"
local dev="$2"
floppy_info "writing $image to $dev"
floppy_run_root dd if="$image" of="$dev" bs=512 conv=fsync status=progress 2>/dev/null \
|| floppy_run_root dd if="$image" of="$dev" bs=512 conv=fsync
sync
}
floppy_confirm_read() {
local dev="$1"
local image="$2"
local yes="${3:-0}"
echo ""
echo " source: $dev"
floppy_device_show "$dev"
echo " output: $image"
if [[ -f "$image" ]]; then
echo " note: output file exists and will be overwritten"
fi
echo ""
[[ "$yes" -eq 1 ]] && return 0
echo -n "Archive floppy to $image? [y/N] "
read -r ans
[[ "$ans" =~ ^[Yy]$ ]] || floppy_die "aborted"
}
floppy_cmd_read() {
local name="${1:-}"
local device="${2:-}"
local yes="${3:-0}"
local force="${4:-0}"
local strict="${5:-0}"
local image sectors
floppy_require_cmd dd lsblk blockdev
floppy_ensure_diskdir
device="$(floppy_resolve_burn_device "$device")"
if ! floppy_device_has_media "$device"; then
floppy_die "no media in $device — insert a floppy (size should show as 360K/720K/1.4M, not 0B)"
fi
if [[ -z "$name" ]]; then
name="$(floppy_device_default_name "$device")"
floppy_info "using output name: $name (from volume label or mount name)"
fi
name="${name%.img}"
image="$(floppy_image_path_in_diskdir "$name")"
if [[ -f "$image" && "$force" -ne 1 ]]; then
floppy_die "output exists: $image (use --force to overwrite)"
fi
sectors="$(floppy_device_sectors "$device")"
floppy_confirm_read "$device" "$image" "$yes"
if floppy_device_mounted "$device"; then
floppy_unmount_device "$device"
sleep 1
fi
floppy_copy_to_image "$device" "$image" "$sectors" "$strict"
floppy_info "read complete: $image ($(du -h "$image" | cut -f1), target ${sectors} sectors)"
}
floppy_cmd_dump() {
local output="${1:-}"
local device="${2:-}"
local yes="${3:-0}"
local force="${4:-0}"
local strict="${5:-0}"
local sectors
[[ -n "$output" ]] || floppy_die "usage: floppy dump -o FILE [-d DEVICE] [-y] [--force]"
floppy_require_cmd dd blockdev
device="$(floppy_resolve_burn_device "$device")"
if ! floppy_device_has_media "$device"; then
floppy_die "no media in $device — insert a floppy"
fi
if [[ "$output" != "-" ]]; then
output="$(readlink -f "$output" 2>/dev/null || echo "$output")"
if [[ -e "$output" && "$force" -ne 1 ]]; then
floppy_die "output exists: $output (use --force)"
fi
mkdir -p "$(dirname "$output")" 2>/dev/null || true
fi
sectors="$(floppy_device_sectors "$device")"
floppy_confirm_read "$device" "$output" "$yes"
if floppy_device_mounted "$device"; then
floppy_unmount_device "$device"
sleep 1
fi
if [[ "$output" == "-" ]]; then
floppy_info "dumping $sectors sectors from $device to stdout"
floppy_run_root dd if="$device" bs=512 count="$sectors" conv=noerror,sync 2>/dev/null
else
floppy_copy_to_image "$device" "$output" "$sectors" "$strict"
floppy_info "dump complete: $output"
fi
}
floppy_cmd_burn() {
local image="$1"
local device="${2:-}"
local yes="${3:-0}"
floppy_require_cmd dd lsblk
image="$(floppy_resolve_image "$image")"
device="$(floppy_resolve_burn_device "$device")"
floppy_confirm_burn "$image" "$device" "$yes"
if floppy_device_mounted "$device"; then
floppy_unmount_device "$device"
fi
floppy_copy_to_device "$image" "$device"
floppy_info "burn complete"
}
# Parse losetup -a lines into "loop|backing_file".
floppy_foreach_loop_backing() {
local line loop backing
while IFS= read -r line; do
[[ "$line" =~ ^/dev/loop ]] || continue
loop="${line%%:*}"
if [[ "$line" =~ \((.+)\)$ ]]; then
backing="${BASH_REMATCH[1]}"
printf '%s|%s\n' "$loop" "$backing"
fi
done < <(losetup -a 2>/dev/null)
}
floppy_cmd_list() {
local verbose="${1:-0}"
local loop backing mp name label size fstype dev
local had_loop=0 had_phys=0
local -A attached_images=()
floppy_require_cmd losetup lsblk findmnt
floppy_ensure_diskdir 2>/dev/null || true
while IFS='|' read -r loop backing; do
[[ -n "$backing" ]] && attached_images["$backing"]=1
done < <(floppy_foreach_loop_backing)
echo "Loop-attached disk images:"
while IFS='|' read -r loop backing; do
[[ -n "$loop" && -n "$backing" ]] || continue
[[ "$backing" == *.img ]] || continue
had_loop=1
name="$(floppy_image_basename "$backing")"
mp="$(floppy_mountpoint_for_loop "$loop" 2>/dev/null || true)"
if [[ -n "$mp" ]]; then
printf ' %-16s %-10s → %s\n' "$name" "$loop" "$mp"
else
printf ' %-16s %-10s (not mounted)\n' "$name" "$loop"
fi
if [[ "$verbose" -eq 1 ]]; then
printf ' %s\n' "$backing"
fi
done < <(floppy_foreach_loop_backing)
[[ "$had_loop" -eq 1 ]] || echo " (none)"
echo ""
echo "Physical floppy media:"
while IFS= read -r dev; do
[[ -n "$dev" ]] || continue
if ! floppy_device_has_media "$dev" && ! floppy_device_mounted "$dev"; then
continue
fi
had_phys=1
size="$(floppy_device_field "$dev" SIZE)"
label="$(floppy_device_label "$dev" 2>/dev/null || echo "—")"
mp="$(floppy_device_field "$dev" MOUNTPOINT)"
if [[ -n "$mp" ]]; then
printf ' %-16s %-6s label=%s → %s\n' "$dev" "$size" "$label" "$mp"
else
printf ' %-16s %-6s label=%s (not mounted)\n' "$dev" "$size" "$label"
fi
if [[ "$verbose" -eq 1 ]]; then
printf ' model=%s fstype=%s\n' \
"$(floppy_device_field "$dev" MODEL)" "$(floppy_device_field "$dev" FSTYPE)"
fi
done < <(lsblk -dpno NAME,RM,TYPE 2>/dev/null | awk '$2==1 && $3=="disk" && $1 ~ /^\/dev\/(sd|fd)/ {print $1}')
[[ "$had_phys" -eq 1 ]] || echo " (none)"
if [[ "$verbose" -eq 1 && -d "$FLOPPY_DISKDIR" ]]; then
echo ""
echo "Disk images in $FLOPPY_DISKDIR:"
local img
for img in "$FLOPPY_DISKDIR"/*.img; do
[[ -f "$img" ]] || continue
name="$(floppy_image_basename "$img")"
if [[ -n "${attached_images[$img]:-}" ]]; then
printf ' %-16s %s [loop-attached]\n' "$name" "$(du -h "$img" | cut -f1)"
else
printf ' %-16s %s\n' "$name" "$(du -h "$img" | cut -f1)"
fi
done
fi
}
floppy_cmd_status() {
floppy_require_cmd losetup lsblk findmnt
local dev
echo "Disk directory: $FLOPPY_DISKDIR"
echo "Mount base: $FLOPPY_MEDIADIR"
if [[ -n "${FLOPPY_DEVICE:-}" ]]; then
echo "FLOPPY_DEVICE: $FLOPPY_DEVICE"
fi
echo ""
echo "Attached images (loop):"
if losetup -a 2>/dev/null | grep -q '\.img'; then
losetup -a | grep '\.img' || true
else
echo " (none)"
fi
echo ""
echo "Mounts under $FLOPPY_MEDIADIR:"
if [[ -d "$FLOPPY_MEDIADIR" ]]; then
local m found=0
for m in "$FLOPPY_MEDIADIR"/*; do
[[ -e "$m" ]] || continue
found=1
findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS "$m" 2>/dev/null \
|| echo " $m (present, not mounted)"
done
[[ "$found" -eq 1 ]] || echo " (empty)"
else
echo " (directory does not exist)"
fi
echo ""
echo "Removable drives:"
floppy_list_devices 1 || echo " (none)"
}
floppy_usage() {
cat <<'EOF'
floppy — create, mount, and burn floppy disk images (USB floppy drives)
Usage:
floppy make [name] [-s SIZE] Create blank .img (default 1440K)
floppy attach [name|path] [-s SIZE] Create/open image, loop-mount for editing
floppy detach <name|mount|loop> Unmount and detach loop device
floppy refresh [-d DEV] [--mount] Re-probe drive after swapping floppies
floppy disconnect [-d DEV] Unmount, power off USB — safe to unplug
floppy read [name] [-d DEV] [-y] Archive physical floppy to FLOPPY_DISKDIR
floppy dump -o FILE [-d DEV] [-y] Raw sector dump to any path (or - for stdout)
floppy burn <image> [-d DEV] [-y] Write image to removable drive
floppy devices [-v] List candidate USB/removable block devices
floppy list [-v] List attached images and physical media
floppy status Full status (config, loops, mounts, drives)
floppy help [command] Show this help
Options:
-s, --size SIZE Image size in KB: 360, 720, or 1440 (default: 1440)
-d, --device DEV Target block device for burn (e.g. /dev/sdb)
-y, --yes Skip confirmation prompt
--force Overwrite existing output file (read/dump)
--strict Abort read/dump on first I/O error (default: noerror,sync)
-o, --output FILE Output path (dump only; use - for stdout)
--mount Mount new media via udisks after refresh
--eject Run eject(1) before re-probe (some USB FDD drives)
-w, --wait SEC Seconds to wait for media settle (default: 2)
--format Force vfat format on attach (new images only by default)
--no-format Do not format even if image is blank
Environment:
FLOPPY_DISKDIR Directory for .img files (default: ~/Retro/BLANKS)
FLOPPY_MEDIADIR Base mount directory (default: /media/$USER)
FLOPPY_DEVICE Default physical drive (e.g. /dev/sda for Mitsumi UFDD)
FLOPPY_DEVICE_MATCH Regex (extended) to prefer USB floppy models in device selection
FLOPPY_DEFAULT_SIZE_KB Default image size (default: 1440)
Config file (optional):
~/.config/floppy-utils/config Shell snippet, e.g. FLOPPY_DISKDIR=/path/to/BLANKS
Legacy wrappers (same commands):
floppy-make, floppy-attach, floppy-burn
Examples:
floppy make mydisk -s 720
floppy attach mydisk # mount ~/Retro/BLANKS/mydisk.img
floppy attach # new random 1.44M image, formatted & mounted
floppy detach mydisk
floppy devices -v
floppy refresh --mount # after swapping disks in the USB drive
floppy read # archive to ~/Retro/BLANKS/<volume-label>.img
floppy read bootdisk -d /dev/sda
floppy dump -o /tmp/bootdisk.img
floppy burn mydisk.img -d /dev/sda
FLOPPY_DEVICE=/dev/sda floppy read bootdisk -y
EOF
}