Managing thousands of media files in WordPress becomes tedious through the dashboard. WP-CLI transforms media library management from a manual chore into an automated, scriptable process, saving hours of work and ensuring consistency across large image collections.
Understanding WordPress Media Management
WordPress stores media files in the uploads directory while maintaining database records for each attachment. These records include metadata like file paths, dimensions, captions, alt text, and custom fields. Effective media automation requires managing both filesystem operations and database records simultaneously.
The media library grows rapidly on content-heavy sites. Manual management becomes impractical when dealing with bulk operations like regenerating thumbnails, updating metadata, or migrating files. WP-CLI provides the tools to automate these tasks efficiently.
Bulk Image Import and Upload
Traditional WordPress uploads through the dashboard are slow for large batches. WP-CLI enables efficient bulk imports from local directories or remote URLs.
# Import single image
wp media import /path/to/image.jpg --post_id=123 --title="Image Title" --alt="Alt text"
# Import all images from a directory
wp media import /path/to/images/*.jpg --porcelain
# Import images and assign to specific post
for file in /path/to/images/*.jpg; do
wp media import "$file" --post_id=456 --featured_image
done
# Import from URL
wp media import https://example.com/image.jpg --title="Remote Image"
# Bulk import with metadata from CSV
while IFS=',' read -r filepath title alt caption; do
wp media import "$filepath" --title="$title" --alt="$alt" --caption="$caption"
done < images.csv
Create a comprehensive bulk import script:
#!/bin/bash
# bulk-media-import.sh - Import media with metadata
set -euo pipefail
WP_PATH="/var/www/html"
IMPORT_DIR="/path/to/import"
LOG_FILE="/var/log/media-import-$(date +%Y%m%d).log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Check if import directory exists
if [ ! -d "$IMPORT_DIR" ]; then
log "ERROR: Import directory does not exist: $IMPORT_DIR"
exit 1
fi
log "Starting bulk media import from $IMPORT_DIR"
# Count files to process
TOTAL_FILES=$(find "$IMPORT_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" \) | wc -l)
log "Found $TOTAL_FILES files to import"
IMPORTED=0
FAILED=0
# Process each image
find "$IMPORT_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" \) | while read -r file; do
FILENAME=$(basename "$file")
log "Processing: $FILENAME"
# Extract metadata from filename (example: product-123-description.jpg)
if [[ "$FILENAME" =~ ^([^-]+)-([0-9]+)-(.+)\..+$ ]]; then
TYPE="${BASH_REMATCH[1]}"
POST_ID="${BASH_REMATCH[2]}"
DESCRIPTION="${BASH_REMATCH[3]}"
# Import with metadata
if ATTACHMENT_ID=$(wp --path="$WP_PATH" media import "$file" \
--post_id="$POST_ID" \
--title="$DESCRIPTION" \
--alt="$DESCRIPTION" \
--porcelain 2>&1); then
log "SUCCESS: Imported $FILENAME (ID: $ATTACHMENT_ID)"
((IMPORTED++))
# Move processed file to archive
mkdir -p "$IMPORT_DIR/processed"
mv "$file" "$IMPORT_DIR/processed/"
else
log "ERROR: Failed to import $FILENAME: $ATTACHMENT_ID"
((FAILED++))
fi
else
# Import without post association
if ATTACHMENT_ID=$(wp --path="$WP_PATH" media import "$file" --porcelain 2>&1); then
log "SUCCESS: Imported $FILENAME (ID: $ATTACHMENT_ID)"
((IMPORTED++))
mv "$file" "$IMPORT_DIR/processed/"
else
log "ERROR: Failed to import $FILENAME"
((FAILED++))
fi
fi
done
log "Import completed. Imported: $IMPORTED, Failed: $FAILED"
Thumbnail Regeneration
Changing themes or image sizes requires regenerating thumbnails for existing images. WP-CLI makes this process efficient.
# Regenerate all thumbnails
wp media regenerate --yes
# Regenerate only missing thumbnails
wp media regenerate --only-missing --yes
# Regenerate for specific image sizes
wp media regenerate --image_size=thumbnail,medium --yes
# Regenerate images from specific year
wp media regenerate --yes $(wp post list --post_type=attachment --year=2024 --format=ids)
# Regenerate for specific attachment IDs
wp media regenerate 123 456 789 --yes
# Skip thumbnail generation (only register sizes)
wp media regenerate --skip-delete --yes
Automated thumbnail regeneration with progress tracking:
#!/bin/bash
# regenerate-thumbnails.sh - Regenerate with progress tracking
WP_PATH="/var/www/html"
# Get total attachment count
TOTAL=$(wp --path="$WP_PATH" post list --post_type=attachment --post_mime_type=image --format=count)
echo "Total images to process: $TOTAL"
# Process in batches
BATCH_SIZE=50
PROCESSED=0
while [ $PROCESSED -lt $TOTAL ]; do
echo "Processing batch: $PROCESSED to $((PROCESSED + BATCH_SIZE))"
# Get batch of attachment IDs
IDS=$(wp --path="$WP_PATH" post list \
--post_type=attachment \
--post_mime_type=image \
--posts_per_page=$BATCH_SIZE \
--offset=$PROCESSED \
--format=ids)
if [ -n "$IDS" ]; then
wp --path="$WP_PATH" media regenerate $IDS --yes
fi
PROCESSED=$((PROCESSED + BATCH_SIZE))
echo "Progress: $PROCESSED / $TOTAL ($(( PROCESSED * 100 / TOTAL ))%)"
# Sleep to avoid overloading server
sleep 2
done
echo "Thumbnail regeneration completed"
Image Optimization
Optimize images to reduce file sizes and improve site performance.
# Install optimization plugin
wp plugin install ewww-image-optimizer --activate
# Configure optimization settings
wp option update ewww_image_optimizer_auto true
wp option update ewww_image_optimizer_jpg_level 30
wp option update ewww_image_optimizer_png_level 10
# Optimize all existing images
wp ewwwio optimize all
# Optimize specific images
wp media regenerate --yes --image_size=full
Alternative optimization with ImageMagick:
#!/bin/bash
# optimize-images.sh - Optimize images using ImageMagick
WP_PATH="/var/www/html"
UPLOADS_DIR="$WP_PATH/wp-content/uploads"
QUALITY=85
echo "Optimizing images in $UPLOADS_DIR"
# Find and optimize JPG files
find "$UPLOADS_DIR" -type f -iname "*.jpg" -o -iname "*.jpeg" | while read -r file; do
echo "Optimizing: $file"
# Create backup
cp "$file" "$file.backup"
# Optimize with ImageMagick
convert "$file" -strip -interlace Plane -quality $QUALITY "$file"
# Check if optimization saved space
ORIGINAL_SIZE=$(stat -f%z "$file.backup" 2>/dev/null || stat -c%s "$file.backup")
NEW_SIZE=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")
if [ "$NEW_SIZE" -lt "$ORIGINAL_SIZE" ]; then
SAVED=$((ORIGINAL_SIZE - NEW_SIZE))
echo "Saved: $SAVED bytes"
rm "$file.backup"
else
# Restore if no improvement
mv "$file.backup" "$file"
fi
done
# Optimize PNG files
find "$UPLOADS_DIR" -type f -iname "*.png" | while read -r file; do
echo "Optimizing: $file"
optipng -o2 "$file"
done
echo "Image optimization completed"
Metadata Management
Update alt text, captions, titles, and descriptions programmatically.
# Update alt text for specific attachment
wp post meta update 123 _wp_attachment_image_alt "New alt text"
# Update attachment title
wp post update 123 --post_title="New Title"
# Update caption
wp post update 123 --post_excerpt="New caption"
# Update description
wp post update 123 --post_content="New description"
# Bulk update alt text based on filename
for id in $(wp post list --post_type=attachment --format=ids); do
FILENAME=$(wp post get $id --field=post_title)
# Generate alt text from filename
ALT_TEXT=$(echo "$FILENAME" | sed 's/-/ /g' | sed 's/\.[^.]*$//')
wp post meta update $id _wp_attachment_image_alt "$ALT_TEXT"
echo "Updated alt text for ID $id: $ALT_TEXT"
done
Comprehensive metadata update script:
#!/bin/bash
# update-media-metadata.sh - Bulk metadata updates
WP_PATH="/var/www/html"
CSV_FILE="media-metadata.csv"
# CSV format: attachment_id,title,alt,caption,description
while IFS=',' read -r id title alt caption description; do
# Skip header row
if [ "$id" == "attachment_id" ]; then
continue
fi
echo "Updating attachment ID: $id"
# Update title
if [ -n "$title" ]; then
wp --path="$WP_PATH" post update "$id" --post_title="$title"
fi
# Update alt text
if [ -n "$alt" ]; then
wp --path="$WP_PATH" post meta update "$id" _wp_attachment_image_alt "$alt"
fi
# Update caption
if [ -n "$caption" ]; then
wp --path="$WP_PATH" post update "$id" --post_excerpt="$caption"
fi
# Update description
if [ -n "$description" ]; then
wp --path="$WP_PATH" post update "$id" --post_content="$description"
fi
echo "Metadata updated for attachment $id"
done < "$CSV_FILE"
echo "Bulk metadata update completed"
Media Library Cleanup
Remove unused, broken, or duplicate media files.
# Find unattached media (not assigned to any post)
wp post list --post_type=attachment --post_parent=0 --format=ids
# Delete unattached media
wp post delete $(wp post list --post_type=attachment --post_parent=0 --format=ids) --force
# Find and remove broken attachments (missing files)
for id in $(wp post list --post_type=attachment --format=ids); do
FILE=$(wp post meta get $id _wp_attached_file)
FULL_PATH="/var/www/html/wp-content/uploads/$FILE"
if [ ! -f "$FULL_PATH" ]; then
echo "Missing file for attachment $id: $FILE"
wp post delete $id --force
fi
done
# Find duplicate images by hash
find /var/www/html/wp-content/uploads -type f -exec md5sum {} + | \
sort | \
uniq -w32 -D
Automated cleanup script:
#!/bin/bash
# media-cleanup.sh - Clean up media library
set -euo pipefail
WP_PATH="/var/www/html"
DRY_RUN=false
if [ "${1:-}" == "--dry-run" ]; then
DRY_RUN=true
echo "Running in dry-run mode (no changes will be made)"
fi
echo "=== Media Library Cleanup ==="
# Find orphaned attachments (older than 30 days, never used)
echo "Finding orphaned attachments..."
ORPHANED_IDS=$(wp --path="$WP_PATH" post list \
--post_type=attachment \
--post_parent=0 \
--post_status=inherit \
--format=ids \
--date_query='[{"before":"30 days ago"}]')
if [ -n "$ORPHANED_IDS" ]; then
ORPHANED_COUNT=$(echo "$ORPHANED_IDS" | wc -w)
echo "Found $ORPHANED_COUNT orphaned attachments"
if [ "$DRY_RUN" == false ]; then
echo "Deleting orphaned attachments..."
wp --path="$WP_PATH" post delete $ORPHANED_IDS --force
else
echo "Would delete: $ORPHANED_IDS"
fi
else
echo "No orphaned attachments found"
fi
# Check for broken file links
echo "Checking for broken file links..."
UPLOADS_DIR="$WP_PATH/wp-content/uploads"
BROKEN_COUNT=0
for id in $(wp --path="$WP_PATH" post list --post_type=attachment --format=ids); do
FILE=$(wp --path="$WP_PATH" post meta get $id _wp_attached_file)
FULL_PATH="$UPLOADS_DIR/$FILE"
if [ ! -f "$FULL_PATH" ]; then
echo "Broken attachment $id: $FILE"
((BROKEN_COUNT++))
if [ "$DRY_RUN" == false ]; then
wp --path="$WP_PATH" post delete $id --force
fi
fi
done
echo "Found $BROKEN_COUNT broken attachments"
# Remove unused image sizes
echo "Cleaning up unused image sizes..."
find "$UPLOADS_DIR" -type f -regextype posix-extended \
-regex '.*-[0-9]+x[0-9]+\.(jpg|jpeg|png|gif)$' \
-mtime +90 | head -10
echo "Media cleanup completed"
Featured Image Management
Automate featured image assignment across posts.
# Set featured image for specific post
wp post meta set 123 _thumbnail_id 456
# Assign first attached image as featured image
for post_id in $(wp post list --post_type=post --format=ids); do
# Get first attachment
ATTACHMENT_ID=$(wp post list --post_type=attachment --post_parent=$post_id --posts_per_page=1 --format=ids)
if [ -n "$ATTACHMENT_ID" ]; then
wp post meta set $post_id _thumbnail_id $ATTACHMENT_ID
echo "Set featured image for post $post_id"
fi
done
# Download and set featured image from URL
URL="https://example.com/image.jpg"
POST_ID=123
ATTACHMENT_ID=$(wp media import "$URL" --post_id=$POST_ID --porcelain)
wp post meta set $POST_ID _thumbnail_id $ATTACHMENT_ID
File Organization and Migration
Reorganize uploads directory or migrate media between sites.
#!/bin/bash
# organize-uploads.sh - Organize uploads by year/month
WP_PATH="/var/www/html"
UPLOADS_DIR="$WP_PATH/wp-content/uploads"
# Enable year/month organization
wp --path="$WP_PATH" option update uploads_use_yearmonth_folders 1
# Migrate existing files to year/month structure
for id in $(wp --path="$WP_PATH" post list --post_type=attachment --format=ids); do
# Get attachment date
DATE=$(wp --path="$WP_PATH" post get $id --field=post_date)
YEAR=$(date -d "$DATE" +%Y 2>/dev/null || echo "")
MONTH=$(date -d "$DATE" +%m 2>/dev/null || echo "")
if [ -n "$YEAR" ] && [ -n "$MONTH" ]; then
# Get current file path
FILE=$(wp --path="$WP_PATH" post meta get $id _wp_attached_file)
# Skip if already organized
if [[ "$FILE" =~ ^[0-9]{4}/[0-9]{2}/ ]]; then
continue
fi
# Create target directory
TARGET_DIR="$UPLOADS_DIR/$YEAR/$MONTH"
mkdir -p "$TARGET_DIR"
# Move file
BASENAME=$(basename "$FILE")
if [ -f "$UPLOADS_DIR/$FILE" ]; then
mv "$UPLOADS_DIR/$FILE" "$TARGET_DIR/$BASENAME"
# Update database
NEW_PATH="$YEAR/$MONTH/$BASENAME"
wp --path="$WP_PATH" post meta update $id _wp_attached_file "$NEW_PATH"
echo "Moved: $FILE -> $NEW_PATH"
fi
fi
done
echo "Upload organization completed"
Media Export and Backup
Create backups of media library with metadata.
#!/bin/bash
# backup-media.sh - Backup media library
WP_PATH="/var/www/html"
BACKUP_DIR="/backups/media"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="$BACKUP_DIR/media-backup-$TIMESTAMP"
mkdir -p "$BACKUP_PATH"
echo "Starting media backup..."
# Export attachment metadata
wp --path="$WP_PATH" post list \
--post_type=attachment \
--format=json > "$BACKUP_PATH/attachments-metadata.json"
# Backup uploads directory
tar -czf "$BACKUP_PATH/uploads.tar.gz" \
-C "$WP_PATH/wp-content" \
uploads/
# Get backup size
BACKUP_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1)
echo "Backup completed: $BACKUP_SIZE at $BACKUP_PATH"
# Clean old backups (keep last 7)
ls -t "$BACKUP_DIR" | tail -n +8 | xargs -I {} rm -rf "$BACKUP_DIR/{}"

Leave a Reply