feat: pos media sync — incremental Music→USB sync (mp3/mp4) + shared lib/usb-lib.sh
gates / consistency-and-conventions (push) Successful in 46s
gates / consistency-and-conventions (push) Successful in 46s
This commit is contained in:
+16
-164
@@ -6,6 +6,7 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
||||
source "$(dirname "$0")/../lib/usb-lib.sh" 2>/dev/null || source "$(dirname "$0")/usb-lib.sh"
|
||||
|
||||
load_system_env
|
||||
EFF_ROOTS="${BACKUP_SERVICE_ROOTS:-/srv $HOME/srv}"
|
||||
@@ -42,7 +43,10 @@ Environment:
|
||||
BACKUP_USB_ROOT USB root to copy finished backups to
|
||||
(default: auto-detect mounted USB storage)
|
||||
BACKUP_MOUNT_BASE Where to mount an unmounted USB stick offered
|
||||
during the copy (default: /media)
|
||||
during the copy (default: /media; alias of
|
||||
USB_MOUNT_BASE)
|
||||
BACKUP_USB_BYID by-id dir used to corroborate USB detection
|
||||
(default: /dev/disk/by-id; alias of USB_BYID)
|
||||
(loaded from ~/.config/linux_post_install/system.env unless exported)
|
||||
EOF
|
||||
exit 0
|
||||
@@ -55,79 +59,28 @@ command -v tar &>/dev/null || err "tar not found"
|
||||
# the archive was being made is found. The copy lands in <usb>/backups/
|
||||
# and the transfer is proven 100% (sha256 source vs copy) before any
|
||||
# success is announced. BACKUP_USB_ROOT pins the root and skips
|
||||
# detection; otherwise USB storage is auto-detected.
|
||||
#
|
||||
# "Is it really USB?" — lsblk's TRAN column decides per device (rm==1
|
||||
# card readers / hot-swap bays report sata and are skipped). When TRAN
|
||||
# is unavailable, /dev/disk/by-id/usb-* symlinks and lsusb text
|
||||
# corroborate. A stick that is plugged in but not mounted (the CLI-box
|
||||
# case) is offered a mount (sudo, usb-automount scheme: /media/<label>,
|
||||
# fallback /media/usb-<devname>) before the copy; cron/EOF skips.
|
||||
# detection; otherwise usb_pick_root (lib/usb-lib.sh) auto-detects,
|
||||
# offers to mount unmounted sticks, and picks the target.
|
||||
usb_copy_offer() {
|
||||
local archive="$1" pass=0 resp="" i=0 root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
||||
local -a roots=()
|
||||
local offered=0
|
||||
local archive="$1" root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
||||
|
||||
command -v lsblk &>/dev/null || { warn "lsblk not found — USB copy skipped"; return 0; }
|
||||
command -v jq &>/dev/null || { warn "jq not found — USB copy skipped"; return 0; }
|
||||
|
||||
section "USB copy"
|
||||
|
||||
while :; do
|
||||
pass=$((pass + 1))
|
||||
roots=()
|
||||
|
||||
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
||||
roots+=("$BACKUP_USB_ROOT")
|
||||
else
|
||||
detect_usb
|
||||
roots=("${USB_MOUNTED[@]}")
|
||||
|
||||
if [ ${#roots[@]} -eq 0 ] && [ ${#USB_UNMOUNTED[@]} -gt 0 ]; then
|
||||
if [ "$offered" -ge 1 ]; then
|
||||
warn "USB stick still not mounted — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
offered=1
|
||||
mount_offer "${USB_UNMOUNTED[@]}" || {
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
}
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
[ ${#roots[@]} -gt 0 ] && break
|
||||
|
||||
# None found — offer one re-scan before giving up.
|
||||
if [ "$pass" -ge 2 ]; then
|
||||
warn "Still no USB storage detected — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
warn "No USB storage detected"
|
||||
read -rp "Plug a USB drive in now and press Enter to re-check (or 's' to skip): " resp || return 0
|
||||
case "$resp" in
|
||||
s|S) log "Skipped — backup stays local: $archive"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#roots[@]} -eq 1 ]; then
|
||||
root="${roots[0]}"
|
||||
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
||||
root="$BACKUP_USB_ROOT"
|
||||
if ! confirm "Copy backup to ${root%/}/backups/?" n; then
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo "Multiple USB storages found:"
|
||||
for i in "${!roots[@]}"; do
|
||||
printf "%2d) %s\n" "$((i + 1))" "${roots[$i]}"
|
||||
done
|
||||
read -rp "Copy backup to which one? [1-${#roots[@]}] (0 = skip): " resp || return 0
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#roots[@]} )); then
|
||||
usb_pick_root "Copy backup to" "backups" "backup stays local: $archive" || {
|
||||
log "Skipped — backup stays local: $archive"
|
||||
return 0
|
||||
fi
|
||||
root="${roots[$((resp - 1))]}"
|
||||
}
|
||||
root="$USB_ROOT"
|
||||
fi
|
||||
|
||||
dest_dir="${root%/}/backups"
|
||||
@@ -153,110 +106,9 @@ usb_copy_offer() {
|
||||
notify_send "Backup copied to USB: $dest (sha256 verified)"
|
||||
}
|
||||
|
||||
# ── USB detection ────────────────────────────────────────────────
|
||||
# Fills USB_MOUNTED (mountpoints, one per removable USB storage) and
|
||||
# USB_UNMOUNTED ("path|label|size|model" entries) from lsblk JSON.
|
||||
# TRAN=="usb" is the per-device deciding signal; when TRAN is empty,
|
||||
# usb_related_present() corroborates. Non-USB removables are skipped.
|
||||
detect_usb() {
|
||||
local out path="" mp="" label="" size="" model="" tran="" type="" children=""
|
||||
USB_MOUNTED=()
|
||||
USB_UNMOUNTED=()
|
||||
out="$(lsblk -J -o NAME,PATH,LABEL,MOUNTPOINT,RM,TYPE,TRAN,SIZE,MODEL 2>/dev/null)" || return 0
|
||||
|
||||
while IFS=$'\x1f' read -r path mp label size model tran type children; do
|
||||
[ -n "$path" ] || continue
|
||||
if [ "$tran" = "usb" ]; then
|
||||
:
|
||||
elif [ -z "$tran" ] && usb_related_present; then
|
||||
warn "TRAN unavailable — assuming USB (lsusb/by-id corroboration)"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ -n "$mp" ]; then
|
||||
USB_MOUNTED+=("$mp")
|
||||
elif [ "$children" = "0" ]; then
|
||||
USB_UNMOUNTED+=("$path|$label|$size|$model")
|
||||
fi
|
||||
done < <(printf '%s' "$out" | jq -r '
|
||||
.. | objects
|
||||
| select(.rm == true and (.type == "part" or .type == "disk"))
|
||||
| [.path, (.mountpoint // ""), (.label // ""), (.size // ""),
|
||||
(.model // ""), (.tran // ""), (.type // ""),
|
||||
((.children // []) | length)]
|
||||
| join("\u001f")')
|
||||
}
|
||||
|
||||
# Whole-system "is any USB storage attached?" — udev by-id usb-* symlinks
|
||||
# are definitive; lsusb text is a secondary hint for storage-ish devices.
|
||||
# BACKUP_USB_BYID points at the by-id dir (test seam; default real one).
|
||||
usb_related_present() {
|
||||
[ -n "$(ls "${BACKUP_USB_BYID:-/dev/disk/by-id}"/usb-* 2>/dev/null)" ] && return 0
|
||||
command -v lsusb &>/dev/null \
|
||||
&& lsusb 2>/dev/null | grep -qiE 'mass storage|card reader|flash disk|usb.*(disk|drive|storage)|reader|external'
|
||||
}
|
||||
|
||||
# ── Mount an unmounted USB stick (CLI-box case) ──────────────────
|
||||
# Asks to mount a detected-but-unmounted device (sudo, mirroring
|
||||
# usb-automount: /media/<label>, fallback /media/usb-<devname>, -o
|
||||
# umask=000 world-writable). Returns 0 → caller re-scans (mounted now
|
||||
# or the user will mount manually); 1 → skipped/EOF (backup stays local).
|
||||
mount_offer() {
|
||||
local -a devs=("$@")
|
||||
local i=0 dev="" label="" size="" model="" mp="" resp="" n=""
|
||||
local -a chosen=()
|
||||
|
||||
if [ ${#devs[@]} -gt 1 ]; then
|
||||
echo "Multiple unmounted USB storages found:"
|
||||
for i in "${!devs[@]}"; do
|
||||
IFS='|' read -r dev label size model <<< "${devs[$i]}"
|
||||
printf "%2d) %s (%s, %s)\n" "$((i + 1))" "$dev" "$size" "${model:-no label}"
|
||||
done
|
||||
read -rp "Copy backup to which one? [1-${#devs[@]}] (0 = skip): " resp || return 1
|
||||
if ! [[ "$resp" =~ ^[0-9]+$ ]] || (( resp < 1 || resp > ${#devs[@]} )); then
|
||||
return 1
|
||||
fi
|
||||
chosen=("${devs[$((resp - 1))]}")
|
||||
else
|
||||
chosen=("${devs[0]}")
|
||||
fi
|
||||
|
||||
IFS='|' read -r dev label size model <<< "${chosen[0]}"
|
||||
label="${label//\//_}"
|
||||
mp="${BACKUP_MOUNT_BASE:-/media}/${label:-usb-$(basename "$dev")}"
|
||||
if [ -d "$mp" ] && mountpoint -q "$mp" 2>/dev/null; then
|
||||
n=2
|
||||
while [ -d "${mp}-${n}" ] && mountpoint -q "${mp}-${n}" 2>/dev/null; do
|
||||
n=$((n + 1))
|
||||
done
|
||||
mp="${mp}-${n}"
|
||||
fi
|
||||
|
||||
echo "Found USB storage not mounted: $dev ($size, ${model:-no label})"
|
||||
if confirm "Mount it at $mp (world-writable) so the backup can go there?" n; then
|
||||
run sudo mkdir -p "$mp"
|
||||
if ! run sudo mount -o umask=000 "$dev" "$mp"; then
|
||||
run sudo mount "$dev" "$mp" || true
|
||||
fi
|
||||
if [ "${DRY_RUN:-0}" -eq 1 ] || mountpoint -q "$mp" 2>/dev/null; then
|
||||
ok "Mounted $dev at $mp"
|
||||
return 0
|
||||
fi
|
||||
warn "Mount failed — do it manually, then we'll copy:"
|
||||
log " sudo mkdir -p $mp && sudo mount $dev $mp"
|
||||
else
|
||||
warn "Mount it manually, then we'll copy:"
|
||||
log " sudo mkdir -p $mp"
|
||||
log " sudo mount $dev $mp"
|
||||
fi
|
||||
|
||||
read -rp "Press Enter once mounted (or 's' to skip): " resp || return 1
|
||||
case "$resp" in
|
||||
s|S) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── USB detection / mount offer / pick flow ─────────────────────
|
||||
# Shared with pos-media-sync: lib/usb-lib.sh (usb_detect /
|
||||
# usb_related_present / usb_mount_offer / usb_pick_root).
|
||||
SERVICE=0
|
||||
ENCRYPT=1
|
||||
[ "${BACKUP_ENCRYPT:-1}" = "0" ] && ENCRYPT=0
|
||||
|
||||
Reference in New Issue
Block a user