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
- Connect your iPhone to your MacBook via USB
- Trust the computer on your iPhone when prompted
- 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
-nfor 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/MMMfolders 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:
| Type | Extensions | Pattern Example |
|---|---|---|
| Photos | .jpg, .heic | *.jpg or *.heic |
| Videos | .mov, .mp4 | *.mov or *.mp4 |
| Screenshots | .png | *.png |
| Live Photos | .heic, .mov | IMG_*.heic + IMG_*.mov |
| All media | all 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:
- Make sure your iPhone is connected via USB cable
- Unlock your iPhone
- Tap “Trust” when prompted on your iPhone
- Run
idevicepair pairif you haven’t already
Failed to Mount iPhone
Error: Failed to mount iPhone
Solution:
- Try unplugging and reconnecting your iPhone
- Check if another process is using the iPhone:
umount /tmp/iphone_mount 2>/dev/null - Restart your iPhone and try again
- 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
FUSE-Related Errors on macOS
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:
- Verify files are on your MacBook
- Check file counts match
- Delete photos from iPhone via Photos app
- 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:
- Download Phase: Copy to temporary file (
.tmp.$$suffix) - Verification Phase: Check file size and optionally compute checksum
- Commit Phase: Atomically move temp file to final destination
- 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:
- Before downloading each file, it checks the state directory
- If a state file exists, it verifies the destination file still exists and matches the checksum
- If verification passes, the file is skipped (no re-download)
- 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
-nflag 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!