Files
home-scripts/musicbox
2026-05-20 00:07:52 -04:00

263 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env zsh
emulate -L zsh
setopt pipefail
program="${0:t}"
remote="${MUSICBOX_REMOTE:-NytegearMusic}"
inbox="${MUSICBOX_INBOX:-/music/inbox/Unchecked}"
delete_source=1
usage() {
cat >&2 <<EOF
usage:
$program [options] /path/to/folder
Options:
--keep-source Keep the source folder after verified transfer.
--delete-source Delete the source folder after verified transfer. Default.
--remote HOST SSH host alias/name. Default: $remote
--inbox PATH Remote inbox path. Default: $inbox
-h, --help Show this help.
Environment:
MUSICBOX_REMOTE Default remote host.
MUSICBOX_INBOX Default remote inbox path.
MUSICBOX_RSYNC Explicit rsync binary to use.
EOF
}
die() {
print -u2 "$program: $*"
exit 1
}
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
rsync_supports() {
local opt="$1"
"$rsync_bin" --help 2>/dev/null | grep -q -- "$opt"
}
typeset -a positional
while (( $# )); do
case "$1" in
--keep-source)
delete_source=0
shift
;;
--delete-source)
delete_source=1
shift
;;
--remote)
(( $# >= 2 )) || die "--remote requires a value"
remote="$2"
shift 2
;;
--remote=*)
remote="${1#--remote=}"
shift
;;
--inbox)
(( $# >= 2 )) || die "--inbox requires a value"
inbox="$2"
shift 2
;;
--inbox=*)
inbox="${1#--inbox=}"
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
positional+=("$@")
break
;;
-*)
die "unknown option: $1"
;;
*)
positional+=("$1")
shift
;;
esac
done
if (( ${#positional} != 1 )); then
usage
exit 2
fi
[[ -n "$remote" ]] || die "remote may not be empty"
[[ -n "$inbox" ]] || die "inbox may not be empty"
have_cmd ssh || die "ssh not found"
have_cmd base64 || die "base64 not found"
have_cmd grep || die "grep not found"
if [[ -n "${MUSICBOX_RSYNC:-}" ]]; then
rsync_bin="$MUSICBOX_RSYNC"
else
rsync_bin="$(command -v rsync 2>/dev/null || true)"
fi
[[ -n "$rsync_bin" && -x "$rsync_bin" ]] || die "rsync not found"
if "$rsync_bin" --version 2>&1 | grep -qi 'openrsync'; then
die "OpenRsync detected at $rsync_bin. This script needs GNU rsync for --secluded-args and reliable remote path handling. Install GNU rsync and set MUSICBOX_RSYNC if needed."
fi
if ! "$rsync_bin" --help 2>/dev/null | grep -q -- '--secluded-args'; then
die "rsync at $rsync_bin does not support --secluded-args; refusing because remote paths may contain spaces/special characters"
fi
src="${positional[1]:A}"
[[ "$src" != "/" ]] || die "refusing to upload /"
[[ -d "$src" ]] || die "source must be an existing folder: ${positional[1]}"
name="${src:t}"
[[ -n "$name" ]] || die "could not determine folder name from source: $src"
target="${inbox%/}/$name"
marker="$target/.musicbox-rsync-in-progress"
b64() {
printf '%s' "$1" | base64 | tr -d '\n'
}
target_b64="$(b64 "$target")"
marker_b64="$(b64 "$marker")"
print "musicbox: source: $src"
print "musicbox: remote: $remote"
print "musicbox: target: $target"
ssh "$remote" "TARGET_B64='$target_b64' MARKER_B64='$marker_b64' sh -s" <<'EOF'
decode_b64() {
printf '%s' "$1" | base64 -d 2>/dev/null || printf '%s' "$1" | base64 -D
}
target="$(decode_b64 "$TARGET_B64")"
marker="$(decode_b64 "$MARKER_B64")"
if [ -e "$target" ]; then
if [ -f "$marker" ]; then
echo "musicbox: resuming interrupted transfer: $target" >&2
exit 0
else
echo "musicbox: destination already exists, refusing to overwrite: $target" >&2
exit 17
fi
fi
mkdir -p "$target" || exit 1
touch "$marker" || exit 1
EOF
prep_status=$?
if (( prep_status != 0 )); then
exit "$prep_status"
fi
typeset -a transfer_opts
transfer_opts=(
-a
-h
-v
--partial
--append-verify
--secluded-args
)
if "$rsync_bin" --help 2>/dev/null | grep -q -- '--no-i-r'; then
transfer_opts+=(--no-i-r)
fi
if "$rsync_bin" --help 2>/dev/null | grep -q -- '--info='; then
transfer_opts+=(--info=progress2,stats2,name0)
else
transfer_opts+=(--progress --stats)
fi
if "$rsync_bin" --help 2>/dev/null | grep -q -- '--outbuf='; then
transfer_opts+=(--outbuf=L)
fi
print "musicbox: transferring..."
echo "$rsync_bin" "${transfer_opts[@]}" -- \
"$src"/ \
"$remote:$target"/
"$rsync_bin" "${transfer_opts[@]}" -- \
"$src"/ \
"$remote:$target"/
rsync_status=$?
if (( rsync_status != 0 )); then
print -u2 "musicbox: transfer interrupted or failed. Re-run the same command to resume."
print -u2 "musicbox: source was NOT deleted."
exit "$rsync_status"
fi
print "musicbox: verifying destination..."
verify_output="$(
"$rsync_bin" -a -h -n -c \
--delete \
--itemize-changes \
--exclude='.musicbox-rsync-in-progress' \
--secluded-args \
-- \
"$src"/ \
"$remote:$target"/
)"
verify_status=$?
if (( verify_status != 0 )); then
print -u2 "musicbox: verification failed. Source was NOT deleted."
exit "$verify_status"
fi
if [[ -n "$verify_output" ]]; then
print -u2 "musicbox: verification found differences. Source was NOT deleted."
print -u2 "$verify_output"
exit 1
fi
ssh "$remote" "MARKER_B64='$marker_b64' sh -s" <<'EOF'
decode_b64() {
printf '%s' "$1" | base64 -d 2>/dev/null || printf '%s' "$1" | base64 -D
}
marker="$(decode_b64 "$MARKER_B64")"
rm -f -- "$marker"
EOF
cleanup_status=$?
if (( cleanup_status != 0 )); then
print -u2 "musicbox: warning: transfer verified, but failed to remove remote marker: $marker"
exit "$cleanup_status"
fi
print "musicbox: transfer verified."
if (( delete_source )); then
print "musicbox: deleting source folder: $src"
rm -rf -- "$src"
print "musicbox: done."
else
print "musicbox: source kept in place."
print "musicbox: done."
fi