icerbox.com bash dl cli
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

215 line
6.8KB

  1. #!/usr/bin/env bash
  2. set -ueo pipefail
  3. api='https://icerbox.com/api/v1/'
  4. jwt=~/.icerbox.token
  5. cfg=~/.icerbox.conf
  6. ct='Content-Type: application/json'
  7. err() {
  8. echo "err: ${1}" >/dev/stderr
  9. exit 1
  10. }
  11. warn() {
  12. echo "warn: ${1}" >/dev/stderr
  13. }
  14. nfo() {
  15. echo "info: ${1}" >/dev/stdout
  16. }
  17. join_by() {
  18. local d=${1-} f=${2-}
  19. if shift 2; then
  20. printf %s "$f" "${@/#/$d}"
  21. fi
  22. }
  23. icerbox_handle_response() {
  24. test -n "$1" || err "empty http status code"
  25. test -s "$2" || err "empty json reponse body"
  26. if [ "$1" -ne 200 ]; then
  27. jq '.' "${2}" >/dev/stderr || true
  28. err "request failed with http ${1}"
  29. fi
  30. }
  31. icerbox_auth_login() {
  32. local out='/dev/shm/icerbox.auth.login.json'
  33. test -f "$out" && rm "$out"
  34. local code=$(curl -s -w '%{http_code}' -H "$ct" -o "$out" -d "email=${1}" -d "password=${2}" "${api}auth/login")
  35. test $? -eq 0 || err "auth/login: curl request failed"
  36. icerbox_handle_response "$code" "$out"
  37. token=$(jq -r '.token?' "$out")
  38. test -n "$token" || err "auth/login: empty jwt token"
  39. }
  40. icerbox_auth_refresh() {
  41. local out='/dev/shm/icerbox.auth_refresh.json'
  42. test -f "$out" && rm "$out"
  43. # force POST with empty -d
  44. local code=$(curl -s -w '%{http_code}' --oauth2-bearer "$token" -H "$ct" -o "$out" -d "" "${api}auth/refresh")
  45. test $? -eq 0 || err "auth/refresh: curl request failed"
  46. icerbox_handle_response "$code" "$out"
  47. token=$(jq -r '.token?' "$out")
  48. test -n "$token" || err "auth/refresh: empty jwt token"
  49. }
  50. icerbox_user_account() {
  51. local out='/dev/shm/icerbox.user.account.json'
  52. test -f "$out" && rm "$out"
  53. local code=$(curl -s -w '%{http_code}' --oauth2-bearer "$token" -H "$ct" -o "$out" "${api}user/account")
  54. test $? -eq 0 || err "user/account: curl request failed"
  55. icerbox_handle_response "$code" "$out"
  56. }
  57. icerbox_download_quota() {
  58. local in='/dev/shm/icerbox.user.account.json'
  59. test -f "$in"
  60. jq -r '.data.package.volume - .data.downloaded_today' "$in"
  61. }
  62. icerbox_file() {
  63. local out='/dev/shm/icerbox.file.json'
  64. test -f "$out" && rm "$out"
  65. local code=$(curl -s -w '%{http_code}' --oauth2-bearer "$token" -H "$ct" -o "$out" -d id="$2" -G "${api}file")
  66. test $? -eq 0 || err "file: curl request failed"
  67. icerbox_handle_response "$code" "$out"
  68. local status=$(jq -r '.data?.status?' "$out")
  69. test $? -eq 0 || err "file: failed parsing json"
  70. test -n "$status" || err "file: empty status"
  71. if [ "$status" != "active" ]; then
  72. warn "skip: file id ${2} is not active"
  73. return 1
  74. fi
  75. local name=$(jq -r '.data?.name?' "$out")
  76. test $? -eq 0 || err "file: failed parsing json"
  77. test -n "$name" || err "file: empty name"
  78. if [ -f "$name" ]; then
  79. warn "skip: file exists: ${name}"
  80. return 1
  81. fi
  82. return 0
  83. }
  84. icerbox_files() {
  85. local out='/dev/shm/icerbox.files.json'
  86. test -f "$out" && rm "$out"
  87. local code=$(curl -s -w '%{http_code}' --oauth2-bearer "$token" -H "$ct" -o "$out" -d ids="$1" -G "${api}files")
  88. test $? -eq 0 || err "files: curl request failed"
  89. icerbox_handle_response "$code" "$out"
  90. }
  91. icerbox_filter_files() {
  92. local in='/dev/shm/icerbox.files.json'
  93. test -f "$in" || err "filter files: no input file"
  94. local size_sum=0
  95. local IFS=$'\t'
  96. while read -a arr; do
  97. if [ -f "${arr[1]}" ]; then
  98. local local_size=$(stat --printf '%s' "${arr[1]}")
  99. test -n "$local_size" || err "filter_files: empty file size"
  100. test "$local_size" -eq 0 && err "abort: file exists and is empty: ${arr[1]}"
  101. if [ $local_size -eq ${arr[2]} ]; then
  102. warn "skip: file of same size exists: ${arr[0]}: ${arr[1]}"
  103. continue
  104. elif [ $local_size -lt ${arr[2]} ]; then
  105. warn "skip: file with smaller size exists: ${arr[0]}: ${arr[1]}"
  106. continue
  107. elif [ $local_size -gt ${arr[2]} ]; then
  108. err "abort: local file is larger than remote: ${arr[0]}: ${arr[1]}"
  109. fi
  110. fi
  111. size_sum=$(( $size_sum + ${arr[2]} ))
  112. if [ $size_sum -gt $download_quota ]; then
  113. warn "break: ${arr[0]}: above download quota"
  114. break
  115. fi
  116. to_download+=("${arr[0]}")
  117. done < <(jq -r '.data[] | select(.status="active") | [.id,.name,.size] | @tsv' $in)
  118. numfmt --format='download size: %f' --to=iec $size_sum
  119. }
  120. icerbox_dl_ticket() {
  121. local out='/dev/shm/icerbox.dl.ticket.json'
  122. test -f "$out" && rm "$out"
  123. # endpoint does not accept file parameter with json as content type
  124. local code=$(curl -s -w '%{http_code}' --oauth2-bearer "$token" -o "$out" -d file="$1" "${api}dl/ticket")
  125. test $? -eq 0 || err "curl request failed"
  126. if [ "$code" -eq 422 ]; then
  127. warn "skip: file is unavailable: $1"
  128. return
  129. fi
  130. icerbox_handle_response "$code" "$out"
  131. local url=$(jq -r '.url?' "$out")
  132. test $? -eq 0 || err "failed parsing json"
  133. test -n "$url" || err "empty ticket url"
  134. tickets+=(-OJL "$url")
  135. }
  136. icerbox_file_id() {
  137. test -n "$1" || err "empty url"
  138. test "${1:0:20}" = "https://icerbox.com/" || err "not a icerbox url: $1"
  139. test "${#1}" -ge 28 || err "url $1 does not contain valid file id"
  140. file_ids+=("${1:20:8}")
  141. }
  142. test "$#" -eq 0 && err "usage $0 https://icerbox.com/abcdefgh https://icerbox.com/abcdefgh ..."
  143. test "$#" -gt 666 && err "too many arguments. http request will fail with 414 Request-URI Too Large"
  144. test -f "$cfg" && source "$cfg" || err "please set 'email' and 'password' in ${cfg}"
  145. test -n "$email" || err "email not set"
  146. test -n "$password" || err "password not set"
  147. type curl >/dev/null || err "need curl"
  148. type jq >/dev/null || err "need jq"
  149. if [ -f $jwt ] && [ -s $jwt ]; then
  150. token=$(< $jwt)
  151. token_age=$(stat --format=%Y "$jwt")
  152. now=$(date +%s)
  153. if [ $token_age -le $(( $now - 2700 )) ]; then
  154. icerbox_auth_refresh
  155. echo "$token" > "${jwt}.tmp"
  156. mv "${jwt}.tmp" "$jwt"
  157. nfo "session refreshed"
  158. else
  159. nfo "using active session"
  160. fi
  161. else
  162. icerbox_auth_login "$email" "$password"
  163. echo "$token" > "${jwt}.tmp"
  164. mv "${jwt}.tmp" "${jwt}"
  165. nfo "new session acquired"
  166. fi
  167. test -n "$token" || err "no session token"
  168. icerbox_user_account
  169. download_quota=$(icerbox_download_quota)
  170. numfmt --format='daily download limit: %f' --to=iec "$download_quota"
  171. # globals filled by functions
  172. file_ids=()
  173. to_download=()
  174. tickets=()
  175. nfo "requested: $#"
  176. for url in $@; do
  177. icerbox_file_id "$url"
  178. done
  179. icerbox_files "$(join_by , "${file_ids[@]}")"
  180. icerbox_filter_files
  181. test ${#to_download[@]} -gt 0 || (nfo "nothing to download" ; exit 1)
  182. nfo "to download: ${#to_download[@]}"
  183. for file_id in ${to_download[@]}; do
  184. icerbox_dl_ticket "$file_id"
  185. done
  186. test ${#tickets[@]} -gt 0 || (nfo "no download tickets" ; exit 1)
  187. nfo "download tickets: $(( ${#tickets[@]} / 2 ))"
  188. curl -s -w '%{filename_effective}\t%{size_download} bytes\t%{speed_download} bytes/sec\n' "${tickets[@]}"
  189. test $? -eq 0 || warn "curl exit error code $?"