Auth: convert auth views and verification email to Nova layout
This commit is contained in:
@@ -4,8 +4,10 @@ namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\ForumCategory;
|
||||
use App\Models\User;
|
||||
use App\Models\ForumThread;
|
||||
use App\Models\ForumPost;
|
||||
use Exception;
|
||||
@@ -13,12 +15,19 @@ use App\Services\BbcodeConverter;
|
||||
|
||||
class ForumMigrateOld extends Command
|
||||
{
|
||||
protected $signature = 'forum:migrate-old {--dry-run} {--only=} {--limit=} {--chunk=500} {--report}';
|
||||
protected $signature = 'forum:migrate-old {--dry-run} {--only=} {--limit=} {--chunk=500} {--report} {--repair-orphans}';
|
||||
|
||||
protected $description = 'Migrate legacy forum data from legacy DB into new forum tables';
|
||||
|
||||
protected string $logPath;
|
||||
|
||||
protected ?int $limit = null;
|
||||
|
||||
protected ?int $deletedUserId = null;
|
||||
|
||||
/** @var array<int,int> */
|
||||
protected array $missingUserIds = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
@@ -33,6 +42,17 @@ class ForumMigrateOld extends Command
|
||||
$dry = $this->option('dry-run');
|
||||
$only = $this->option('only');
|
||||
$chunk = (int)$this->option('chunk');
|
||||
$this->limit = $this->option('limit') !== null ? max(0, (int) $this->option('limit')) : null;
|
||||
|
||||
$only = $only === 'attachments' ? 'gallery' : $only;
|
||||
if ($only && !in_array($only, ['categories', 'threads', 'posts', 'gallery', 'repair-orphans'], true)) {
|
||||
$this->error('Invalid --only value. Allowed: categories, threads, posts, gallery (or attachments), repair-orphans.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($chunk < 1) {
|
||||
$chunk = 500;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$only || $only === 'categories') {
|
||||
@@ -51,6 +71,10 @@ class ForumMigrateOld extends Command
|
||||
$this->migrateGallery($dry, $chunk);
|
||||
}
|
||||
|
||||
if ($this->option('repair-orphans') || $only === 'repair-orphans') {
|
||||
$this->repairOrphanPosts($dry);
|
||||
}
|
||||
|
||||
if ($this->option('report')) {
|
||||
$this->generateReport();
|
||||
}
|
||||
@@ -74,8 +98,13 @@ class ForumMigrateOld extends Command
|
||||
->select('root_id')
|
||||
->distinct()
|
||||
->where('root_id', '>', 0)
|
||||
->orderBy('root_id')
|
||||
->pluck('root_id');
|
||||
|
||||
if ($this->limit !== null && $this->limit > 0) {
|
||||
$roots = $roots->take($this->limit);
|
||||
}
|
||||
|
||||
$this->info('Found ' . $roots->count() . ' legacy root ids');
|
||||
|
||||
foreach ($roots as $rootId) {
|
||||
@@ -90,10 +119,12 @@ class ForumMigrateOld extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
ForumCategory::updateOrCreate(
|
||||
['id' => $rootId],
|
||||
['name' => $name, 'slug' => $slug]
|
||||
);
|
||||
DB::transaction(function () use ($rootId, $name, $slug) {
|
||||
ForumCategory::updateOrCreate(
|
||||
['id' => $rootId],
|
||||
['name' => $name, 'slug' => $slug]
|
||||
);
|
||||
}, 3);
|
||||
}
|
||||
|
||||
$this->info('Categories migrated');
|
||||
@@ -107,15 +138,26 @@ class ForumMigrateOld extends Command
|
||||
$query = $legacy->table('forum_topics')->orderBy('topic_id');
|
||||
|
||||
$total = $query->count();
|
||||
if ($this->limit !== null && $this->limit > 0) {
|
||||
$total = min($total, $this->limit);
|
||||
}
|
||||
$this->info("Total threads to process: {$total}");
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
$processed = 0;
|
||||
$limit = $this->limit;
|
||||
|
||||
// chunk by legacy primary key `topic_id`
|
||||
$query->chunkById($chunk, function ($rows) use ($dry, $bar) {
|
||||
$query->chunkById($chunk, function ($rows) use ($dry, $bar, &$processed, $limit) {
|
||||
foreach ($rows as $r) {
|
||||
if ($limit !== null && $limit > 0 && $processed >= $limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
$processed++;
|
||||
|
||||
$data = [
|
||||
'id' => $r->topic_id,
|
||||
@@ -137,7 +179,9 @@ class ForumMigrateOld extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
ForumThread::updateOrCreate(['id' => $data['id']], $data);
|
||||
DB::transaction(function () use ($data) {
|
||||
ForumThread::updateOrCreate(['id' => $data['id']], $data);
|
||||
}, 3);
|
||||
}
|
||||
}, 'topic_id');
|
||||
|
||||
@@ -153,15 +197,26 @@ class ForumMigrateOld extends Command
|
||||
|
||||
$query = $legacy->table('forum_posts')->orderBy('post_id');
|
||||
$total = $query->count();
|
||||
if ($this->limit !== null && $this->limit > 0) {
|
||||
$total = min($total, $this->limit);
|
||||
}
|
||||
$this->info("Total posts to process: {$total}");
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
$processed = 0;
|
||||
$limit = $this->limit;
|
||||
|
||||
// legacy forum_posts uses `post_id` as primary key
|
||||
$query->chunkById($chunk, function ($rows) use ($dry, $bar) {
|
||||
$query->chunkById($chunk, function ($rows) use ($dry, $bar, &$processed, $limit) {
|
||||
foreach ($rows as $r) {
|
||||
if ($limit !== null && $limit > 0 && $processed >= $limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
$processed++;
|
||||
|
||||
$data = [
|
||||
'id' => $r->post_id,
|
||||
@@ -177,7 +232,9 @@ class ForumMigrateOld extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
ForumPost::updateOrCreate(['id' => $data['id']], $data);
|
||||
DB::transaction(function () use ($data) {
|
||||
ForumPost::updateOrCreate(['id' => $data['id']], $data);
|
||||
}, 3);
|
||||
}
|
||||
}, 'post_id');
|
||||
|
||||
@@ -243,15 +300,50 @@ class ForumMigrateOld extends Command
|
||||
protected function resolveUserId($userId)
|
||||
{
|
||||
if (empty($userId)) {
|
||||
return 1;
|
||||
return $this->resolveDeletedUserId();
|
||||
}
|
||||
|
||||
// check users table in default connection
|
||||
if (\DB::table('users')->where('id', $userId)->exists()) {
|
||||
if (DB::table('users')->where('id', $userId)->exists()) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
return 1; // fallback system user
|
||||
$uid = (int) $userId;
|
||||
if ($uid > 0 && !in_array($uid, $this->missingUserIds, true)) {
|
||||
$this->missingUserIds[] = $uid;
|
||||
}
|
||||
|
||||
return $this->resolveDeletedUserId();
|
||||
}
|
||||
|
||||
protected function resolveDeletedUserId(): int
|
||||
{
|
||||
if ($this->deletedUserId !== null) {
|
||||
return $this->deletedUserId;
|
||||
}
|
||||
|
||||
$userOne = User::query()->find(1);
|
||||
if ($userOne) {
|
||||
$this->deletedUserId = 1;
|
||||
return $this->deletedUserId;
|
||||
}
|
||||
|
||||
$fallback = User::query()->orderBy('id')->first();
|
||||
if ($fallback) {
|
||||
$this->deletedUserId = (int) $fallback->id;
|
||||
return $this->deletedUserId;
|
||||
}
|
||||
|
||||
$created = User::query()->create([
|
||||
'name' => 'Deleted User',
|
||||
'email' => 'deleted-user+forum@skinbase.local',
|
||||
'password' => Hash::make(Str::random(64)),
|
||||
'role' => 'user',
|
||||
]);
|
||||
|
||||
$this->deletedUserId = (int) $created->id;
|
||||
|
||||
return $this->deletedUserId;
|
||||
}
|
||||
|
||||
protected function convertLegacyMessage($msg)
|
||||
@@ -260,6 +352,114 @@ class ForumMigrateOld extends Command
|
||||
return $converter->convert($msg);
|
||||
}
|
||||
|
||||
protected function repairOrphanPosts(bool $dry): void
|
||||
{
|
||||
$this->info('Repairing orphan posts');
|
||||
|
||||
$orphansQuery = ForumPost::query()->whereDoesntHave('thread')->orderBy('id');
|
||||
$orphanCount = (clone $orphansQuery)->count();
|
||||
|
||||
if ($orphanCount === 0) {
|
||||
$this->info('No orphan posts found.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warn("Found {$orphanCount} orphan posts.");
|
||||
|
||||
$repairThread = $this->resolveOrCreateOrphanRepairThread($dry);
|
||||
|
||||
if ($repairThread === null) {
|
||||
$this->warn('Unable to resolve/create repair thread in dry-run mode. Reporting only.');
|
||||
(clone $orphansQuery)->limit(20)->get(['id', 'thread_id', 'user_id', 'created_at'])->each(function (ForumPost $post): void {
|
||||
$this->line("- orphan post id={$post->id} thread_id={$post->thread_id} user_id={$post->user_id}");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line("Repair target thread: {$repairThread->id} ({$repairThread->slug})");
|
||||
|
||||
if ($dry) {
|
||||
$this->info("[dry] Would reassign {$orphanCount} orphan posts to thread {$repairThread->id}");
|
||||
(clone $orphansQuery)->limit(20)->get(['id', 'thread_id', 'user_id', 'created_at'])->each(function (ForumPost $post) use ($repairThread): void {
|
||||
$this->log("[dry] orphan post {$post->id}: {$post->thread_id} -> {$repairThread->id}");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$updated = 0;
|
||||
|
||||
(clone $orphansQuery)->chunkById(500, function ($posts) use ($repairThread, &$updated): void {
|
||||
DB::transaction(function () use ($posts, $repairThread, &$updated): void {
|
||||
/** @var ForumPost $post */
|
||||
foreach ($posts as $post) {
|
||||
$post->thread_id = $repairThread->id;
|
||||
$post->is_edited = true;
|
||||
$post->edited_at = $post->edited_at ?: now();
|
||||
$post->save();
|
||||
$updated++;
|
||||
}
|
||||
}, 3);
|
||||
}, 'id');
|
||||
|
||||
$latestPostAt = ForumPost::query()
|
||||
->where('thread_id', $repairThread->id)
|
||||
->max('created_at');
|
||||
|
||||
if ($latestPostAt) {
|
||||
$repairThread->last_post_at = $latestPostAt;
|
||||
$repairThread->save();
|
||||
}
|
||||
|
||||
$this->info("Repaired orphan posts: {$updated}");
|
||||
$this->log("Repaired orphan posts: {$updated} -> thread {$repairThread->id}");
|
||||
}
|
||||
|
||||
protected function resolveOrCreateOrphanRepairThread(bool $dry): ?ForumThread
|
||||
{
|
||||
$slug = 'migration-orphaned-posts';
|
||||
|
||||
$existing = ForumThread::query()->where('slug', $slug)->first();
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$category = ForumCategory::query()->ordered()->first();
|
||||
|
||||
if (!$category && !$dry) {
|
||||
$category = ForumCategory::query()->create([
|
||||
'name' => 'Migration Repairs',
|
||||
'slug' => 'migration-repairs',
|
||||
'parent_id' => null,
|
||||
'position' => 9999,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($dry) {
|
||||
return new ForumThread([
|
||||
'id' => 0,
|
||||
'slug' => $slug,
|
||||
'category_id' => $category->id,
|
||||
]);
|
||||
}
|
||||
|
||||
return ForumThread::query()->create([
|
||||
'category_id' => $category->id,
|
||||
'user_id' => $this->resolveDeletedUserId(),
|
||||
'title' => 'Migration: Orphaned Posts Recovery',
|
||||
'slug' => $slug,
|
||||
'content' => 'Automatic recovery thread for legacy posts whose source thread no longer exists after migration.',
|
||||
'views' => 0,
|
||||
'is_locked' => false,
|
||||
'is_pinned' => false,
|
||||
'visibility' => 'staff',
|
||||
'last_post_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function generateReport()
|
||||
{
|
||||
$this->info('Generating migration report');
|
||||
@@ -275,12 +475,32 @@ class ForumMigrateOld extends Command
|
||||
'categories' => ForumCategory::count(),
|
||||
'threads' => ForumThread::count(),
|
||||
'posts' => ForumPost::count(),
|
||||
'attachments' => \DB::table('forum_attachments')->count(),
|
||||
'attachments' => DB::table('forum_attachments')->count(),
|
||||
];
|
||||
|
||||
$orphans = ForumPost::query()
|
||||
->whereDoesntHave('thread')
|
||||
->count();
|
||||
|
||||
$legacyThreadsWithLastUpdate = $legacy->table('forum_topics')->whereNotNull('last_update')->count();
|
||||
$newThreadsWithLastPost = ForumThread::query()->whereNotNull('last_post_at')->count();
|
||||
$legacyPostsWithPostDate = $legacy->table('forum_posts')->whereNotNull('post_date')->count();
|
||||
$newPostsWithCreatedAt = ForumPost::query()->whereNotNull('created_at')->count();
|
||||
|
||||
$report = [
|
||||
'missing_users_count' => count($this->missingUserIds),
|
||||
'missing_users' => $this->missingUserIds,
|
||||
'orphan_posts' => $orphans,
|
||||
'timestamp_mismatches' => [
|
||||
'threads_last_post_gap' => max(0, $legacyThreadsWithLastUpdate - $newThreadsWithLastPost),
|
||||
'posts_created_at_gap' => max(0, $legacyPostsWithPostDate - $newPostsWithCreatedAt),
|
||||
],
|
||||
];
|
||||
|
||||
$this->info('Legacy counts: ' . json_encode($legacyCounts));
|
||||
$this->info('New counts: ' . json_encode($newCounts));
|
||||
$this->log('Report: legacy=' . json_encode($legacyCounts) . ' new=' . json_encode($newCounts));
|
||||
$this->info('Report: ' . json_encode($report));
|
||||
$this->log('Report: legacy=' . json_encode($legacyCounts) . ' new=' . json_encode($newCounts) . ' extra=' . json_encode($report));
|
||||
}
|
||||
|
||||
protected function log(string $msg)
|
||||
@@ -301,14 +521,25 @@ class ForumMigrateOld extends Command
|
||||
|
||||
$query = $legacy->table('forum_topics_gallery')->orderBy('id');
|
||||
$total = $query->count();
|
||||
if ($this->limit !== null && $this->limit > 0) {
|
||||
$total = min($total, $this->limit);
|
||||
}
|
||||
$this->info("Total gallery items to process: {$total}");
|
||||
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
$query->chunkById($chunk, function ($rows) use ($dry, $bar) {
|
||||
$processed = 0;
|
||||
$limit = $this->limit;
|
||||
|
||||
$query->chunkById($chunk, function ($rows) use ($dry, $bar, &$processed, $limit) {
|
||||
foreach ($rows as $r) {
|
||||
if ($limit !== null && $limit > 0 && $processed >= $limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
$processed++;
|
||||
|
||||
// expected legacy fields: id, name, category (topic id), folder, datum, description
|
||||
$topicId = $r->category ?? ($r->topic_id ?? null);
|
||||
@@ -368,16 +599,21 @@ class ForumMigrateOld extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
\App\Models\ForumAttachment::create([
|
||||
'post_id' => $postId,
|
||||
'file_path' => $relativePath,
|
||||
'file_size' => $fileSize ?? 0,
|
||||
'mime_type' => $mimeType,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::transaction(function () use ($postId, $relativePath, $fileSize, $mimeType, $width, $height) {
|
||||
\App\Models\ForumAttachment::query()->updateOrCreate(
|
||||
[
|
||||
'post_id' => $postId,
|
||||
'file_path' => $relativePath,
|
||||
],
|
||||
[
|
||||
'file_size' => $fileSize ?? 0,
|
||||
'mime_type' => $mimeType,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}, 3);
|
||||
}
|
||||
}, 'id');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user