Copy Media Files Between Sites Within a Multisite Network

Recently I’ve been working with this functionality in one of my plugins and now I decided to share it with you.

But I always want my tutorials to be more practical, so check what we are going to create:

move images to another website within a WordPress multisite network

The whole tutorial consists of two parts – in the first one I will show you a ready to use function that allows to move media from one site of a multisite network to another, in the second – we will create a custom bulk action for media files.

I am more than certain to recommend you my Simple Multisite Crossposting plugin that allows to copy and sync posts within a WordPress Multisite Network.

I know that you’re waiting for the code, here is the first part. You can insert it to you current theme functions.php file for example. But do not forget about theme updates.

/**
 * 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;

}

At the beginning I had an idea to use only WordPress functions for this purpose but I give up very soon at least because I think that using only copy() function is going to be so much faster than the whole wp_handle_sideload() function. Also I have also seen a solution of moving file to a temporary folder with the help of 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 adds numeric suffixes.

In the code above I decided to use this approach because it is the default WordPress behaviour. But you can just skip the image if it already exists! In order 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 the 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 maybe you don’t even need it at all.

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 «Store»';
	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', 'misha_bulk_action_notices' );
function misha_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 has been copied to &laquo;Store&raquo;.', '%d images have been copied to &laquo;Store&raquo;.', absint( $_REQUEST[ 'rudr_bulk_media' ] ) ) . '</p></div>', $_REQUEST[ 'rudr_bulk_media' ] );
	}
}
Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 10 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