Removing Legacy ms-files.php from Multisite

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

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 with the following code. You’ll need to give it execute permissions chmod +x ~/private/ 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");'
    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";'

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

    # 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/


    # 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

    # Remove folder
    rm -rf ${site_id}


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

# 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 
    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 
    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

Before running ~/private/, 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! 🎉