3 namespace Cli\Commands;
5 use Minicli\Command\CommandCall;
6 use RecursiveDirectoryIterator;
8 use Symfony\Component\Process\ExecutableFinder;
9 use Symfony\Component\Process\Process;
12 final class BackupCommand
14 public function __construct(
15 protected string $appDir
19 public function handle(CommandCall $input)
21 $handleDatabase = !$input->hasFlag('no-database');
22 $handleUploads = !$input->hasFlag('no-uploads');
23 $suggestedOutPath = $input->subcommand ?: '';
25 // TODO - Validate DB vars
26 // TODO - Backup themes directory, extra flag for no-themes
27 // TODO - Backup the running phar? For easier direct restore...
28 // TODO - Error handle each stage
29 // TODO - Validate zip (and any other extensions required) are active.
31 $zipOutFile = $this->buildZipFilePath($suggestedOutPath);
32 $dumpTempFile = $this->createDatabaseDump();
34 // Create a new ZIP file
35 $zipTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
36 $zip = new ZipArchive();
37 $zip->open($zipTempFile, ZipArchive::CREATE);
38 $zip->addFile($this->appDir . DIRECTORY_SEPARATOR . '.env', '.env');
40 if ($handleDatabase) {
41 $zip->addFile($dumpTempFile, 'db.sql');
45 $this->addUploadFoldersToZip($zip);
48 // Close off our zip and move it to the required location
50 rename($zipTempFile, $zipOutFile);
52 // Delete our temporary DB dump file
53 unlink($dumpTempFile);
55 // Announce end and display errors
60 * Build a full zip path from the given suggestion, which may be empty,
61 * a path to a folder, or a path to a file in relative or absolute form.
63 protected function buildZipFilePath(string $suggestedOutPath): string
65 $zipDir = getcwd() ?: $this->appDir;
66 $zipName = "bookstack-backup-" . date('Y-m-d-His') . '.zip';
68 if ($suggestedOutPath) {
69 if (is_dir($suggestedOutPath)) {
70 $zipDir = realpath($suggestedOutPath);
71 } else if (is_dir(dirname($suggestedOutPath))) {
72 $zipDir = realpath(dirname($suggestedOutPath));
73 $zipName = basename($suggestedOutPath);
75 // TODO - Handle not found output
79 $fullPath = $zipDir . DIRECTORY_SEPARATOR . $zipName;
81 if (file_exists($fullPath)) {
89 * Add app-relative upload folders to the provided zip archive.
90 * Will recursively go through all directories to add all files.
92 protected function addUploadFoldersToZip(ZipArchive $zip): void
95 $this->appDir . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'uploads' => 'public/uploads',
96 $this->appDir . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'uploads' => 'storage/uploads',
99 foreach ($fileDirs as $fullFileDir => $relativeFileDir) {
100 $dirIter = new RecursiveDirectoryIterator($fullFileDir);
101 $fileIter = new \RecursiveIteratorIterator($dirIter);
102 /** @var SplFileInfo $file */
103 foreach ($fileIter as $file) {
104 if (!$file->isDir()) {
105 $zip->addFile($file->getPathname(), $relativeFileDir . '/' . $fileIter->getSubPathname());
112 * Create a database dump and return the path to the dumped SQL output.
114 protected function createDatabaseDump(): string
116 $dbHost = ($_SERVER['DB_HOST'] ?? '');
117 $dbUser = ($_SERVER['DB_USERNAME'] ?? '');
118 $dbPass = ($_SERVER['DB_PASSWORD'] ?? '');
119 $dbDatabase = ($_SERVER['DB_DATABASE'] ?? '');
121 // Create a mysqldump for the BookStack database
122 $executableFinder = new ExecutableFinder();
123 $mysqldumpPath = $executableFinder->find('mysqldump');
125 $process = new Process([
130 '--single-transaction',
137 $dumpTempFile = tempnam(sys_get_temp_dir(), 'bsbackup');
138 $dumpTempFileResource = fopen($dumpTempFile, 'w');
139 foreach ($process as $type => $data) {
140 if ($process::OUT === $type) {
141 fwrite($dumpTempFileResource, $data);
142 } else { // $process::ERR === $type
143 $errors .= $data . "\n";
146 fclose($dumpTempFileResource);
148 // TODO - Throw errors if existing
149 return $dumpTempFile;