A Script to download Photos, Videos and Images from your iPhone to your Macbook (by creation date and a file name filter)

Annoying Apple never quite got around to making it easy to offload images from your iPhone to your Macbook. So below is a complete guide to automatically download photos and videos from your iPhone to your MacBook, with options to filter by pattern and date, and organize into folders by creation date.

Prerequisites

Install the required tools using Homebrew:

cat > install_iphone_util.sh << 'EOF'
#!/bin/bash
set -e

echo "Installing tools..."

echo "Installing macFUSE"
brew install --cask macfuse

echo "Adding Brew Tap" 
brew tap gromgit/fuse

echo "Installing ifuse-mac" 
brew install gromgit/fuse/ifuse-mac

echo "Installing libimobiledevice" 
brew install libimobiledevice

echo "Installing exiftool"
brew install exiftool

echo "Done! Tools installed."
EOF

echo "Making executable..."
chmod +x install_iphone_util.sh

./install_iphone_util.sh

Setup/Pair your iPhone to your Macbook

  1. Connect your iPhone to your MacBook via USB
  2. Trust the computer on your iPhone when prompted
  3. Verify the connection:
idevicepair validate

If not paired, run:

idevicepair pair

Download Script

Run the script below to create the file download-iphone-media.sh in your current directory:

#!/bin/bash
cat > download-iphone-media.sh << 'OUTER_EOF'
#!/bin/bash
# iPhone Media Downloader
# Downloads photos and videos from iPhone to MacBook
# Supports resumable, idempotent downloads
set -e
# Default values
PATTERN="*"
OUTPUT_DIR="."
ORGANIZE_BY_DATE=false
START_DATE=""
END_DATE=""
MOUNT_POINT="/tmp/iphone_mount"
STATE_DIR=""
VERIFY_CHECKSUM=true
# Usage function
usage() {
cat << 'INNER_EOF'
Usage: $0 [OPTIONS]
Download photos and videos from iPhone to MacBook.
OPTIONS:
-p PATTERN          File pattern to match (e.g., "*.jpg", "*.mp4", "IMG_*")
Default: * (all files)
-o OUTPUT_DIR       Output directory (default: current directory)
-d                  Organize files by creation date into YYYY/MMM folders
-s START_DATE       Start date filter (YYYY-MM-DD)
-e END_DATE         End date filter (YYYY-MM-DD)
-r                  Resume incomplete downloads (default: true)
-n                  Skip checksum verification (faster, less safe)
-h                  Show this help message
EXAMPLES:
# Download all photos and videos to current directory
$0
# Download only JPG files to ~/Pictures/iPhone
$0 -p "*.jpg" -o ~/Pictures/iPhone
# Download all media organized by date
$0 -d -o ~/Pictures/iPhone
# Download videos from specific date range
$0 -p "*.mov" -s 2025-01-01 -e 2025-01-31 -d -o ~/Videos/iPhone
# Download specific IMG files organized by date
$0 -p "IMG_*.{jpg,heic}" -d -o ~/Photos
INNER_EOF
exit 1
}
# Parse command line arguments
while getopts "p:o:ds:e:rnh" opt; do
case $opt in
p) PATTERN="$OPTARG" ;;
o) OUTPUT_DIR="$OPTARG" ;;
d) ORGANIZE_BY_DATE=true ;;
s) START_DATE="$OPTARG" ;;
e) END_DATE="$OPTARG" ;;
r) ;; # Resume is default, keeping for backward compatibility
n) VERIFY_CHECKSUM=false ;;
h) usage ;;
*) usage ;;
esac
done
# Create output directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"
OUTPUT_DIR=$(cd "$OUTPUT_DIR" && pwd)
# Set up state directory for tracking downloads
STATE_DIR="$OUTPUT_DIR/.iphone_download_state"
mkdir -p "$STATE_DIR"
# Create mount point
mkdir -p "$MOUNT_POINT"
echo "=== iPhone Media Downloader ==="
echo "Pattern: $PATTERN"
echo "Output: $OUTPUT_DIR"
echo "Organize by date: $ORGANIZE_BY_DATE"
[ -n "$START_DATE" ] && echo "Start date: $START_DATE"
[ -n "$END_DATE" ] && echo "End date: $END_DATE"
echo ""
# Check if iPhone is connected
echo "Checking for iPhone connection..."
if ! ideviceinfo -s > /dev/null 2>&1; then
echo "Error: No iPhone detected. Please connect your iPhone and trust this computer."
exit 1
fi
# Mount iPhone
echo "Mounting iPhone..."
if ! ifuse "$MOUNT_POINT" 2>/dev/null; then
echo "Error: Failed to mount iPhone. Make sure you've trusted this computer on your iPhone."
exit 1
fi
# Cleanup function
cleanup() {
local exit_code=$?
echo ""
if [ $exit_code -ne 0 ]; then
echo "⚠ Download interrupted. Run the script again to resume."
fi
echo "Unmounting iPhone..."
umount "$MOUNT_POINT" 2>/dev/null || true
rmdir "$MOUNT_POINT" 2>/dev/null || true
}
trap cleanup EXIT
# Find DCIM folder
DCIM_PATH="$MOUNT_POINT/DCIM"
if [ ! -d "$DCIM_PATH" ]; then
echo "Error: DCIM folder not found on iPhone"
exit 1
fi
echo "Scanning for files matching pattern: $PATTERN"
echo ""
# Counter
TOTAL_FILES=0
COPIED_FILES=0
SKIPPED_FILES=0
RESUMED_FILES=0
FAILED_FILES=0
# Function to compute file checksum
compute_checksum() {
local file="$1"
if [ -f "$file" ]; then
shasum -a 256 "$file" 2>/dev/null | awk '{print $1}'
fi
}
# Function to get file size
get_file_size() {
local file="$1"
if [ -f "$file" ]; then
stat -f "%z" "$file" 2>/dev/null
fi
}
# Function to mark file as completed
mark_completed() {
local source_file="$1"
local dest_file="$2"
local checksum="$3"
local state_file="$STATE_DIR/$(echo "$source_file" | shasum -a 256 | awk '{print $1}')"
echo "$dest_file|$checksum|$(date +%s)" > "$state_file"
}
# Function to check if file was previously completed
is_completed() {
local source_file="$1"
local dest_file="$2"
local state_file="$STATE_DIR/$(echo "$source_file" | shasum -a 256 | awk '{print $1}')"
if [ ! -f "$state_file" ]; then
return 1
fi
# Read state file
local saved_dest saved_checksum saved_timestamp
IFS='|' read -r saved_dest saved_checksum saved_timestamp < "$state_file"
# Check if destination file exists and matches
if [ "$saved_dest" = "$dest_file" ] && [ -f "$dest_file" ]; then
if [ "$VERIFY_CHECKSUM" = true ]; then
local current_checksum=$(compute_checksum "$dest_file")
if [ "$current_checksum" = "$saved_checksum" ]; then
return 0
fi
else
# Without checksum verification, just check file exists
return 0
fi
fi
return 1
}
# Convert dates to timestamps for comparison
START_TIMESTAMP=""
END_TIMESTAMP=""
if [ -n "$START_DATE" ]; then
START_TIMESTAMP=$(date -j -f "%Y-%m-%d" "$START_DATE" "+%s" 2>/dev/null || echo "")
if [ -z "$START_TIMESTAMP" ]; then
echo "Error: Invalid start date format. Use YYYY-MM-DD"
exit 1
fi
fi
if [ -n "$END_DATE" ]; then
END_TIMESTAMP=$(date -j -f "%Y-%m-%d" "$END_DATE" "+%s" 2>/dev/null || echo "")
if [ -z "$END_TIMESTAMP" ]; then
echo "Error: Invalid end date format. Use YYYY-MM-DD"
exit 1
fi
# Add 24 hours to include the entire end date
END_TIMESTAMP=$((END_TIMESTAMP + 86400))
fi
# Process files
find "$DCIM_PATH" -type f | while read -r file; do
filename=$(basename "$file")
# Check if filename matches pattern (basic glob matching)
if [[ ! "$filename" == $PATTERN ]]; then
continue
fi
TOTAL_FILES=$((TOTAL_FILES + 1))
# Get file creation date
if command -v exiftool > /dev/null 2>&1; then
# Try to get date from EXIF data
CREATE_DATE=$(exiftool -s3 -DateTimeOriginal -d "%Y-%m-%d %H:%M:%S" "$file" 2>/dev/null)
if [ -z "$CREATE_DATE" ]; then
# Fallback to file modification time
CREATE_DATE=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$file" 2>/dev/null)
fi
else
# Use file modification time
CREATE_DATE=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$file" 2>/dev/null)
fi
# Extract date components
if [ -n "$CREATE_DATE" ]; then
FILE_DATE=$(echo "$CREATE_DATE" | cut -d' ' -f1)
FILE_TIMESTAMP=$(date -j -f "%Y-%m-%d" "$FILE_DATE" "+%s" 2>/dev/null || echo "")
# Check date filters
if [ -n "$START_TIMESTAMP" ] && [ -n "$FILE_TIMESTAMP" ] && [ "$FILE_TIMESTAMP" -lt "$START_TIMESTAMP" ]; then
SKIPPED_FILES=$((SKIPPED_FILES + 1))
continue
fi
if [ -n "$END_TIMESTAMP" ] && [ -n "$FILE_TIMESTAMP" ] && [ "$FILE_TIMESTAMP" -ge "$END_TIMESTAMP" ]; then
SKIPPED_FILES=$((SKIPPED_FILES + 1))
continue
fi
# Determine output path with YYYY/MMM structure
if [ "$ORGANIZE_BY_DATE" = true ]; then
YEAR=$(echo "$FILE_DATE" | cut -d'-' -f1)
MONTH_NUM=$(echo "$FILE_DATE" | cut -d'-' -f2)
# Convert month number to 3-letter abbreviation
case "$MONTH_NUM" in
01) MONTH="Jan" ;;
02) MONTH="Feb" ;;
03) MONTH="Mar" ;;
04) MONTH="Apr" ;;
05) MONTH="May" ;;
06) MONTH="Jun" ;;
07) MONTH="Jul" ;;
08) MONTH="Aug" ;;
09) MONTH="Sep" ;;
10) MONTH="Oct" ;;
11) MONTH="Nov" ;;
12) MONTH="Dec" ;;
*) MONTH="Unknown" ;;
esac
DEST_DIR="$OUTPUT_DIR/$YEAR/$MONTH"
else
DEST_DIR="$OUTPUT_DIR"
fi
else
DEST_DIR="$OUTPUT_DIR"
fi
# Create destination directory
mkdir -p "$DEST_DIR"
# Determine destination path
DEST_PATH="$DEST_DIR/$filename"
# Check if this file was previously completed successfully
if is_completed "$file" "$DEST_PATH"; then
echo "✓ Already downloaded: $filename"
SKIPPED_FILES=$((SKIPPED_FILES + 1))
continue
fi
# Check if file already exists with same content (for backward compatibility)
if [ -f "$DEST_PATH" ]; then
if cmp -s "$file" "$DEST_PATH"; then
echo "✓ Already exists (identical): $filename"
# Mark as completed for future runs
SOURCE_CHECKSUM=$(compute_checksum "$DEST_PATH")
mark_completed "$file" "$DEST_PATH" "$SOURCE_CHECKSUM"
SKIPPED_FILES=$((SKIPPED_FILES + 1))
continue
else
# Add timestamp to avoid overwriting different file
BASE="${filename%.*}"
EXT="${filename##*.}"
DEST_PATH="$DEST_DIR/${BASE}_$(date +%s).$EXT"
fi
fi
# Use temporary file for atomic copy
TEMP_PATH="${DEST_PATH}.tmp.$$"
# Copy to temporary file
echo "⬇ Downloading: $filename → $DEST_PATH"
if ! cp "$file" "$TEMP_PATH" 2>/dev/null; then
echo "✗ Failed to copy: $filename"
rm -f "$TEMP_PATH"
FAILED_FILES=$((FAILED_FILES + 1))
continue
fi
# Verify size matches (basic corruption check)
SOURCE_SIZE=$(get_file_size "$file")
TEMP_SIZE=$(get_file_size "$TEMP_PATH")
if [ "$SOURCE_SIZE" != "$TEMP_SIZE" ]; then
echo "✗ Size mismatch for $filename (source: $SOURCE_SIZE, copied: $TEMP_SIZE)"
rm -f "$TEMP_PATH"
FAILED_FILES=$((FAILED_FILES + 1))
continue
fi
# Compute checksum for verification and tracking
if [ "$VERIFY_CHECKSUM" = true ]; then
SOURCE_CHECKSUM=$(compute_checksum "$TEMP_PATH")
else
SOURCE_CHECKSUM="skipped"
fi
# Preserve timestamps
if [ -n "$CREATE_DATE" ]; then
touch -t $(date -j -f "%Y-%m-%d %H:%M:%S" "$CREATE_DATE" "+%Y%m%d%H%M.%S" 2>/dev/null) "$TEMP_PATH" 2>/dev/null || true
fi
# Atomic move from temp to final destination
if mv "$TEMP_PATH" "$DEST_PATH" 2>/dev/null; then
echo "✓ Completed: $filename"
# Mark as successfully completed
mark_completed "$file" "$DEST_PATH" "$SOURCE_CHECKSUM"
COPIED_FILES=$((COPIED_FILES + 1))
else
echo "✗ Failed to finalize: $filename"
rm -f "$TEMP_PATH"
FAILED_FILES=$((FAILED_FILES + 1))
fi
done
echo ""
echo "=== Summary ==="
echo "Total files matching pattern: $TOTAL_FILES"
echo "Files downloaded: $COPIED_FILES"
echo "Files already present: $SKIPPED_FILES"
if [ $FAILED_FILES -gt 0 ]; then
echo "Files failed: $FAILED_FILES"
echo ""
echo "⚠ Some files failed to download. Run the script again to retry."
exit 1
fi
echo ""
echo "✓ Download complete! All files transferred successfully."
OUTER_EOF
echo "Making the script executable..."
chmod +x download-iphone-media.sh
echo "✓ Script created successfully: download-iphone-media.sh"

