Last active
December 24, 2025 10:36
-
-
Save Milz0/2905440d96b8e9fce626537aedb80a3f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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