Skip to content

Instantly share code, notes, and snippets.

@mluggy
Created April 29, 2026 20:27
Show Gist options
  • Select an option

  • Save mluggy/cb04d7d02e841715ea4ab33b9550ae5a to your computer and use it in GitHub Desktop.

Select an option

Save mluggy/cb04d7d02e841715ea4ab33b9550ae5a to your computer and use it in GitHub Desktop.
iphone copy
#!/usr/bin/env bash
# Copy iPhone photos & videos to ~/Desktop/yuval-bak.
# No installs needed — uses macOS's built-in Image Capture via AppleScript.
#
# Plug iPhone in over USB, unlock it, tap "Trust This Computer", then run:
# bash sync_iphone.sh
#
# It loops, retrying until every file the device exposes has landed locally.
set -u
DEST="${HOME}/Desktop/yuval-bak"
LOG_DIR="${DEST}/.sync"
LOG="${LOG_DIR}/sync.log"
MAX_PASSES=30 # hard cap on retry passes
PASS_DELAY=8 # seconds between passes
PER_FILE_DELAY=0 # set to e.g. 0.1 if your device chokes on rapid reads
mkdir -p "$DEST" "$LOG_DIR"
exec > >(tee -a "$LOG") 2>&1
echo
echo "=== iPhone sync started $(date) ==="
echo "destination: $DEST"
echo
# ---------------------------------------------------------------------------
# One pass: ask Image Capture to download every file that isn't local yet.
# Returns a line like "ok=12 skip=340 fail=2 missing=0" or "ERROR: ...".
# ---------------------------------------------------------------------------
import_pass() {
/usr/bin/osascript - "$DEST" <<'APPLESCRIPT'
on run argv
set destPath to item 1 of argv
set destPosix to POSIX file destPath
set destAlias to destPosix as alias
tell application "Image Capture"
-- find an iOS device / camera
set candidates to {}
try
set candidates to every device whose has camera is true
end try
if (count of candidates) is 0 then
try
set candidates to every device
end try
end if
if (count of candidates) is 0 then
return "ERROR: no device — plug in iPhone, unlock, trust this Mac"
end if
set theCamera to item 1 of candidates
try
set theFiles to every image of theCamera
on error errMsg
return "ERROR: cannot list images: " & errMsg
end try
set total to count of theFiles
set okCount to 0
set skipCount to 0
set failCount to 0
repeat with i from 1 to total
set theFile to item i of theFiles
try
set fname to name of theFile
on error
set fname to "item-" & i
end try
set localPath to destPath & "/" & fname
-- skip if already local (any non-zero file with that name)
set existsLocally to false
try
tell application "System Events"
if exists file localPath then
set existsLocally to (size of file localPath) is greater than 0
end if
end tell
end try
if existsLocally then
set skipCount to skipCount + 1
else
try
download theFile to destAlias
set okCount to okCount + 1
on error errMsg
set failCount to failCount + 1
end try
end if
end repeat
return "total=" & total & " ok=" & okCount & " skip=" & skipCount & " fail=" & failCount
end tell
end run
APPLESCRIPT
}
# ---------------------------------------------------------------------------
# main retry loop
# ---------------------------------------------------------------------------
prev_local_count=-1
stable_passes=0
for pass in $(seq 1 "$MAX_PASSES"); do
echo "--- pass $pass / $MAX_PASSES ---"
result=$(import_pass) || true
echo "$result"
if [[ "$result" == ERROR:* ]]; then
echo " Image Capture error — waiting ${PASS_DELAY}s and retrying."
sleep "$PASS_DELAY"
continue
fi
# count media files in dest so we can detect when nothing new arrives
local_count=$(find "$DEST" -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.heic' \
-o -iname '*.heif' -o -iname '*.mov' -o -iname '*.mp4' -o -iname '*.m4v' \
-o -iname '*.gif' -o -iname '*.tif' -o -iname '*.tiff' -o -iname '*.dng' \
-o -iname '*.aae' -o -iname '*.webp' \) 2>/dev/null | wc -l | tr -d ' ')
echo " local files now: $local_count"
ok=$(echo "$result" | sed -nE 's/.*ok=([0-9]+).*/\1/p')
fail=$(echo "$result" | sed -nE 's/.*fail=([0-9]+).*/\1/p')
# if the pass had no new copies and no failures, we're done
if [[ "${ok:-0}" == "0" && "${fail:-0}" == "0" ]]; then
echo "Nothing left to copy — done."
break
fi
# secondary stop: file count hasn't changed for 3 consecutive passes
if [[ "$local_count" == "$prev_local_count" ]]; then
stable_passes=$((stable_passes + 1))
if [[ $stable_passes -ge 3 ]]; then
echo "File count stable for 3 passes — giving up on the rest."
echo "Anything still failing is likely unreadable on the device."
break
fi
else
stable_passes=0
fi
prev_local_count="$local_count"
sleep "$PASS_DELAY"
done
echo
echo "=== iPhone sync ended $(date) ==="
final=$(find "$DEST" -type f ! -path "*/.sync/*" 2>/dev/null | wc -l | tr -d ' ')
echo "final file count in $DEST: $final"
echo "log: $LOG"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment