#!/usr/bin/env bash # BinaryPHP API multipart uploader — pure bash + curl + jq. # # Usage: # API_KEY=bphp_live_xxx ./upload.sh path/to/big.zip [domain] [mac] [webhook] # # Behaviour: # - File ≤100 MB → single POST /upload/{key} # - File >100 MB → /upload/{key}/init → PUT each part → /upload/{key}/complete # # Output: JSON containing download_url, expires_at, license, etc. set -euo pipefail [[ -n "${API_KEY:-}" ]] || { echo "set API_KEY=bphp_live_..." >&2; exit 1; } [[ -n "${1:-}" ]] || { echo "usage: $0 [domain] [mac] [webhook]" >&2; exit 1; } FILE="$1" [[ -r "$FILE" ]] || { echo "cannot read $FILE" >&2; exit 1; } DOMAIN="${2:-*}" MAC="${3:-}" WEBHOOK="${4:-}" BASE="${BPHP_BASE:-https://webhook.binaryphp.com}" SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE") NAME=$(basename "$FILE") THRESHOLD=$((100 * 1024 * 1024)) # CF edge single-PUT cap # ─── ≤100 MB: simple single POST ─────────────────────────────────── if (( SIZE <= THRESHOLD )); then echo "→ single PUT ($SIZE bytes)" >&2 curl -sS -X POST "$BASE/upload/$API_KEY" \ -H "X-BPHP-Filename: $NAME" \ -H "X-BPHP-Domain: $DOMAIN" \ ${MAC:+-H "X-BPHP-Mac: $MAC"} \ ${WEBHOOK:+-H "X-BPHP-Webhook: $WEBHOOK"} \ --data-binary "@$FILE" echo exit fi # ─── >100 MB: multipart ──────────────────────────────────────────── echo "→ multipart upload ($SIZE bytes)" >&2 INIT=$(curl -sS -X POST "$BASE/upload/$API_KEY/init" \ -H "X-BPHP-Filename: $NAME" \ -H "X-BPHP-Total-Size: $SIZE") UPLOAD_ID=$(echo "$INIT" | jq -r .upload_id) R2_KEY=$( echo "$INIT" | jq -r .r2_key) PART_SIZE=$(echo "$INIT" | jq -r .part_size) PART_COUNT=$(echo "$INIT" | jq -r .part_count) [[ "$UPLOAD_ID" != "null" ]] || { echo "init failed: $INIT" >&2; exit 1; } echo " upload_id=$UPLOAD_ID, $PART_COUNT × $PART_SIZE bytes" >&2 # Upload each part. Capture ETag from response headers. PARTS_JSON='[]' for ((i=1; i<=PART_COUNT; i++)); do OFFSET=$(( (i-1) * PART_SIZE )) URL=$(echo "$INIT" | jq -r ".parts[$((i-1))].upload_url") # Stream just this slice of the file via dd → curl. HEADERS=$(dd if="$FILE" bs=1M skip=$((OFFSET / 1048576)) count=$((PART_SIZE / 1048576)) 2>/dev/null \ | curl -sS -X PUT "$URL" --data-binary @- -D - -o /dev/null \ | tr -d '\r') ETAG=$(echo "$HEADERS" | awk -F': ' 'tolower($1)=="etag" {print $2}') [[ -n "$ETAG" ]] || { echo "part $i: no ETag returned" >&2; exit 1; } echo " part $i/$PART_COUNT etag=$ETAG" >&2 PARTS_JSON=$(echo "$PARTS_JSON" | jq ". + [{\"part_no\": $i, \"etag\": $ETAG}]") done # Finalize → encode → download URL. COMPLETE_BODY=$(jq -n \ --arg uid "$UPLOAD_ID" \ --arg key "$R2_KEY" \ --arg fn "$NAME" \ --arg dom "$DOMAIN" \ --arg mac "$MAC" \ --arg wh "$WEBHOOK" \ --argjson parts "$PARTS_JSON" \ '{upload_id:$uid, r2_key:$key, filename:$fn, domain:$dom, mac:$mac, webhook:$wh, parts:$parts}') curl -sS -X POST "$BASE/upload/$API_KEY/complete" \ -H "Content-Type: application/json" \ -d "$COMPLETE_BODY" echo