I created the following script to automate removing ms-files.php from a legacy WordPress multisite hosted with Kinsta. It was a large network with hundreds of subsites. Doing this manually would have been a huge undertaking.
Not familiar with ms-files.php? I don’t blame you.
It was a legacy multisite format for handling uploads prior to WordPress 3.5. Files used to be stored in folders /wp-content/blogs.dir/<site-id>/files/
whereas the current multisite format is /wp-content/uploads/sites/<site-id>/
. To hide the ugly blogs.dir
folder all uploads were routed through a /files/
folder which was handled by a special PHP script called ms-files.php
. Any multisite created prior to WordPress 3.5 still is stuck in the old format with no automated way to upgrade to the new format.
There is no harm in keeping the old format however running all uploads through a PHP script vs direct file access is going to take a performance hit. Big credit goes to Mika Epstein who basically outlined the steps necessary to pull this off https://halfelf.org/2012/dumping-ms-files/.
I wrote this for a subdirectory multisite not subdomain multisite.
That said, a developer could use it as starting point to fill in the missing pieces for subdomain networks. This was simply not something I needed. Also it might not fully be compatible with files and folders with spaces in them. Again not something I needed. Please don’t run on production. ⚠️
Create a file under ~/private/ named drop-legacy-multisite.sh
with the following code. You’ll need to give it execute permissions chmod +x ~/private/drop-legacy-multisite.sh
before running it.
cd ~/public
# check for existing record
record_check=$( wp db query 'SELECT * FROM wp_sitemeta WHERE meta_key like "ms_files_rewriting";' )
if [[ $record_check == "" ]]; then
echo "Inserting record to disable ms-files.php"
wp db query 'INSERT INTO wp_sitemeta (meta_id, site_id, meta_key, meta_value) VALUES (NULL, "1", "ms_files_rewriting", "0");'
else
echo "Updating existing record to disable ms-files.php"
wp db query 'UPDATE wp_sitemeta SET meta_value = "0" WHERE wp_sitemeta.meta_key = "ms_files_rewriting";'
fi
cd ~/public/wp-content/blogs.dir/
mkdir -p ../uploads/sites/
for site_id in */; do
# Verify site_id is not empty
if [[ $site_id == "" ]]; then
continue
fi
# Root site, attempt to move files
if [[ $site_id == "1/" ]]; then
echo "Top level site found. Attempting to move files however please verify ~/public/wp-content/blogs.dir/1/ is empty"
# Move any top level files or folders
for file in $( ls ${site_id}/files/ ); do
echo "moving ${site_id}$file to ../uploads/"
mv ${site_id}files/$file ../uploads/
done
continue
fi
# Not root site, move to new sites location
rm -rf ../uploads/sites/$site_id
echo "moving ${site_id}files/ to ../uploads/sites/$site_id"
mv ${site_id}files/ ../uploads/sites/$site_id
# Move any top level files or folders
for file in $( ls $site_id ); do
echo "moving ${site_id}$file to ../uploads/sites/$site_id"
mv ${site_id}$file ../uploads/sites/$site_id
done
# Remove folder
rm -rf ${site_id}
done
cd ~/public/
# Drop legacy upload paths
for site in $( wp site list --field=url ); do
echo "Setting $site upload_path to default."
wp option set upload_path "" --url=$site
done
# The main multisite url
root_home=$( wp option get home )
# Correct urls for each site
for site_id in $( wp site list --field=blog_id ); do
# Skip root site
if [[ $site_id == "1" ]]; then
continue
fi
home_url=$( wp db query "SELECT option_value from wp_${site_id}_options where option_name = 'home';" --skip-column-names --batch )
# Require site to end in /
if [[ $home_url != *"/" ]]; then
home_url="${home_url}/";
fi
if [[ $home_url == "https"* ]]; then
site_name=$( basename $home_url )
wp search-replace ${home_url}files/ ${home_url}wp-content/uploads/sites/${site_id}/ wp_${site_id}_* --network --report-changed-only
wp search-replace ${root_home}/files/ ${home_url}wp-content/uploads/sites/${site_id}/ wp_${site_id}_* --network --report-changed-only
fi
done
Before running ~/private/drop-legacy-multisite.sh
, I recommend running screen -R
and start a new background session. This will insure the script completes running in it’s entirety even if disconnected from SSH.
I ran this script on a staging copy of the site first to verify everything worked correctly then ran on production. Watching it run on production was quite magical. Literally hundreds of search and replace URLs updated and folders shifted. After about 20 minutes, everything was completed and switched over to the new upload format. Success! 🎉