Copy Media from One Site to Another in a Multisite Network

Since I work with WordPress Multisite a lot, I’ve already had a chance to implement a similar functionality of copying media between sites in some of my plugins. Below, in this guide, I am going to share different ways of how you can do it, too.

The whole tutorial consists of two parts:

  • In the first part, I’ll share with you how you can manage media files across your entire WordPress multisite network with the help of plugins.
  • In the second part, you will learn how you can copy a media file from one site to another with the help of bulk actions. We are going to create one from scratch. So, it is going to be a programmatic approach. This part is for those who would like to create a solution themselves or to learn how it is actually working under the hood.

What if you’re not using a WordPress Multisite or simply not sure what it is? In each chapter of this tutorial, I will also show you a way of doing a similar thing for standalone sites (not a part of the WordPress Multisite network).

Using a WordPress Plugin to Copy Media Between Sub-sites

This part of the tutorial is going to be a little bit more about media library management, not only about copying a media file to another site of your network.

Let’s take a look at two approaches here.

Copy a media file to another site of a network as a part of a post

In this part, I would like you to consider taking a look at my Simple Multisite Crossposting plugin, which allows you to copy and sync posts within a WordPress Multisite Network and media files of those posts, for example:

  • Featured images,
  • Media files (images and other types of attachments) in custom fields,
  • WooCommerce product images and product gallery images.
  • Images from widgets in page builders (Elementor, the Bricks Builder, etc).

Oh, I almost forgot: if you’re not using a multisite network, but just a couple of standalone WordPress sites, it is also possible with my other plugin – Simple WP Crossposting.

Creating a Multisite shared media library

However, if your sites are part of a WordPress Multisite network, why even bother to copy media from one site to another when you can use the original file?

Didn’t expect that? But it is possible with my other plugin – Multisite Shared Media Library.

The whole idea is that at first, you decide which site is going to be the main one, and we are going to use media files from that specific sub-site across the whole multisite network. By default, the plugin takes the first website as a “network media library site”, but you can change it easily in the plugin settings:

Change the network media library site

After activating the plugin and deciding on the main network library site – congratulations, now the media files from this site are shared across your whole multisite network, and you don’t even need to copy media to another site to use it, which, by the way, is intended to save tons of your server disk space.

Another cool thing about the shared media library plugin is that it also allows you to choose how you would like to display and use your shared media library – whether it will completely replace the media library of a current sub-site or will be displayed in a separate “Global media” tab like in the screenshot below:

Custom shared media tab title

Creating a Bulk Action (a Programmatic Approach)

Want to copy media from one site to another programmatically? Let’s do it in this chapter, then!

This is how it is going to work:

WordPress copy media from one site to another within a multisite network

A PHP function that allows you to copy a media file from the current sub-site to a target sub-site

Below is the first part of the code, which is basically just a function that allows copying a specific media file with an ID $attachment_id to a site with an ID $blog_id.

Yes, guys, as easy as that!

/**
 * Copies attachment to a specific blog within a WordPress Multisite network
 *
 * @author Misha Rudrastyh
 *
 * @param int $attachment_id Attachment ID
 * @param int $blog_id Blog ID where to move a media file
 * @return bool true on success, false if error occurs
 */
function rudr_copy_attachment_to_blog( $attachment_id, $blog_id ) {

	// get image path unscaled or you can use get_attached_file() if it is not necessary to copy full-sized originals 
	$file = wp_get_original_image_path( $attachment_id );

	// exit the function if an attachment with this specific ID doesn't exist
	if( ! $file ) {
		return false;
	}

	// switching to a blog we are going to copy the image to
	switch_to_blog( $blog_id );

	$uploads = wp_upload_dir();

	$filename = wp_unique_filename( $uploads[ 'path' ], basename( $file ) );
	$new_file = $uploads[ 'path' ] . "/$filename";
	$new_file_url = $uploads[ 'url' ] . "/$filename";

	// copy the media file into another multisite subsite uploads directory
	$sideload = @copy( $file, $new_file );

	if( false === $sideload ) {
		return false;
	}

	// it is time to insert media file into media gallery
	$inserted_attachment_id = wp_insert_attachment(
		array(
			'guid' => $new_file_url,
			'post_mime_type' => mime_content_type( $new_file ),
			'post_title'     => preg_replace( '/\.[^.]+$/', '', $filename ),
			'post_content'   => '',
			'post_status'    => 'inherit',
		),
		$new_file
	);

	// make sure this file is included, because wp_generate_attachment_metadata() depends on it
	require_once( ABSPATH . 'wp-admin/includes/image.php' );
	// update the attachment metadata.
	wp_update_attachment_metadata(
		$inserted_attachment_id,
		wp_generate_attachment_metadata( $inserted_attachment_id, $new_file )
	);

	restore_current_blog();

	return true;

}

