}
},
"require-dev": {
- "phpunit/phpunit": "^9.6"
+ "phpunit/phpunit": "^9.6",
+ "ext-mysqli": "*"
}
}
- ./:/cli
depends_on:
db:
- condition: service_completed_successfully
+ condition: service_healthy
db:
image: mysql:8.0
environment:
MYSQL_USER: bookstack
MYSQL_PASSWORD: bookstack
volumes:
- - ./docker-mysql-init.sql:/docker-entrypoint-initdb.d/init.sql
\ No newline at end of file
+ - ./docker-mysql-init.sql:/docker-entrypoint-initdb.d/init.sql
+ healthcheck:
+ test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
+ timeout: 20s
+ retries: 10
\ No newline at end of file
vendor/bin/phpunit
# To clean-up and delete the environment:
-docker compose rm -fsv
+docker compose down -v --remove-orphans
```
Within the environment a pre-existing BookStack instance can be found at `/var/www/bookstack` for testing.
/**
* @throws CommandError
+ * @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$dbDump = $extractDir . DIRECTORY_SEPARATOR . 'db.sql';
$currentEnv = EnvironmentLoader::load($appDir);
$mysql = MySqlRunner::fromEnvOptions($currentEnv);
+
+ // Drop existing tables
+ $dropSqlTempFile = tempnam(sys_get_temp_dir(), 'bs-cli-restore');
+ file_put_contents($dropSqlTempFile, $mysql->dropTablesSql());
+ $mysql->importSqlFile($dropSqlTempFile);
+
+
$mysql->importSqlFile($dbDump);
}
}
public function testConnection(): bool
{
$output = (new ProgramRunner('mysql', '/usr/bin/mysql'))
+ ->withEnvironment(['MYSQL_PWD' => $this->password])
->withTimeout(240)
->withIdleTimeout(5)
->runCapturingStdErr([
'-h', $this->host,
'-P', $this->port,
'-u', $this->user,
- '-p' . $this->password,
$this->database,
'-e', "show tables;"
]);
public function importSqlFile(string $sqlFilePath): void
{
$output = (new ProgramRunner('mysql', '/usr/bin/mysql'))
+ ->withEnvironment(['MYSQL_PWD' => $this->password])
->withTimeout(240)
->withIdleTimeout(5)
->runCapturingStdErr([
'-h', $this->host,
'-P', $this->port,
'-u', $this->user,
- '-p' . $this->password,
$this->database,
- '<', $sqlFilePath
+ '-e', "source {$sqlFilePath}"
]);
if ($output) {
}
}
+ public function dropTablesSql(): string
+ {
+ return <<<'HEREDOC'
+SET FOREIGN_KEY_CHECKS = 0;
+SET GROUP_CONCAT_MAX_LEN=32768;
+SET @tables = NULL;
+SELECT GROUP_CONCAT('`', table_name, '`') INTO @tables
+ FROM information_schema.tables
+ WHERE table_schema = (SELECT DATABASE());
+SELECT IFNULL(@tables,'dummy') INTO @tables;
+
+SET @tables = CONCAT('DROP TABLE IF EXISTS ', @tables);
+PREPARE stmt FROM @tables;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+SET FOREIGN_KEY_CHECKS = 1;
+HEREDOC;
+ }
+
public function runDumpToFile(string $filePath): void
{
$file = fopen($filePath, 'w');
(new ProgramRunner('mysqldump', '/usr/bin/mysqldump'))
->withTimeout(240)
->withIdleTimeout(15)
+ ->withEnvironment(['MYSQL_PWD' => $this->password])
->runWithoutOutputCallbacks([
'-h', $this->host,
'-P', $this->port,
'-u', $this->user,
- '-p' . $this->password,
'--single-transaction',
'--no-tablespaces',
$this->database,
fwrite($file, $data);
$hasOutput = true;
}, function ($error) use (&$errors) {
- if (!str_contains($error, '[Warning] ')) {
- $errors .= $error . "\n";
- }
+ $errors .= $error . "\n";
});
} catch (\Exception $exception) {
fclose($file);
--- /dev/null
+<?php declare(strict_types=1);
+
+namespace Tests\Commands;
+
+use Tests\TestCase;
+
+class RestoreCommandTest extends TestCase
+{
+
+ public function test_restore_into_cwd_by_default_with_all_content_types()
+ {
+ $mysql = new \mysqli('db', 'bookstack', 'bookstack', 'bookstack');
+ $mysql->query('CREATE TABLE xx_testing (labels varchar(255));');
+
+ $result = $mysql->query('SHOW TABLES LIKE \'zz_testing\';');
+ $this->assertEquals(0, mysqli_num_rows($result));
+
+ $zipFile = $this->buildZip(function (\ZipArchive $zip) {
+ $zip->addFromString('.env', "APP_KEY=abc123\nAPP_URL=https://example.com");
+ $zip->addFromString('public/uploads/test.txt', 'hello-public-uploads');
+ $zip->addFromString('storage/uploads/test.txt', 'hello-storage-uploads');
+ $zip->addFromString('themes/test.txt', 'hello-themes');
+ $zip->addFromString('db.sql', "CREATE TABLE zz_testing (names varchar(255));\nINSERT INTO zz_testing values ('barry');");
+ });
+
+ exec('cp -r /var/www/bookstack /var/www/bookstack-restore');
+ chdir('/var/www/bookstack-restore');
+
+ $result = $this->runCommand('restore', [
+ 'backup-zip' => $zipFile,
+ ], [
+ 'yes', '1'
+ ]);
+
+ $result->dumpError();
+ $result->assertSuccessfulExit();
+ $result->assertStdoutContains('Restore operation complete!');
+
+ $result = $mysql->query('SELECT * FROM zz_testing where names = \'barry\';');
+ $this->assertEquals(1, mysqli_num_rows($result));
+ $result = $mysql->query('SHOW TABLES LIKE \'xx_testing\';');
+ $this->assertEquals(0, mysqli_num_rows($result));
+
+ $this->assertStringEqualsFile('/var/www/bookstack-restore/public/uploads/test.txt', 'hello-public-uploads');
+ $this->assertStringEqualsFile('/var/www/bookstack-restore/storage/uploads/test.txt', 'hello-storage-uploads');
+ $this->assertStringEqualsFile('/var/www/bookstack-restore/themes/test.txt', 'hello-themes');
+
+ $mysql->query("DROP TABLE zz_testing;");
+ exec('rm -rf /var/www/bookstack-restore');
+ }
+
+ protected function buildZip(callable $builder): string
+ {
+ $zipFile = tempnam(sys_get_temp_dir(), 'cli-test');
+ $testZip = new \ZipArchive('');
+ $testZip->open($zipFile);
+ $builder($testZip);
+ $testZip->close();
+
+ return $zipFile;
+ }
+}
\ No newline at end of file
return require dirname(__DIR__) . '/src/app.php';
}
- protected function runCommand(string $command, array $args = []): CommandResult
+ protected function runCommand(string $command, array $args = [], array $inputs = []): CommandResult
{
$app = $this->getApp();
$command = $app->find($command);
$err = null;
$commandTester = new CommandTester($command);
+ if (!empty($inputs)) {
+ $commandTester->setInputs($inputs);
+ }
+
try {
$commandTester->execute($args);
} catch (\Exception $exception) {