Usage Examples

Basic Usage

Download all photos and videos to the current directory:

./download-iphone-media.sh

Download with Date Organization

Organize files into folders by creation date (YYYY/MMM structure):

./download-iphone-media.sh -d -o ./Pictures

This creates a structure like:

./Pictures
├── 2024/
│   ├── Jan/
│   │   ├── IMG_1234.jpg
│   │   └── IMG_1235.heic
│   ├── Feb/
│   └── Dec/
├── 2025/
│   ├── Jan/
│   └── Nov/

Filter by File Pattern

Download only specific file types:

# Only JPG files
./download-iphone-media.sh -p "*.jpg" -o ~/Pictures/iPhone
# Only videos (MOV and MP4)
./download-iphone-media.sh -p "*.mov" -o ~/Videos/iPhone
./download-iphone-media.sh -p "*.mp4" -o ~/Videos/iPhone
# Files starting with IMG_
./download-iphone-media.sh -p "IMG_*" -o ~/Pictures
# HEIC photos (iPhone's default format)
./download-iphone-media.sh -p "*.heic" -o ~/Pictures/iPhone

Filter by Date Range

Download photos from a specific date range:

# Photos from January 2025
./download-iphone-media.sh -s 2025-01-01 -e 2025-01-31 -d -o ~/Pictures/January2025
# Photos from last week
./download-iphone-media.sh -s 2025-11-10 -e 2025-11-17 -o ~/Pictures/LastWeek
# Photos after a specific date
./download-iphone-media.sh -s 2025-11-01 -o ~/Pictures/Recent

