|
#!/usr/bin/env bash |
|
# dlwrap: run an updater with curl/wget transparently accelerated by aria2c. |
|
# Install this file as ~/.local/bin/dlwrap and symlink it as |
|
# ~/.local/libexec/dlwrap/{curl,wget}. |
|
|
|
set -u |
|
|
|
prog=${0##*/} |
|
|
|
_die() { |
|
printf 'dlwrap: %s\n' "$*" >&2 |
|
exit 2 |
|
} |
|
|
|
_is_uint() { |
|
[[ ${1:-} =~ ^[0-9]+$ ]] |
|
} |
|
|
|
_find_command_excluding() { |
|
local name=$1 exclude=$2 dir candidate |
|
local old_ifs=$IFS |
|
IFS=: |
|
for dir in $PATH; do |
|
[ -n "$dir" ] || dir=. |
|
[ "$dir" = "$exclude" ] && continue |
|
candidate=$dir/$name |
|
if [ -f "$candidate" ] && [ -x "$candidate" ]; then |
|
printf '%s\n' "$candidate" |
|
IFS=$old_ifs |
|
return 0 |
|
fi |
|
done |
|
IFS=$old_ifs |
|
return 1 |
|
} |
|
|
|
_native_curl() { |
|
local real=${DLWRAP_REAL_CURL:-} |
|
[ -n "$real" ] || _die 'curl fallback required, but no real curl was found' |
|
|
|
local extra=() |
|
if [ "${DLWRAP_VERBOSE:-0}" = 1 ]; then |
|
extra+=(--no-silent --show-error --verbose) |
|
elif [ "${DLWRAP_FORCE_PROGRESS:-0}" = 1 ]; then |
|
extra+=(--no-silent --show-error) |
|
fi |
|
exec "$real" "$@" "${extra[@]}" |
|
} |
|
|
|
_native_wget() { |
|
local real=${DLWRAP_REAL_WGET:-} |
|
[ -n "$real" ] || _die 'wget fallback required, but no real wget was found' |
|
|
|
local extra=() |
|
if [ "${DLWRAP_VERBOSE:-0}" = 1 ]; then |
|
extra+=(--verbose --show-progress --progress=bar:force:noscroll) |
|
elif [ "${DLWRAP_FORCE_PROGRESS:-0}" = 1 ]; then |
|
extra+=(--show-progress --progress=bar:force:noscroll) |
|
fi |
|
exec "$real" "$@" "${extra[@]}" |
|
} |
|
|
|
_aria_base() { |
|
ARIA=( |
|
"${DLWRAP_ARIA2C:?dlwrap: DLWRAP_ARIA2C is not set}" |
|
-x "${DLWRAP_CONNECTIONS:-16}" |
|
-s "${DLWRAP_CONNECTIONS:-16}" |
|
"--max-tries=${DLWRAP_RETRIES:-10}" |
|
"--retry-wait=${DLWRAP_RETRY_WAIT:-2}" |
|
"--timeout=${DLWRAP_TIMEOUT:-30}" |
|
"--connect-timeout=${DLWRAP_CONNECT_TIMEOUT:-10}" |
|
--auto-file-renaming=false |
|
--allow-overwrite=true |
|
--remove-control-file=true |
|
--stderr=true |
|
) |
|
|
|
if [ "${DLWRAP_VERBOSE:-0}" = 1 ]; then |
|
ARIA+=(--quiet=false --show-console-readout=true --summary-interval=1 --console-log-level=info) |
|
elif [ "${DLWRAP_FORCE_PROGRESS:-0}" = 1 ]; then |
|
ARIA+=(--quiet=false --show-console-readout=true --summary-interval=1 --console-log-level=warn) |
|
fi |
|
} |
|
|
|
_run_aria() { |
|
local quiet=$1 output=$2 url=$3 |
|
shift 3 |
|
local extras=("$@") |
|
|
|
_aria_base |
|
|
|
if [ "${DLWRAP_FORCE_PROGRESS:-0}" != 1 ] && [ "${DLWRAP_VERBOSE:-0}" != 1 ]; then |
|
if [ "$quiet" = 1 ]; then |
|
ARIA+=(--quiet=true) |
|
else |
|
ARIA+=(--show-console-readout=true --summary-interval=1 --console-log-level=warn) |
|
fi |
|
fi |
|
|
|
ARIA+=("${extras[@]}") |
|
|
|
if [ -n "$output" ]; then |
|
local dir base |
|
dir=$(dirname -- "$output") |
|
base=$(basename -- "$output") |
|
ARIA+=(-d "$dir" -o "$base") |
|
fi |
|
|
|
ARIA+=("$url") |
|
exec "${ARIA[@]}" |
|
} |
|
|
|
_run_aria_stdout() { |
|
local quiet=$1 url=$2 |
|
shift 2 |
|
local extras=("$@") |
|
local tmpdir status |
|
|
|
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/dlwrap.XXXXXX") || _die 'could not create temporary directory' |
|
|
|
_aria_base |
|
if [ "${DLWRAP_FORCE_PROGRESS:-0}" != 1 ] && [ "${DLWRAP_VERBOSE:-0}" != 1 ]; then |
|
if [ "$quiet" = 1 ]; then |
|
ARIA+=(--quiet=true) |
|
else |
|
ARIA+=(--show-console-readout=true --summary-interval=1 --console-log-level=warn) |
|
fi |
|
fi |
|
ARIA+=("${extras[@]}" -d "$tmpdir" -o data "$url") |
|
|
|
"${ARIA[@]}" |
|
status=$? |
|
if [ "$status" -eq 0 ]; then |
|
cat -- "$tmpdir/data" |
|
status=$? |
|
fi |
|
rm -rf -- "$tmpdir" |
|
return "$status" |
|
} |
|
|
|
_shim_curl() { |
|
local original=("$@") |
|
local quiet=0 show_error=0 output='' remote_name=0 url='' |
|
local unsupported=0 |
|
local extras=() |
|
local arg value chars c |
|
|
|
while [ $# -gt 0 ]; do |
|
arg=$1 |
|
shift |
|
case "$arg" in |
|
--) |
|
[ $# -eq 1 ] || unsupported=1 |
|
if [ $# -gt 0 ]; then |
|
[ -z "$url" ] || unsupported=1 |
|
url=$1 |
|
shift |
|
fi |
|
;; |
|
http://*|https://*|ftp://*) |
|
if [ -n "$url" ]; then unsupported=1; else url=$arg; fi |
|
;; |
|
-o|--output) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
output=$1; shift |
|
[ "$output" != '-' ] || unsupported=1 |
|
;; |
|
--output=*) |
|
output=${arg#*=} |
|
[ "$output" != '-' ] || unsupported=1 |
|
;; |
|
-O|--remote-name) |
|
remote_name=1 |
|
;; |
|
-H|--header) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
value=$1; shift |
|
extras+=("--header=$value") |
|
;; |
|
--header=*) extras+=("--header=${arg#*=}") ;; |
|
-A|--user-agent) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
value=$1; shift |
|
extras+=("--user-agent=$value") |
|
;; |
|
--user-agent=*) extras+=("--user-agent=${arg#*=}") ;; |
|
-e|--referer) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
value=$1; shift |
|
extras+=("--referer=$value") |
|
;; |
|
--referer=*) extras+=("--referer=${arg#*=}") ;; |
|
-C|--continue-at) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
value=$1; shift |
|
[ "$value" = '-' ] && extras+=(--continue=true) || unsupported=1 |
|
;; |
|
--continue-at=-) extras+=(--continue=true) ;; |
|
-k|--insecure) extras+=(--check-certificate=false) ;; |
|
-4|--ipv4) extras+=(--disable-ipv6=true) ;; |
|
-L|--location|--fail|--retry-all-errors|--retry-connrefused) |
|
;; |
|
--retry|--retry-delay|--connect-timeout) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
shift # dlwrap policy intentionally wins |
|
;; |
|
--retry=*|--retry-delay=*|--connect-timeout=*) |
|
;; |
|
--max-time) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
value=$1; shift |
|
_is_uint "$value" && extras+=("--stop=$value") || unsupported=1 |
|
;; |
|
--max-time=*) |
|
value=${arg#*=} |
|
_is_uint "$value" && extras+=("--stop=$value") || unsupported=1 |
|
;; |
|
-s|--silent) quiet=1 ;; |
|
-S|--show-error) show_error=1 ;; |
|
-f|-L|-s|-S) ;; # handled by combined-short parser below when combined |
|
-[!-]*) |
|
# Common compact updater form, e.g. -fsSL. Only accept |
|
# single-letter boolean flags we can reproduce safely. |
|
chars=${arg#-} |
|
while [ -n "$chars" ]; do |
|
c=${chars:0:1} |
|
chars=${chars:1} |
|
case "$c" in |
|
f|L) ;; |
|
s) quiet=1 ;; |
|
S) show_error=1 ;; |
|
k) extras+=(--check-certificate=false) ;; |
|
4) extras+=(--disable-ipv6=true) ;; |
|
O) remote_name=1 ;; |
|
*) unsupported=1 ;; |
|
esac |
|
done |
|
;; |
|
*) unsupported=1 ;; |
|
esac |
|
done |
|
|
|
[ "$unsupported" = 0 ] || _native_curl "${original[@]}" |
|
[ -n "$url" ] || _native_curl "${original[@]}" |
|
|
|
# curl writes to stdout unless -o/-O is present. Buffer simple stdout |
|
# downloads through aria2c; --stream opts back into native streaming. |
|
if [ -z "$output" ] && [ "$remote_name" = 0 ]; then |
|
[ "${DLWRAP_STREAM:-0}" != 1 ] || _native_curl "${original[@]}" |
|
|
|
if [ "$quiet" = 1 ] && [ "$show_error" = 1 ] && \ |
|
[ "${DLWRAP_FORCE_PROGRESS:-0}" != 1 ] && [ "${DLWRAP_VERBOSE:-0}" != 1 ]; then |
|
quiet=0 |
|
extras+=(--show-console-readout=false --summary-interval=0 --console-log-level=error) |
|
fi |
|
_run_aria_stdout "$quiet" "$url" "${extras[@]}" |
|
return $? |
|
fi |
|
|
|
if [ "$remote_name" = 1 ]; then |
|
[ -z "$output" ] || _native_curl "${original[@]}" |
|
value=${url%%\#*} |
|
value=${value%%\?*} |
|
value=${value%/} |
|
output=${value##*/} |
|
[ -n "$output" ] || _native_curl "${original[@]}" |
|
fi |
|
|
|
# curl -sS suppresses progress but still reports errors. aria2's quiet |
|
# mode suppresses everything, so emulate -sS with error-only logging. |
|
if [ "$quiet" = 1 ] && [ "$show_error" = 1 ] && \ |
|
[ "${DLWRAP_FORCE_PROGRESS:-0}" != 1 ] && [ "${DLWRAP_VERBOSE:-0}" != 1 ]; then |
|
quiet=0 |
|
extras+=(--show-console-readout=false --summary-interval=0 --console-log-level=error) |
|
fi |
|
|
|
_run_aria "$quiet" "$output" "$url" "${extras[@]}" |
|
} |
|
|
|
_shim_wget() { |
|
local original=("$@") |
|
local quiet=0 output='' stdout=0 url='' unsupported=0 |
|
local extras=() |
|
local arg value |
|
|
|
while [ $# -gt 0 ]; do |
|
arg=$1 |
|
shift |
|
case "$arg" in |
|
--) |
|
[ $# -eq 1 ] || unsupported=1 |
|
if [ $# -gt 0 ]; then |
|
[ -z "$url" ] || unsupported=1 |
|
url=$1 |
|
shift |
|
fi |
|
;; |
|
http://*|https://*|ftp://*) |
|
if [ -n "$url" ]; then unsupported=1; else url=$arg; fi |
|
;; |
|
-O|--output-document) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
output=$1; shift |
|
if [ "$output" = '-' ]; then stdout=1; output=''; fi |
|
;; |
|
-qO?*) |
|
quiet=1 |
|
output=${arg#-qO} |
|
if [ "$output" = '-' ]; then stdout=1; output=''; fi |
|
;; |
|
-O?*) |
|
output=${arg#-O} |
|
if [ "$output" = '-' ]; then stdout=1; output=''; fi |
|
;; |
|
--output-document=*) |
|
output=${arg#*=} |
|
if [ "$output" = '-' ]; then stdout=1; output=''; fi |
|
;; |
|
-q|--quiet) quiet=1 ;; |
|
--show-progress|--progress=*) quiet=0 ;; |
|
-c|--continue) extras+=(--continue=true) ;; |
|
--header=*) extras+=("--header=${arg#*=}") ;; |
|
--header) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
extras+=("--header=$1"); shift |
|
;; |
|
-U|--user-agent) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
extras+=("--user-agent=$1"); shift |
|
;; |
|
--user-agent=*) extras+=("--user-agent=${arg#*=}") ;; |
|
--referer=*) extras+=("--referer=${arg#*=}") ;; |
|
--referer) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
extras+=("--referer=$1"); shift |
|
;; |
|
--load-cookies=*) extras+=("--load-cookies=${arg#*=}") ;; |
|
--load-cookies) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
extras+=("--load-cookies=$1"); shift |
|
;; |
|
--no-check-certificate) extras+=(--check-certificate=false) ;; |
|
-4|--inet4-only) extras+=(--disable-ipv6=true) ;; |
|
-t|-T|--tries|--timeout|--connect-timeout|--read-timeout|--waitretry) |
|
[ $# -gt 0 ] || { unsupported=1; break; } |
|
shift # dlwrap policy intentionally wins |
|
;; |
|
--tries=*|--timeout=*|--connect-timeout=*|--read-timeout=*|--waitretry=*|--retry-connrefused) |
|
;; |
|
--spider|-6|--inet6-only|--content-disposition|--trust-server-names|-nv|--no-verbose) |
|
unsupported=1 |
|
;; |
|
*) unsupported=1 ;; |
|
esac |
|
done |
|
|
|
[ "$unsupported" = 0 ] || _native_wget "${original[@]}" |
|
[ -n "$url" ] || _native_wget "${original[@]}" |
|
|
|
if [ "$stdout" = 1 ]; then |
|
[ "${DLWRAP_STREAM:-0}" != 1 ] || _native_wget "${original[@]}" |
|
_run_aria_stdout "$quiet" "$url" "${extras[@]}" |
|
return $? |
|
fi |
|
|
|
# With -O, wget truncates/overwrites the requested file, matching the |
|
# aria2 defaults above. Without -O, let aria2 derive the URL filename. |
|
if [ -z "$output" ]; then |
|
_aria_base |
|
# Plain wget does not clobber an existing filename; it chooses .1, .2, ... |
|
# Override the explicit-output policy from _aria_base for this case. |
|
ARIA+=(--auto-file-renaming=true --allow-overwrite=false --remove-control-file=false) |
|
if [ "${DLWRAP_FORCE_PROGRESS:-0}" != 1 ] && [ "${DLWRAP_VERBOSE:-0}" != 1 ]; then |
|
if [ "$quiet" = 1 ]; then |
|
ARIA+=(--quiet=true) |
|
else |
|
ARIA+=(--show-console-readout=true --summary-interval=1 --console-log-level=warn) |
|
fi |
|
fi |
|
ARIA+=("${extras[@]}" "$url") |
|
exec "${ARIA[@]}" |
|
fi |
|
|
|
_run_aria "$quiet" "$output" "$url" "${extras[@]}" |
|
} |
|
|
|
_dlwrap_main() { |
|
local progress=${DLWRAP_FORCE_PROGRESS:-0} |
|
local verbose=${DLWRAP_VERBOSE:-0} |
|
local stream=${DLWRAP_STREAM:-0} |
|
local connections=${DLWRAP_CONNECTIONS:-16} |
|
local retries=${DLWRAP_RETRIES:-10} |
|
local retry_wait=${DLWRAP_RETRY_WAIT:-2} |
|
local timeout=${DLWRAP_TIMEOUT:-30} |
|
local connect_timeout=${DLWRAP_CONNECT_TIMEOUT:-10} |
|
local shim_dir=${DLWRAP_SHIM_DIR:-"$HOME/.local/libexec/dlwrap"} |
|
|
|
while [ $# -gt 0 ]; do |
|
case "$1" in |
|
-p|--progress) progress=1; shift ;; |
|
-v|--verbose) verbose=1; progress=1; shift ;; |
|
--stream) stream=1; shift ;; |
|
--connections|--retries|--retry-wait|--timeout|--connect-timeout) |
|
local opt=$1 |
|
[ $# -ge 2 ] || _die "$opt requires an integer" |
|
_is_uint "$2" || _die "$opt requires a non-negative integer" |
|
case "$opt" in |
|
--connections) connections=$2 ;; |
|
--retries) retries=$2 ;; |
|
--retry-wait) retry_wait=$2 ;; |
|
--timeout) timeout=$2 ;; |
|
--connect-timeout) connect_timeout=$2 ;; |
|
esac |
|
shift 2 |
|
;; |
|
--connections=*|--retries=*|--retry-wait=*|--timeout=*|--connect-timeout=*) |
|
local opt=${1%%=*} val=${1#*=} |
|
_is_uint "$val" || _die "$opt requires a non-negative integer" |
|
case "$opt" in |
|
--connections) connections=$val ;; |
|
--retries) retries=$val ;; |
|
--retry-wait) retry_wait=$val ;; |
|
--timeout) timeout=$val ;; |
|
--connect-timeout) connect_timeout=$val ;; |
|
esac |
|
shift |
|
;; |
|
-h|--help) |
|
cat <<'EOF_HELP' |
|
Usage: dlwrap [OPTIONS] -- COMMAND [ARG...] |
|
|
|
Run COMMAND with curl/wget intercepted and simple file downloads translated |
|
into aria2c. Unsupported invocations fall back to the original downloader. |
|
|
|
Options: |
|
-p, --progress force progress output, overriding curl -s / wget -q |
|
-v, --verbose force verbose downloader output (implies --progress) |
|
--stream preserve native streaming for stdout downloads |
|
--connections N aria2 connections/splits per download (default: 16) |
|
--retries N aria2 max tries (default: 10) |
|
--retry-wait SEC delay between retries (default: 2) |
|
--timeout SEC socket timeout (default: 30) |
|
--connect-timeout SEC |
|
connection-establishment timeout (default: 10) |
|
-h, --help show this help |
|
|
|
Environment variables with matching DLWRAP_* names may also set defaults. |
|
EOF_HELP |
|
exit 0 |
|
;; |
|
--) shift; break ;; |
|
*) _die "unknown option '$1' (COMMAND must follow --)" ;; |
|
esac |
|
done |
|
|
|
[ $# -gt 0 ] || _die 'missing COMMAND after --' |
|
[ -d "$shim_dir" ] || _die "shim directory not found: $shim_dir" |
|
[ -x "$shim_dir/curl" ] || _die "curl shim not found: $shim_dir/curl" |
|
[ -x "$shim_dir/wget" ] || _die "wget shim not found: $shim_dir/wget" |
|
|
|
local aria real_curl real_wget |
|
aria=$(_find_command_excluding aria2c "$shim_dir") || _die 'aria2c not found in PATH' |
|
real_curl=$(_find_command_excluding curl "$shim_dir" || true) |
|
real_wget=$(_find_command_excluding wget "$shim_dir" || true) |
|
|
|
export DLWRAP_FORCE_PROGRESS=$progress |
|
export DLWRAP_VERBOSE=$verbose |
|
export DLWRAP_STREAM=$stream |
|
export DLWRAP_CONNECTIONS=$connections |
|
export DLWRAP_RETRIES=$retries |
|
export DLWRAP_RETRY_WAIT=$retry_wait |
|
export DLWRAP_TIMEOUT=$timeout |
|
export DLWRAP_CONNECT_TIMEOUT=$connect_timeout |
|
export DLWRAP_ARIA2C=$aria |
|
export DLWRAP_REAL_CURL=$real_curl |
|
export DLWRAP_REAL_WGET=$real_wget |
|
export DLWRAP_SHIM_DIR=$shim_dir |
|
export PATH="$shim_dir:$PATH" |
|
|
|
exec "$@" |
|
} |
|
|
|
case "$prog" in |
|
curl) _shim_curl "$@" ;; |
|
wget) _shim_wget "$@" ;; |
|
*) _dlwrap_main "$@" ;; |
|
esac |