From d28770dc075c5d7d220f7c540e4cdd8620724930 Mon Sep 17 00:00:00 2001 From: Jan <96944229+modelrailroader@users.noreply.github.com> Date: Sun, 16 Jun 2024 17:46:28 +0200 Subject: [PATCH 1/3] feat(api): added possibility for backup of content-folder --- docs/openapi.json | 9 +++- docs/openapi.yaml | 7 ++- .../src/phpMyFAQ/Administration/Backup.php | 33 +++++++++++++ .../Controller/Api/BackupController.php | 46 ++++++++++++++++--- phpmyfaq/src/phpMyFAQ/Enums/BackupType.php | 1 + phpmyfaq/src/phpMyFAQ/Faq/Statistics.php | 2 + 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index d9a255da59..aaafd74006 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -82,7 +82,7 @@ { "name": "type", "in": "path", - "description": "The backup type. Can be \"data\" or \"logs\".", + "description": "The backup type. Can be \"data\", \"logs\" or \"content\".", "required": true, "schema": { "type": "string" @@ -91,7 +91,7 @@ ], "responses": { "200": { - "description": "The current backup as a file.", + "description": "The current backup as a file or ZipArchive in case of \"content\"-type.", "headers": { "Accept-Language": { "description": "The language code for the login.", @@ -111,6 +111,11 @@ "schema": { "type": "string" } + }, + "application/zip": { + "schema": { + "type": "string" + } } } }, diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7dee404630..500559d27d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -57,13 +57,13 @@ paths: parameters: - name: type in: path - description: 'The backup type. Can be "data" or "logs".' + description: 'The backup type. Can be "data", "logs" or "content".' required: true schema: type: string responses: '200': - description: 'The current backup as a file.' + description: 'The current backup as a file or ZipArchive in case of "content"-type.' headers: Accept-Language: description: 'The language code for the login.' @@ -77,6 +77,9 @@ paths: application/octet-stream: schema: type: string + application/zip: + schema: + type: string '400': description: 'If the backup type is wrong' headers: diff --git a/phpmyfaq/src/phpMyFAQ/Administration/Backup.php b/phpmyfaq/src/phpMyFAQ/Administration/Backup.php index 48d58af62e..90927e0429 100644 --- a/phpmyfaq/src/phpMyFAQ/Administration/Backup.php +++ b/phpmyfaq/src/phpMyFAQ/Administration/Backup.php @@ -18,10 +18,14 @@ namespace phpMyFAQ\Administration; use phpMyFAQ\Configuration; +use phpMyFAQ\Core\Exception; use phpMyFAQ\Database; use phpMyFAQ\Database\DatabaseHelper; use phpMyFAQ\Enums\BackupType; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use SodiumException; +use ZipArchive; /** * Class Backup @@ -153,4 +157,33 @@ private function getBackupHeader(string $tableNames): array '-- Otherwise this backup will be broken.' ]; } + + public function createContentFolderBackup(): string|bool + { + $zipFile = PMF_ROOT_DIR . DIRECTORY_SEPARATOR . 'content.zip'; + + $zipArchive = new ZipArchive(); + if ($zipArchive->open($zipFile, ZipArchive::CREATE) !== true) { + return false; + } + + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(PMF_CONTENT_DIR) + ); + + foreach ($files as $file) { + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen(PMF_CONTENT_DIR) + 1); + + if ($file->isDir()) { + $zipArchive->addEmptyDir($relativePath); + } else { + $zipArchive->addFile($filePath, $relativePath); + } + } + + $zipArchive->close(); + + return file_exists($zipFile) ? $zipFile : false; + } } diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Api/BackupController.php b/phpmyfaq/src/phpMyFAQ/Controller/Api/BackupController.php index 09567bd2d6..e042170317 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Api/BackupController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Api/BackupController.php @@ -37,22 +37,22 @@ class BackupController extends AbstractController )] #[OA\Parameter( name: 'type', - description: 'The backup type. Can be "data" or "logs".', + description: 'The backup type. Can be "data", "logs" or "content".', in: 'path', required: true, schema: new OA\Schema(type: 'string') )] #[OA\Response( response: 200, - description: 'The current backup as a file.', + description: 'The current backup as a file or a ZipArchive in case of "content"-type.', content: new OA\MediaType( - mediaType: 'application/octet-stream', - schema: new OA\Schema(type: 'string') + mediaType: 'application/octet-stream or application/zip', + schema: new OA\Schema(type: 'string'), ) )] #[OA\Response( response: 400, - description: 'If the backup type is wrong', + description: 'If the backup type is wrong or an internal error occurred', content: new OA\MediaType( mediaType: 'application/octet-stream', schema: new OA\Schema(type: 'string') @@ -75,12 +75,43 @@ public function download(Request $request): Response case 'logs': $backupType = BackupType::BACKUP_TYPE_LOGS; break; + case 'content': + $backupType = BackupType::BACKUP_TYPE_CONTENT; + break; default: return new Response('Invalid backup type.', Response::HTTP_BAD_REQUEST); } $dbHelper = new DatabaseHelper($this->configuration); $backup = new Backup($this->configuration, $dbHelper); + + // Create ZipArchive of content-folder + if ($backupType === BackupType::BACKUP_TYPE_CONTENT) { + $backupFile = $backup->createContentFolderBackup(); + if ($backupFile !== false) { + $response = new Response(file_get_contents($backupFile)); + + $backupFileName = sprintf('content_%s.zip', date('dmY_H-i')); + + $disposition = HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + urlencode($backupFileName) + ); + + $response->headers->set('Content-Type', 'application/zip'); + $response->headers->set('Content-Disposition', $disposition); + $response->setStatusCode(Response::HTTP_OK); + // Remove temporary ZipArchive + unlink($backupFile); + return $response->send(); + } else { + return new Response( + 'An error occurred while creating the zip-file.', + Response::HTTP_INTERNAL_SERVER_ERROR + ); + } + } + $tableNames = $backup->getBackupTableNames($backupType); $backupQueries = $backup->generateBackupQueries($tableNames); @@ -99,7 +130,10 @@ public function download(Request $request): Response $response->setStatusCode(Response::HTTP_OK); return $response->send(); } catch (SodiumException) { - return new Response('An error occurred while creating the backup.', Response::HTTP_INTERNAL_SERVER_ERROR); + return new Response( + 'An error occurred while creating the backup.', + Response::HTTP_INTERNAL_SERVER_ERROR + ); } } } diff --git a/phpmyfaq/src/phpMyFAQ/Enums/BackupType.php b/phpmyfaq/src/phpMyFAQ/Enums/BackupType.php index 76ff7fa801..79768d813f 100644 --- a/phpmyfaq/src/phpMyFAQ/Enums/BackupType.php +++ b/phpmyfaq/src/phpMyFAQ/Enums/BackupType.php @@ -21,4 +21,5 @@ enum BackupType: string { case BACKUP_TYPE_DATA = 'data'; case BACKUP_TYPE_LOGS = 'logs'; + case BACKUP_TYPE_CONTENT = 'content'; } diff --git a/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php b/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php index 1501f18c6b..9a2918c2eb 100644 --- a/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php +++ b/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php @@ -51,6 +51,8 @@ public function __construct(private readonly Configuration $configuration) if ($this->configuration->get('security.permLevel') !== 'basic') { $this->groupSupport = true; + } else { + $this->groupSupport = false; } } From a9606c4ed1861ff9556db15de09bd902b6903c9a Mon Sep 17 00:00:00 2001 From: Jan <96944229+modelrailroader@users.noreply.github.com> Date: Sun, 16 Jun 2024 18:12:06 +0200 Subject: [PATCH 2/3] fix: throw exception --- phpmyfaq/src/phpMyFAQ/Administration/Backup.php | 7 ++++++- phpmyfaq/src/phpMyFAQ/Faq/Statistics.php | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Administration/Backup.php b/phpmyfaq/src/phpMyFAQ/Administration/Backup.php index 90927e0429..dbdf875493 100644 --- a/phpmyfaq/src/phpMyFAQ/Administration/Backup.php +++ b/phpmyfaq/src/phpMyFAQ/Administration/Backup.php @@ -158,13 +158,18 @@ private function getBackupHeader(string $tableNames): array ]; } + /** + * Creates a ZipArchive of the content-folder + * + * @throws \Exception + */ public function createContentFolderBackup(): string|bool { $zipFile = PMF_ROOT_DIR . DIRECTORY_SEPARATOR . 'content.zip'; $zipArchive = new ZipArchive(); if ($zipArchive->open($zipFile, ZipArchive::CREATE) !== true) { - return false; + throw new Exception('Error while creating ZipArchive'); } $files = new RecursiveIteratorIterator( diff --git a/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php b/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php index 9a2918c2eb..1501f18c6b 100644 --- a/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php +++ b/phpmyfaq/src/phpMyFAQ/Faq/Statistics.php @@ -51,8 +51,6 @@ public function __construct(private readonly Configuration $configuration) if ($this->configuration->get('security.permLevel') !== 'basic') { $this->groupSupport = true; - } else { - $this->groupSupport = false; } } From 7c055785f3eeb84dfafec34052d76a8fde928573 Mon Sep 17 00:00:00 2001 From: Jan <96944229+modelrailroader@users.noreply.github.com> Date: Sun, 16 Jun 2024 18:29:16 +0200 Subject: [PATCH 3/3] fix: improved code --- phpmyfaq/src/phpMyFAQ/Administration/Backup.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpmyfaq/src/phpMyFAQ/Administration/Backup.php b/phpmyfaq/src/phpMyFAQ/Administration/Backup.php index dbdf875493..c5c565f7dc 100644 --- a/phpmyfaq/src/phpMyFAQ/Administration/Backup.php +++ b/phpmyfaq/src/phpMyFAQ/Administration/Backup.php @@ -163,7 +163,7 @@ private function getBackupHeader(string $tableNames): array * * @throws \Exception */ - public function createContentFolderBackup(): string|bool + public function createContentFolderBackup(): string { $zipFile = PMF_ROOT_DIR . DIRECTORY_SEPARATOR . 'content.zip'; @@ -189,6 +189,6 @@ public function createContentFolderBackup(): string|bool $zipArchive->close(); - return file_exists($zipFile) ? $zipFile : false; + return $zipFile; } }