Combined Filters

Combine multiple options for precise control:

# Download only videos from January 2025, organized by date
./download-iphone-media.sh -p "*.mov" -s 2025-01-01 -e 2025-01-31 -d -o ~/Videos/Vacation
# Download all HEIC photos from the last month, organized by date
./download-iphone-media.sh -p "*.heic" -s 2025-10-17 -e 2025-11-17 -d -o ~/Pictures/LastMonth

Features

Resumable & Idempotent Downloads

  • Crash recovery: Interrupted downloads can be resumed by running the script again
  • Atomic operations: Files are copied to temporary locations first, then moved atomically
  • State tracking: Maintains a hidden state directory (.iphone_download_state) to track completed files
  • Checksum verification: Uses SHA-256 checksums to verify file integrity (can be disabled with -n for speed)
  • No duplicates: Running the script multiple times won’t re-download existing files
  • Corruption detection: Validates file sizes and optionally checksums after copy

Date-Based Organization

  • Automatic folder structure: Creates YYYY/MMM folders based on photo creation date (e.g., 2025/Jan, 2025/Feb)
  • EXIF data support: Reads actual photo capture date from EXIF metadata when available
  • Fallback mechanism: Uses file modification time if EXIF data is unavailable
  • Fewer folders: Maximum 12 month folders per year instead of up to 365 day folders

