fixes gallery
This commit is contained in:
@@ -21,6 +21,7 @@ class AvatarsMigrate extends Command
|
||||
{--force : Overwrite existing migrated avatars}
|
||||
{--remove-legacy : Remove legacy files after successful migration}
|
||||
{--path=public/files/usericons : Legacy path to scan}
|
||||
{--user-id= : Only migrate a single user by ID}
|
||||
';
|
||||
|
||||
/**
|
||||
@@ -54,8 +55,9 @@ class AvatarsMigrate extends Command
|
||||
$force = $this->option('force');
|
||||
$removeLegacy = $this->option('remove-legacy');
|
||||
$legacyPath = base_path($this->option('path'));
|
||||
$userId = $this->option('user-id') ? (int) $this->option('user-id') : null;
|
||||
|
||||
$this->info('Starting avatar migration' . ($dry ? ' (dry-run)' : ''));
|
||||
$this->info('Starting avatar migration' . ($dry ? ' (dry-run)' : '') . ($userId ? " for user={$userId}" : ''));
|
||||
|
||||
// Detect processing backend: Intervention preferred, GD fallback
|
||||
$useIntervention = class_exists('Intervention\\Image\\ImageManagerStatic');
|
||||
@@ -65,7 +67,12 @@ class AvatarsMigrate extends Command
|
||||
|
||||
$bar = null;
|
||||
|
||||
User::with('profile')->chunk(100, function ($users) use ($dry, $force, $removeLegacy, $legacyPath, &$bar, $useIntervention) {
|
||||
$query = User::with('profile');
|
||||
if ($userId) {
|
||||
$query->where('id', $userId);
|
||||
}
|
||||
|
||||
$query->chunk(100, function ($users) use ($dry, $force, $removeLegacy, $legacyPath, &$bar, $useIntervention) {
|
||||
foreach ($users as $user) {
|
||||
/** @var UserProfile|null $profile */
|
||||
$profile = $user->profile;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\UsernamePolicy;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -12,39 +11,70 @@ use Illuminate\Support\Str;
|
||||
|
||||
class ImportLegacyUsers extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:import-legacy-users {--chunk=200 : Chunk size for processing} {--force-reset-all : Force reset passwords for all imported users}';
|
||||
protected $signature = 'skinbase:import-legacy-users {--chunk=200 : Chunk size for processing} {--force-reset-all : Force reset passwords for all imported users} {--dry-run : Preview which users would be skipped/deleted without making changes}';
|
||||
protected $description = 'Import legacy users into the new auth schema per legacy_users_migration spec';
|
||||
|
||||
protected array $usedUsernames = [];
|
||||
protected array $usedEmails = [];
|
||||
protected string $migrationLogPath;
|
||||
/** @var array<int,true> Legacy user IDs that qualify for import */
|
||||
protected array $activeUserIds = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->migrationLogPath = storage_path('logs/username_migration.log');
|
||||
@file_put_contents($this->migrationLogPath, '['.now()."] Starting legacy username policy migration\n", FILE_APPEND);
|
||||
|
||||
$this->usedUsernames = User::pluck('username', 'username')->filter()->all();
|
||||
$this->usedEmails = User::pluck('email', 'email')->filter()->all();
|
||||
// Build the set of legacy user IDs that have any meaningful activity.
|
||||
// Users outside this set will be skipped (or deleted from the new DB if already imported).
|
||||
$this->activeUserIds = $this->buildActiveUserIds();
|
||||
$this->info('Active legacy users (uploads / comments / forum): ' . count($this->activeUserIds));
|
||||
|
||||
$chunk = (int) $this->option('chunk');
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$chunk = (int) $this->option('chunk');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$purged = 0;
|
||||
|
||||
if (! DB::getPdo()) {
|
||||
if (! DB::connection('legacy')->getPdo()) {
|
||||
$this->error('Legacy DB connection "legacy" is not configured or reachable.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
DB::table('users')
|
||||
->chunkById($chunk, function ($rows) use (&$imported, &$skipped) {
|
||||
DB::connection('legacy')->table('users')
|
||||
->chunkById($chunk, function ($rows) use (&$imported, &$skipped, &$purged, $dryRun) {
|
||||
$ids = $rows->pluck('user_id')->all();
|
||||
$stats = DB::table('users_statistics')
|
||||
$stats = DB::connection('legacy')->table('users_statistics')
|
||||
->whereIn('user_id', $ids)
|
||||
->get()
|
||||
->keyBy('user_id');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$legacyId = (int) $row->user_id;
|
||||
|
||||
// ── Inactive user: no uploads, no comments, no forum activity ──
|
||||
if (! isset($this->activeUserIds[$legacyId])) {
|
||||
// If already imported into the new DB, purge it.
|
||||
$existsInNew = DB::table('users')->where('id', $legacyId)->exists();
|
||||
if ($existsInNew) {
|
||||
if ($dryRun) {
|
||||
$this->warn("[dry] Would DELETE inactive user_id={$legacyId} from new DB");
|
||||
} else {
|
||||
$this->purgeNewUser($legacyId);
|
||||
$this->warn("[purge] Deleted inactive user_id={$legacyId} from new DB");
|
||||
$purged++;
|
||||
}
|
||||
} else {
|
||||
$this->line("[skip] user_id={$legacyId} no activity — skipping");
|
||||
}
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line("[dry] Would import user_id={$legacyId}");
|
||||
$imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->importRow($row, $stats[$row->user_id] ?? null);
|
||||
$imported++;
|
||||
@@ -55,18 +85,59 @@ class ImportLegacyUsers extends Command
|
||||
}
|
||||
}, 'user_id');
|
||||
|
||||
$this->info("Imported: {$imported}, Skipped: {$skipped}");
|
||||
$this->info("Imported: {$imported}, Skipped: {$skipped}, Purged: {$purged}");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lookup array of legacy user IDs that qualify for import:
|
||||
* — uploaded at least one artwork (users_statistics.uploads > 0)
|
||||
* — posted at least one artwork comment (artworks_comments.user_id)
|
||||
* — created or posted to a forum thread (forum_topics / forum_posts)
|
||||
*
|
||||
* @return array<int,true>
|
||||
*/
|
||||
protected function buildActiveUserIds(): array
|
||||
{
|
||||
$rows = DB::connection('legacy')->select("
|
||||
SELECT DISTINCT user_id FROM users_statistics WHERE uploads > 0
|
||||
UNION
|
||||
SELECT DISTINCT user_id FROM artworks_comments WHERE user_id > 0
|
||||
UNION
|
||||
SELECT DISTINCT user_id FROM forum_posts WHERE user_id > 0
|
||||
UNION
|
||||
SELECT DISTINCT user_id FROM forum_topics WHERE user_id > 0
|
||||
");
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $r) {
|
||||
$map[(int) $r->user_id] = true;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all new-DB records for a given legacy user ID.
|
||||
* Covers: users, user_profiles, user_statistics, username_redirects.
|
||||
*/
|
||||
protected function purgeNewUser(int $userId): void
|
||||
{
|
||||
DB::transaction(function () use ($userId) {
|
||||
DB::table('username_redirects')->where('user_id', $userId)->delete();
|
||||
DB::table('user_statistics')->where('user_id', $userId)->delete();
|
||||
DB::table('user_profiles')->where('user_id', $userId)->delete();
|
||||
DB::table('users')->where('id', $userId)->delete();
|
||||
});
|
||||
}
|
||||
|
||||
protected function importRow($row, $statRow = null): void
|
||||
{
|
||||
$legacyId = (int) $row->user_id;
|
||||
$rawLegacyUsername = (string) ($row->uname ?: ('user'.$legacyId));
|
||||
$baseUsername = $this->sanitizeUsername($rawLegacyUsername);
|
||||
$username = $this->uniqueUsername($baseUsername);
|
||||
|
||||
$normalizedLegacy = UsernamePolicy::normalize($rawLegacyUsername);
|
||||
// Use legacy username as-is (sanitized only, no numeric suffixing — was unique in old DB).
|
||||
$username = $this->sanitizeUsername((string) ($row->uname ?: ('user' . $legacyId)));
|
||||
|
||||
$normalizedLegacy = UsernamePolicy::normalize((string) ($row->uname ?? ''));
|
||||
if ($normalizedLegacy !== $username) {
|
||||
@file_put_contents(
|
||||
$this->migrationLogPath,
|
||||
@@ -75,7 +146,9 @@ class ImportLegacyUsers extends Command
|
||||
);
|
||||
}
|
||||
|
||||
$email = $this->prepareEmail($row->email ?? null, $username);
|
||||
// Use the real legacy email; only synthesise a placeholder when missing.
|
||||
$rawEmail = $row->email ? strtolower(trim($row->email)) : null;
|
||||
$email = $rawEmail ?: ($this->sanitizeEmailLocal($username) . '@users.skinbase.org');
|
||||
|
||||
$legacyPassword = $row->password2 ?: $row->password ?: null;
|
||||
|
||||
@@ -100,49 +173,63 @@ class ImportLegacyUsers extends Command
|
||||
|
||||
DB::transaction(function () use ($legacyId, $username, $email, $passwordHash, $row, $uploads, $downloads, $pageviews, $awards) {
|
||||
$now = now();
|
||||
$alreadyExists = DB::table('users')->where('id', $legacyId)->exists();
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => $legacyId,
|
||||
'username' => $username,
|
||||
'username_changed_at' => now(),
|
||||
'name' => $row->real_name ?: $username,
|
||||
'email' => $email,
|
||||
'password' => $passwordHash,
|
||||
'is_active' => (int) ($row->active ?? 1) === 1,
|
||||
// All fields synced from legacy on every run
|
||||
$sharedFields = [
|
||||
'username' => $username,
|
||||
'username_changed_at' => $now,
|
||||
'name' => $row->real_name ?: $username,
|
||||
'email' => $email,
|
||||
'is_active' => (int) ($row->active ?? 1) === 1,
|
||||
'needs_password_reset' => true,
|
||||
'role' => 'user',
|
||||
'role' => 'user',
|
||||
'legacy_password_algo' => null,
|
||||
'last_visit_at' => $row->LastVisit ?: null,
|
||||
'created_at' => $row->joinDate ?: $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
'last_visit_at' => $row->LastVisit ?: null,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
DB::table('user_profiles')->insert([
|
||||
'user_id' => $legacyId,
|
||||
'about' => $row->about_me ?: $row->description ?: null,
|
||||
'avatar' => $row->picture ?: null,
|
||||
'cover_image' => $row->cover_art ?: null,
|
||||
'country' => $row->country ?: null,
|
||||
'country_code' => $row->country_code ? substr($row->country_code, 0, 2) : null,
|
||||
'language' => $row->lang ?: null,
|
||||
'birthdate' => $row->birth ?: null,
|
||||
'gender' => $row->gender ?: 'X',
|
||||
'website' => $row->web ?: null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
if ($alreadyExists) {
|
||||
// Sync all fields from legacy — password is never overwritten on re-runs
|
||||
// (unless --force-reset-all was passed, in which case the caller handles it
|
||||
// separately outside this transaction).
|
||||
DB::table('users')->where('id', $legacyId)->update($sharedFields);
|
||||
} else {
|
||||
DB::table('users')->insert(array_merge($sharedFields, [
|
||||
'id' => $legacyId,
|
||||
'password' => $passwordHash,
|
||||
'created_at' => $row->joinDate ?: $now,
|
||||
]));
|
||||
}
|
||||
|
||||
DB::table('user_profiles')->updateOrInsert(
|
||||
['user_id' => $legacyId],
|
||||
[
|
||||
'about' => $row->about_me ?: $row->description ?: null,
|
||||
'avatar_legacy' => $row->picture ?: null,
|
||||
'cover_image' => $row->cover_art ?: null,
|
||||
'country' => $row->country ?: null,
|
||||
'country_code' => $row->country_code ? substr($row->country_code, 0, 2) : null,
|
||||
'language' => $row->lang ?: null,
|
||||
'birthdate' => $row->birth ?: null,
|
||||
'gender' => $row->gender ?: 'X',
|
||||
'website' => $row->web ?: null,
|
||||
'updated_at' => $now,
|
||||
]
|
||||
);
|
||||
|
||||
// Do not duplicate `website` into `user_social_links` — keep canonical site in `user_profiles.website`.
|
||||
|
||||
DB::table('user_statistics')->insert([
|
||||
'user_id' => $legacyId,
|
||||
'uploads' => $uploads,
|
||||
'downloads' => $downloads,
|
||||
'pageviews' => $pageviews,
|
||||
'awards' => $awards,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
DB::table('user_statistics')->updateOrInsert(
|
||||
['user_id' => $legacyId],
|
||||
[
|
||||
'uploads' => $uploads,
|
||||
'downloads' => $downloads,
|
||||
'pageviews' => $pageviews,
|
||||
'awards' => $awards,
|
||||
'updated_at' => $now,
|
||||
]
|
||||
);
|
||||
|
||||
if (Schema::hasTable('username_redirects')) {
|
||||
$old = UsernamePolicy::normalize((string) ($row->uname ?? ''));
|
||||
@@ -178,37 +265,6 @@ class ImportLegacyUsers extends Command
|
||||
return UsernamePolicy::sanitizeLegacy($username);
|
||||
}
|
||||
|
||||
protected function uniqueUsername(string $base): string
|
||||
{
|
||||
$name = UsernamePolicy::uniqueCandidate($base);
|
||||
$this->usedUsernames[$name] = $name;
|
||||
return $name;
|
||||
}
|
||||
|
||||
protected function prepareEmail(?string $legacyEmail, string $username): string
|
||||
{
|
||||
$legacyEmail = $legacyEmail ? strtolower(trim($legacyEmail)) : null;
|
||||
$baseLocal = $this->sanitizeEmailLocal($username);
|
||||
$domain = 'users.skinbase.org';
|
||||
|
||||
$email = $legacyEmail ?: ($baseLocal . '@' . $domain);
|
||||
$email = $this->uniqueEmail($email, $baseLocal, $domain);
|
||||
return $email;
|
||||
}
|
||||
|
||||
protected function uniqueEmail(string $email, string $baseLocal, string $domain): string
|
||||
{
|
||||
$i = 1;
|
||||
$local = explode('@', $email)[0];
|
||||
$current = $email;
|
||||
while (isset($this->usedEmails[$current]) || DB::table('users')->where('email', $current)->exists()) {
|
||||
$current = $local . $i . '@' . $domain;
|
||||
$i++;
|
||||
}
|
||||
$this->usedEmails[$current] = $current;
|
||||
return $current;
|
||||
}
|
||||
|
||||
protected function sanitizeEmailLocal(string $value): string
|
||||
{
|
||||
$local = strtolower(trim($value));
|
||||
|
||||
Reference in New Issue
Block a user