Files
da-postgresql/exec/lib/BackupQueueService.php
2026-07-04 16:00:04 +02:00

1619 lines
56 KiB
PHP

<?php
declare(strict_types=1);
final class BackupQueueService
{
private Settings $settings;
private DirectAdminUser $daUser;
private string $pluginRoot;
private string $dataDir;
private string $jobsPendingDir;
private string $jobsProcessingDir;
private string $jobsDoneDir;
private string $logsDir;
private string $lockDir;
private string $uploadsDir;
public function __construct(Settings $settings, DirectAdminUser $daUser)
{
$this->settings = $settings;
$this->daUser = $daUser;
$this->pluginRoot = defined('PLUGIN_ROOT') ? PLUGIN_ROOT : dirname(__DIR__, 2);
$this->dataDir = $this->pluginRoot . '/data';
$this->jobsPendingDir = $this->dataDir . '/jobs/pending';
$this->jobsProcessingDir = $this->dataDir . '/jobs/processing';
$this->jobsDoneDir = $this->dataDir . '/jobs/done';
$this->logsDir = $this->dataDir . '/logs';
$this->lockDir = $this->dataDir . '/lock';
$this->uploadsDir = $this->dataDir . '/uploads';
}
public function ensureInfrastructure(): void
{
$dirs = [
$this->dataDir,
$this->dataDir . '/jobs',
$this->jobsPendingDir,
$this->jobsProcessingDir,
$this->jobsDoneDir,
$this->logsDir,
$this->lockDir,
$this->dataDir . '/pids',
$this->dataDir . '/cancel',
$this->uploadsDir,
$this->uploadsDir . '/' . $this->daUser->username(),
];
foreach ($dirs as $dir) {
if (!is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) {
throw new RuntimeException('Nie można utworzyć katalogu: ' . $dir);
}
@chmod($dir, 0700);
}
}
public function backupEnabled(): bool
{
return $this->settings->enableBackup();
}
public function uploadBackupEnabled(): bool
{
return $this->settings->enableUploadBackup();
}
public function userBackupRoot(): string
{
$root = $this->settings->userBaseBackupDir($this->daUser->username());
if ($root === '') {
$root = '/home/' . $this->daUser->username() . '/postgres_backup';
}
return rtrim($root, '/');
}
public function scriptBackupRoot(): string
{
$root = $this->settings->hitmeBackupLocation();
if ($root === '') {
$root = '/home/admin/postgres_backup';
}
return rtrim($root, '/');
}
public function userBackupDateDir(): string
{
$format = $this->settings->userBackupDateFormat();
$dir = date($format);
if ($dir === '' || strpos($dir, '..') !== false || strpos($dir, '/') !== false || strpos($dir, '\\') !== false) {
return date('d.m.Y-H.i');
}
if (!preg_match('/[Hhis]/', $format)) {
$dir .= '-' . date('H.i.s');
}
$base = $dir;
$root = $this->userBackupRoot();
$attempt = 0;
while (is_dir($root . '/' . $dir)) {
$attempt++;
$dir = $base . '-' . date('His') . ($attempt > 1 ? '-' . $attempt : '');
}
return $dir;
}
public function queueBackupJob(string $dbName): string
{
if (!$this->backupEnabled()) {
throw new RuntimeException('Tworzenie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$this->assertDatabaseBelongsToUser($dbName);
$jobId = $this->buildJobId('backup');
$lockPath = $this->dbLockPath($dbName);
$compress = $this->settings->compressBackup() ? '1' : '0';
$backupRoot = $this->userBackupRoot();
$dateDir = $this->userBackupDateDir();
return $this->withQueueLock(function () use ($dbName, $jobId, $lockPath, $compress, $backupRoot, $dateDir): string {
$this->acquireDbLock($dbName, 'backup', $jobId, $lockPath);
try {
$jobFile = $this->jobsPendingDir . '/backup-' . $jobId . '.env';
$payload = [
'JOB_ID' => $jobId,
'JOB_TYPE' => 'backup',
'DA_USER' => $this->daUser->username(),
'DB_NAME' => $dbName,
'START_TS' => (string)time(),
'DATE_DIR' => $dateDir,
'COMPRESS_BACKUP' => $compress,
'USER_BASE_BACKUP_DIR' => $backupRoot,
'DB_LOCK_FILE' => $lockPath,
];
$this->writeJobFile($jobFile, $payload);
} catch (Throwable $e) {
$this->releaseDbLock($lockPath);
throw $e;
}
$this->triggerWorker();
return $jobId;
});
}
public function queueRestoreFromLocal(string $relativeBackupPath, string $restoreMode = 'overwrite', string $targetDb = ''): string
{
if (!$this->uploadBackupEnabled()) {
throw new RuntimeException('Przywracanie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$resolved = $this->resolveLocalBackupEntry($relativeBackupPath);
$sourceDb = $resolved['db_name'];
if ($sourceDb === '') {
throw new RuntimeException('Nie można ustalić docelowej bazy dla backupu lokalnego.');
}
$restoreMode = strtolower(trim($restoreMode));
if (!in_array($restoreMode, ['overwrite', 'new_db'], true)) {
$restoreMode = 'overwrite';
}
$dbName = $sourceDb;
if ($restoreMode === 'new_db') {
if ($targetDb === '') {
throw new RuntimeException('Wskaż docelową bazę danych.');
}
$dbName = $targetDb;
}
$this->assertDatabaseBelongsToUser($dbName);
$jobId = $this->buildJobId('restore');
$lockPath = $this->dbLockPath($dbName);
return $this->withQueueLock(function () use ($dbName, $jobId, $lockPath, $resolved, $restoreMode, $sourceDb): string {
$this->acquireDbLock($dbName, 'restore', $jobId, $lockPath);
try {
$jobFile = $this->jobsPendingDir . '/restore-' . $jobId . '.env';
$payload = [
'JOB_ID' => $jobId,
'JOB_TYPE' => 'restore',
'DA_USER' => $this->daUser->username(),
'DB_NAME' => $dbName,
'SOURCE_DB_NAME' => $sourceDb,
'RESTORE_MODE' => $restoreMode,
'SOURCE_FILE' => $resolved['path'],
'SOURCE_KIND' => 'local',
'START_TS' => (string)time(),
'DATE_DIR' => $resolved['date'],
'DB_LOCK_FILE' => $lockPath,
];
$this->writeJobFile($jobFile, $payload);
} catch (Throwable $e) {
$this->releaseDbLock($lockPath);
throw $e;
}
$this->triggerWorker();
return $jobId;
});
}
public function queueRestoreFromFilePath(string $dbName, string $filePath, string $restoreMode = 'overwrite', string $sourceDbName = ''): string
{
if (!$this->uploadBackupEnabled()) {
throw new RuntimeException('Przywracanie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$this->assertDatabaseBelongsToUser($dbName);
$filePath = trim($filePath);
if ($filePath === '') {
throw new RuntimeException('Wybierz plik .sql/.gz/.sql.gz lub wskaż ścieżkę pliku na serwerze.');
}
if (!is_file($filePath) || !is_readable($filePath)) {
throw new RuntimeException('Nie można odczytać pliku: ' . $filePath);
}
$realPath = @realpath($filePath);
if ($realPath === false) {
throw new RuntimeException('Nie można odczytać pliku: ' . $filePath);
}
$allowedRoots = [
'/home/' . $this->daUser->username(),
$this->uploadsDir . '/' . $this->daUser->username(),
];
$inAllowedRoot = false;
foreach ($allowedRoots as $rootPath) {
$realRoot = @realpath($rootPath);
if ($realRoot === false) {
continue;
}
if ($realPath === $realRoot || strpos($realPath, rtrim($realRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) === 0) {
$inAllowedRoot = true;
break;
}
}
if (!$inAllowedRoot) {
throw new RuntimeException('Ścieżka pliku musi znajdować się w katalogu /home/' . $this->daUser->username() . ' lub pochodzić z uploadu formularza.');
}
if (!$this->isAllowedSqlFile($filePath)) {
throw new RuntimeException('Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.');
}
$jobId = $this->buildJobId('restore');
$safeBase = preg_replace('/[^A-Za-z0-9._-]+/', '_', basename($filePath)) ?? 'restore.sql';
$targetDir = $this->uploadsDir . '/' . $this->daUser->username();
if (!is_dir($targetDir) && !@mkdir($targetDir, 0700, true) && !is_dir($targetDir)) {
throw new RuntimeException('Nie można utworzyć katalogu upload: ' . $targetDir);
}
@chmod($targetDir, 0700);
$copiedFile = $targetDir . '/' . $jobId . '_' . $safeBase;
if (!@copy($filePath, $copiedFile)) {
throw new RuntimeException('Nie udało się skopiować pliku do kolejki restore.');
}
@chmod($copiedFile, 0600);
$restoreMode = strtolower(trim($restoreMode));
if (!in_array($restoreMode, ['overwrite', 'new_db'], true)) {
$restoreMode = 'overwrite';
}
// Bezpieczeństwo: dla restore z pliku ignorujemy źródłową nazwę bazy.
// Import ma być wykonywany wyłącznie do DB wskazanej przez użytkownika.
$sourceDbName = '';
$lockPath = $this->dbLockPath($dbName);
try {
return $this->withQueueLock(function () use ($dbName, $jobId, $lockPath, $copiedFile, $restoreMode, $sourceDbName): string {
$this->acquireDbLock($dbName, 'restore', $jobId, $lockPath);
try {
$jobFile = $this->jobsPendingDir . '/restore-' . $jobId . '.env';
$payload = [
'JOB_ID' => $jobId,
'JOB_TYPE' => 'restore',
'DA_USER' => $this->daUser->username(),
'DB_NAME' => $dbName,
'RESTORE_MODE' => $restoreMode,
'SOURCE_DB_NAME' => $sourceDbName,
'SOURCE_FILE' => $copiedFile,
'SOURCE_KIND' => 'file',
'START_TS' => (string)time(),
'DATE_DIR' => date('Y-m-d'),
'DB_LOCK_FILE' => $lockPath,
];
$this->writeJobFile($jobFile, $payload);
} catch (Throwable $e) {
$this->releaseDbLock($lockPath);
throw $e;
}
$this->triggerWorker();
return $jobId;
});
} catch (Throwable $e) {
@unlink($copiedFile);
throw $e;
}
}
public function stageRestoreUploadFromStream(string $originalName): string
{
if (!$this->uploadBackupEnabled()) {
throw new RuntimeException('Przywracanie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$originalName = basename(trim($originalName));
if ($originalName === '') {
throw new RuntimeException('Wybierz plik .sql, .gz lub .sql.gz do przywrócenia.');
}
if (!$this->isAllowedSqlFile($originalName)) {
throw new RuntimeException('Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.');
}
$targetFile = $this->buildStagedUploadPath($originalName, 'stream');
$in = false;
$candidates = (PHP_SAPI === 'cli')
? ['php://stdin', 'php://input']
: ['php://input', 'php://stdin'];
foreach ($candidates as $candidate) {
$stream = @fopen($candidate, 'rb');
if (is_resource($stream)) {
$in = $stream;
break;
}
}
if (!is_resource($in)) {
throw new RuntimeException('Nie można odczytać przesyłanego pliku.');
}
$out = @fopen($targetFile, 'wb');
if (!is_resource($out)) {
@fclose($in);
throw new RuntimeException('Nie można zapisać przesyłanego pliku na serwerze.');
}
$written = @stream_copy_to_stream($in, $out);
@fclose($out);
@fclose($in);
if ($written === false || (int)$written <= 0) {
$len = (string)($_SERVER['CONTENT_LENGTH'] ?? (getenv('CONTENT_LENGTH') ?: ''));
@error_log(sprintf(
"[%s] UPLOAD_BLOB_STREAM_EMPTY user=%s name=%s content_length=%s target=%s\n",
date('c'),
$this->daUser->username(),
$safeBase,
$len,
$targetFile
), 3, PLUGIN_ROOT . '/error.log');
@unlink($targetFile);
throw new RuntimeException('Przesłany plik jest pusty lub uszkodzony.');
}
@chmod($targetFile, 0600);
return $targetFile;
}
/**
* @param array<string,mixed> $upload
*/
public function stageRestoreUploadFromPhpUpload(array $upload, string $nameHint = ''): string
{
if (!$this->uploadBackupEnabled()) {
throw new RuntimeException('Przywracanie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$error = isset($upload['error']) ? (int)$upload['error'] : UPLOAD_ERR_NO_FILE;
if ($error !== UPLOAD_ERR_OK) {
if ($error === UPLOAD_ERR_NO_FILE) {
throw new RuntimeException('Wybierz plik .sql, .gz lub .sql.gz do przywrócenia.');
}
throw new RuntimeException('Nie udało się przesłać pliku backupu (kod błędu: ' . $error . ').');
}
$tmpName = isset($upload['tmp_name']) ? trim((string)$upload['tmp_name']) : '';
if ($tmpName === '' || !is_file($tmpName) || !is_readable($tmpName)) {
throw new RuntimeException('Nie można odczytać przesłanego pliku backupu.');
}
$originalName = isset($upload['name']) ? basename((string)$upload['name']) : '';
if ($originalName === '') {
$originalName = basename(trim($nameHint));
}
if ($originalName === '') {
$originalName = basename($tmpName);
}
if ($originalName === '' || !$this->isAllowedSqlFile($originalName)) {
throw new RuntimeException('Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.');
}
$targetFile = $this->buildStagedUploadPath($originalName, 'php');
$moved = @move_uploaded_file($tmpName, $targetFile);
if (!$moved) {
if (!@copy($tmpName, $targetFile)) {
throw new RuntimeException('Nie udało się zapisać przesłanego pliku do kolejki restore.');
}
@unlink($tmpName);
}
@chmod($targetFile, 0600);
clearstatcache(true, $targetFile);
$size = @filesize($targetFile);
if (!is_int($size) || $size <= 0) {
@unlink($targetFile);
throw new RuntimeException('Przesłany plik jest pusty lub uszkodzony.');
}
return $targetFile;
}
public function stageRestoreUploadFromDirectAdminTemp(string $uploadReference, string $originalName = ''): string
{
if (!$this->uploadBackupEnabled()) {
throw new RuntimeException('Przywracanie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$uploadReference = trim($uploadReference);
if ($uploadReference === '') {
throw new RuntimeException('Wybierz plik .sql, .gz lub .sql.gz do przywrócenia.');
}
$token = basename(str_replace('\\', '/', $uploadReference));
if ($token === '') {
throw new RuntimeException('Nie można odczytać przesłanego pliku backupu.');
}
$sourceCandidates = [];
if (str_starts_with($uploadReference, '/') && is_file($uploadReference)) {
$sourceCandidates[] = $uploadReference;
}
$tmpDirs = [
(string)(getenv('UPLOAD_TMP_DIR') ?: ''),
(string)(getenv('DA_UPLOAD_TMP_DIR') ?: ''),
'/home/tmp',
'/tmp',
'/var/tmp',
sys_get_temp_dir(),
];
foreach ($tmpDirs as $dir) {
$dir = trim((string)$dir);
if ($dir === '') {
continue;
}
$sourceCandidates[] = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $token;
}
$sourceCandidates = array_values(array_unique($sourceCandidates));
$sourcePath = '';
foreach ($sourceCandidates as $candidate) {
if (is_file($candidate) && is_readable($candidate)) {
$sourcePath = $candidate;
break;
}
}
if ($sourcePath === '') {
@error_log(sprintf(
"[%s] UPLOAD_DA_TMP_MISSING user=%s ref=%s token=%s candidates=%s\n",
date('c'),
$this->daUser->username(),
$uploadReference,
$token,
implode('|', $sourceCandidates)
), 3, PLUGIN_ROOT . '/error.log');
throw new RuntimeException('Nie można odczytać przesłanego pliku backupu. Wybierz plik ponownie.');
}
$originalName = basename(trim($originalName));
if ($originalName === '' || !$this->isAllowedSqlFile($originalName)) {
$originalName = $this->isAllowedSqlFile($token) ? $token : 'restore.sql';
}
if (!$this->isAllowedSqlFile($originalName)) {
throw new RuntimeException('Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.');
}
$targetFile = $this->buildStagedUploadPath($originalName, 'da');
if (!@copy($sourcePath, $targetFile)) {
throw new RuntimeException('Nie udało się zapisać przesłanego pliku do kolejki restore.');
}
@chmod($targetFile, 0600);
clearstatcache(true, $targetFile);
$size = @filesize($targetFile);
if (!is_int($size) || $size <= 0) {
@unlink($targetFile);
throw new RuntimeException('Przesłany plik jest pusty lub uszkodzony.');
}
@error_log(sprintf(
"[%s] UPLOAD_DA_TMP_STAGED user=%s ref=%s src=%s dst=%s size=%s\n",
date('c'),
$this->daUser->username(),
$uploadReference,
$sourcePath,
$targetFile,
(string)$size
), 3, PLUGIN_ROOT . '/error.log');
return $targetFile;
}
/**
* @param array<string,mixed> $upload
*/
public function queueRestoreFromUploadedFile(string $dbName, array $upload, string $restoreMode = 'overwrite'): string
{
if (!$this->uploadBackupEnabled()) {
throw new RuntimeException('Przywracanie backupu jest wyłączone w konfiguracji pluginu.');
}
$this->ensureInfrastructure();
$this->assertDatabaseBelongsToUser($dbName);
$error = isset($upload['error']) ? (int)$upload['error'] : UPLOAD_ERR_NO_FILE;
if ($error !== UPLOAD_ERR_OK) {
if ($error === UPLOAD_ERR_NO_FILE) {
throw new RuntimeException('Wybierz plik .sql, .gz lub .sql.gz do przywrócenia.');
}
throw new RuntimeException('Nie udało się przesłać pliku backupu (kod błędu: ' . $error . ').');
}
$tmpName = isset($upload['tmp_name']) ? trim((string)$upload['tmp_name']) : '';
if ($tmpName === '' || !is_file($tmpName) || !is_readable($tmpName)) {
throw new RuntimeException('Nie można odczytać przesłanego pliku backupu.');
}
$originalName = isset($upload['name']) ? basename((string)$upload['name']) : '';
if ($originalName === '') {
$originalName = 'restore.sql';
}
if (!$this->isAllowedSqlFile($originalName)) {
throw new RuntimeException('Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.');
}
$jobId = $this->buildJobId('restore');
$restoreMode = strtolower(trim($restoreMode));
if (!in_array($restoreMode, ['overwrite', 'new_db'], true)) {
$restoreMode = 'overwrite';
}
$safeBase = preg_replace('/[^A-Za-z0-9._-]+/', '_', $originalName) ?? 'restore.sql';
$targetDir = $this->uploadsDir . '/' . $this->daUser->username();
if (!is_dir($targetDir) && !@mkdir($targetDir, 0700, true) && !is_dir($targetDir)) {
throw new RuntimeException('Nie można utworzyć katalogu upload: ' . $targetDir);
}
@chmod($targetDir, 0700);
$copiedFile = $targetDir . '/' . $jobId . '_' . $safeBase;
$moved = @move_uploaded_file($tmpName, $copiedFile);
if (!$moved) {
if (!@copy($tmpName, $copiedFile)) {
throw new RuntimeException('Nie udało się zapisać przesłanego pliku do kolejki restore.');
}
@unlink($tmpName);
}
@chmod($copiedFile, 0600);
$lockPath = $this->dbLockPath($dbName);
try {
return $this->withQueueLock(function () use ($dbName, $jobId, $lockPath, $copiedFile, $restoreMode): string {
$this->acquireDbLock($dbName, 'restore', $jobId, $lockPath);
try {
$jobFile = $this->jobsPendingDir . '/restore-' . $jobId . '.env';
$payload = [
'JOB_ID' => $jobId,
'JOB_TYPE' => 'restore',
'DA_USER' => $this->daUser->username(),
'DB_NAME' => $dbName,
'RESTORE_MODE' => $restoreMode,
'SOURCE_FILE' => $copiedFile,
'SOURCE_KIND' => 'file',
'START_TS' => (string)time(),
'DATE_DIR' => date('Y-m-d'),
'DB_LOCK_FILE' => $lockPath,
];
$this->writeJobFile($jobFile, $payload);
} catch (Throwable $e) {
$this->releaseDbLock($lockPath);
throw $e;
}
$this->triggerWorker();
return $jobId;
});
} catch (Throwable $e) {
@unlink($copiedFile);
throw $e;
}
}
/**
* @return array<string,array<int,array{id:string,file:string,path:string,date:string,time:string,size:string,source_db:string,mtime:int}>>
*/
public function listLocalBackupsByDate(): array
{
$root = $this->scriptBackupRoot();
if (!is_dir($root)) {
return [];
}
$out = [];
$dateDirs = [];
$items = @scandir($root);
if ($items === false) {
return [];
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$full = $root . '/' . $item;
if (is_dir($full)) {
$dateDirs[] = $full;
}
}
if (empty($dateDirs)) {
$dateDirs[] = $root;
}
foreach ($dateDirs as $dirPath) {
$dirName = basename($dirPath);
$files = @scandir($dirPath);
if ($files === false) {
continue;
}
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$fullPath = $dirPath . '/' . $file;
if (!is_file($fullPath) || !$this->isAllowedSqlFile($fullPath)) {
continue;
}
$mtime = (int)@filemtime($fullPath);
$rawDateKey = $dirPath === $root ? date('Y-m-d', $mtime) : $dirName;
$dateKey = $this->normalizeDateKey($rawDateKey, $mtime);
$relative = ltrim(substr($fullPath, strlen(rtrim($root, '/'))), '/');
$sourceDb = $this->extractSourceDbFromBackupFile($file);
if ($sourceDb !== '' && strpos($sourceDb, $this->daUser->prefix()) !== 0) {
continue;
}
$entry = [
'id' => $relative,
'file' => $file,
'path' => $fullPath,
'date' => $dateKey,
'time' => $mtime > 0 ? date('H:i:s', $mtime) : '',
'size' => $this->humanFileSize((int)@filesize($fullPath)),
'source_db' => $sourceDb,
'mtime' => $mtime,
];
if (!isset($out[$dateKey])) {
$out[$dateKey] = [];
}
$out[$dateKey][] = $entry;
}
}
foreach ($out as $date => $entries) {
usort($entries, static fn(array $a, array $b): int => ($b['mtime'] <=> $a['mtime']));
$out[$date] = $entries;
}
uksort($out, static fn(string $a, string $b): int => strcmp($b, $a));
return $out;
}
/**
* @return array<string,array<int,array{
* job_id:string,
* db_name:string,
* date:string,
* time:string,
* mtime:int,
* size:string,
* status:string,
* log_exists:bool,
* target_file:string,
* has_file:bool
* }>>
*/
public function listUserBackupsByDate(int $limit = 500): array
{
$this->ensureInfrastructure();
$root = $this->userBackupRoot();
$out = [];
if (!is_dir($root)) {
return [];
}
$rootReal = @realpath($root);
$jobs = $this->listJobsForCurrentUser($limit);
$jobsByTarget = [];
foreach ($jobs as $job) {
if (($job['type'] ?? '') !== 'backup') {
continue;
}
$jobId = (string)($job['job_id'] ?? '');
if ($jobId === '') {
continue;
}
$targetFile = $this->resolveBackupTargetPathFromJob($job, $jobId);
if ($targetFile === '') {
continue;
}
$realTarget = @realpath($targetFile);
if ($realTarget === false) {
continue;
}
if ($rootReal !== false) {
if (strpos($realTarget, rtrim($rootReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) !== 0 && $realTarget !== $rootReal) {
continue;
}
}
$jobsByTarget[$realTarget] = $job;
}
$dateDirs = [];
$items = @scandir($root);
if (is_array($items)) {
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$full = $root . '/' . $item;
if (is_dir($full)) {
$dateDirs[] = $full;
}
}
}
if (empty($dateDirs)) {
$dateDirs[] = $root;
}
$dirFormat = $this->settings->userBackupDateFormat();
foreach ($dateDirs as $dirPath) {
$dirName = basename($dirPath);
$dirTs = 0;
if ($dirPath !== $root && $dirName !== '') {
$dt = @DateTime::createFromFormat($dirFormat, $dirName);
if ($dt instanceof DateTime && $dt->format($dirFormat) === $dirName) {
$dirTs = $dt->getTimestamp();
}
}
$files = @scandir($dirPath);
if ($files === false) {
continue;
}
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
if (preg_match('/-current\\.sql(\\.gz)?$/i', $file)) {
continue;
}
$fullPath = $dirPath . '/' . $file;
if (!is_file($fullPath) || !$this->isAllowedSqlFile($fullPath)) {
continue;
}
$realPath = @realpath($fullPath);
if ($realPath === false) {
continue;
}
if ($rootReal !== false) {
if (strpos($realPath, rtrim($rootReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) !== 0 && $realPath !== $rootReal) {
continue;
}
}
$mtime = (int)@filemtime($realPath);
$ts = $dirTs > 0 ? $dirTs : $mtime;
if ($ts <= 0) {
$ts = (int)time();
}
$dateKey = date('Y-m-d', $ts);
$timeLabel = date('H:i:s', $ts);
$sourceDb = $this->extractSourceDbFromBackupFile($file);
if ($sourceDb !== '' && strpos($sourceDb, $this->daUser->prefix()) !== 0) {
continue;
}
$job = $jobsByTarget[$realPath] ?? null;
$jobId = is_array($job) ? (string)($job['job_id'] ?? '') : '';
$status = is_array($job) ? (string)($job['status'] ?? '') : 'done_ok';
$logExists = is_array($job) ? (bool)($job['log_exists'] ?? false) : false;
$dbName = is_array($job) && (string)($job['db_name'] ?? '') !== '' ? (string)$job['db_name'] : $sourceDb;
$relative = ltrim(substr($realPath, strlen(rtrim($root, '/'))), '/');
$entry = [
'job_id' => $jobId,
'db_name' => $dbName,
'date' => $dateKey,
'time' => $timeLabel,
'mtime' => $ts,
'size' => $this->humanFileSize((int)@filesize($realPath)),
'status' => $status,
'log_exists' => $logExists,
'target_file' => $realPath,
'has_file' => true,
'path' => $realPath,
'rel_path' => $relative,
];
if (!isset($out[$dateKey])) {
$out[$dateKey] = [];
}
$out[$dateKey][] = $entry;
}
}
foreach ($out as $date => $entries) {
usort($entries, static fn(array $a, array $b): int => ((int)($b['mtime'] ?? 0) <=> (int)($a['mtime'] ?? 0)));
$out[$date] = $entries;
}
uksort($out, static fn(string $a, string $b): int => strcmp($b, $a));
return $out;
}
public function resolveUserBackupFileForCurrentUser(string $relativePath): string
{
$relativePath = ltrim(trim($relativePath), '/');
if ($relativePath === '' || strpos($relativePath, '..') !== false) {
throw new RuntimeException('Nieprawidłowa ścieżka pliku backupu.');
}
$root = $this->userBackupRoot();
$candidate = $root . '/' . $relativePath;
$realRoot = @realpath($root);
$realPath = @realpath($candidate);
if ($realRoot === false || $realPath === false) {
throw new RuntimeException('Nie można zlokalizować pliku backupu.');
}
if (!str_starts_with($realPath, rtrim($realRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Plik backupu znajduje się poza dozwolonym katalogiem.');
}
if (!is_file($realPath) || !$this->isAllowedSqlFile($realPath)) {
throw new RuntimeException('Nie można odczytać wskazanego pliku backupu.');
}
return $realPath;
}
public function deleteUserBackupFileForCurrentUser(string $relativePath): string
{
$path = $this->resolveUserBackupFileForCurrentUser($relativePath);
$currentCandidate = '';
if (preg_match('/^(.*)\\.sql(\\.gz)?$/i', $path, $m)) {
$currentCandidate = $m[1] . '-current.sql' . ($m[2] ?? '');
}
if (!@unlink($path)) {
throw new RuntimeException('Nie udało się usunąć pliku backupu: ' . $path);
}
if ($currentCandidate !== '' && is_file($currentCandidate)) {
@unlink($currentCandidate);
}
$parentDir = dirname($path);
if (is_dir($parentDir)) {
$items = @scandir($parentDir) ?: [];
$onlyDots = true;
foreach ($items as $item) {
if ($item !== '.' && $item !== '..') {
$onlyDots = false;
break;
}
}
if ($onlyDots) {
@rmdir($parentDir);
}
}
return $path;
}
private function normalizeDateKey(string $raw, int $fallbackTs = 0): string
{
$raw = trim($raw);
if ($raw === '') {
return $fallbackTs > 0 ? date('Y-m-d', $fallbackTs) : '';
}
$formats = ['Y-m-d', 'd-m-Y', 'Y.m.d', 'd.m.Y'];
foreach ($formats as $fmt) {
$dt = @DateTime::createFromFormat($fmt, $raw);
if ($dt instanceof DateTime && $dt->format($fmt) === $raw) {
return $dt->format('Y-m-d');
}
}
if ($fallbackTs > 0) {
return date('Y-m-d', $fallbackTs);
}
return $raw;
}
/**
* @return array<int,array{
* job_id:string,
* type:string,
* db_name:string,
* status:string,
* start_ts:int,
* updated_ts:int,
* file:string,
* log_exists:bool,
* source_file:string,
* target_file:string,
* date_dir:string,
* restore_mode:string,
* source_db_name:string
* }>
*/
public function listJobsForCurrentUser(int $limit = 150): array
{
$this->ensureInfrastructure();
$candidates = [];
$this->collectJobFiles($this->jobsPendingDir, 'pending', $candidates);
$this->collectJobFiles($this->jobsProcessingDir, 'processing', $candidates);
$this->collectDoneJobFiles($this->jobsDoneDir, $candidates);
$out = [];
foreach ($candidates as $candidate) {
$meta = $this->parseJobFile($candidate['path']);
if (($meta['DA_USER'] ?? '') !== $this->daUser->username()) {
$fallback = $this->inferJobMetaFromLog($candidate['job_id']);
if ($fallback === null) {
continue;
}
$meta = array_merge($fallback, $meta);
}
$jobId = (string)($meta['JOB_ID'] ?? $candidate['job_id']);
if ($jobId === '') {
continue;
}
$out[] = [
'job_id' => $jobId,
'type' => (string)($meta['JOB_TYPE'] ?? 'unknown'),
'db_name' => (string)($meta['DB_NAME'] ?? ''),
'status' => $candidate['status'],
'start_ts' => (int)($meta['START_TS'] ?? @filemtime($candidate['path']) ?: 0),
'updated_ts' => (int)@filemtime($candidate['path']),
'file' => $candidate['path'],
'log_exists' => is_file($this->logsDir . '/' . $jobId . '.log'),
'source_file' => (string)($meta['SOURCE_FILE'] ?? ''),
'target_file' => (string)($meta['TARGET_FILE'] ?? ''),
'date_dir' => (string)($meta['DATE_DIR'] ?? ''),
'restore_mode' => (string)($meta['RESTORE_MODE'] ?? ''),
'source_db_name' => (string)($meta['SOURCE_DB_NAME'] ?? ''),
];
}
usort(
$out,
static function (array $a, array $b): int {
if ($a['start_ts'] === $b['start_ts']) {
return strcmp($b['job_id'], $a['job_id']);
}
return $b['start_ts'] <=> $a['start_ts'];
}
);
if ($limit > 0 && count($out) > $limit) {
return array_slice($out, 0, $limit);
}
return $out;
}
/**
* @return array{
* job_id:string,
* type:string,
* db_name:string,
* status:string,
* start_ts:int,
* updated_ts:int,
* file:string,
* log_exists:bool,
* source_file:string,
* target_file:string,
* date_dir:string
* }
*/
public function getJobForCurrentUser(string $jobId): array
{
$jobId = trim($jobId);
if ($jobId === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $jobId)) {
throw new RuntimeException('Nieprawidłowy identyfikator zadania.');
}
$jobs = $this->listJobsForCurrentUser(500);
foreach ($jobs as $job) {
if ($job['job_id'] === $jobId) {
return $job;
}
}
throw new RuntimeException('Nie znaleziono zadania lub brak dostępu.');
}
public function resolveBackupTargetFileForCurrentUser(string $jobId): string
{
$job = $this->getJobForCurrentUser($jobId);
if ($job['type'] !== 'backup') {
throw new RuntimeException('Wskazane zadanie nie jest zadaniem backupu.');
}
if ($job['status'] !== 'done_ok') {
throw new RuntimeException('Backup nie został zakończony powodzeniem.');
}
$path = $this->resolveBackupTargetPathFromJob($job, $jobId);
if ($path === '') {
throw new RuntimeException('Brak ścieżki pliku backupu w metadanych zadania.');
}
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException('Plik backupu nie istnieje lub brak uprawnień odczytu (sprawdź uprawnienia katalogu i pliku backupu dla diradmin).');
}
$userRoot = $this->userBackupRoot();
$globalRoot = rtrim($this->settings->hitmeBackupLocation(), '/');
if (!$this->pathInDir($path, $userRoot) && !$this->pathInDir($path, $globalRoot)) {
throw new RuntimeException('Plik backupu znajduje się poza dozwolonym katalogiem.');
}
return $path;
}
public function hasBackupTargetFileForCurrentUser(string $jobId): bool
{
try {
$path = $this->resolveBackupTargetFileForCurrentUser($jobId);
return $path !== '' && is_file($path) && is_readable($path);
} catch (Throwable $e) {
return false;
}
}
public function deleteBackupTargetFileForCurrentUser(string $jobId): string
{
$path = $this->resolveBackupTargetFileForCurrentUser($jobId);
if (!is_file($path)) {
throw new RuntimeException('Plik backupu nie istnieje.');
}
$currentCandidate = '';
if (preg_match('/^(.*)\\.sql(\\.gz)?$/i', $path, $m)) {
$currentCandidate = $m[1] . '-current.sql' . ($m[2] ?? '');
}
if (!@unlink($path)) {
throw new RuntimeException('Nie udało się usunąć pliku backupu: ' . $path);
}
if ($currentCandidate !== '' && is_file($currentCandidate)) {
@unlink($currentCandidate);
}
$parentDir = dirname($path);
if (is_dir($parentDir)) {
$items = @scandir($parentDir) ?: [];
$onlyDots = true;
foreach ($items as $item) {
if ($item !== '.' && $item !== '..') {
$onlyDots = false;
break;
}
}
if ($onlyDots) {
@rmdir($parentDir);
}
}
return $path;
}
public function readLogForCurrentUser(string $jobId): string
{
$jobId = trim($jobId);
if ($jobId === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $jobId)) {
throw new RuntimeException('Nieprawidłowy identyfikator zadania.');
}
$jobs = $this->listJobsForCurrentUser();
$allowed = false;
foreach ($jobs as $job) {
if ($job['job_id'] === $jobId) {
$allowed = true;
break;
}
}
if (!$allowed) {
throw new RuntimeException('Brak dostępu do logu zadania.');
}
$path = $this->logsDir . '/' . $jobId . '.log';
if (!is_file($path) || !is_readable($path)) {
return 'Brak logu dla wybranego zadania.';
}
$content = @file_get_contents($path);
if ($content === false || trim($content) === '') {
return 'Log jest pusty.';
}
return $content;
}
public function deleteBackupLogForCurrentUser(string $jobId): string
{
$jobId = trim($jobId);
if ($jobId === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $jobId)) {
throw new RuntimeException('Nieprawidłowy identyfikator zadania.');
}
$jobs = $this->listJobsForCurrentUser();
$allowed = false;
foreach ($jobs as $job) {
if ($job['job_id'] === $jobId) {
$allowed = true;
break;
}
}
if (!$allowed) {
throw new RuntimeException('Brak dostępu do logu zadania.');
}
$path = $this->logsDir . '/' . $jobId . '.log';
if (!is_file($path)) {
throw new RuntimeException('Log nie istnieje.');
}
if (!@unlink($path)) {
throw new RuntimeException('Nie udało się usunąć logu zadania.');
}
return $path;
}
public function isDbLocked(string $dbName): bool
{
return is_file($this->dbLockPath($dbName));
}
/**
* @return array{path:string,date:string,db_name:string}
*/
public function getLocalBackupEntry(string $relativePath): array
{
return $this->resolveLocalBackupEntry($relativePath);
}
public function dbLockPath(string $dbName): string
{
$safe = preg_replace('/[^A-Za-z0-9_.-]+/', '_', $dbName) ?? 'db';
return $this->lockDir . '/' . $safe . '.lock';
}
private function assertDatabaseBelongsToUser(string $dbName): void
{
if (strpos($dbName, $this->daUser->prefix()) !== 0) {
throw new RuntimeException('Baza danych nie należy do konta DirectAdmin: ' . $dbName);
}
}
/**
* @return array{path:string,date:string,db_name:string}
*/
private function resolveLocalBackupEntry(string $relativePath): array
{
$relativePath = trim(str_replace('\\', '/', $relativePath));
if ($relativePath === '') {
throw new RuntimeException('Nie wybrano pliku backupu z lokalnego katalogu.');
}
if (strpos($relativePath, '..') !== false || str_starts_with($relativePath, '/')) {
throw new RuntimeException('Nieprawidłowa ścieżka backupu.');
}
$root = $this->scriptBackupRoot();
$fullPath = $root . '/' . ltrim($relativePath, '/');
if (!is_file($fullPath) || !is_readable($fullPath)) {
throw new RuntimeException('Nie można odczytać wskazanego pliku backupu.');
}
if (!$this->isAllowedSqlFile($fullPath)) {
throw new RuntimeException('Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.');
}
$realRoot = @realpath($root);
$realPath = @realpath($fullPath);
if ($realRoot === false || $realPath === false || !str_starts_with($realPath, rtrim($realRoot, '/') . '/')) {
throw new RuntimeException('Plik backupu znajduje się poza dozwolonym katalogiem.');
}
$dateDir = basename(dirname($realPath));
if ($dateDir === '' || $dateDir === '.' || $dateDir === '..') {
$dateDir = date('Y-m-d');
}
$dbName = $this->extractSourceDbFromBackupFile(basename($realPath));
if ($dbName === '') {
throw new RuntimeException('Nie można ustalić docelowej bazy dla backupu lokalnego.');
}
return ['path' => $realPath, 'date' => $dateDir, 'db_name' => $dbName];
}
public function detectSourceDatabaseFromBackupName(string $pathOrFile): string
{
$candidate = $this->extractSourceDbFromBackupFile(basename($pathOrFile));
if ($candidate === '') {
return '';
}
if (strpos($candidate, $this->daUser->prefix()) !== 0) {
return '';
}
return $candidate;
}
public function isAllowedSqlFile(string $path): bool
{
return (bool)preg_match('/\.(?:sql\.gz|sql|gz)$/i', basename($path));
}
private function extractSourceDbFromBackupFile(string $file): string
{
$name = preg_replace('/\.(?:sql\.gz|sql|gz)$/i', '', $file) ?? $file;
if ($name === '') {
return '';
}
$parts = explode('__', $name, 2);
$candidate = strtolower(trim($parts[0]));
if ($candidate === '' || !preg_match('/^[a-z0-9_]+$/', $candidate)) {
return '';
}
return $candidate;
}
private function buildStagedUploadPath(string $originalName, string $prefix): string
{
$safeBase = preg_replace('/[^A-Za-z0-9._-]+/', '_', basename($originalName)) ?? 'restore.sql';
$targetDir = $this->uploadsDir . '/' . $this->daUser->username();
if (!is_dir($targetDir) && !@mkdir($targetDir, 0700, true) && !is_dir($targetDir)) {
throw new RuntimeException('Nie można utworzyć katalogu upload: ' . $targetDir);
}
@chmod($targetDir, 0700);
$token = date('YmdHis') . '-' . bin2hex(random_bytes(4));
return $targetDir . '/' . $prefix . '-' . $token . '_' . $safeBase;
}
private function buildJobId(string $type): string
{
return $type . '-' . date('YmdHis') . '-' . bin2hex(random_bytes(4));
}
private function withQueueLock(callable $callback)
{
$lockDir = $this->dataDir . '/jobs/queue.lock';
$tries = 0;
while (!@mkdir($lockDir, 0700)) {
$tries++;
if (!is_dir($lockDir)) {
continue;
}
$mtime = (int)@filemtime($lockDir);
if ($mtime > 0 && (time() - $mtime) > 120) {
@rmdir($lockDir);
}
if ($tries >= 40) {
throw new RuntimeException('Nie można uzyskać blokady kolejki zadań. Spróbuj ponownie.');
}
usleep(100000);
}
try {
return $callback();
} finally {
@rmdir($lockDir);
}
}
private function acquireDbLock(string $dbName, string $jobType, string $jobId, string $lockPath): void
{
$handle = @fopen($lockPath, 'x');
if ($handle === false) {
$existing = is_file($lockPath) ? trim((string)@file_get_contents($lockPath)) : '';
$msg = 'Na bazie danych trwa już operacja. Zaczekaj na zakończenie poprzedniego zadania.';
if ($existing !== '') {
$msg .= ' [' . $existing . ']';
}
throw new RuntimeException($msg);
}
$content = implode(
"\n",
[
'DB_NAME=' . $dbName,
'JOB_TYPE=' . $jobType,
'JOB_ID=' . $jobId,
'DA_USER=' . $this->daUser->username(),
'CREATED_AT=' . date('c'),
]
) . "\n";
fwrite($handle, $content);
fclose($handle);
@chmod($lockPath, 0600);
}
private function releaseDbLock(string $lockPath): void
{
if (is_file($lockPath)) {
@unlink($lockPath);
}
}
/**
* @param array<string,string> $payload
*/
private function writeJobFile(string $path, array $payload): void
{
$lines = [];
foreach ($payload as $key => $value) {
$lines[] = $key . '=' . $this->envQuote($value);
}
$content = implode("\n", $lines) . "\n";
if (@file_put_contents($path, $content, LOCK_EX) === false) {
throw new RuntimeException('Nie udało się zapisać pliku zadania: ' . $path);
}
@chmod($path, 0600);
}
private function envQuote(string $value): string
{
return "'" . str_replace("'", "'\\''", $value) . "'";
}
private function triggerWorker(): void
{
$script = $this->pluginRoot . '/scripts/worker.sh';
if (!is_file($script)) {
return;
}
$euid = null;
if (function_exists('posix_geteuid')) {
$euid = @posix_geteuid();
} else {
$raw = @shell_exec('id -u 2>/dev/null');
if (is_string($raw) && preg_match('/^[0-9]+$/', trim($raw))) {
$euid = (int)trim($raw);
}
}
// Worker powinien wykonywać zadania z crona jako root, aby mieć dostęp
// do katalogów backupu i konfiguracji systemowej.
if ($euid !== null && $euid !== 0) {
return;
}
$cmd = '/bin/bash ' . escapeshellarg($script) . ' >/dev/null 2>&1 &';
@exec($cmd);
}
/**
* @param array<int,array{path:string,status:string,job_id:string}> $target
*/
private function collectJobFiles(string $dir, string $status, array &$target): void
{
if (!is_dir($dir)) {
return;
}
$files = glob($dir . '/*.env');
if ($files === false) {
return;
}
foreach ($files as $path) {
$base = basename($path);
$jobId = substr($base, 0, -4);
$target[] = ['path' => $path, 'status' => $status, 'job_id' => $jobId];
}
}
/**
* @param array<int,array{path:string,status:string,job_id:string}> $target
*/
private function collectDoneJobFiles(string $dir, array &$target): void
{
if (!is_dir($dir)) {
return;
}
$patterns = ['*.ok' => 'done_ok', '*.fail' => 'done_fail', '*.cancel' => 'done_cancel'];
foreach ($patterns as $pattern => $status) {
$files = glob($dir . '/' . $pattern);
if ($files === false) {
continue;
}
foreach ($files as $path) {
$base = basename($path);
$jobId = preg_replace('/\.(ok|fail|cancel)$/', '', $base) ?? $base;
$target[] = ['path' => $path, 'status' => $status, 'job_id' => $jobId];
}
}
}
/**
* @return array<string,string>
*/
private function parseJobFile(string $path): array
{
$out = [];
if (!is_readable($path)) {
return $out;
}
$lines = @file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $out;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $m)) {
continue;
}
$key = $m[1];
$value = trim((string)$m[2]);
if (strlen($value) >= 2 && $value[0] === '\'' && substr($value, -1) === '\'') {
$value = substr($value, 1, -1);
$value = str_replace("'\\''", "'", $value);
} elseif (strlen($value) >= 2 && $value[0] === '"' && substr($value, -1) === '"') {
$value = stripcslashes(substr($value, 1, -1));
}
$out[$key] = $value;
}
return $out;
}
private function humanFileSize(int $bytes): string
{
if ($bytes <= 0) {
return '0 B';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$power = (int)floor(log($bytes, 1024));
$power = min($power, count($units) - 1);
$value = $bytes / (1024 ** $power);
return number_format($value, $power === 0 ? 0 : 2, '.', ' ') . ' ' . $units[$power];
}
private function pathInDir(string $path, string $dir): bool
{
$realPath = @realpath($path);
$realDir = @realpath($dir);
if ($realPath === false || $realDir === false) {
return false;
}
$realDir = rtrim($realDir, '/');
if ($realPath === $realDir) {
return true;
}
return str_starts_with($realPath, $realDir . '/');
}
/**
* @param array<string,mixed> $job
*/
private function resolveBackupTargetPathFromJob(array $job, string $jobId): string
{
$path = trim((string)($job['target_file'] ?? ''));
if ($path === '') {
$fallback = $this->inferJobMetaFromLog($jobId);
$path = $fallback !== null ? trim((string)($fallback['TARGET_FILE'] ?? '')) : '';
}
return $path;
}
/**
* @return array<string,string>|null
*/
private function inferJobMetaFromLog(string $jobId): ?array
{
$jobId = trim($jobId);
if ($jobId === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $jobId)) {
return null;
}
$logPath = $this->logsDir . '/' . $jobId . '.log';
if (!is_file($logPath) || !is_readable($logPath)) {
return null;
}
$lines = @file($logPath, FILE_IGNORE_NEW_LINES);
if ($lines === false || empty($lines)) {
return null;
}
$type = 'unknown';
$dbName = '';
$sourceFile = '';
$targetFile = '';
$startTs = 0;
$restoreMode = '';
$sourceDbName = '';
foreach ($lines as $lineRaw) {
$line = trim((string)$lineRaw);
if ($line === '') {
continue;
}
if ($startTs === 0 && preg_match('/^\[([0-9:-]{19})\]/', $line, $mTs)) {
$parsed = strtotime($mTs[1]);
if ($parsed !== false) {
$startTs = (int)$parsed;
}
}
if (stripos($line, 'Start backup job') !== false) {
$type = 'backup';
} elseif (stripos($line, 'Start restore job') !== false) {
$type = 'restore';
}
if ($dbName === '' && preg_match('/Tworzenie backupu bazy\s+([a-z0-9_]+)/iu', $line, $mDb)) {
$dbName = strtolower($mDb[1]);
}
if ($dbName === '' && preg_match('/Przywracanie bazy\s+([a-z0-9_]+)/iu', $line, $mDb2)) {
$dbName = strtolower($mDb2[1]);
}
if (preg_match('/Sanityzacja dumpa:\s*([a-z0-9_]+)\s*->\s*([a-z0-9_]+)/iu', $line, $mSan)) {
$sourceDbName = strtolower($mSan[1]);
$restoreMode = (strtolower($mSan[1]) === strtolower($mSan[2])) ? 'overwrite' : 'new_db';
}
if ($targetFile === '' && preg_match('/Backup zapisany:\s+(.+)$/u', $line, $mTarget)) {
$targetFile = trim($mTarget[1]);
}
if ($sourceFile === '' && preg_match('/Przywracanie bazy\s+[a-z0-9_]+\s+z pliku\s+(.+)$/iu', $line, $mSource)) {
$sourceFile = trim($mSource[1]);
}
}
if ($type === 'unknown') {
if (str_starts_with($jobId, 'backup-') || str_starts_with($jobId, 'backup-backup-')) {
$type = 'backup';
} elseif (str_starts_with($jobId, 'restore-') || str_starts_with($jobId, 'restore-restore-')) {
$type = 'restore';
}
}
if ($dbName === '' || strpos($dbName, $this->daUser->prefix()) !== 0) {
return null;
}
return [
'DA_USER' => $this->daUser->username(),
'JOB_TYPE' => $type,
'DB_NAME' => $dbName,
'SOURCE_FILE' => $sourceFile,
'TARGET_FILE' => $targetFile,
'START_TS' => $startTs > 0 ? (string)$startTs : '',
'RESTORE_MODE' => $restoreMode,
'SOURCE_DB_NAME' => $sourceDbName,
];
}
}