--- /dev/null
+<?php
+
+namespace Cli\Commands;
+
+use Minicli\Command\CommandCall;
+use RecursiveDirectoryIterator;
+use Symfony\Component\Process\ExecutableFinder;
+use Symfony\Component\Process\Process;
+use ZipArchive;
+
+final class BackupCommand
+{
+ public function __construct(
+ protected string $appDir
+ ) {
+ }
+
+ public function handle(CommandCall $input)
+ {
+ // TODO - Customizable output file
+ // TODO - Database only command
+ // TODO - Validate DB vars
+ // TODO - Error handle each stage
+ // TODO - Validate zip (and any other extensions required) are active.
+
+ $zipOutFile = getcwd() . DIRECTORY_SEPARATOR . 'backup.zip';
+
+ $dbHost = ($_SERVER['DB_HOST'] ?? '');
+ $dbUser = ($_SERVER['DB_USERNAME'] ?? '');
+ $dbPass = ($_SERVER['DB_PASSWORD'] ?? '');
+ $dbDatabase = ($_SERVER['DB_DATABASE'] ?? '');
+
+ // Create a mysqldump for the BookStack database
+ $executableFinder = new ExecutableFinder();
+ $mysqldumpPath = $executableFinder->find('mysqldump');
+
+ $process = new Process([
+ $mysqldumpPath,
+ '-h', $dbHost,
+ '-u', $dbUser,
+ '-p' . $dbPass,
+ '--single-transaction',
+ '--no-tablespaces',
+ $dbDatabase,
+ ]);
+ $process->start();
+
+ $errors = "";
+ $dumpTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
+ $dumpTempFileResource = fopen($dumpTempFile, 'w');
+ foreach ($process as $type => $data) {
+ if ($process::OUT === $type) {
+ fwrite($dumpTempFileResource, $data);
+ } else { // $process::ERR === $type
+ $errors .= $data . "\n";
+ }
+ }
+ fclose($dumpTempFileResource);
+
+
+ // Create a new ZIP file
+ $zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
+ $zip = new ZipArchive();
+ $sep = DIRECTORY_SEPARATOR;
+ $zip->open($zipTempFile, ZipArchive::CREATE);
+ $zip->addFile($this->appDir . $sep . '.env', '.env');
+ $zip->addFile($dumpTempFile, 'db.sql');
+
+ $fileDirs = [
+ $this->appDir . $sep . 'public' . $sep . 'uploads' => 'public/uploads',
+ $this->appDir . $sep . 'storage' . $sep . 'uploads' => 'storage/uploads',
+ ];
+
+ foreach ($fileDirs as $fullFileDir => $relativeFileDir) {
+ $dirIter = new RecursiveDirectoryIterator($fullFileDir);
+ $fileIter = new \RecursiveIteratorIterator($dirIter);
+ /** @var \SplFileInfo $file */
+ foreach ($fileIter as $file) {
+ if (!$file->isDir()) {
+ $zip->addFile($file->getPathname(), $relativeFileDir . '/' . $fileIter->getSubPathname());
+ }
+ }
+ }
+
+ // Close off our zip and move it to the required location
+ $zip->close();
+ rename($zipTempFile, $zipOutFile);
+
+ // Delete our temporary DB dump file
+ unlink($dumpTempFile);
+
+ // Announce end and display errors
+ echo "Finished";
+ if ($errors) {
+ echo " with the following errors:\n" . $errors;
+ }
+ }
+}
// Add the rest of the apps files
$phar->addFile(__DIR__ . '/run', 'run');
$phar->buildFromDirectory(__DIR__, '/vendor(.*)/');
+ $phar->buildFromDirectory(__DIR__, '/Commands(.*)/');
// Customize the stub to add the shebang
$stub = "#!/usr/bin/env php \n" . $defaultStub;
{
"require": {
"minicli/minicli": "^3.2",
+ "symfony/process": "^6.0",
"vlucas/phpdotenv": "^5.5"
},
+ "autoload": {
+ "psr-4": {
+ "Cli\\Commands\\": "Commands/"
+ }
+ },
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d834df279b63d647dcaf31ae96468a27",
+ "content-hash": "b56ccc782d009482249ce5e3a4b62147",
"packages": [
{
"name": "graham-campbell/result-type",
],
"time": "2022-11-03T14:55:06+00:00"
},
+ {
+ "name": "symfony/process",
+ "version": "v6.0.19",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "2114fd60f26a296cc403a7939ab91478475a33d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/2114fd60f26a296cc403a7939ab91478475a33d4",
+ "reference": "2114fd60f26a296cc403a7939ab91478475a33d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v6.0.19"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-01-01T08:36:10+00:00"
+ },
{
"name": "vlucas/phpdotenv",
"version": "v5.5.0",
require __DIR__ . '/vendor/autoload.php';
+use Cli\Commands\BackupCommand;
use Minicli\App;
-use Minicli\Command\CommandCall;
// Get the directory of the CLI "entrypoint", adjusted to be the real
// location where running via a phar.
if (str_starts_with($scriptDir, 'phar://')) {
$scriptDir = dirname(Phar::running(false));
}
+$bsDir = dirname($scriptDir);
// Load in our .env file from the parent directory
-$dotenv = Dotenv\Dotenv::createImmutable(dirname($scriptDir));
+$dotenv = Dotenv\Dotenv::createImmutable($bsDir);
$dotenv->safeLoad();
// Setup our CLI
$app = new App();
-$app->setSignature('./run mycommand');
+$app->setSignature('./run');
-$app->registerCommand('mycommand', function (CommandCall $input) use ($scriptDir) {
- echo "My Command!";
- echo "BS URL is: " . ($_SERVER['BS_URL'] ?? '') . "\n";
- echo "DB HOST is: " . ($_SERVER['DB_HOST'] ?? '') . "\n";
- echo dirname($scriptDir);
-
-// var_dump($input);
-});
+$app->registerCommand('backup', [new BackupCommand($bsDir), 'handle']);
$app->runCommand($argv);