Introduce floppy-utils: a CLI for creating, editing, archiving, and restoring floppy disk images on Linux with USB floppy drive support. Includes main floppy command, legacy wrappers, dependency checker, install script, and comprehensive documentation.
1019 lines
33 KiB
Bash
1019 lines
33 KiB
Bash
# 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
|
|
} |