Created
April 29, 2026 20:27
-
-
Save mluggy/cb04d7d02e841715ea4ab33b9550ae5a to your computer and use it in GitHub Desktop.
iphone copy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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