← back to Designer Wallcoverings
pending-approval/refresh-product-image.sh
58 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Reusable: replace a Shopify product's image(s) with a local hi-res file.
# GATED PROD WRITE — default = DRY RUN. Destructive (deletes images) only with CONFIRM=1.
#
# Usage:
# PID=<product_id> IMG=<file> ALT="<alt text>" DEL="id1 id2 ..." \
# [CONFIRM=1] bash refresh-product-image.sh
#
# Adds IMG (base64 attachment, position 1, given ALT), then deletes each id in DEL.
# Idempotent on add: skips if an image with IMG's basename is already attached.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SHOP="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
: "${PID:?set PID}"; : "${IMG:?set IMG}"; : "${ALT:?set ALT}"; DEL="${DEL:-}"
[ -f "$IMG" ] || { echo "ERROR: image file not found: $IMG"; exit 1; }
TOKEN="${SHOPIFY_ADMIN_TOKEN:-}"
[ -z "$TOKEN" ] && TOKEN="$(grep -hE '^SHOPIFY_ADMIN_TOKEN=' "$HOME/Projects/secrets-manager/.env" 2>/dev/null | head -1 | cut -d= -f2-)"
[ -z "$TOKEN" ] && { echo "ERROR: no SHOPIFY_ADMIN_TOKEN"; exit 1; }
BASE="https://$SHOP/admin/api/$API"; H=(-H "X-Shopify-Access-Token: $TOKEN"); HJ=(-H "Content-Type: application/json")
DRY=1; [ "${CONFIRM:-0}" = "1" ] && DRY=0
BN="$(basename "$IMG")"
say(){ printf '\n\033[1m%s\033[0m\n' "$*"; }
DIMS="$(sips -g pixelWidth -g pixelHeight "$IMG" 2>/dev/null | awk '/pixelWidth/{w=$2}/pixelHeight/{h=$2}END{print w"x"h}')"
say "Product $PID on $SHOP | image: $BN ($DIMS) | mode: $([ $DRY = 1 ] && echo DRY-RUN || echo LIVE)"
say "0) Current images"
curl -s "${H[@]}" "$BASE/products/$PID.json" \
| python3 -c 'import json,sys;p=json.load(sys.stdin)["product"];print(" title:",p["title"]);[print(" ",i["id"],i["width"],"x",i["height"],i["src"].split("/")[-1]) for i in p["images"]]'
say "1) Add hi-res image (alt: $ALT)"
ALREADY="$(curl -s "${H[@]}" "$BASE/products/$PID.json" | python3 -c "import json,sys;print('yes' if any('${BN%.*}' in (i.get('src') or '') for i in json.load(sys.stdin)['product']['images']) else 'no')")"
if [ "$ALREADY" = yes ]; then echo " already attached — skip add";
elif [ $DRY = 1 ]; then echo " [dry-run] would POST base64 attachment ($(wc -c <"$IMG") bytes) at position 1";
else
PAY=$(mktemp /tmp/rp-img.XXXXXX)
python3 - "$IMG" "$ALT" "$BN" > "$PAY" <<'PY'
import base64,json,sys
b64=base64.b64encode(open(sys.argv[1],'rb').read()).decode()
print(json.dumps({"image":{"attachment":b64,"filename":sys.argv[3],"position":1,"alt":sys.argv[2]}}))
PY
R=$(curl -s -w $'\n%{http_code}' -X POST "$BASE/products/$PID/images.json" "${H[@]}" "${HJ[@]}" --data-binary @"$PAY")
C="${R##*$'\n'}"; B="${R%$'\n'*}"; rm -f "$PAY"; echo " POST HTTP $C"
case "$C" in 200|201) echo "$B" | python3 -c 'import json,sys;i=json.load(sys.stdin)["image"];print(" added",i["id"],i["width"],"x",i["height"])';; *) echo " ERR: $B"; exit 1;; esac
fi
say "2) Delete listed images: ${DEL:-(none)}"
for IID in $DEL; do
if [ $DRY = 1 ]; then echo " [dry-run] would DELETE image $IID";
else curl -s -X DELETE "$BASE/products/$PID/images/$IID.json" "${H[@]}" -o /dev/null -w " delete $IID HTTP %{http_code}\n"; fi
done
say "3) $([ $DRY = 1 ] && echo '(dry-run — re-run with CONFIRM=1 to apply)' || { echo 'Verify'; curl -s "${H[@]}" "$BASE/products/$PID.json" | python3 -c 'import json,sys;p=json.load(sys.stdin)["product"];print(" images:",[(i["id"],i["width"],"x",i["height"]) for i in p["images"]])'; })"