Skip to content

Instantly share code, notes, and snippets.

@christianwach
Last active October 30, 2025 13:59
Show Gist options
  • Select an option

  • Save christianwach/1adc33da22515471c749f5ba55e8726a to your computer and use it in GitHub Desktop.

Select an option

Save christianwach/1adc33da22515471c749f5ba55e8726a to your computer and use it in GitHub Desktop.
Remove "ms-files.php" dependency from a WordPress Multi-Network install.
#!/bin/sh
# ----------------------------------------------------------------------------------------
# Title: Remove "ms-files.php" dependency.
# Description: Removes the "ms-files.php" dependency from a WordPress Multi-Network install.
# This script will also work on a single WordPress Multisite instance because
# in that case there will only be one record in the `wp_sites` table.
# Author: Christian Wach <[email protected]>
# Props: Austin Ginder <https://anchor.host/removing-legacy-ms-files-php-from-multisite/>
# Version: 1.0.0
# Gist URL: https://gist.github.com/christianwach/1adc33da22515471c749f5ba55e8726a
# ----------------------------------------------------------------------------------------
# Where is your WP-CLI? Just put "wp" if it's already aliased.
wp_cli="/path/to/wp-cli.phar"
# WordPress Primary Network URL.
# In WordPress Multisite, this is the Main Site URL.
wp_main_site="https://example.org"
# WordPress database prefix.
wp_db_prefix="wp_"
# Where is your project root directory?
project_root="/path/to/project/root"
# WordPress root directory.
wp_root="${project_root}/httpdocs"
# WordPress directory locations.
wp_content="${wp_root}/wp-content"
wp_mu_plugins="${wp_content}/mu-plugins"
wp_plugins="${wp_content}/plugins"
wp_themes="${wp_content}/themes"
wp_uploads="${wp_content}/uploads"
wp_sites="${wp_uploads}/sites"
wp_blogs_dir="${wp_content}/blogs.dir"
# That's all, stop editing! Happy upgrading.
# ----------------------------------------------------------------------------------------
# Set default modifier vars.
# ----------------------------------------------------------------------------------------
# For safety, make "dry run" the default.
dry_run="1"
if [ "$1" = "-live" ]; then
dry_run="0"
fi
# Make feedback the default.
feedback="1"
if [ "$1" = "-quiet" ]; then
feedback="0"
fi
# We need a minimum of the URL param to use WP-CLI.
wp_init="${wp_cli} --url=${wp_main_site}"
# ----------------------------------------------------------------------------------------
# Update options for all Sites.
# ----------------------------------------------------------------------------------------
if [[ ${feedback} == "1" ]]; then
echo ""
echo "--------------------------------------------------------------------------------"
echo "Updating options for all Sites."
echo "--------------------------------------------------------------------------------"
echo ""
echo "Changing directory to: ${wp_root}"
echo ""
fi
cd ${wp_root}
# # Loop through all Sites.
site_urls=$( ${wp_init} site list --field=url )
for site_url in ${site_urls}; do
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Site URL: ${site_url}"
fi
#
# Clear legacy 'upload_path' option.
#
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Setting 'upload_path' to empty default."
if [[ ${dry_run} == "1" ]]; then
echo "${wp_cli} option set upload_path '' --url=${site_url}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
${wp_cli} option set upload_path "" --url=${site_url}
fi
#
# Clear legacy 'upload_url_path' option.
#
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Setting 'upload_url_path' to empty default."
if [[ ${dry_run} == "1" ]]; then
echo "${wp_cli} option set upload_url_path '' --url=${site_url}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
${wp_cli} option set upload_url_path "" --url=${site_url}
fi
#
# Remove legacy 'fileupload_url' option.
#
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Deleting legacy 'fileupload_url' option."
if [[ ${dry_run} == "1" ]]; then
echo "${wp_cli} option delete fileupload_url --url=${site_url}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
${wp_cli} option delete fileupload_url --url=${site_url}
fi
done
# ----------------------------------------------------------------------------------------
# Search and replace asset URLs on all Sites.
# ----------------------------------------------------------------------------------------
if [[ ${feedback} == "1" ]]; then
echo ""
echo "--------------------------------------------------------------------------------"
echo "Search and replace asset URLs on all Sites."
echo "--------------------------------------------------------------------------------"
echo ""
echo "Changing directory to: ${wp_root}"
echo ""
fi
cd ${wp_root}
# # Correct urls for each site
site_ids=$( ${wp_init} site list --field=blog_id )
for site_id in ${site_ids}; do
#
# Skip Main Network.
#
# Note: The ID should ideally be checked by calling `is_main_network()` because
# it can be modified via:
#
# * The 'PRIMARY_NETWORK_ID' constant.
# * The 'get_main_network_id' filter.
#
# My install does neither if these things.
#
# @see https://developer.wordpress.org/reference/functions/is_main_network/
# @see https://developer.wordpress.org/reference/functions/get_main_network_id/
#
if [[ ${site_id} == "1" ]]; then
continue
fi
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Site ID: ${site_id}"
fi
# Require Home URL to end in a trailing slash.
home_url=$( ${wp_init} db query "SELECT option_value from ${wp_db_prefix}${site_id}_options where option_name = 'home';" --skip-column-names --batch )
home_url="${home_url%/}/"
# Sanity check.
if [[ ${home_url} == "https"* ]]; then
# Search-and-replace 'files' URLs.
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Search and replace 'files' URLs."
echo "Search for: ${home_url}files/"
echo "Replace with: ${home_url}wp-content/uploads/sites/${site_id}/"
if [[ ${dry_run} == "1" ]]; then
echo "${wp_init} search-replace ${home_url}files/ ${home_url}wp-content/uploads/sites/${site_id}/ --all-tables --report-changed-only"
fi
fi
if [[ ${dry_run} == "0" ]]; then
# Search-and-replace in all tables, because assets can be referenced on any Site.
${wp_init} search-replace ${home_url}files/ ${home_url}wp-content/uploads/sites/${site_id}/ --all-tables --report-changed-only
fi
# Search-and-replace 'blogs.dir' URLs.
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Search and replace 'blogs.dir' URLs."
echo "Search for: ${home_url}wp-content/blogs.dir/${site_id}/files/"
echo "Replace with: ${home_url}wp-content/uploads/sites/${site_id}/"
if [[ ${dry_run} == "1" ]]; then
echo "${wp_init} search-replace ${home_url}wp-content/blogs.dir/${site_id}/files/ ${home_url}wp-content/uploads/sites/${site_id}/ --all-tables --report-changed-only"
fi
fi
if [[ ${dry_run} == "0" ]]; then
# Search-and-replace in all tables, because assets can be referenced on any Site.
${wp_init} search-replace ${home_url}wp-content/blogs.dir/${site_id}/files/ ${home_url}wp-content/uploads/sites/${site_id}/ --all-tables --report-changed-only
fi
fi
# Remove all transients.
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Deleting transients."
if [[ ${dry_run} == "1" ]]; then
echo "${wp_init} db query \"DELETE FROM ${wp_db_prefix}${site_id}_options WHERE option_name LIKE '%_transient_%';\""
fi
fi
if [[ ${dry_run} == "0" ]]; then
${wp_init} db query "DELETE FROM ${wp_db_prefix}${site_id}_options WHERE option_name LIKE '%_transient_%';"
fi
done
# ----------------------------------------------------------------------------------------
# Make sure the 'uploads/sites' directory exists.
# ----------------------------------------------------------------------------------------
if [[ ${feedback} == "1" ]]; then
echo ""
echo "--------------------------------------------------------------------------------"
echo "Make sure the 'uploads/sites' directory exists."
echo "--------------------------------------------------------------------------------"
echo ""
fi
# Make sure the 'uploads/sites' directory exists.
if [ -d "${wp_sites}" ]; then
if [[ ${feedback} == "1" ]]; then
echo "Directory '${wp_sites}' already exists"
fi
else
if [[ ${feedback} == "1" ]]; then
echo "Creating directory '${wp_sites}'"
if [[ ${dry_run} == "1" ]]; then
echo "mkdir -p ${wp_sites}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
mkdir -p ${wp_sites}
fi
fi
# ----------------------------------------------------------------------------------------
# Move 'blogs.dir/*/files' directories to 'uploads/sites/*'.
# ----------------------------------------------------------------------------------------
if [[ ${feedback} == "1" ]]; then
echo ""
echo "--------------------------------------------------------------------------------"
echo "Move 'blogs.dir/*/files' directories to 'uploads/sites/*'."
echo "--------------------------------------------------------------------------------"
echo ""
echo "Changing directory to: ${wp_blogs_dir}"
echo ""
fi
cd $wp_blogs_dir
# Loop through all the Site IDs that have their assets here.
for site_id in */; do
# Verify that the Site ID is not empty.
if [[ ${site_id} == "" ]]; then
continue
fi
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Site ID: ${site_id}"
echo ""
fi
#
# Main Network: attempt to move files to 'wp-content/uploads'.
#
# Note: The ID should ideally be checked by calling `is_main_network()` because
# it can be modified via:
#
# * The 'PRIMARY_NETWORK_ID' constant.
# * The 'get_main_network_id' filter.
#
# My install does neither if these things.
#
# @see https://developer.wordpress.org/reference/functions/is_main_network/
# @see https://developer.wordpress.org/reference/functions/get_main_network_id/
#
if [[ ${site_id} == "1/" ]]; then
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Top level site found. Attempting to move files."
echo "Please verify that '${wp_blogs_dir}/1/' is empty."
fi
# Move any top level files or folders.
for handle in $( ls ${site_id}files/ ); do
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Moving ${site_id}files/${handle} to ${wp_uploads}"
if [[ ${dry_run} == "1" ]]; then
echo "mv ${site_id}files/${handle} ${wp_uploads}/"
fi
fi
if [[ ${dry_run} == "0" ]]; then
mv ${site_id}files/${handle} ${wp_uploads}/
fi
done
continue
fi
#
# Not Main Network: move files to 'wp-content/uploads/sites/*'.
#
# This is the same regardless of whether the Site is the Main Site for a Network
# or, if not, which Network the Site is assigned to.
#
# Delete the 'uploads/sites/*' directory if it exists.
if [ -d "${wp_sites}/${site_id}" ]; then
if [[ ${feedback} == "1" ]]; then
echo "Directory '${wp_sites}/${site_id}' already exists - deleting it..."
if [[ ${dry_run} == "1" ]]; then
echo "rm -rf ${wp_sites}/${site_id}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
rm -rf ${wp_sites}/${site_id}
fi
else
if [[ ${feedback} == "1" ]]; then
echo "Directory '${wp_sites}/${site_id}' does not exist - okay to proceed..."
fi
fi
# Move the 'blogs.dir/*/files' directory.
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Moving ${site_id}files/ to ${wp_sites}/${site_id}"
if [[ ${dry_run} == "1" ]]; then
echo "mv ${site_id}files/ ${wp_sites}/${site_id}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
mv ${site_id}files/ ${wp_sites}/${site_id}
fi
#
# Move any top-level files or folders.
#
# This shouldn't move anything in a properly configured Network or Site
# but it's good to be thorough.
#
for handle in $( ls ${site_id} ); do
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Moving ${site_id}${handle} to ${wp_sites}/${site_id}"
if [[ ${dry_run} == "1" ]]; then
echo "mv ${site_id}${handle} ${wp_sites}/${site_id}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
mv ${site_id}${handle} ${wp_sites}/${site_id}
fi
done
done
# ----------------------------------------------------------------------------------------
# Make sure the 'ms_files_rewriting' setting is correct for every Network.
# ----------------------------------------------------------------------------------------
if [[ ${feedback} == "1" ]]; then
echo ""
echo "--------------------------------------------------------------------------------"
echo "Ensure 'ms_files_rewriting' setting is correct for every Network."
echo "--------------------------------------------------------------------------------"
echo ""
echo "Changing directory to: ${wp_root}"
echo ""
fi
cd ${wp_root}
# Loop through all Networks.
network_ids=$( ${wp_init} db query "SELECT id FROM ${wp_db_prefix}site ORDER BY id;" --skip-column-names )
for network_id in ${network_ids}; do
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Network ID: ${network_id}"
fi
# Check for existing 'ms_files_rewriting' record.
rewriting=$( ${wp_init} network meta get ${network_id} ms_files_rewriting )
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Rewriting: ${rewriting}"
fi
if [ -z ${rewriting} ]; then
if [[ ${feedback} == "1" ]]; then
echo "Adding record to disable ms-files.php"
if [[ ${dry_run} == "1" ]]; then
echo "${wp_init} network meta add ${network_id} ms_files_rewriting '0'"
fi
fi
if [[ ${dry_run} == "0" ]]; then
${wp_init} network meta add ${network_id} ms_files_rewriting '0'
fi
elif [[ ${rewriting} == "1" ]]; then
if [[ ${feedback} == "1" ]]; then
echo "Updating existing record to disable ms-files.php"
if [[ ${dry_run} == "1" ]]; then
echo "${wp_init} network meta update ${network_id} ms_files_rewriting '0'"
fi
fi
if [[ ${dry_run} == "0" ]]; then
${wp_init} network meta update ${network_id} ms_files_rewriting '0'
fi
else
if [[ ${feedback} == "1" ]]; then
echo "Existing record already disables ms-files.php"
fi
fi
done
# ----------------------------------------------------------------------------------------
# Finish up.
# ----------------------------------------------------------------------------------------
# Remove the 'blogs.dir' directory.
if [[ ${feedback} == "1" ]]; then
echo ""
echo "Removing ${wp_blogs_dir} directory"
if [[ ${dry_run} == "1" ]]; then
echo "rm -rf ${wp_blogs_dir}"
fi
fi
if [[ ${dry_run} == "0" ]]; then
rm -rf ${wp_blogs_dir}
fi
# Ping!
tput bel
# Final feedback.
if [[ ${feedback} == "1" ]]; then
echo ""
echo "--------------------------------------------------------------------------------"
echo "Done!"
echo "--------------------------------------------------------------------------------"
echo ""
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment