-<?php
+<?php declare(strict_types=1);
namespace Cli\Commands;
use Cli\Services\AppLocator;
use Cli\Services\EnvironmentLoader;
use Cli\Services\MySqlRunner;
+use Cli\Services\Paths;
+use FilesystemIterator;
use RecursiveDirectoryIterator;
use SplFileInfo;
use Symfony\Component\Console\Command\Command;
{
$this->setName('backup');
$this->setDescription('Backup a BookStack installation to a single compressed ZIP file.');
- $this->addArgument('backup-path', InputArgument::OPTIONAL, 'Outfile file or directory to store the resulting backup file.', '');
- $this->addOption('no-database', null, null, "Skip adding a database dump to the backup");
- $this->addOption('no-uploads', null, null, "Skip adding uploaded files to the backup");
- $this->addOption('no-themes', null, null, "Skip adding the themes folder to the backup");
+ $this->addArgument('backup-path', InputArgument::OPTIONAL, 'Outfile file or directory to store the resulting backup ZIP file.', '');
+ $this->addOption('no-database', null, InputOption::VALUE_NONE, "Skip adding a database dump to the backup");
+ $this->addOption('no-uploads', null, InputOption::VALUE_NONE, "Skip adding uploaded files to the backup");
+ $this->addOption('no-themes', null, InputOption::VALUE_NONE, "Skip adding the themes folder to the backup");
$this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to backup', '');
}
$zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
$dumpTempFile = '';
$zip = new ZipArchive();
- $zip->open($zipTempFile, ZipArchive::CREATE);
+ $zip->open($zipTempFile, ZipArchive::OVERWRITE);
- // Add default files (.env config file and this CLI)
- $zip->addFile($appDir . DIRECTORY_SEPARATOR . '.env', '.env');
- $zip->addFile($appDir . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'run', 'run');
+ // Add default files (.env config file and this CLI if existing)
+ $zip->addFile(Paths::join($appDir, '.env'), '.env');
+ $cliPath = Paths::join($appDir, 'bookstack-system-cli');
+ if (file_exists($cliPath)) {
+ $zip->addFile($cliPath, 'bookstack-system-cli');
+ }
if ($handleDatabase) {
$output->writeln("<info>Dumping the database via mysqldump...</info>");
- $dumpTempFile = $this->createDatabaseDump($appDir);
+ $dumpTempFile = $this->createDatabaseDump($appDir, $output);
$output->writeln("<info>Adding database dump to backup archive...</info>");
$zip->addFile($dumpTempFile, 'db.sql');
}
if ($handleThemes) {
$output->writeln("<info>Adding BookStack theme folders to backup archive...</info>");
- $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$appDir, 'themes']), 'themes');
+ $this->addFolderToZipRecursive($zip, Paths::join($appDir, 'themes'), 'themes');
}
+ $output->writeln("<info>Saving backup archive...</info>");
// Close off our zip and move it to the required location
$zip->close();
// Delete our temporary DB dump file if exists. Must be done after zip close.
rename($zipTempFile, $zipOutFile);
// Announce end
- $output->writeln("<info>Backup finished.</info>");
+ $output->writeln("<success>Backup finished.</success>");
$output->writeln("Output ZIP saved to: {$zipOutFile}");
return Command::SUCCESS;
/**
* Build a full zip path from the given suggestion, which may be empty,
* a path to a folder, or a path to a file in relative or absolute form.
+ * Targets the <app>/backups directory by default if existing, otherwise <app>.
* @throws CommandError
*/
protected function buildZipFilePath(string $suggestedOutPath, string $appDir): string
{
- $zipDir = getcwd() ?: $appDir;
+ $zipDir = Paths::join($appDir, 'storage', 'backups');
$zipName = "bookstack-backup-" . date('Y-m-d-His') . '.zip';
if ($suggestedOutPath) {
+ $suggestedOutPath = Paths::resolve($suggestedOutPath);
if (is_dir($suggestedOutPath)) {
$zipDir = realpath($suggestedOutPath);
} else if (is_dir(dirname($suggestedOutPath))) {
} else {
throw new CommandError("Could not resolve provided [{$suggestedOutPath}] path to an existing folder.");
}
+ } else {
+ if (!is_dir($zipDir)) {
+ $zipDir = $appDir;
+ }
}
- $fullPath = $zipDir . DIRECTORY_SEPARATOR . $zipName;
+ $fullPath = Paths::join($zipDir, $zipName);
if (file_exists($fullPath)) {
throw new CommandError("Target ZIP output location at [{$fullPath}] already exists.");
+ } else if (!is_dir($zipDir)) {
+ throw new CommandError("Target ZIP output directory [{$fullPath}] could not be found.");
+ } else if (!is_writable($zipDir)) {
+ throw new CommandError("Target ZIP output directory [{$fullPath}] is not writable.");
}
return $fullPath;
*/
protected function addUploadFoldersToZip(ZipArchive $zip, string $appDir): void
{
- $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$appDir, 'public', 'uploads']), 'public/uploads');
- $this->addFolderToZipRecursive($zip, implode(DIRECTORY_SEPARATOR, [$appDir, 'storage', 'uploads']), 'storage/uploads');
+ $this->addFolderToZipRecursive($zip, Paths::join($appDir, 'public', 'uploads'), 'public/uploads');
+ $this->addFolderToZipRecursive($zip, Paths::join($appDir, 'storage', 'uploads'), 'storage/uploads');
}
/**
*/
protected function addFolderToZipRecursive(ZipArchive $zip, string $dirPath, string $targetZipPath): void
{
- $dirIter = new RecursiveDirectoryIterator($dirPath);
+ $dirIter = new RecursiveDirectoryIterator(
+ $dirPath,
+ FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::FOLLOW_SYMLINKS
+ );
$fileIter = new \RecursiveIteratorIterator($dirIter);
/** @var SplFileInfo $file */
foreach ($fileIter as $file) {
if (!$file->isDir()) {
- $zip->addFile($file->getPathname(), $targetZipPath . '/' . $fileIter->getSubPathname());
+ $zip->addFile($file->getRealPath(), $targetZipPath . '/' . $fileIter->getSubPathname());
}
}
}
* Create a database dump and return the path to the dumped SQL output.
* @throws CommandError
*/
- protected function createDatabaseDump(string $appDir): string
+ protected function createDatabaseDump(string $appDir, OutputInterface $output): string
{
$envOptions = EnvironmentLoader::loadMergedWithCurrentEnv($appDir);
$mysql = MySqlRunner::fromEnvOptions($envOptions);
$dumpTempFile = tempnam(sys_get_temp_dir(), 'bsdbdump');
try {
- $mysql->runDumpToFile($dumpTempFile);
+ $warnings = $mysql->runDumpToFile($dumpTempFile);
+ if ($warnings) {
+ $output->writeln("<warn>Received warnings during mysqldump:\n{$warnings}</warn>");
+ }
} catch (\Exception $exception) {
unlink($dumpTempFile);
throw new CommandError($exception->getMessage());