How to move assets between containers in Statamic 5
I recently realized that in one of my blueprints, inside a custom “captioned image” Bard set I was unintentionally storing images in a local asset container, instead of my Digital Ocean-backed container. On top of that, these images were in some cases very large (20MB+ on the extreme end), as I was not running any optimizations, and just let the team upload any image size they desired. The largest possible image resolution displayed on the website is well under 2,000×2,000 pixels, and so it was just a waste of resources. After activating static image caching for assets, the server also started to crash on some pages, when glide tag tried to optimize a few of these 20MB+ images at once. As a result, I had two problems to tackle:
Migrate (hundreds of) images to a different asset container
Cut down the size of images to something reasonable
As far as I know Statamic doesn’t let you natively move assets between containers, but I was able to come up with a quick custom Artisan command doing just that. As a bonus, after setting a `max_upload_size` preset, and activating process source images setting on my target container, I was also able to automatically have the size of the images trimmed to 2,000×2,000 pixels. Here’s the command I used:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Statamic\Facades\Asset;
class ChangeAssetContainer extends Command
{
protected $signature = 'assets:change-container {path} {fromContainer} {toContainer}';
protected $description = 'Move asset to another container.';
public function handle()
{
$path = $this->argument('path');
$fromContainer = $this->argument('fromContainer');
$toContainer = $this->argument('toContainer');
$oldAsset = Asset::query()->where('container', $fromContainer)->where('path', $path)->get()->first();
if (! $oldAsset) {
$this->error('Asset not found in the source container.');
return 1;
}
$newAsset = Asset::query()->where('container', $toContainer)->where('path', $path)->get()->first();
if ($newAsset) {
$this->error('Asset already exists in the destination container.');
return 1;
}
$tempFilePath = $oldAsset->path();
$this->info('Copying asset to temp file...');
Storage::writeStream($tempFilePath, $oldAsset->stream());
$uploadedFile = new UploadedFile(
Storage::path($tempFilePath),
$oldAsset->basename,
$oldAsset->mimeType(),
);
$newAsset = Asset::make()->container($toContainer)->path($path);
$newAsset->data($oldAsset->data());
$newAsset->save();
$this->info('Uploading asset to the new container...');
$newAsset->upload($uploadedFile);
$this->info('Deleting temp file...');
Storage::delete($tempFilePath);
$this->info('Deleting old asset...');
$oldAsset->delete();
}
}
You run the command as:
php artisan assets:change-container your_image.jpg old_container_name new_container_name
You can of course programmatically select images to migrate. In my case, I ran the following one-off script, to move all images from that captioned_image
Bard set in articles
collection.
\Statamic\Facades\Entry::whereCollection('articles')->each(function ($entry) {
collect($entry->content)->each(function ($value) {
if ($value->type == 'captioned_image') {
$path = $value->toArray()['image']->raw();
\Illuminate\Support\Facades\Artisan::call('assets:change-container', [
'path' => $path,
'fromContainer' => 'assets',
'toContainer' => 'spaces',
]);
}
});
});
This little script helped me save quite a bit of time. Let me know in the comments section if you found it useful too.