From 172b022aec9092c9c6589c87ee214e6a1973f9e1 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Sat, 11 Jul 2026 11:02:22 -0400 Subject: [PATCH] Add install.php: browser/CLI installer for database and schema Creates config.php from the example if missing, creates the database (and app DB user when admin credentials are given), applies schema.sql idempotently, optionally creates the first login account, and locks itself with .install.lock after a successful run. Co-Authored-By: Claude Fable 5 --- .gitignore | 3 + install.php | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 install.php diff --git a/.gitignore b/.gitignore index d8834ab..e99ed3e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ config.php # Video library lives outside version control videos/ + +# Written by install.php after a successful install +.install.lock diff --git a/install.php b/install.php new file mode 100644 index 0000000..5244ce5 --- /dev/null +++ b/install.php @@ -0,0 +1,256 @@ + PDO::ERRMODE_EXCEPTION]; + $dsnServer = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';charset=utf8mb4'; + $useAdmin = $adminUser !== ''; + + $pdo = new PDO($dsnServer, $useAdmin ? $adminUser : DB_USER, + $useAdmin ? $adminPass : DB_PASS, $opts); + $steps[] = [true, 'Connected to MySQL at ' . DB_HOST . ':' . DB_PORT + . ' as ' . ($useAdmin ? $adminUser : DB_USER) . '.']; + + // Database + $dbQuoted = '`' . str_replace('`', '', DB_NAME) . '`'; + $pdo->exec("CREATE DATABASE IF NOT EXISTS $dbQuoted + CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); + $steps[] = [true, 'Database "' . DB_NAME . '" is ready.']; + + // App DB user (only possible with admin privileges) + if ($useAdmin) { + foreach (['localhost', '127.0.0.1'] as $host) { + $u = $pdo->quote(DB_USER) . '@' . $pdo->quote($host); + $pdo->exec("CREATE USER IF NOT EXISTS $u IDENTIFIED BY " . $pdo->quote(DB_PASS)); + $pdo->exec("GRANT ALL PRIVILEGES ON $dbQuoted.* TO $u"); + } + $pdo->exec('FLUSH PRIVILEGES'); + $steps[] = [true, 'App DB user "' . DB_USER . '" created/updated and granted access.']; + } + + // Schema + $sql = @file_get_contents(__DIR__ . '/schema.sql'); + if ($sql === false) { + throw new RuntimeException('Cannot read schema.sql.'); + } + $pdo->exec("USE $dbQuoted"); + $applied = 0; + foreach (splitSqlStatements($sql) as $stmt) { + if (preg_match('/^\s*(CREATE\s+DATABASE|USE)\b/i', $stmt)) { + continue; // installer already handled DB creation/selection + } + $pdo->exec($stmt); + $applied++; + } + $tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN); + $steps[] = [true, "Applied $applied schema statements — tables: " . implode(', ', $tables) . '.']; + + // Verify the app credentials from config.php actually work + $dsnDb = $dsnServer . ';dbname=' . DB_NAME; + try { + $app = new PDO($dsnDb, DB_USER, DB_PASS, $opts); + $steps[] = [true, 'Verified config.php credentials — the app can connect.']; + } catch (PDOException $e) { + throw new RuntimeException('The app credentials in config.php cannot connect (' + . $e->getMessage() . '). Fix DB_USER/DB_PASS, or re-run with database ' + . 'admin credentials so the installer can create the user.'); + } + + // Optional first login account + if ($appUser !== '') { + if (!preg_match('/^[A-Za-z0-9_.-]{3,50}$/', $appUser)) { + throw new RuntimeException('Account username must be 3-50 characters (letters, digits, _ . -).'); + } + if (strlen($appPass) < 6) { + throw new RuntimeException('Account password must be at least 6 characters.'); + } + $stmt = $app->prepare('SELECT id FROM users WHERE username = ?'); + $stmt->execute([$appUser]); + if ($stmt->fetch()) { + $steps[] = [true, 'Account "' . $appUser . '" already exists — left unchanged.']; + } else { + $app->prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)') + ->execute([$appUser, password_hash($appPass, PASSWORD_DEFAULT)]); + $steps[] = [true, 'Created login account "' . $appUser . '".']; + } + } + + if (!is_dir(VIDEOS_ROOT)) { + $steps[] = [false, 'Warning: VIDEOS_ROOT (' . VIDEOS_ROOT . ') does not exist yet — ' + . 'set it in config.php before scanning.']; + } + + $notice = 'Installation complete. Open the app, log in, and use "Rescan library" ' + . '(or run: php scan.php) to index your videos.'; + return [true, $steps, $notice]; + } catch (Throwable $e) { + $steps[] = [false, $e->getMessage()]; + return [false, $steps, '']; + } +} + +/** Split a .sql file into statements (strips -- comments; no procedure support). */ +function splitSqlStatements(string $sql): array +{ + $lines = array_filter( + explode("\n", $sql), + fn($l) => !preg_match('/^\s*--/', $l) + ); + $stmts = array_map('trim', explode(';', implode("\n", $lines))); + return array_values(array_filter($stmts, fn($s) => $s !== '')); +} + +function renderPage(?array $steps, bool $done, string $notice): void +{ + $e = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); + ?> + + + + + +CourseHub installer + + + + +
+

CourseHub installer

+ + +
+ + + +
    + +
  • + + +
  • + +
+ + Open CourseHub + + + + +
+
+
+
Database admin credentials (optional)
+
+ Needed only on first install, to create the database and the app DB user + (). Used once, never stored. +
+
+
+
+
+
+
+
First login account (optional)
+
+
+
+
+
+ +
+
+ +
+ + +