login update

This commit is contained in:
2026-03-05 11:24:37 +01:00
parent 5a33ca55a1
commit f6772f673b
67 changed files with 10640 additions and 116 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use App\Models\User;
use App\Models\UserProfile;
use Intervention\Image\ImageManagerStatic as Image;
@@ -39,6 +40,7 @@ class AvatarsMigrate extends Command
protected $allowed = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
@@ -47,7 +49,7 @@ class AvatarsMigrate extends Command
*
* @var int[]
*/
protected $sizes = [32, 64, 128, 256, 512];
protected $sizes = [32, 40, 64, 128, 256, 512];
public function handle(): int
{
@@ -56,6 +58,7 @@ class AvatarsMigrate extends Command
$removeLegacy = $this->option('remove-legacy');
$legacyPath = base_path($this->option('path'));
$userId = $this->option('user-id') ? (int) $this->option('user-id') : null;
$verbose = $this->output->isVerbose();
$this->info('Starting avatar migration' . ($dry ? ' (dry-run)' : '') . ($userId ? " for user={$userId}" : ''));
@@ -72,7 +75,7 @@ class AvatarsMigrate extends Command
$query->where('id', $userId);
}
$query->chunk(100, function ($users) use ($dry, $force, $removeLegacy, $legacyPath, &$bar, $useIntervention) {
$query->chunk(100, function ($users) use ($dry, $force, $removeLegacy, $legacyPath, &$bar, $useIntervention, $verbose) {
foreach ($users as $user) {
/** @var UserProfile|null $profile */
$profile = $user->profile;
@@ -87,10 +90,13 @@ class AvatarsMigrate extends Command
continue;
}
$source = $this->findLegacyFile($profile, $user->id, $legacyPath);
$source = $this->findLegacyFile($profile, $user->id, $legacyPath, 'legacy');
//dd($source);
if (!$source) {
$this->line("[noop] user={$user->id} no legacy file found");
if ($verbose) {
$this->line("[noop] user={$user->id} no legacy file found");
}
continue;
}
@@ -123,14 +129,19 @@ class AvatarsMigrate extends Command
$contentPart = substr(sha1($originalBlob), 0, 12);
$hash = sprintf('%s_%s', $idPart, $contentPart);
// Precompute storage dir for dry-run and real run
$hashPrefix1 = substr($hash, 0, 2);
$hashPrefix2 = substr($hash, 2, 2);
$dir = "avatars/{$hashPrefix1}/{$hashPrefix2}/{$hash}";
// CDN base for public URLs
$cdnBase = rtrim((string) config('cdn.avatar_url', 'https://files.skinbase.org'), '/');
if ($dry) {
$this->line("[dry] user={$user->id} would write avatars for hash={$hash}");
$absPathDry = Storage::disk('public')->path("{$dir}/original.webp");
$publicUrlDry = sprintf('%s/%s/original.webp?v=%s', $cdnBase, $dir, $hash);
$this->line("[dry] user={$user->id} would write avatars for hash={$hash} path={$absPathDry} url={$publicUrlDry}");
} else {
// Use hash-based directory structure: avatars/ab/cd/{hash}/
$hashPrefix1 = substr($hash, 0, 2);
$hashPrefix2 = substr($hash, 2, 2);
$dir = "avatars/{$hashPrefix1}/{$hashPrefix2}/{$hash}";
Storage::disk('public')->makeDirectory($dir);
// Save original.webp
Storage::disk('public')->put("{$dir}/original.webp", $originalBlob);
@@ -155,7 +166,9 @@ class AvatarsMigrate extends Command
$profile->avatar_updated_at = Carbon::now();
$profile->save();
$this->line("[ok] user={$user->id} migrated hash={$hash}");
$absPath = Storage::disk('public')->path("{$dir}/original.webp");
$publicUrl = sprintf('%s/%s/original.webp?v=%s', $cdnBase, $dir, $hash);
$this->line("[ok] user={$user->id} migrated hash={$hash} path={$absPath} url={$publicUrl}");
if ($removeLegacy && !empty($profile->avatar_legacy)) {
$legacyFile = base_path("public/files/usericons/{$profile->avatar_legacy}");
@@ -185,8 +198,19 @@ class AvatarsMigrate extends Command
* @param string $legacyBase
* @return string|null
*/
protected function findLegacyFile(UserProfile $profile, int $userId, string $legacyBase): ?string
protected function findLegacyFile(UserProfile $profile, int $userId, string $legacyBase, ?string $legacyConnection = null): ?string
{
$avatar = DB::connection('legacy')->table('users')->where('user_id', $userId)->value('icon');
if (!empty($profile->avatar_legacy)) {
$p = $legacyBase . DIRECTORY_SEPARATOR . $avatar;
if (file_exists($p)) {
return $p;
}
}
// 1) If profile->avatar_legacy looks like a filename, try it
if (!empty($profile->avatar_legacy)) {
$p = $legacyBase . DIRECTORY_SEPARATOR . $profile->avatar_legacy;
@@ -212,6 +236,34 @@ class AvatarsMigrate extends Command
}
}
// 4) Fallback: try legacy database connection (connection name 'legacy')
// If a legacy DB connection is configured, query `users.icon` for avatar filename.
try {
$conn = $legacyConnection ?: (config('database.connections.legacy') ? 'legacy' : null);
if ($conn) {
$icon = DB::connection($conn)->table('users')->where('id', $userId)->value('icon');
if (!empty($icon)) {
// If icon looks like an absolute path, use it directly; otherwise resolve under legacy base path
$p = $icon;
if (!file_exists($p)) {
$p = $legacyBase . DIRECTORY_SEPARATOR . ltrim($icon, '\/');
}
if (file_exists($p)) {
if ($this->output->isVerbose()) {
$this->line("[legacy-db] user={$userId} icon={$icon} resolved={$p}");
}
return $p;
}
if ($this->output->isVerbose()) {
$this->line("[legacy-db] user={$userId} icon={$icon} not found at resolved path {$p}");
}
}
}
} catch (\Throwable $e) {
// Non-fatal: just skip legacy DB if query fails or connection missing
}
return null;
}
@@ -308,6 +360,53 @@ class AvatarsMigrate extends Command
return imagecreatefromwebp($path);
}
return false;
case 'image/gif':
if (function_exists('imagecreatefromgif')) {
$res = imagecreatefromgif($path);
if (!$res) {
return false;
}
// Ensure returned resource is truecolor (WebP requires truecolor)
if (!imageistruecolor($res)) {
$w = imagesx($res);
$h = imagesy($res);
$true = imagecreatetruecolor($w, $h);
// Preserve transparency where possible
imagealphablending($true, false);
imagesavealpha($true, true);
// Fill with fully transparent color
$transparent = imagecolorallocatealpha($true, 0, 0, 0, 127);
imagefilledrectangle($true, 0, 0, $w, $h, $transparent);
// If the source has an indexed transparent color, try to preserve it
$transIndex = imagecolortransparent($res);
if ($transIndex >= 0) {
try {
$colorTotal = imagecolorstotal($res);
if ($transIndex >= 0 && $transIndex < $colorTotal) {
$colors = imagecolorsforindex($res, $transIndex);
if (is_array($colors)) {
$alphaColor = imagecolorallocatealpha($true, $colors['red'], $colors['green'], $colors['blue'], 127);
imagefilledrectangle($true, 0, 0, $w, $h, $alphaColor);
}
}
} catch (\Throwable $e) {
// Non-fatal: skip preserving indexed transparent color
}
}
// Copy pixels
imagecopy($true, $res, 0, 0, 0, 0, $w, $h);
imagedestroy($res);
return $true;
}
return $res;
}
return false;
default:
return false;
}