389 lines
13 KiB
PHP
389 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use App\Models\ForumCategory;
|
|
use App\Models\ForumThread;
|
|
use App\Models\ForumPost;
|
|
use Exception;
|
|
use App\Services\BbcodeConverter;
|
|
|
|
class ForumMigrateOld extends Command
|
|
{
|
|
protected $signature = 'forum:migrate-old {--dry-run} {--only=} {--limit=} {--chunk=500} {--report}';
|
|
|
|
protected $description = 'Migrate legacy forum data from legacy DB into new forum tables';
|
|
|
|
protected string $logPath;
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->logPath = storage_path('logs/forum_migration.log');
|
|
}
|
|
|
|
public function handle(): int
|
|
{
|
|
$this->info('Starting forum migration');
|
|
$this->log('Starting forum migration');
|
|
|
|
$dry = $this->option('dry-run');
|
|
$only = $this->option('only');
|
|
$chunk = (int)$this->option('chunk');
|
|
|
|
try {
|
|
if (!$only || $only === 'categories') {
|
|
$this->migrateCategories($dry);
|
|
}
|
|
|
|
if (!$only || $only === 'threads') {
|
|
$this->migrateThreads($dry, $chunk);
|
|
}
|
|
|
|
if (!$only || $only === 'posts') {
|
|
$this->migratePosts($dry, $chunk);
|
|
}
|
|
|
|
if (!$only || $only === 'gallery') {
|
|
$this->migrateGallery($dry, $chunk);
|
|
}
|
|
|
|
if ($this->option('report')) {
|
|
$this->generateReport();
|
|
}
|
|
|
|
$this->info('Forum migration finished');
|
|
$this->log('Forum migration finished');
|
|
return 0;
|
|
} catch (Exception $e) {
|
|
$this->error('Migration failed: ' . $e->getMessage());
|
|
$this->log('Migration failed: ' . $e->getMessage());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
protected function migrateCategories(bool $dry)
|
|
{
|
|
$this->info('Migrating categories');
|
|
$legacy = DB::connection('legacy');
|
|
|
|
$roots = $legacy->table('forum_topics')
|
|
->select('root_id')
|
|
->distinct()
|
|
->where('root_id', '>', 0)
|
|
->pluck('root_id');
|
|
|
|
$this->info('Found ' . $roots->count() . ' legacy root ids');
|
|
|
|
foreach ($roots as $rootId) {
|
|
$row = $legacy->table('forum_topics')->where('topic_id', $rootId)->first();
|
|
$name = $row->topic ?? 'Category ' . $rootId;
|
|
$slug = Str::slug(substr($name, 0, 150));
|
|
|
|
$this->line("-> root {$rootId}: {$name}");
|
|
|
|
if ($dry) {
|
|
$this->log("[dry] create category {$name} ({$slug})");
|
|
continue;
|
|
}
|
|
|
|
ForumCategory::updateOrCreate(
|
|
['id' => $rootId],
|
|
['name' => $name, 'slug' => $slug]
|
|
);
|
|
}
|
|
|
|
$this->info('Categories migrated');
|
|
}
|
|
|
|
protected function migrateThreads(bool $dry, int $chunk)
|
|
{
|
|
$this->info('Migrating threads');
|
|
$legacy = DB::connection('legacy');
|
|
|
|
$query = $legacy->table('forum_topics')->orderBy('topic_id');
|
|
|
|
$total = $query->count();
|
|
$this->info("Total threads to process: {$total}");
|
|
|
|
$bar = $this->output->createProgressBar($total);
|
|
$bar->start();
|
|
|
|
// chunk by legacy primary key `topic_id`
|
|
$query->chunkById($chunk, function ($rows) use ($dry, $bar) {
|
|
foreach ($rows as $r) {
|
|
$bar->advance();
|
|
|
|
$data = [
|
|
'id' => $r->topic_id,
|
|
'category_id' => $this->resolveCategoryId($r->root_id ?? null, $r->topic_id),
|
|
// resolve user id or assign to system user (1) when missing or not found
|
|
'user_id' => $this->resolveUserId($r->user_id ?? null),
|
|
'title' => $r->topic,
|
|
'slug' => $this->uniqueSlug(Str::slug(substr($r->topic,0,200)) ?: 'thread-'.$r->topic_id, $r->topic_id),
|
|
'content' => $r->preview ?? '',
|
|
'views' => $r->views ?? 0,
|
|
'is_locked' => isset($r->open) ? !((bool)$r->open) : false,
|
|
'is_pinned' => false,
|
|
'visibility' => $this->mapPrivilegeToVisibility($r->privilege ?? 0),
|
|
'last_post_at' => $this->normalizeDate($r->last_update ?? null),
|
|
];
|
|
|
|
if ($dry) {
|
|
$this->log('[dry] thread: ' . $r->topic_id . ' - ' . $r->topic);
|
|
continue;
|
|
}
|
|
|
|
ForumThread::updateOrCreate(['id' => $data['id']], $data);
|
|
}
|
|
}, 'topic_id');
|
|
|
|
$bar->finish();
|
|
$this->line('');
|
|
$this->info('Threads migrated');
|
|
}
|
|
|
|
protected function migratePosts(bool $dry, int $chunk)
|
|
{
|
|
$this->info('Migrating posts');
|
|
$legacy = DB::connection('legacy');
|
|
|
|
$query = $legacy->table('forum_posts')->orderBy('post_id');
|
|
$total = $query->count();
|
|
$this->info("Total posts to process: {$total}");
|
|
|
|
$bar = $this->output->createProgressBar($total);
|
|
$bar->start();
|
|
|
|
// legacy forum_posts uses `post_id` as primary key
|
|
$query->chunkById($chunk, function ($rows) use ($dry, $bar) {
|
|
foreach ($rows as $r) {
|
|
$bar->advance();
|
|
|
|
$data = [
|
|
'id' => $r->post_id,
|
|
'thread_id' => $r->topic_id,
|
|
'user_id' => $r->user_id ?? null,
|
|
'content' => $this->convertLegacyMessage($r->message ?? ''),
|
|
'is_edited' => isset($r->isupdated) ? (bool)$r->isupdated : false,
|
|
'edited_at' => $r->updated ?? null,
|
|
];
|
|
|
|
if ($dry) {
|
|
$this->log('[dry] post: ' . $r->post_id);
|
|
continue;
|
|
}
|
|
|
|
ForumPost::updateOrCreate(['id' => $data['id']], $data);
|
|
}
|
|
}, 'post_id');
|
|
|
|
$bar->finish();
|
|
$this->line('');
|
|
$this->info('Posts migrated');
|
|
}
|
|
|
|
protected function mapPrivilegeToVisibility($priv)
|
|
{
|
|
// legacy privilege: 0 public, 1 members, 4 staff? adjust mapping conservatively
|
|
if ($priv >= 4) return 'staff';
|
|
if ($priv >= 1) return 'members';
|
|
return 'public';
|
|
}
|
|
|
|
protected function normalizeDate($val)
|
|
{
|
|
if (empty($val)) return null;
|
|
$s = trim((string)$val);
|
|
// legacy sometimes contains sentinel invalid dates like -0001-11-30 or zero dates
|
|
if (strpos($s, '-0001') !== false) return null;
|
|
if (strpos($s, '0000-00-00') !== false) return null;
|
|
if (strtotime($s) === false) return null;
|
|
return date('Y-m-d H:i:s', strtotime($s));
|
|
}
|
|
|
|
protected function uniqueSlug(string $base, int $id)
|
|
{
|
|
$slug = $base;
|
|
$i = 0;
|
|
while (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) {
|
|
$i++;
|
|
$slug = $base . '-' . $id;
|
|
// if somehow still exists, append counter
|
|
if (ForumThread::where('slug', $slug)->where('id', '<>', $id)->exists()) {
|
|
$slug = $base . '-' . $id . '-' . $i;
|
|
}
|
|
}
|
|
return $slug;
|
|
}
|
|
|
|
protected function resolveCategoryId($rootId, $topicId)
|
|
{
|
|
// prefer explicit rootId
|
|
if (!empty($rootId)) {
|
|
// ensure category exists
|
|
if (ForumCategory::where('id', $rootId)->exists()) return $rootId;
|
|
}
|
|
|
|
// if this topic itself is a category
|
|
if (ForumCategory::where('id', $topicId)->exists()) return $topicId;
|
|
|
|
// fallback: use first available category
|
|
$first = ForumCategory::first();
|
|
if ($first) return $first->id;
|
|
|
|
// as last resort, create Uncategorized
|
|
$cat = ForumCategory::create(['name' => 'Uncategorized', 'slug' => 'uncategorized']);
|
|
return $cat->id;
|
|
}
|
|
|
|
protected function resolveUserId($userId)
|
|
{
|
|
if (empty($userId)) {
|
|
return 1;
|
|
}
|
|
|
|
// check users table in default connection
|
|
if (\DB::table('users')->where('id', $userId)->exists()) {
|
|
return $userId;
|
|
}
|
|
|
|
return 1; // fallback system user
|
|
}
|
|
|
|
protected function convertLegacyMessage($msg)
|
|
{
|
|
$converter = new BbcodeConverter();
|
|
return $converter->convert($msg);
|
|
}
|
|
|
|
protected function generateReport()
|
|
{
|
|
$this->info('Generating migration report');
|
|
$legacy = DB::connection('legacy');
|
|
|
|
$legacyCounts = [
|
|
'categories' => $legacy->table('forum_topics')->where('root_id','>',0)->distinct('root_id')->count('root_id'),
|
|
'threads' => $legacy->table('forum_topics')->count(),
|
|
'posts' => $legacy->table('forum_posts')->count(),
|
|
];
|
|
|
|
$newCounts = [
|
|
'categories' => ForumCategory::count(),
|
|
'threads' => ForumThread::count(),
|
|
'posts' => ForumPost::count(),
|
|
'attachments' => \DB::table('forum_attachments')->count(),
|
|
];
|
|
|
|
$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));
|
|
}
|
|
|
|
protected function log(string $msg)
|
|
{
|
|
$line = '[' . date('c') . '] ' . $msg . "\n";
|
|
file_put_contents($this->logPath, $line, FILE_APPEND | LOCK_EX);
|
|
}
|
|
|
|
protected function migrateGallery(bool $dry, int $chunk)
|
|
{
|
|
$this->info('Migrating gallery (forum_topics_gallery → forum_attachments)');
|
|
$legacy = DB::connection('legacy');
|
|
|
|
if (!$legacy->getSchemaBuilder()->hasTable('forum_topics_gallery')) {
|
|
$this->info('No legacy forum_topics_gallery table found, skipping');
|
|
return;
|
|
}
|
|
|
|
$query = $legacy->table('forum_topics_gallery')->orderBy('id');
|
|
$total = $query->count();
|
|
$this->info("Total gallery items to process: {$total}");
|
|
|
|
$bar = $this->output->createProgressBar($total);
|
|
$bar->start();
|
|
|
|
$query->chunkById($chunk, function ($rows) use ($dry, $bar) {
|
|
foreach ($rows as $r) {
|
|
$bar->advance();
|
|
|
|
// expected legacy fields: id, name, category (topic id), folder, datum, description
|
|
$topicId = $r->category ?? ($r->topic_id ?? null);
|
|
$fileName = $r->name ?? null;
|
|
if (empty($topicId) || empty($fileName)) {
|
|
$this->log('Skipping gallery row with missing topic or name: ' . json_encode($r));
|
|
continue;
|
|
}
|
|
|
|
$nid = floor($topicId / 100);
|
|
$relativePath = "files/news/{$nid}/{$topicId}/{$fileName}";
|
|
$publicPath = public_path($relativePath);
|
|
|
|
$fileSize = null;
|
|
$mimeType = null;
|
|
$width = null;
|
|
$height = null;
|
|
|
|
if (file_exists($publicPath)) {
|
|
$fileSize = filesize($publicPath);
|
|
$img = @getimagesize($publicPath);
|
|
if ($img !== false) {
|
|
$width = $img[0];
|
|
$height = $img[1];
|
|
$mimeType = $img['mime'] ?? null;
|
|
} else {
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_file($finfo, $publicPath);
|
|
finfo_close($finfo);
|
|
}
|
|
}
|
|
|
|
// find legacy first post id for this topic
|
|
$legacy = DB::connection('legacy');
|
|
$firstPostId = $legacy->table('forum_posts')
|
|
->where('topic_id', $topicId)
|
|
->orderBy('post_date')
|
|
->value('post_id');
|
|
|
|
// map to new forum_posts id (we preserved ids when migrating)
|
|
$postId = null;
|
|
if ($firstPostId && \App\Models\ForumPost::where('id', $firstPostId)->exists()) {
|
|
$postId = $firstPostId;
|
|
} else {
|
|
// fallback: find any post in new DB for thread
|
|
$post = \App\Models\ForumPost::where('thread_id', $topicId)->orderBy('created_at')->first();
|
|
if ($post) $postId = $post->id;
|
|
}
|
|
|
|
if (empty($postId)) {
|
|
$this->log('No target post found for gallery item: topic ' . $topicId . ' file ' . $fileName);
|
|
continue;
|
|
}
|
|
|
|
if ($dry) {
|
|
$this->log("[dry] attach {$relativePath} -> post {$postId}");
|
|
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(),
|
|
]);
|
|
}
|
|
}, 'id');
|
|
|
|
$bar->finish();
|
|
$this->line('');
|
|
$this->info('Gallery migrated');
|
|
}
|
|
}
|