Compare commits
6 Commits
d06f24b1fa
...
2a22a3e027
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a22a3e027 | |||
| c948b1e39d | |||
| 840058873a | |||
| d12b94b4b1 | |||
| 26b9e6fe97 | |||
| 3aab29cd03 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@
|
||||
|
||||
/references/
|
||||
composer.lock
|
||||
.continue
|
||||
@@ -71,6 +71,9 @@ RUN mkdir /var/www/html/var/cache
|
||||
RUN mkdir /var/www/html/var/log
|
||||
|
||||
RUN chown -R 33:33 /var/www/html /data
|
||||
RUN chmod -R 755 /var/www/html /data
|
||||
RUN find /var/www/html -type d -exec chmod 755 '{}' \;
|
||||
RUN find /var/www/html -type f -exec chmod 644 '{}' \;
|
||||
RUN chmod 755 /data
|
||||
RUN chmod 644 /data/*
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
11
config/packages/csrf.yaml
Normal file
11
config/packages/csrf.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
@@ -1,20 +1,40 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
connections:
|
||||
default:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
transfer:
|
||||
url: '%env(default::XFER_DATABASE_URL)%'
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '16'
|
||||
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
# report_fields_where_declared: true
|
||||
# validate_xml_mapping: true
|
||||
# auto_mapping: true
|
||||
default_entity_manager: default
|
||||
entity_managers:
|
||||
default:
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
transfer:
|
||||
connection: transfer
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
@@ -23,7 +43,7 @@ doctrine:
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: true
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
@@ -50,3 +70,6 @@ when@prod:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
|
||||
parameters:
|
||||
env(default_url): ''
|
||||
@@ -22,3 +22,6 @@ services:
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
App\Service\DatabaseTransferService:
|
||||
arguments:
|
||||
$projectDir: '%kernel.project_dir%'
|
||||
49
migrations/Version20260217014215.php
Normal file
49
migrations/Version20260217014215.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260217014215 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__messenger_messages AS SELECT id, body, headers, queue_name, created_at, available_at, delivered_at FROM messenger_messages');
|
||||
$this->addSql('DROP TABLE messenger_messages');
|
||||
$this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
|
||||
)');
|
||||
$this->addSql('INSERT INTO messenger_messages (id, body, headers, queue_name, created_at, available_at, delivered_at) SELECT id, body, headers, queue_name, created_at, available_at, delivered_at FROM __temp__messenger_messages');
|
||||
$this->addSql('DROP TABLE __temp__messenger_messages');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__messenger_messages AS SELECT id, body, headers, queue_name, created_at, available_at, delivered_at FROM messenger_messages');
|
||||
$this->addSql('DROP TABLE messenger_messages');
|
||||
$this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||
, delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
|
||||
)');
|
||||
$this->addSql('INSERT INTO messenger_messages (id, body, headers, queue_name, created_at, available_at, delivered_at) SELECT id, body, headers, queue_name, created_at, available_at, delivered_at FROM __temp__messenger_messages');
|
||||
$this->addSql('DROP TABLE __temp__messenger_messages');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Entity\User;
|
||||
use App\Entity\Speaker;
|
||||
use App\Entity\Series;
|
||||
use App\Entity\SharedNote;
|
||||
use App\Service\DatabaseTransferService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -40,6 +41,7 @@ class DefaultController extends AbstractController
|
||||
'last4Notes' => $last4Notes,
|
||||
'reverseNoteSort' => $openNotes,
|
||||
'isAdmin' => $this->isGranted('ROLE_ADMIN'),
|
||||
'xferDB' => DatabaseTransferService::isTransferEnabled(),
|
||||
'meta' => $meta,
|
||||
'speakers' => $speakers,
|
||||
'series' => $series,
|
||||
@@ -89,7 +91,7 @@ class DefaultController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/reference-editor', name: 'app_reference_editor')]
|
||||
#[Route('/reference-editor', 'app_reference_editor')]
|
||||
public function referenceEditor(EntityManagerInterface $emi): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -97,10 +99,63 @@ class DefaultController extends AbstractController
|
||||
return $this->render('editors/reference-editor.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/template-editor', name: 'app_template_editor')]
|
||||
#[Route('/template-editor', 'app_template_editor')]
|
||||
public function templateEditor(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
return $this->render('editors/template-editor.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/xfer-database', name: 'app_admin_transfer_db')]
|
||||
public function transfer(DatabaseTransferService $service, Request $request): Response
|
||||
{
|
||||
$step = $request->query->get('step', 'init');
|
||||
$session = $request->getSession();
|
||||
|
||||
if ($step === 'init') {
|
||||
$service->createSchema();
|
||||
$session->remove('transfer_logs');
|
||||
return $this->redirectToRoute('app_admin_transfer_db', ['step' => 0]);
|
||||
}
|
||||
|
||||
if ($step === 'summary') {
|
||||
// Finalize
|
||||
$service->finalizeEnvSwap();
|
||||
|
||||
return $this->render('default/transfer_summary.html.twig', [
|
||||
'logs' => $session->get('transfer_logs', [])
|
||||
]);
|
||||
}
|
||||
|
||||
$classes = $service->getEntityClasses();
|
||||
$totalClasses = count($classes);
|
||||
|
||||
if (isset($classes[$step])) {
|
||||
$class = explode("\\", $classes[$step]);
|
||||
$className = end($class);
|
||||
$func = "transfer{$className}Table";
|
||||
|
||||
if (method_exists($service, $func)) {
|
||||
$skippedCount = $service->{$func}();
|
||||
|
||||
if ($skippedCount > 0) {
|
||||
$logs = $session->get('transfer_logs', []);
|
||||
$logs[] = "Skipped $skippedCount orphaned records in the $className table.";
|
||||
$session->set('transfer_logs', $logs);
|
||||
}
|
||||
|
||||
$progress = round((($step+1) / $totalClasses) * 100);
|
||||
$nextStep = ($step + 1 < $totalClasses) ? ($step + 1) : 'summary';
|
||||
|
||||
return $this->render('default/transfer_progress.html.twig', [
|
||||
'current' => $className,
|
||||
'progress' => $progress,
|
||||
'next_step' => $nextStep,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7: Logout and redirect
|
||||
return $this->redirectToRoute('app_logout');
|
||||
}
|
||||
}
|
||||
|
||||
501
src/Service/DatabaseTransferService.php
Normal file
501
src/Service/DatabaseTransferService.php
Normal file
@@ -0,0 +1,501 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Bible;
|
||||
use App\Entity\Note;
|
||||
use App\Entity\Reference;
|
||||
use App\Entity\Series;
|
||||
use App\Entity\Speaker;
|
||||
use App\Entity\Template;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
class DatabaseTransferService
|
||||
{
|
||||
private PDO $srcDB;
|
||||
private PDO $destDB;
|
||||
|
||||
public function __construct(
|
||||
private string $projectDir,
|
||||
private ManagerRegistry $doctrine
|
||||
) {
|
||||
// connect the source database
|
||||
switch (explode(":", $_ENV['DATABASE_URL'])[0]) {
|
||||
case 'pdo-sqlite';
|
||||
list($driverName, $dbFile) = explode(":/", $_ENV['DATABASE_URL']);
|
||||
$this->srcDB = new PDO('sqlite:'.$dbFile);
|
||||
break;
|
||||
case 'pdo-mysql';
|
||||
case 'pdo-pgsql';
|
||||
$dsn = $this->convertDBURLString($_ENV['DATABASE_URL']);
|
||||
$this->srcDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']);
|
||||
}
|
||||
|
||||
// connect the destination database
|
||||
switch(explode(':', $_ENV['XFER_DATABASE_URL'])[0]) {
|
||||
case 'pdo-sqlite':
|
||||
list($driverName, $dbFile) = explode(":/", $_ENV['XFER_DATABASE']);
|
||||
$this->destDB = new PDO('sqlite:'.$dbFile);
|
||||
break;
|
||||
case 'pdo-mysql':
|
||||
case 'pdo-pgsql':
|
||||
$dsn = $this->convertDBURLString($_ENV['XFER_DATABASE_URL']);
|
||||
$this->destDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']);
|
||||
}
|
||||
}
|
||||
|
||||
public function createSchema(): void
|
||||
{
|
||||
$destEm = $this->doctrine->getManager('transfer');
|
||||
$metadata = $destEm->getMetadataFactory()->getAllMetadata();
|
||||
$tool = new SchemaTool($destEm);
|
||||
|
||||
$tool->dropSchema($metadata);
|
||||
$tool->createSchema($metadata);
|
||||
}
|
||||
|
||||
public function getEntityClasses(): array
|
||||
{
|
||||
return [
|
||||
User::class,
|
||||
Bible::class,
|
||||
Reference::class,
|
||||
Template::class,
|
||||
Series::class,
|
||||
Speaker::class,
|
||||
Note::class
|
||||
];
|
||||
}
|
||||
|
||||
public function transferUserTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM user";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO user (id, email, roles, password, name, meta_data, home_church_rss) ".
|
||||
"VALUES ".
|
||||
"(:id, :email, :roles, :password, :name, :meta_data, :home_church)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$params = [
|
||||
'id' => $row['id'],
|
||||
'email' => $row['email'],
|
||||
'roles' => $row['roles'],
|
||||
'password' => $row['password'],
|
||||
'name' => $row['name'],
|
||||
'meta_data' => $row['meta_data'],
|
||||
'home_church' => $row['home_church_rss'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function transferBibleTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM bible";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO bible (id, book, chapter, verse, content, book_index, label) ".
|
||||
"VALUES ".
|
||||
"(:id, :book, :chapter, :verse, :content, :book_index, :label)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$uuid = Uuid::fromString($row['id']);
|
||||
|
||||
$params = [
|
||||
'id' => $uuid->toBinary(),
|
||||
'book' => $row['book'],
|
||||
'chapter' => $row['chapter'],
|
||||
'verse' => $row['verse'],
|
||||
'content' => $row['content'],
|
||||
'book_index' => $row['book_index'],
|
||||
'label' => $row['label'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function transferReferenceTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM reference";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO reference (id, type, name, label, ndx, content) ".
|
||||
"VALUES ".
|
||||
"(:id, :type, :name, :label, :ndx, :content)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$params = [
|
||||
'id' => $row['id'],
|
||||
'type' => $row['type'],
|
||||
'name' => $row['name'],
|
||||
'label' => $row['label'],
|
||||
'ndx' => $row['ndx'],
|
||||
'content' => $row['content'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function transferTemplateTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM template";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO template (id, user_id, name, content) ".
|
||||
"VALUES ".
|
||||
"(:id, :user_id, :name, :content)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$params = [
|
||||
'id' => $row['id'],
|
||||
'user_id' => $row['user_id'],
|
||||
'name' => $row['name'],
|
||||
'content' => $row['content'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function transferSpeakerTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM speaker";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO speaker (id, name, user_id) ".
|
||||
"VALUES ".
|
||||
"(:id, :name, :user_id)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$params = [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['name'],
|
||||
'user_id' => $row['user_id'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function transferSeriesTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM series";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO series (id, name, user_id, template_id) ".
|
||||
"VALUES ".
|
||||
"(:id, :name, :user_id, :template_id)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$params = [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['name'],
|
||||
'user_id' => $row['user_id'],
|
||||
'template_id' => $row['template_id'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function transferNoteTable(): int
|
||||
{
|
||||
$sql = "SELECT * FROM note";
|
||||
$stmt = $this->srcDB->prepare($sql);
|
||||
$stmt->execute();
|
||||
$insQuery = "INSERT INTO note (id, title, date, passage, refs, text, speaker_id, series_id, user_id, recording) ".
|
||||
"VALUES ".
|
||||
"(:id, :title, :date, :passage, :refs, :text, :speaker_id, :series_id, :user_id, :recording)";
|
||||
$destStmt = $this->destDB->prepare($insQuery);
|
||||
$skippedCount = 0;
|
||||
|
||||
$this->destDB->beginTransaction();
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$params = [
|
||||
'id' => $row['id'],
|
||||
'speaker_id' => $row['speaker_id'],
|
||||
'series_id' => $row['series_id'],
|
||||
'user_id' => $row['user_id'],
|
||||
'title' => $row['title'],
|
||||
'date' => $row['date'],
|
||||
'passage' => $row['passage'],
|
||||
'refs' => $row['refs'],
|
||||
'text' => $row['text'],
|
||||
'recording' => $row['recording'],
|
||||
];
|
||||
|
||||
$this->destDB->exec("SAVEPOINT row_insert");
|
||||
|
||||
try {
|
||||
$destStmt->execute($params);
|
||||
|
||||
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
|
||||
} catch (PDOException $e) {
|
||||
$skipCodes = [
|
||||
'23000',
|
||||
'23503',
|
||||
'23505',
|
||||
];
|
||||
if (in_array($e->getCode(), $skipCodes, true)) {
|
||||
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
|
||||
$skippedCount++; // 4. Increment the counter
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->destDB->rollBack();
|
||||
print_r($params);
|
||||
die("Critical Error: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->destDB->commit();
|
||||
|
||||
return $skippedCount;
|
||||
}
|
||||
|
||||
public function finalizeEnvSwap(): void
|
||||
{
|
||||
$envPath = $this->projectDir . '/.env';
|
||||
|
||||
if (!file_exists($envPath)) {
|
||||
throw new \Exception(".env file not found at $envPath");
|
||||
}
|
||||
|
||||
$lines = file($envPath);
|
||||
$newLines = [];
|
||||
$xferUrlValue = null;
|
||||
|
||||
// 1. First pass: Find the value of XFER_DATABASE_URL
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^XFER_DATABASE_URL=(.*)/', trim($line), $matches)) {
|
||||
$xferUrlValue = $matches[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$xferUrlValue) {
|
||||
throw new \Exception("XFER_DATABASE_URL not found in .env file.");
|
||||
}
|
||||
|
||||
// 2. Second pass: Perform the swap
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
|
||||
// Prepend #OLD_ to the existing DATABASE_URL
|
||||
if (preg_match('/^DATABASE_URL=/', $trimmed)) {
|
||||
$newLines[] = "# OLD_" . $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rename XFER_DATABASE_URL to DATABASE_URL
|
||||
if (preg_match('/^XFER_DATABASE_URL=/', $trimmed)) {
|
||||
$newLines[] = "DATABASE_URL=" . $xferUrlValue . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
$newLines[] = $line;
|
||||
}
|
||||
|
||||
file_put_contents($envPath, implode('', $newLines));
|
||||
}
|
||||
|
||||
public static function isTransferEnabled(): bool
|
||||
{
|
||||
return (isset($_ENV['XFER_DATABASE_URL']) && !empty($_ENV['XFER_DATABASE_URL']));
|
||||
}
|
||||
|
||||
private function convertDBURLString(string $DBURL): array
|
||||
{
|
||||
$dsn = preg_split("/[:|\/|\@]+/", $DBURL);
|
||||
if (count($dsn) > 6) {
|
||||
die('You cannot have special characters in your password for the database entry during this process. Change the database user password to just use alpha-numeric characters, then run again. You can change it back to more secure after this transfer is complete');
|
||||
}
|
||||
|
||||
$res = [
|
||||
'dsn' => substr($dsn[0], 4).':'.
|
||||
'host='.$dsn[3].';'.
|
||||
'port='.$dsn[4].';'.
|
||||
'dbname='.$dsn[5],
|
||||
'username' => $dsn[1],
|
||||
'password' => $dsn[2]
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@
|
||||
<li><a href="#" onclick="saveNote()">Save Note</a></li>
|
||||
{% if isAdmin is defined and isAdmin %}
|
||||
<li><a href='/reference-editor'>Reference Editor</a></li>
|
||||
{% if xferDB is defined and xferDB %}
|
||||
<li><a href='/xfer-database'>Transfer Database</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li><a href='#' onclick="openRef()">Open Reference</a></li>
|
||||
<li><a href='/template-editor'>Template Editor</a></li>
|
||||
|
||||
24
templates/default/transfer_progress.html.twig
Normal file
24
templates/default/transfer_progress.html.twig
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-5 text-center">
|
||||
<h2>Transferring Database...</h2>
|
||||
<p>Processing: <strong>{{ current }}</strong></p>
|
||||
|
||||
<div class="progress mb-3" style="height: 30px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: {{ progress }}%;">
|
||||
{{ progress }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Please do not close this window.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Automatically move to the next step after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = "{{ path('app_admin_transfer_db', {step: next_step}) }}";
|
||||
}, 500);
|
||||
</script>
|
||||
{% endblock %}
|
||||
29
templates/default/transfer_summary.html.twig
Normal file
29
templates/default/transfer_summary.html.twig
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-5 text-center">
|
||||
<h2 class="text-success">Database Transfer Complete!</h2>
|
||||
<p>Your data has been successfully moved to the new database, and the environment variables have been updated.</p>
|
||||
|
||||
{% if logs|length > 0 %}
|
||||
<div class="alert alert-warning text-start mx-auto" style="max-width: 600px;">
|
||||
<h5 class="alert-heading">Data Clean-up Notice</h5>
|
||||
<p>During the transfer, the following orphaned records were safely ignored to maintain database integrity:</p>
|
||||
<ul>
|
||||
{% for log in logs %}
|
||||
<li>{{ log }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success mx-auto" style="max-width: 600px;">
|
||||
Perfect transfer! No orphaned records were found.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-muted">You must log in again to establish a connection with the new database.</p>
|
||||
<a href="{{ path('app_logout') }}" class="btn btn-primary btn-lg">Acknowledge & Relogin</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user