In the beginning, I had an idea to use only WordPress functions for this purpose, but I gave up very soon, at least because I thought that using only the copy() function was going to be so much faster than the whole wp_handle_sideload() function. Also, I have seen a solution of moving files to a temporary folder with the help of the download_url() function, which really downloads an image using HTTP requests. What?

Also, when copying the same image multiple times, we have this moment:

image copies in WordPress media library
At the end of filenames, WordPress automatically adds numeric suffixes.

In the code above, I decided to use this approach because it is the default WordPress behavior. But you can just skip the image if it already exists! To do that, just replace the following lines:

$filename = basename( $file );
$new_file = $uploads[ 'path' ] . "/$filename";
$new_file_url = $uploads[ 'url' ] . "/$filename";
// do not copy file if it is already exists
if( file_exists( $new_file ) ) {
	return false;
}

And last but not least, if you think your code is too slow, just do this:

// make sure this file is included, because wp_generate_attachment_metadata() depends on it
// require_once( ABSPATH . 'wp-admin/includes/image.php' );
// update the attachment metadata.
// wp_update_attachment_metadata(
//		$inserted_attachment_id,
//		wp_generate_attachment_metadata( $inserted_attachment_id, $new_file )
//);

Yes, I commented (or you can remove) the whole part of the code that creates image sizes on a sub-site. It may be super-slow in terms of performance depending on how many image sizes are registered on a sub-site. So there is a chance you don’t even need it at all.

Creating a custom bulk action

And now let’s create a custom bulk action.

// add bulk action
add_filter( 'bulk_actions-upload', 'rudr_upload_bulk_actions' );
function rudr_upload_bulk_actions( $bulk_array ) {
	
	if( 2 == get_current_blog_id() ) {
		return $bulk_array;
	}
	
	$bulk_array[ 'rudr_copy_attachment_to' ] = 'Move to Site 2';
	return $bulk_array;
}
// perform bulk action
add_filter( 'handle_bulk_actions-upload', 'rudr_multisite_move_media', 10, 3 );
function rudr_multisite_move_media( $redirect, $doaction, $object_ids ) {
	// do something for our bulk action
	if( 'rudr_copy_attachment_to' === $doaction ) {
		$count = 0;
		$blog_id = 2;
		foreach( $object_ids as $attachment_id ) { // for each media selected
			if( rudr_copy_attachment_to_blog( $attachment_id, $blog_id ) ) {
				$count++;
			}
		}
		$redirect = add_query_arg( 'rudr_bulk_media', $count, $redirect );
	}
	return $redirect;
}
// print notices in admin
add_action( 'admin_notices', 'rudr_bulk_action_notices' );
function rudr_bulk_action_notices() {
	// but you can create an awesome message
	if( ! empty( $_REQUEST[ 'rudr_bulk_media' ] ) ) {
		// depending on how many posts have been changed, our message may be different
		printf( 
			'<div id="message" class="updated notice is-dismissible"><p>' . _n( '%d image copied to Site 2.', '%d images copied to Site 2.', absint( $_REQUEST[ 'rudr_bulk_media' ] ) ) . '</p></div>', 
			$_REQUEST[ 'rudr_bulk_media' ] 
		);
	}
}
  • I created the only bulk action for just one sub-site; that’s why $blog_id = 2 is hardcoded. But you can use the get_sites() function to do it for all blogs within a network.
  • Sometimes, an image may not be copied to a sub-site because of an error or because it already exists there (we previously discussed it), so I decided that using a custom $count variable is a good idea here.
  • Since I named the bulk action “Move to…”, I think I need to explain how to move images between sub-sites (not only copy them). It is as simple as one extra line of code, which you will need to insert in the loop: wp_delete_attachment( $attachment_id, true ).

By the way, if you’re looking for a bulk action programmatic approach for standalone sites, outside of a WordPress Multisite network, you can find an example here.

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 15 years I've been doing my best to share with you some superb WordPress guides and tips for free.

Need some developer help? Contact me

Follow me on X