Smart File Handling

  • Duplicate detection: Skips files that already exist with identical content
  • Conflict resolution: Adds timestamp suffix to filename if different file with same name exists
  • Timestamp preservation: Maintains original creation dates on copied files
  • Error tracking: Reports failed files and provides clear exit codes

Progress Feedback

  • Real-time progress updates showing each file being downloaded
  • Summary statistics at the end (total found, downloaded, skipped, failed)
  • Clear error messages for troubleshooting
  • Helpful resume instructions if interrupted

Common File Patterns

iPhone typically uses these file formats:

TypeExtensionsPattern Example
Photos.jpg.heic*.jpg or *.heic
Videos.mov.mp4*.mov or *.mp4
Screenshots.png*.png
Live Photos.heic.movIMG_*.heic + IMG_*.mov
All mediaall above* (default)

5. Handling Interrupted Downloads

If a download is interrupted (disconnection, error, etc.), simply run the script again:

# Script was interrupted - just run it again
./download-iphone-media.sh -d -o ~/Pictures/iPhone

The script will:

  • Skip all successfully downloaded files
  • Retry any failed files
  • Continue from where it left off

6. Fast Mode (Skip Checksum Verification)

For faster transfers on reliable connections, disable checksum verification:

# Skip checksums for speed (still verifies file sizes)
./download-iphone-media.sh -n -d -o ~/Pictures/iPhone

