Skip to content

Instantly share code, notes, and snippets.

@Milz0
Last active December 24, 2025 10:36
Show Gist options
  • Select an option

  • Save Milz0/2905440d96b8e9fce626537aedb80a3f to your computer and use it in GitHub Desktop.

Select an option

Save Milz0/2905440d96b8e9fce626537aedb80a3f to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -euo pipefail
: "${R2_BUCKET:?R2_BUCKET is required}"
DEST_BASE="${DEST_BASE:-$HOME}"
PARALLEL_DOWNLOADS="${PARALLEL_DOWNLOADS:-4}"
REMOTE="r2:${R2_BUCKET}"
current_path=""
strip_ansi() { sed -r 's/\x1B\[[0-9;]*[A-Za-z]//g'; }
human_bytes() {
local b="${1:-0}"
awk -v bytes="$b" 'BEGIN{
split("B KB MB GB TB PB",u," ")
i=1
while(bytes>=1024 && i<6){bytes/=1024;i++}
printf "%.1f %s", bytes, u[i]
}'
}
path_join() {
local a="$1" b="$2"
if [ -z "$a" ]; then echo "$b"; else echo "$a/$b"; fi
}
make_banner() {
local path_display="/${current_path}"
[ -z "$current_path" ] && path_display="/"
cat <<EOF
╔══════════════════════════════════════════════════════════════════════╗
║ R2 INTERACTIVE BROWSER ║
╠══════════════════════════════════════════════════════════════════════╣
║ Bucket: ${R2_BUCKET}
║ Path: ${path_display}
║ Dest: ${DEST_BASE}
║ DL: parallel=${PARALLEL_DOWNLOADS}
╠══════════════════════════════════════════════════════════════════════╣
║ Controls: ↑/↓ navigate | TAB toggle | ENTER confirm | ESC exit ║
║ Actions: [DIR] enter | [BACK] up | [ROOT] top | select [FILE] to DL ║
╚══════════════════════════════════════════════════════════════════════╝
EOF
}
while true; do
remote_path="$REMOTE"
[ -n "$current_path" ] && remote_path="$REMOTE/$current_path"
# Build internal list (TAB-delimited fields):
# TYPE \t BYTES \t HUMAN \t NAME
entries=""
if [ -n "$current_path" ]; then
entries+="BACK\t-\t-\t..\n"
entries+="ROOT\t-\t-\t/\n"
fi
# Directories (S3/R2-safe)
dirs="$(rclone lsf "$remote_path" --dirs-only --max-depth 1 2>/dev/null | sed 's:/$::' || true)"
if [ -n "$dirs" ]; then
while IFS= read -r d; do
[ -z "$d" ] && continue
entries+="DIR\t-\t-\t${d}\n"
done <<<"$dirs"
fi
# Files only when not at root (your requirement)
file_map=""
if [ -n "$current_path" ]; then
file_lines="$(rclone ls "$remote_path" --max-depth 1 2>/dev/null || true)"
if [ -n "$file_lines" ]; then
while IFS= read -r line; do
[ -z "$line" ] && continue
bytes="$(awk '{print $1}' <<<"$line")"
name="$(awk '{ $1=""; sub(/^ /,""); print }' <<<"$line")"
[ -z "$name" ] && continue
human="$(human_bytes "$bytes")"
entries+="FILE\t${bytes}\t${human}\t${name}\n"
file_map+="${name}"$'\t'"${bytes}"$'\n'
done <<<"$file_lines"
fi
fi
entries+="EXIT\t-\t-\t(exit)\n"
# Colorize TYPE for display (keep tabs intact)
colored="$(printf "%b" "$entries" | awk -F'\t' '
function c(s,color){ return sprintf("\033[%sm%s\033[0m", color, s) }
{
t=$1; b=$2; h=$3; n=$4
if(t=="DIR") t=c("[DIR] ", "36")
else if(t=="FILE") t=c("[FILE]", "32")
else if(t=="BACK") t=c("[BACK]", "33")
else if(t=="ROOT") t=c("[ROOT]", "33")
else if(t=="EXIT") t=c("[EXIT]", "31")
printf "%s\t%s\t%s\n", t, h, n
}
')"
header="$(make_banner)"
selection="$(
printf "%s" "$colored" | fzf -m --ansi \
--header "$header" \
--header-first \
--delimiter=$'\t' \
--with-nth=1,2,3 \
--prompt="Select > " \
--height=70% \
--border \
--bind "tab:toggle" \
--bind "shift-tab:toggle" \
--bind "ctrl-a:select-all" \
--bind "ctrl-d:deselect-all"
)"
[ -z "$selection" ] && exit 0
sel_clean="$(printf "%s\n" "$selection" | strip_ansi)"
# Detect if any FILE selected
has_file=0
while IFS=$'\t' read -r t h n; do
[[ "$t" == "[FILE]"* ]] && has_file=1
done <<<"$sel_clean"
if [ "$has_file" -eq 0 ]; then
# Navigation mode: first nav item wins
while IFS=$'\t' read -r t h n; do
case "$t" in
"[EXIT]"*) exit 0 ;;
"[BACK]"*)
current_path="$(dirname "$current_path")"
[ "$current_path" = "." ] && current_path=""
break
;;
"[ROOT]"*) current_path=""; break ;;
"[DIR]"*) current_path="$(path_join "$current_path" "$n")"; break ;;
esac
done <<<"$sel_clean"
continue
fi
# Download mode
total_bytes=0
files_to_get=()
while IFS=$'\t' read -r t h n; do
case "$t" in
"[FILE]"*)
files_to_get+=("$n")
b="$(awk -F'\t' -v key="$n" '$1==key{print $2; exit}' <<<"$file_map")"
b="${b:-0}"
total_bytes=$(( total_bytes + b ))
;;
"[EXIT]"*) exit 0 ;;
esac
done <<<"$sel_clean"
count="${#files_to_get[@]}"
[ "$count" -eq 0 ] && continue
total_human="$(human_bytes "$total_bytes")"
echo
echo "Selected: ${count} file(s)"
echo "Total: ${total_human}"
echo "Dest: ${DEST_BASE}/$current_path"
echo
read -r -p "Proceed with download? [y/N] " ans
case "${ans:-N}" in
y|Y|yes|YES) ;;
*) echo "Cancelled."; continue ;;
esac
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
for f in "${files_to_get[@]}"; do
src="$REMOTE/$current_path/$f"
dest="$DEST_BASE/$current_path/$f"
mkdir -p "$(dirname "$dest")"
printf "%s\t%s\n" "$src" "$dest" >>"$tmp"
done
echo
echo "Downloading in parallel (P=${PARALLEL_DOWNLOADS})..."
echo
ok=0
fail=0
# IMPORTANT:
# - rclone progress goes to /dev/tty (so you SEE it)
# - stdout prints ONLY OK/FAIL so counting is correct
while IFS= read -r token; do
if [ "$token" = "OK" ]; then ok=$((ok+1)); fi
if [ "$token" = "FAIL" ]; then fail=$((fail+1)); fi
done < <(
cat "$tmp" | xargs -P "$PARALLEL_DOWNLOADS" -n 2 bash -lc '
src="$1"; dest="$2";
if rclone copyto "$src" "$dest" --progress >/dev/tty 2>/dev/tty; then
echo OK
else
echo FAIL
fi
' _
)
echo
echo "Download summary: OK=${ok} FAIL=${fail}"
echo "Press ENTER to continue browsing."
read -r
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment