to storage/covers/ * b. Insert a thesis_files row with file_type='cover' * c. Clear theses.banner_path * 2. Remove the now-empty storage/banners/ directory (best-effort). * * Safe to re-run: if a cover record already exists for a thesis, the banner * migration for that thesis is skipped. */ defined('APP_ROOT') || define('APP_ROOT', dirname(__DIR__, 2)); defined('STORAGE_ROOT') || define('STORAGE_ROOT', APP_ROOT . '/storage'); $dbPath = APP_ROOT . '/storage/xamxam.db'; if (!file_exists($dbPath)) { echo "ERROR: database not found at $dbPath\n"; exit(1); } $pdo = new PDO('sqlite:' . $dbPath); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('PRAGMA foreign_keys = ON'); $coverDir = STORAGE_ROOT . '/covers/'; $bannerDir = STORAGE_ROOT . '/banners/'; if (!is_dir($coverDir)) { mkdir($coverDir, 0755, true); echo "Created covers/ directory.\n"; } // Fetch all theses with a non-null banner_path $stmt = $pdo->query("SELECT id, banner_path FROM theses WHERE banner_path IS NOT NULL"); $rows = $stmt->fetchAll(); if (empty($rows)) { echo "No banners to migrate.\n"; } else { foreach ($rows as $row) { $thesisId = (int)$row['id']; $bannerPath = $row['banner_path']; // e.g. "banners/abc123.png" // Skip if a cover record already exists for this thesis $check = $pdo->prepare("SELECT id FROM thesis_files WHERE thesis_id = ? AND file_type = 'cover' LIMIT 1"); $check->execute([$thesisId]); if ($check->fetch()) { echo " Thesis $thesisId: cover record already exists — skipping banner migration.\n"; // Still clear banner_path so UI stays clean $pdo->prepare("UPDATE theses SET banner_path = NULL WHERE id = ?")->execute([$thesisId]); continue; } $srcAbs = STORAGE_ROOT . '/' . $bannerPath; $filename = basename($bannerPath); $dstAbs = $coverDir . $filename; $dstRel = 'covers/' . $filename; if (!file_exists($srcAbs)) { echo " Thesis $thesisId: source file missing ($srcAbs) — inserting DB record with new path anyway, skipping file copy.\n"; } else { if (!copy($srcAbs, $dstAbs)) { echo " ERROR: could not copy $srcAbs → $dstAbs — skipping thesis $thesisId.\n"; continue; } chmod($dstAbs, 0644); echo " Thesis $thesisId: copied $bannerPath → $dstRel\n"; } // Determine MIME from extension $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $mime = match($ext) { 'jpg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', default => 'image/jpeg', }; // Get file size $size = file_exists($dstAbs) ? filesize($dstAbs) : 0; // Insert cover record $ins = $pdo->prepare( "INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type, sort_order) VALUES (?, 'cover', ?, ?, ?, ?, 0)" ); $ins->execute([$thesisId, $dstRel, $filename, $size, $mime]); echo " Thesis $thesisId: inserted cover record → $dstRel\n"; // Clear banner_path $pdo->prepare("UPDATE theses SET banner_path = NULL WHERE id = ?")->execute([$thesisId]); echo " Thesis $thesisId: cleared banner_path.\n"; } } // Remove old banner files that were successfully copied $remaining = glob($bannerDir . '*') ?: []; $allClear = true; foreach ($remaining as $f) { $basename = basename($f); if (file_exists($coverDir . $basename)) { @unlink($f); echo "Removed migrated banner file: banners/$basename\n"; } else { echo "WARNING: banners/$basename has no corresponding cover — leaving in place.\n"; $allClear = false; } } // Remove the now-empty banners/ directory (best-effort, ignoring .gitkeep) if ($allClear && is_dir($bannerDir)) { $leftovers = array_diff(scandir($bannerDir), ['.', '..', '.gitkeep']); if (empty($leftovers)) { // Remove .gitkeep if present, then the dir $gitkeep = $bannerDir . '.gitkeep'; if (file_exists($gitkeep)) { @unlink($gitkeep); } @rmdir($bannerDir); echo "Removed banners/ directory.\n"; } else { echo "WARNING: banners/ directory still has files after migration — leaving in place.\n"; } } echo "\nMigration 016 complete.\n";