mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-27 15:10:28 +01:00
452 lines
14 KiB
PHP
452 lines
14 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Xentral\Components\Backup;
|
||
|
|
||
|
use PHPUnit\Runner\Exception;
|
||
|
use Xentral\Components\Backup\Logger\BackupLog;
|
||
|
use Xentral\Components\Backup\Exception\BackupException;
|
||
|
use ZipArchive;
|
||
|
|
||
|
final class FileBackup implements FileBackupInterface
|
||
|
{
|
||
|
|
||
|
/** @var string backup path */
|
||
|
private $sUserPath;
|
||
|
|
||
|
/** @var BackupLog $logger */
|
||
|
private $logger;
|
||
|
|
||
|
/** @var string $cacheTmp */
|
||
|
private $cacheTmp;
|
||
|
|
||
|
/**
|
||
|
* @param BackupLog $logger
|
||
|
* @param string $cacheTmp
|
||
|
*/
|
||
|
public function __construct(BackupLog $logger, $cacheTmp)
|
||
|
{
|
||
|
$this->logger = $logger;
|
||
|
$this->cacheTmp = $cacheTmp;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function getMainPath()
|
||
|
{
|
||
|
$asPath = explode(DIRECTORY_SEPARATOR, $this->sUserPath);
|
||
|
array_pop($asPath);
|
||
|
|
||
|
return implode(DIRECTORY_SEPARATOR, $asPath) . DIRECTORY_SEPARATOR;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* returns the full file name path
|
||
|
*
|
||
|
* @param string $filename
|
||
|
* @param bool $bIsSnapshots
|
||
|
* @param null $userPath
|
||
|
*
|
||
|
* @throws BackupException
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getLocalPath($filename, $userPath = null, $bIsSnapshots = true)
|
||
|
{
|
||
|
if (null !== $userPath) {
|
||
|
$this->sUserPath = $userPath;
|
||
|
}
|
||
|
$path = $this->getMainPath();
|
||
|
if ($bIsSnapshots === true) {
|
||
|
$path .= FileBackupInterface::SNAPSHOTS_FOLDER . DIRECTORY_SEPARATOR;
|
||
|
}
|
||
|
if (!file_exists($path) && !@mkdir($path) && !is_dir($path)) {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $path));
|
||
|
throw new BackupException(sprintf('Directory "%s" was not created', $path));
|
||
|
}
|
||
|
|
||
|
return $path . $filename;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return string
|
||
|
*/
|
||
|
private function tmpDir()
|
||
|
{
|
||
|
return $this->getMainPath() . 'backup/.backup' . DIRECTORY_SEPARATOR;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getSnapshotsDir()
|
||
|
{
|
||
|
return $this->getMainPath() . FileBackupInterface::SNAPSHOTS_FOLDER . DIRECTORY_SEPARATOR;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param $path
|
||
|
*
|
||
|
* @return false|int
|
||
|
*/
|
||
|
protected function addLock($path)
|
||
|
{
|
||
|
return $this->logger->write(time(), $path, FileBackupInterface::PID_FILE, false, false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return bool
|
||
|
*/
|
||
|
private function tryPurgePidFile()
|
||
|
{
|
||
|
$pidFile = $this->tmpDir() . FileBackupInterface::PID_FILE;
|
||
|
|
||
|
$time = file_get_contents($pidFile);
|
||
|
|
||
|
if ((time() - (int)$time > FileBackupInterface::TIME_OUT)) {
|
||
|
return unlink($pidFile);
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string|null $userPath
|
||
|
*
|
||
|
* @throws BackupException
|
||
|
* @return string|null
|
||
|
*/
|
||
|
public function begin($userPath = null)
|
||
|
{
|
||
|
if (null !== $userPath) {
|
||
|
$this->sUserPath = $userPath;
|
||
|
}
|
||
|
$path = $this->tmpDir();
|
||
|
|
||
|
if (is_dir($path)) {
|
||
|
@exec('rm -rf ' . $path);
|
||
|
}
|
||
|
|
||
|
$backupDir = $this->getMainPath() . 'backup';
|
||
|
if (file_exists($backupDir . DIRECTORY_SEPARATOR . 'status.txt')) {
|
||
|
@unlink($backupDir . DIRECTORY_SEPARATOR . 'status.txt');
|
||
|
}
|
||
|
|
||
|
if (file_exists($backupDir . DIRECTORY_SEPARATOR . 'session.txt')) {
|
||
|
@unlink($backupDir . DIRECTORY_SEPARATOR . 'session.txt');
|
||
|
}
|
||
|
|
||
|
if (!file_exists($path) && !@mkdir($path, 0777, true) && !is_dir($path)) {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $path));
|
||
|
throw new BackupException(sprintf('Directory "%s" was not created', $path));
|
||
|
}
|
||
|
|
||
|
if ($this->getLockStatus() === FileBackupInterface::STATUS_WORKING && $this->tryPurgePidFile() === false) {
|
||
|
return null;
|
||
|
//throw new BackupException(sprintf('Backup is Running'));
|
||
|
}
|
||
|
|
||
|
if ($this->addLock($path) === false) {
|
||
|
$this->logger->writePersistent('Failed start backup');
|
||
|
throw new BackupException('Failed start backup');
|
||
|
}
|
||
|
|
||
|
return $path;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $file
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function cleanUp($file)
|
||
|
{
|
||
|
$path = $this->tmpDir();
|
||
|
if ($this->moveDir($path . $file, $this->getLocalPath($file)) === true) {
|
||
|
return $this->deleteDir($path);
|
||
|
}
|
||
|
$this->logger->writePersistent(sprintf('Clean Up of %s failed', $path));
|
||
|
throw new BackupException(sprintf('Clean Up of %s failed', $path));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $class_name
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function classExists($class_name)
|
||
|
{
|
||
|
return class_exists($class_name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param ZipArchive $oZip
|
||
|
* @param string $fileName
|
||
|
* @param int $flags
|
||
|
*
|
||
|
* @return mixed
|
||
|
*/
|
||
|
protected function openZipObject($oZip, $fileName, $flags = 0)
|
||
|
{
|
||
|
return $oZip->open($fileName, $flags);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $oldDir
|
||
|
* @param string $newDir
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function moveDir($oldDir, $newDir)
|
||
|
{
|
||
|
return @rename($oldDir, $newDir);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $dir
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
protected function isDir($dir)
|
||
|
{
|
||
|
return @mkdir($dir) || is_dir($dir);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getBackupExtension()
|
||
|
{
|
||
|
return FileBackupInterface::COMPRESS_EXTENSION;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $filename Zipped file name
|
||
|
* @param string $userPath local directory to backup
|
||
|
* @param string|null $sMySQLFile MySQL Backup file
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function createBackup($filename, $userPath, $sMySQLFile = null)
|
||
|
{
|
||
|
$this->sUserPath = $userPath;
|
||
|
$rootPath = realpath($userPath);
|
||
|
|
||
|
if (!file_exists($userPath)) {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" was not found', $userPath));
|
||
|
throw new BackupException(sprintf('Directory "%s" was not found', $userPath));
|
||
|
}
|
||
|
|
||
|
$tmpFilename = $this->tmpDir() . $filename;
|
||
|
$sMySQLFullPath = $this->tmpDir() . $sMySQLFile;
|
||
|
|
||
|
if (null !== $sMySQLFile && is_file($sMySQLFullPath) && filesize($sMySQLFullPath) > 1024) {
|
||
|
$this->logger->write('Add MySQL file to Zip');
|
||
|
exec('cd ' . $rootPath . ' && mv ' . $sMySQLFullPath . ' ' . $sMySQLFile);
|
||
|
}
|
||
|
|
||
|
exec('cd ' . $rootPath . ' && zip -r -9 ' . $tmpFilename . ' * .[^.]* -x "wiki/*"');
|
||
|
|
||
|
if (null !== $sMySQLFile) {
|
||
|
exec('cd ' . $rootPath . ' && rm -f ' . $sMySQLFile);
|
||
|
}
|
||
|
|
||
|
return $this->cleanUp($filename);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $dirPath
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
private function deleteDir($dirPath)
|
||
|
{
|
||
|
if (is_dir($dirPath)) {
|
||
|
if (substr($dirPath, strlen($dirPath) - 1, 1) !== '/') {
|
||
|
$dirPath .= '/';
|
||
|
}
|
||
|
$files = glob($dirPath . '*', GLOB_MARK);
|
||
|
foreach ($files as $file) {
|
||
|
if (is_dir($file)) {
|
||
|
$this->deleteDir($file);
|
||
|
} else {
|
||
|
unlink($file);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return rmdir($dirPath);
|
||
|
}
|
||
|
$this->logger->writePersistent(sprintf('Deleted DIR %s failed', $dirPath));
|
||
|
throw new BackupException(sprintf('Deleted DIR %s failed', $dirPath));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $backupFile
|
||
|
* @param string $userPath
|
||
|
* @param array $options
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function restoreFileSystem($backupFile, $userPath, $options = [])
|
||
|
{
|
||
|
$default = ['template_file_dir' => null, 'exclude_dir' => ['wiki']];
|
||
|
$options = array_merge($default, $options);
|
||
|
$templateFileDir = $options['template_file_dir'];
|
||
|
$this->sUserPath = $userPath;
|
||
|
$bIsSnapshots = null === $templateFileDir;
|
||
|
|
||
|
$this->sUserPath = null === $templateFileDir ? $userPath : $templateFileDir;
|
||
|
|
||
|
if (file_exists($file = $this->getLocalPath($backupFile, null, $bIsSnapshots))) {
|
||
|
$userDataPath = realpath($userPath);
|
||
|
$tmpExtract = $this->tmpDir() . FileBackupInterface::LOCAL_FILES_DIR_NAME . 'tmp';
|
||
|
if (!file_exists($tmpExtract) && !@mkdir($tmpExtract) && !is_dir($tmpExtract)) {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $tmpExtract));
|
||
|
throw new BackupException(sprintf('Directory "%s" was not created', $tmpExtract));
|
||
|
}
|
||
|
|
||
|
if (!$this->classExists('ZipArchive')) {
|
||
|
$this->logger->writePersistent('Class ZipArchive is missing!');
|
||
|
throw new BackupException('Class ZipArchive is missing!');
|
||
|
}
|
||
|
|
||
|
$oZip = new ZipArchive();
|
||
|
|
||
|
if ($this->openZipObject($oZip, $file, ZipArchive::CHECKCONS) !== true) {
|
||
|
$this->logger->writePersistent(sprintf('Failure to open file in "%s"', $file));
|
||
|
throw new BackupException(sprintf('Failure to open file in "%s"', $file));
|
||
|
}
|
||
|
|
||
|
$oZip->extractTo($tmpExtract);
|
||
|
$oZip->close();
|
||
|
|
||
|
// move user data
|
||
|
$shortTmp = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.rmTmp';
|
||
|
$sBeforeTmp = $shortTmp . uniqid('', true) . 'before';
|
||
|
|
||
|
if (!$this->isDir($sBeforeTmp)) {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $sBeforeTmp));
|
||
|
throw new BackupException(sprintf('Directory "%s" was not created', $sBeforeTmp));
|
||
|
}
|
||
|
|
||
|
$this->logger->write('Moving userdata away');
|
||
|
|
||
|
if (!$this->moveDir($userDataPath, $sBeforeTmp)) {
|
||
|
$this->logger->writePersistent(sprintf('Moving %s into %s failed! ', $userDataPath, $sBeforeTmp));
|
||
|
throw new BackupException(sprintf('Moving %s into %s failed! ', $userDataPath, $sBeforeTmp));
|
||
|
}
|
||
|
|
||
|
if (array_key_exists('exclude_dir', $options) && is_array($options['exclude_dir'])) {
|
||
|
$this->excludeDirectory($options['exclude_dir'], $tmpExtract, $sBeforeTmp);
|
||
|
}
|
||
|
|
||
|
$this->logger->write('Recovering userData');
|
||
|
if (!$this->moveDir($tmpExtract, $userDataPath)) {
|
||
|
$this->logger->writePersistent(sprintf('Moving %s into %s failed!', $tmpExtract, $userDataPath));
|
||
|
throw new BackupException(sprintf('Moving %s into %s failed!', $tmpExtract, $userDataPath));
|
||
|
}
|
||
|
|
||
|
// FIX TMP ISSUE
|
||
|
if (!empty($this->cacheTmp) && is_dir($this->cacheTmp)) {
|
||
|
$this->logger->write('Delete DB tmp');
|
||
|
$this->deleteDir($this->cacheTmp);
|
||
|
}
|
||
|
|
||
|
// remove DB if exists
|
||
|
$backupFileExploded = explode('.', $backupFile);
|
||
|
array_pop($backupFileExploded);
|
||
|
$tmpSql = implode('.', $backupFileExploded) . '.sql.gz';
|
||
|
if (is_file($userDataPath . DIRECTORY_SEPARATOR . $tmpSql)) {
|
||
|
exec('cd ' . $userDataPath . ' && rm -f ' . $tmpSql);
|
||
|
}
|
||
|
|
||
|
return $this->deleteDir($this->tmpDir()) && $this->deleteDir($sBeforeTmp);
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $excludeDir
|
||
|
* @param string $tmpDir extracted temporally directory
|
||
|
* @param string $oldUserDataDir
|
||
|
*/
|
||
|
protected function excludeDirectory($excludeDir = [], $tmpDir, $oldUserDataDir)
|
||
|
{
|
||
|
foreach ($excludeDir as $directory) {
|
||
|
// EXCLUDE WIKI DIRECTORY
|
||
|
$keepPath = $tmpDir . DIRECTORY_SEPARATOR . $directory;
|
||
|
$keepDirTmp = $oldUserDataDir . DIRECTORY_SEPARATOR . $directory;
|
||
|
|
||
|
if (!is_dir($keepPath) && $directory !== 'wiki') {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" cannot be skipped', $keepPath));
|
||
|
throw new BackupException(sprintf('Directory "%s" cannot be skipped', $keepPath));
|
||
|
}
|
||
|
|
||
|
if ($directory !== 'wiki') {
|
||
|
$this->deleteDir($keepPath);
|
||
|
}
|
||
|
|
||
|
if (file_exists($keepDirTmp)) {
|
||
|
$oldDirTmp = rtrim(
|
||
|
sys_get_temp_dir(),
|
||
|
DIRECTORY_SEPARATOR
|
||
|
) . DIRECTORY_SEPARATOR . '.' . $directory . 'Tmp' . uniqid('', true);
|
||
|
if (!$this->isDir($oldDirTmp)) {
|
||
|
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $oldDirTmp));
|
||
|
throw new BackupException(sprintf('Directory "%s" was not created', $oldDirTmp));
|
||
|
}
|
||
|
$oldDir = $oldDirTmp . DIRECTORY_SEPARATOR . $directory;
|
||
|
if (!$this->moveDir($keepDirTmp, $oldDir)) {
|
||
|
$this->logger->writePersistent(sprintf('Could not move %s directory into "%s"', $directory,
|
||
|
$oldDir));
|
||
|
throw new BackupException(sprintf('Could not move %s directory into "%s"', $directory,
|
||
|
$oldDir));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reset Latest WIKI Directory
|
||
|
if (isset($oldDir) && is_dir($oldDir)) {
|
||
|
$this->logger->write('Reset Wiki Directory');
|
||
|
|
||
|
if (!$this->moveDir($oldDir, $keepPath)) {
|
||
|
$this->logger->writePersistent(sprintf('Could not move %s directory into "%s"', $oldDir,
|
||
|
$keepPath));
|
||
|
throw new BackupException(sprintf('Could not move %s directory into "%s"', $oldDir, $keepPath));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string|null $userDataDir
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getLockStatus($userDataDir = null)
|
||
|
{
|
||
|
if (null !== $userDataDir) {
|
||
|
$this->sUserPath = $userDataDir;
|
||
|
}
|
||
|
$path = $this->tmpDir();
|
||
|
if (file_exists($path . FileBackupInterface::PID_FILE) &&
|
||
|
($time = file_get_contents($path . FileBackupInterface::PID_FILE)) &&
|
||
|
(time() - (int)$time < FileBackupInterface::TIME_OUT)
|
||
|
) {
|
||
|
return FileBackupInterface::STATUS_WORKING;
|
||
|
}
|
||
|
|
||
|
return FileBackupInterface::STATUS_WAITING;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clean everything without files move. This might be used, when breaking started backup job
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function breakCleanUp()
|
||
|
{
|
||
|
return $this->deleteDir($this->tmpDir());
|
||
|
}
|
||
|
}
|