Note: This is generally safe but won’t detect corruption as thoroughly.

7. Clean State and Re-download

If you want to force a re-download of all files:

# Remove state directory to start fresh
rm -rf ~/Pictures/iPhone/.iphone_download_state
./download-iphone-media.sh -d -o ~/Pictures/iPhone

Troubleshooting

iPhone Not Detected

Error: No iPhone detected. Please connect your iPhone and trust this computer.

Solution:

  1. Make sure your iPhone is connected via USB cable
  2. Unlock your iPhone
  3. Tap “Trust” when prompted on your iPhone
  4. Run idevicepair pair if you haven’t already

Failed to Mount iPhone

Error: Failed to mount iPhone

Solution:

  1. Try unplugging and reconnecting your iPhone
  2. Check if another process is using the iPhone:umount /tmp/iphone_mount 2>/dev/null
  3. Restart your iPhone and try again
  4. On macOS Ventura or later, check System Settings → Privacy & Security → Files and Folders

Permission Denied

Solution:
Make sure the script has executable permissions:

chmod +x download-iphone-media.sh

Missing Tools

Error: Commands not found

Solution:
Install the required tools:

brew install libimobiledevice ifuse exiftool

On newer macOS versions, you may need to install macFUSE:

brew install --cask macfuse

After installation, you may need to restart your Mac and allow the kernel extension in System Settings → Privacy & Security.

Tips and Best Practices

1. Regular Backups

Create a scheduled backup script:

#!/bin/bash
# Save as ~/bin/backup-iphone-photos.sh
DATE=$(date +%Y-%m-%d)
BACKUP_DIR=~/Pictures/iPhone-Backups/$DATE
./download-iphone-media.sh -d -o "$BACKUP_DIR"
echo "Backup completed to $BACKUP_DIR"

2. Incremental Downloads

The script is fully idempotent and tracks completed downloads, making it perfect for incremental backups:

