You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pixelfed/app/Console/Commands/FixBugs/AvatarStorage.php

294 lines
9.5 KiB
PHTML

4 years ago
<?php
namespace App\Console\Commands\FixBugs;
4 years ago
9 months ago
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
use App\Models\Avatar;
use App\Models\Profile;
use App\Services\AccountService;
4 years ago
use App\Util\Lexer\PrettyNumber;
9 months ago
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
4 years ago
class AvatarStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'avatar:storage';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Manage avatar storage';
public $found = 0;
9 months ago
public $notFetched = 0;
9 months ago
public $fixed = 0;
9 months ago
public $missing = 0;
4 years ago
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Pixelfed Avatar Storage Manager');
$this->line(' ');
$segments = [
[
'Local',
Avatar::whereNull('is_remote')->count(),
9 months ago
PrettyNumber::size(Avatar::whereNull('is_remote')->sum('size')),
4 years ago
],
[
'Remote',
Avatar::whereIsRemote(true)->count(),
9 months ago
PrettyNumber::size(Avatar::whereIsRemote(true)->sum('size')),
4 years ago
],
[
'Cached (CDN)',
Avatar::whereNotNull('cdn_url')->count(),
9 months ago
PrettyNumber::size(Avatar::whereNotNull('cdn_url')->sum('size')),
4 years ago
],
[
'Uncached',
Avatar::whereNull('cdn_url')->count(),
9 months ago
PrettyNumber::size(Avatar::whereNull('cdn_url')->sum('size')),
4 years ago
],
[
'------------',
'----------',
9 months ago
'-----',
4 years ago
],
[
'Total',
Avatar::count(),
9 months ago
PrettyNumber::size(Avatar::sum('size')),
4 years ago
],
];
$this->table(
['Segment', 'Count', 'Space Used'],
$segments
);
$this->line(' ');
9 months ago
if ((bool) config_cache('pixelfed.cloud_storage')) {
4 years ago
$this->info('✅ - Cloud storage configured');
$this->line(' ');
}
9 months ago
if (config('instance.avatar.local_to_cloud')) {
4 years ago
$this->info('✅ - Store avatars on cloud filesystem');
$this->line(' ');
}
9 months ago
if ((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
4 years ago
$disk = Storage::disk(config_cache('filesystems.cloud'));
$exists = $disk->exists('cache/avatars/default.jpg');
$state = $exists ? '✅' : '❌';
9 months ago
$msg = $state.' - Cloud default avatar exists';
4 years ago
$this->info($msg);
}
$options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
4 years ago
[
'Cancel',
4 years ago
'Upload default avatar to cloud',
'Move local avatars to cloud',
9 months ago
'Re-fetch remote avatars',
] : [
'Cancel',
9 months ago
'Re-fetch remote avatars',
];
$this->missing = Profile::where('created_at', '<', now()->subDays(1))->doesntHave('avatar')->count();
9 months ago
if ($this->missing != 0) {
$options[] = 'Fix missing avatars';
}
$choice = $this->choice(
'Select action:',
$options,
4 years ago
0
);
return $this->handleChoice($choice);
}
protected function handleChoice($id)
{
switch ($id) {
case 'Cancel':
return;
4 years ago
case 'Upload default avatar to cloud':
return $this->uploadDefaultAvatar();
case 'Move local avatars to cloud':
return $this->uploadAvatarsToCloud();
case 'Re-fetch remote avatars':
return $this->refetchRemoteAvatars();
case 'Fix missing avatars':
return $this->fixMissingAvatars();
4 years ago
}
}
protected function uploadDefaultAvatar()
{
9 months ago
if (! $this->confirm('Are you sure you want to upload the default avatar to the cloud storage disk?')) {
return;
}
4 years ago
$disk = Storage::disk(config_cache('filesystems.cloud'));
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
Avatar::whereMediaPath('public/avatars/default.jpg')->update(['cdn_url' => $disk->url('cache/avatars/default.jpg')]);
$this->info('Successfully uploaded default avatar to cloud storage!');
$this->info($disk->url('cache/avatars/default.jpg'));
}
protected function uploadAvatarsToCloud()
{
9 months ago
if (! (bool) config_cache('pixelfed.cloud_storage') || ! config('instance.avatar.local_to_cloud')) {
$this->error('Enable cloud storage and avatar cloud storage to perform this action');
9 months ago
return;
}
$confirm = $this->confirm('Are you sure you want to move local avatars to cloud storage?');
9 months ago
if (! $confirm) {
$this->error('Aborted action');
9 months ago
return;
}
$disk = Storage::disk(config_cache('filesystems.cloud'));
9 months ago
if ($disk->missing('cache/avatars/default.jpg')) {
$disk->put('cache/avatars/default.jpg', Storage::get('public/avatars/default.jpg'));
}
9 months ago
Avatar::whereNull('is_remote')->chunk(5, function ($avatars) use ($disk) {
foreach ($avatars as $avatar) {
if ($avatar->media_path === 'public/avatars/default.jpg') {
$avatar->cdn_url = $disk->url('cache/avatars/default.jpg');
$avatar->save();
} else {
if (! $avatar->media_path || ! Str::startsWith($avatar->media_path, 'public/avatars/')) {
continue;
}
$ext = pathinfo($avatar->media_path, PATHINFO_EXTENSION);
9 months ago
$newPath = 'cache/avatars/'.$avatar->profile_id.'/avatar_'.strtolower(Str::random(6)).'.'.$ext;
$existing = Storage::disk('local')->get($avatar->media_path);
9 months ago
if (! $existing) {
continue;
}
$newMediaPath = $disk->put($newPath, $existing);
$avatar->media_path = $newPath;
$avatar->cdn_url = $disk->url($newPath);
$avatar->save();
}
9 months ago
Cache::forget('avatar:'.$avatar->profile_id);
Cache::forget(AccountService::CACHE_KEY.$avatar->profile_id);
}
});
}
protected function refetchRemoteAvatars()
{
9 months ago
if (! $this->confirm('Are you sure you want to refetch all remote avatars? This could take a while.')) {
return;
}
1 week ago
if ((bool) config_cache('pixelfed.cloud_storage') === false && config_cache('federation.avatars.store_local') == false) {
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
9 months ago
return;
}
$count = Profile::has('avatar')
->with('avatar')
->whereNull('user_id')
->count();
9 months ago
$this->info('Found '.$count.' remote avatars to re-fetch');
$this->line(' ');
$bar = $this->output->createProgressBar($count);
Profile::has('avatar')
->with('avatar')
->whereNull('user_id')
9 months ago
->chunk(50, function ($profiles) use ($bar) {
foreach ($profiles as $profile) {
$avatar = $profile->avatar;
$avatar->last_fetched_at = null;
$avatar->save();
RemoteAvatarFetch::dispatch($profile)->onQueue('low');
$bar->advance();
}
});
$this->line(' ');
$this->line(' ');
$this->info('Finished dispatching avatar refetch jobs!');
$this->line(' ');
$this->info('This may take a few minutes to complete, you may need to run "php artisan cache:clear" after the jobs are processed.');
$this->line(' ');
}
protected function incr($name)
{
9 months ago
switch ($name) {
case 'found':
$this->found = $this->found + 1;
9 months ago
break;
case 'notFetched':
$this->notFetched = $this->notFetched + 1;
9 months ago
break;
case 'fixed':
$this->fixed++;
9 months ago
break;
}
}
protected function fixMissingAvatars()
{
9 months ago
if (! $this->confirm('Are you sure you want to fix missing avatars?')) {
return;
}
9 months ago
$this->info('Found '.$this->missing.' accounts with missing profiles');
Profile::where('created_at', '<', now()->subDays(1))
->doesntHave('avatar')
9 months ago
->chunk(50, function ($profiles) {
foreach ($profiles as $profile) {
Avatar::updateOrCreate([
9 months ago
'profile_id' => $profile->id,
], [
'media_path' => 'public/avatars/default.jpg',
9 months ago
'is_remote' => $profile->domain == null && $profile->private_key == null,
]);
$this->incr('fixed');
}
9 months ago
});
$this->line(' ');
$this->line(' ');
9 months ago
$this->info('Fixed '.$this->fixed.' accounts with a blank avatar');
}
4 years ago
}