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:

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:

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 «Store».', '%d images have been copied to «Store».', 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 useget_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. - Just to remind you – there is a tutorial about bulk actions on my blog as well.

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
Found this very helpful. Thank you so much!
I’m glad, you’re welcome!
Hi, Misha;
I tried to adopt this into a bespoke plugin for a multisite of mine; but, sadly, could not get it to work, or, rather, adopt to my given surroundings. I’m not enough of a developer to resolve the hard-coded portions limiting the use of the above study into a dynamically adaptable instance by myself.
Is there perhaps a chance you might make this into one complete instance of code which loads the available blog names and allows to copy selectively, not just statically to “store” and from blog with id=2?
That would be most kind.
Thank you, in the event that you will, and best regards –
Hi,
I already have a plugin for that ;)
Hi Misha, just reading these comments. Does your multisite crossposting plugin support copying of media files and associated taxonomies between subsites?
If not do you intent to offer a plugin specifically for media files?
thanks
Luna
Hi Luna,
Here is information about media files and here is about taxonomies ;)
Thanks Misha. I think I may have asked the wrong question. I dont want to cross post articles and images, I JUST want to use images from ones subsite across all other subsites as you have shown in you blog article.
So the question is, have you created a plugin that does what you wrote about in the blog article on sharing media across subsites.
thanks
Luna
Hello, thank you for your tutorial. It was really helpful, but I have a problem with creating copy of post image with method “wp_insert_attachment”. I have 3 multisite domain and when I use this part of code it always create 5 copies of the same records in posts table with the same post_title, post_name (with _1, _2 etc in the end of filename), Post_type: “attachment” and post_mime_type: “image/jpeg”.
Do you have any idea, why this happens and how to solve it?
Thanks! Works great, I only had to change
wp_get_original_image_path()
toget_attached_file()
because the image is auto-scaled and renamed to -scaled