# Run daily to get new photos - only new files will be downloaded
./download-iphone-media.sh -d -o ~/Pictures/iPhone

The script maintains state in .iphone_download_state/ within your output directory, ensuring:

  • Already downloaded files are skipped instantly (no re-copying)
  • Interrupted downloads can be resumed
  • File integrity is verified with checksums

3. Free Up iPhone Storage

After confirming successful download:

  1. Verify files are on your MacBook
  2. Check file counts match
  3. Delete photos from iPhone via Photos app
  4. Empty “Recently Deleted” album

4. Convert HEIC to JPG (Optional)

If you need JPG files for compatibility:

# Install ImageMagick
brew install imagemagick
# Convert all HEIC files to JPG
find ~/Pictures/iPhone -name "*.heic" -exec sh -c 'magick "$0" "${0%.heic}.jpg"' {} \;

How Idempotent Recovery Works

The script implements several mechanisms to ensure safe, resumable downloads:

1. State Tracking

A hidden directory .iphone_download_state/ is created in your output directory. For each successfully downloaded file, a state file is created containing:

  • Destination file path
  • SHA-256 checksum (if verification enabled)
  • Completion timestamp

2. Atomic Operations

Each file is downloaded using a two-phase commit:

  1. Download Phase: Copy to temporary file (.tmp.$$ suffix)
  2. Verification Phase: Check file size and optionally compute checksum
  3. Commit Phase: Atomically move temp file to final destination
  4. Record Phase: Write completion state

If the script is interrupted at any point, incomplete temporary files are cleaned up automatically.

3. Idempotent Behavior

When you run the script:

  1. Before downloading each file, it checks the state directory
  2. If a state file exists, it verifies the destination file still exists and matches the checksum
  3. If verification passes, the file is skipped (no re-download)
  4. If verification fails or no state exists, the file is downloaded

This means:

  • ✓ Safe to run multiple times
  • ✓ Interrupted downloads can be resumed
  • ✓ Corrupted files are detected and re-downloaded
  • ✓ No wasted bandwidth on already-downloaded files

4. Checksum Verification

By default, SHA-256 checksums are computed and verified:

  • During download: Checksum computed after copy completes
  • On resume: Existing files are verified against stored checksum
  • Optional: Use -n flag to skip checksums for speed (still verifies file sizes)

Example Recovery Scenario

# Start downloading 1000 photos
./download-iphone-media.sh -d -o ~/Pictures/iPhone
# Script is interrupted after 500 files
# Press Ctrl+C or cable disconnects
# Simply run again - picks up where it left off
./download-iphone-media.sh -d -o ~/Pictures/iPhone
# Output:
# ✓ Already downloaded: IMG_0001.heic
# ✓ Already downloaded: IMG_0002.heic
# ...
# ⬇ Downloading: IMG_0501.heic → ~/Pictures/iPhone/2025/Jan/IMG_0501.heic

Performance Notes

  • Transfer speed: Depends on USB connection (USB 2.0 vs USB 3.0)
  • Large libraries: May take significant time for thousands of photos
  • EXIF reading: Adds minimal overhead but provides accurate dates
  • Pattern matching: Processed client-side, so all files are scanned

Conclusion

This script provides a robust, production-ready solution for downloading photos and videos from your iPhone to your MacBook. Key capabilities:

Core Features:

  • Filter by file patterns (type, name)
  • Filter by date ranges
  • Organize automatically into date-based folders
  • Preserve original file metadata

Reliability:

  • Fully idempotent – safe to run multiple times
  • Resumable downloads with automatic crash recovery
  • Atomic file operations prevent corruption
  • Checksum verification ensures data integrity
  • Clear error reporting and recovery instructions

For regular use, consider creating aliases in your ~/.zshrc:

# Add to ~/.zshrc
alias iphone-backup='~/download-iphone-media.sh -d -o ~/Pictures/iPhone'
alias iphone-videos='~/download-iphone-media.sh -p "*.mov" -d -o ~/Videos/iPhone'

Then simply run iphone-backup whenever you want to download your photos!

Resources

Leave a Reply

Your email address will not be published. Required fields are marked *