a13f7ca6a6
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Update all references in Makefile, manifest.xml, .gitignore, and CI workflows (ci-joomla, pr-check, repo-health) to use source/ as the primary directory with src/ as a fallback for compatibility. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
2.8 KiB
PHP
124 lines
2.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoJoomBackup
|
|
* @subpackage com_mokobackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*
|
|
* Restores files from a staging directory to the Joomla root.
|
|
* Skips database.sql and sensitive files that should not be overwritten.
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoBackup\Administrator\Engine;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
class FileRestorer
|
|
{
|
|
private string $sourceDir;
|
|
private string $targetDir;
|
|
|
|
/**
|
|
* Files that should never be overwritten during restore.
|
|
* configuration.php is handled separately by the RestoreEngine.
|
|
*/
|
|
private const SKIP_FILES = [
|
|
'configuration.php',
|
|
'.htaccess',
|
|
'web.config',
|
|
];
|
|
|
|
/**
|
|
* Files that are backup artifacts, not part of the site.
|
|
*/
|
|
private const EXCLUDE_FILES = [
|
|
'database.sql',
|
|
];
|
|
|
|
public function __construct(string $sourceDir, string $targetDir)
|
|
{
|
|
$this->sourceDir = rtrim($sourceDir, '/\\');
|
|
$this->targetDir = rtrim($targetDir, '/\\');
|
|
}
|
|
|
|
/**
|
|
* Copy files from staging to target, preserving directory structure.
|
|
*
|
|
* @return int Number of files restored
|
|
*/
|
|
public function restore(): int
|
|
{
|
|
$count = 0;
|
|
$this->restoreDirectory('', $count);
|
|
|
|
return $count;
|
|
}
|
|
|
|
private function restoreDirectory(string $relativePath, int &$count): void
|
|
{
|
|
$sourcePath = $this->sourceDir . ($relativePath ? '/' . $relativePath : '');
|
|
|
|
if (!is_dir($sourcePath)) {
|
|
return;
|
|
}
|
|
|
|
$handle = opendir($sourcePath);
|
|
|
|
if ($handle === false) {
|
|
return;
|
|
}
|
|
|
|
while (($entry = readdir($handle)) !== false) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$entryRelative = $relativePath ? $relativePath . '/' . $entry : $entry;
|
|
$entrySource = $sourcePath . '/' . $entry;
|
|
$entryTarget = $this->targetDir . '/' . $entryRelative;
|
|
|
|
if (is_dir($entrySource)) {
|
|
// Create target directory if it doesn't exist
|
|
if (!is_dir($entryTarget)) {
|
|
mkdir($entryTarget, 0755, true);
|
|
}
|
|
|
|
$this->restoreDirectory($entryRelative, $count);
|
|
} elseif (is_file($entrySource)) {
|
|
// Skip excluded files
|
|
if (in_array($entry, self::EXCLUDE_FILES, true)) {
|
|
continue;
|
|
}
|
|
|
|
// Skip protected files (only at root level)
|
|
if ($relativePath === '' && in_array($entry, self::SKIP_FILES, true)) {
|
|
continue;
|
|
}
|
|
|
|
// Ensure parent directory exists
|
|
$parentDir = dirname($entryTarget);
|
|
|
|
if (!is_dir($parentDir)) {
|
|
mkdir($parentDir, 0755, true);
|
|
}
|
|
|
|
// Copy file, preserving permissions
|
|
if (copy($entrySource, $entryTarget)) {
|
|
// Try to match original permissions
|
|
$perms = fileperms($entrySource);
|
|
|
|
if ($perms !== false) {
|
|
@chmod($entryTarget, $perms);
|
|
}
|
|
|
|
$count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
closedir($handle);
|
|
}
|
|
}
|