.htaccess
1 year ago
Backup.php
4 months ago
BackupAccount.php
1 year ago
BackupConfig.php
1 year ago
index.html
1 year ago
web.config
1 year ago
Backup.php
540 lines
| 1 | <?php |
| 2 | |
| 3 | namespace JetBackup\Backup; |
| 4 | |
| 5 | use Exception; |
| 6 | use JetBackup\Alert\Alert; |
| 7 | use JetBackup\Archive\Archive; |
| 8 | use JetBackup\Archive\Gzip; |
| 9 | use JetBackup\Archive\Header\Header; |
| 10 | use JetBackup\BackupJob\BackupJob; |
| 11 | use JetBackup\Cron\Task\Task; |
| 12 | use JetBackup\Data\Engine; |
| 13 | use JetBackup\Destination\Destination; |
| 14 | use JetBackup\Destination\Tree; |
| 15 | use JetBackup\Destination\Vendors\Local\Local; |
| 16 | use JetBackup\DirIterator\DirIterator; |
| 17 | use JetBackup\Entities\Util; |
| 18 | use JetBackup\Exception\ArchiveException; |
| 19 | use JetBackup\Exception\BackupException; |
| 20 | use JetBackup\Exception\DBException; |
| 21 | use JetBackup\Exception\DestinationException; |
| 22 | use JetBackup\Exception\DirIteratorFileVanishedException; |
| 23 | use JetBackup\Exception\IOVanishedException; |
| 24 | use JetBackup\Exception\JBException; |
| 25 | use JetBackup\Exception\QueueException; |
| 26 | use JetBackup\Exception\ScheduleException; |
| 27 | use JetBackup\Factory; |
| 28 | use JetBackup\JetBackup; |
| 29 | use JetBackup\Log\LogController; |
| 30 | use JetBackup\Queue\Queue; |
| 31 | use JetBackup\Queue\QueueItem; |
| 32 | use JetBackup\Queue\QueueItemBackup; |
| 33 | use JetBackup\Schedule\Schedule; |
| 34 | use JetBackup\Snapshot\Snapshot; |
| 35 | use JetBackup\Wordpress\Init; |
| 36 | use JetBackup\Wordpress\Wordpress; |
| 37 | use SleekDB\Exceptions\InvalidArgumentException; |
| 38 | use SleekDB\Exceptions\IOException; |
| 39 | |
| 40 | if (!defined( '__JETBACKUP__')) die('Direct access is not allowed'); |
| 41 | |
| 42 | abstract class Backup { |
| 43 | |
| 44 | |
| 45 | const WP_CONFIG_FILE = "%swp-config.php"; |
| 46 | const HTACCESS_FILE = "%s.htaccess"; |
| 47 | |
| 48 | private Task $_task; |
| 49 | private QueueItemBackup $_queue_item_backup; |
| 50 | private BackupJob $_backup_job; |
| 51 | |
| 52 | public function __construct(Task $task) { |
| 53 | $this->_task = $task; |
| 54 | |
| 55 | $this->_queue_item_backup = $this->getQueueItem()->getItemData(); |
| 56 | $this->_backup_job = new BackupJob($this->getQueueItemBackup()->getJobId()); |
| 57 | } |
| 58 | |
| 59 | public function getTask():Task { return $this->_task; } |
| 60 | public function getLogController():LogController { return $this->getTask()->getLogController(); } |
| 61 | public function getQueueItem(): QueueItem { return $this->getTask()->getQueueItem(); } |
| 62 | public function getQueueItemBackup(): QueueItemBackup { return $this->_queue_item_backup; } |
| 63 | public function getBackupJob(): BackupJob { return $this->_backup_job; } |
| 64 | public function getSnapshotDirectory(): string { return $this->getQueueItem()->getWorkspace() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName(); } |
| 65 | |
| 66 | /** |
| 67 | * Calculate optimal gzip chunk size based on execution time limit. |
| 68 | * Smaller chunks allow more frequent execution time checks for graceful exit. |
| 69 | */ |
| 70 | protected function _getCompressionChunkSize(): int { |
| 71 | $limit = $this->getTask()->getExecutionTimeLimit(); |
| 72 | if ($limit <= 0) return Gzip::DEFAULT_COMPRESS_CHUNK_SIZE; // No limit, use default 10MB |
| 73 | if ($limit <= 30) return 1048576; // 1MB for ≤30s |
| 74 | if ($limit <= 60) return 2097152; // 2MB for ≤60s |
| 75 | return Gzip::DEFAULT_COMPRESS_CHUNK_SIZE; // 10MB for higher limits |
| 76 | } |
| 77 | |
| 78 | abstract public function execute():void; |
| 79 | |
| 80 | protected function _archiveFiles($source): void { |
| 81 | |
| 82 | $archive_file = $this->getSnapshotDirectory() . JetBackup::SEP . Snapshot::SKELETON_FILES_DIRNAME . JetBackup::SEP . Snapshot::SKELETON_FILES_ARCHIVE_NAME; |
| 83 | $files_list_file = $this->getSnapshotDirectory() . JetBackup::SEP . Snapshot::SKELETON_META_DIRNAME . JetBackup::SEP . Snapshot::SKELETON_FILES_LIST_FILENAME; |
| 84 | |
| 85 | $this->getLogController()->logDebug('[_archiveFiles] Archive file: ' . $archive_file); |
| 86 | $this->getLogController()->logDebug('[_archiveFiles] Tree file: ' . $files_list_file); |
| 87 | |
| 88 | try { |
| 89 | |
| 90 | $archive = new Archive($archive_file, false, Archive::OPT_SPARSE, 0, $this->getSnapshotDirectory() . JetBackup::SEP . Snapshot::SKELETON_TEMP_DIRNAME); |
| 91 | $archive->setLogController($this->getLogController()); |
| 92 | |
| 93 | $list_fd = fopen($files_list_file, 'a'); |
| 94 | |
| 95 | $archive->setCreateFileCallback(function(Header $header) use ($list_fd) { |
| 96 | fwrite($list_fd, "{$header->getSize(false)} {$header->getMtime(false)} {$header->getFilename()}\n"); |
| 97 | }); |
| 98 | |
| 99 | $this->getTask()->scan($source, function(DirIterator $scan, $data) use ($archive, $source) { |
| 100 | //$this->getLogController()->logDebug('[_archiveFiles] Data: ' . print_r($data, true)); |
| 101 | $this->getLogController()->logDebug('[_archiveFiles] Source: ' .$source); |
| 102 | |
| 103 | if (!$data->total_size) { |
| 104 | $this->getLogController()->logMessage('[_archiveFiles] No files to archive (total_size=0). This may indicate all files are excluded or the source directory is empty.'); |
| 105 | return; // Skip archiving if there are no files |
| 106 | } |
| 107 | |
| 108 | $archive->setAppend(!($data->total_size == $data->current_pos)); |
| 109 | |
| 110 | if(!$archive->isAppend()) { |
| 111 | $this->getQueueItem()->getProgress()->setMessage("Archiving..."); |
| 112 | $this->getQueueItem()->save(); |
| 113 | |
| 114 | $this->getLogController()->logMessage('Inside Archive Manager First run'); |
| 115 | $this->getLogController()->logMessage('Total tree size: ' . $data->total_size); |
| 116 | $this->getLogController()->logMessage('Current tree POS: ' . $data->current_pos); |
| 117 | $this->getLogController()->logMessage('Source: ' . $scan->getSource()); |
| 118 | $this->getLogController()->logMessage('Excludes: '); |
| 119 | $this->getLogController()->logMessage(print_r($scan->getExcludes(), true)); |
| 120 | } |
| 121 | |
| 122 | try { |
| 123 | $fd = $archive->getFileFD(); |
| 124 | $current_file = $scan->next($fd ? $fd->tell() : 0); |
| 125 | } catch (DirIteratorFileVanishedException $e) { |
| 126 | $this->getLogController()->logMessage('[ WARNING ] File Vanished : ' . $e->getMessage()); |
| 127 | return; |
| 128 | } |
| 129 | |
| 130 | if($scan->getSource() == $current_file->getName()) return; |
| 131 | |
| 132 | $progress = $this->getQueueItem()->getProgress(); |
| 133 | $progress->setSubMessage("Archiving: {$current_file->getName()}"); |
| 134 | $progress->setTotalSubItems($data->total_size); |
| 135 | $progress->setCurrentSubItem($data->total_size - $data->current_pos); |
| 136 | |
| 137 | $this->getQueueItem()->save(); |
| 138 | |
| 139 | $this->getLogController()->logMessage("[". ($data->total_size - $data->current_pos)."/$data->total_size] Archiving: {$current_file->getName()}"); |
| 140 | |
| 141 | try { |
| 142 | |
| 143 | $file = substr($current_file->getName(), strlen($source)+1); |
| 144 | $archive->appendFileChunked($current_file, $file, function() use ($data) { |
| 145 | |
| 146 | // We should return true end exit later, however I want to try to exit here to see if this makes issues |
| 147 | $this->getTask()->checkExecutionTime(function() use ($data) { |
| 148 | |
| 149 | $progress = $this->getQueueItem()->getProgress(); |
| 150 | $progress->setSubMessage("Waiting for next cron iteration"); |
| 151 | $progress->setTotalSubItems($data->total_size); |
| 152 | $progress->setCurrentSubItem($data->total_size - $data->current_pos); |
| 153 | $this->getQueueItem()->save(); |
| 154 | }); |
| 155 | |
| 156 | return false; |
| 157 | |
| 158 | }, Factory::getSettingsPerformance()->getReadChunkSizeBytes()); |
| 159 | } catch( Exception|ArchiveException $e) { |
| 160 | //this will throw exception if the file has been changed more than 3 times |
| 161 | $this->getLogController()->logError('[Backup] Error while trying to archive: ' . $e->getMessage()); |
| 162 | } |
| 163 | |
| 164 | }, $this->getBackupJob()->getAllExcludes()); |
| 165 | |
| 166 | $archive->save(); |
| 167 | $this->getLogController()->logMessage('Archive Done'); |
| 168 | //touch($directory_tree_file_done); |
| 169 | |
| 170 | } catch ( Exception $e) { |
| 171 | |
| 172 | // Handle the exception (e.g., log the error, display a message, etc.) |
| 173 | Alert::add('Error', 'Error during archive creation:' . $e->getMessage(), Alert::LEVEL_CRITICAL); |
| 174 | |
| 175 | $this->getQueueItem()->updateStatus(Queue::STATUS_FAILED); |
| 176 | $this->getQueueItem()->updateProgress('Error occurred'); |
| 177 | |
| 178 | throw $e; |
| 179 | |
| 180 | } finally { |
| 181 | // Always close the file handle, even if an exception occurred |
| 182 | if (isset($list_fd) && is_resource($list_fd)) { |
| 183 | fclose($list_fd); |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | protected function _createWorkspace():void { |
| 189 | $directories = [ |
| 190 | $this->getSnapshotDirectory(), |
| 191 | '%s' . Snapshot::SKELETON_DATABASE_DIRNAME, |
| 192 | '%s' . Snapshot::SKELETON_FILES_DIRNAME, |
| 193 | '%s' . Snapshot::SKELETON_CONFIG_DIRNAME, |
| 194 | '%s' . Snapshot::SKELETON_TEMP_DIRNAME, |
| 195 | '%s' . Snapshot::SKELETON_LOG_DIRNAME, |
| 196 | '%s' . Snapshot::SKELETON_META_DIRNAME, |
| 197 | ]; |
| 198 | |
| 199 | foreach($directories as $folder) { |
| 200 | $folder = sprintf($folder, $this->getSnapshotDirectory() . JetBackup::SEP); |
| 201 | $this->getLogController()->logDebug("Creating directory: $folder"); |
| 202 | Util::secureFolder($folder); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | protected function _compressFiles() { |
| 207 | |
| 208 | $file_backup_archive = $this->getSnapshotDirectory() . JetBackup::SEP . Snapshot::SKELETON_FILES_DIRNAME . JetBackup::SEP . Snapshot::SKELETON_FILES_ARCHIVE_NAME; |
| 209 | |
| 210 | $this->getLogController()->logMessage('Starting compression for: ' . $file_backup_archive); |
| 211 | |
| 212 | // Use smaller chunk size when execution time is limited to allow graceful exit |
| 213 | // gzencode on large chunks can exceed the time buffer |
| 214 | $chunkSize = $this->_getCompressionChunkSize(); |
| 215 | $this->getLogController()->logMessage("Compression chunk size: " . ($chunkSize / 1048576) . "MB (execution limit: {$this->getTask()->getExecutionTimeLimit()}s)"); |
| 216 | |
| 217 | Gzip::compress( |
| 218 | $file_backup_archive, |
| 219 | $chunkSize, |
| 220 | Gzip::DEFAULT_COMPRESSION_LEVEL, |
| 221 | function($byteRead, $totalSize) { |
| 222 | |
| 223 | $progress = $this->getQueueItem()->getProgress(); |
| 224 | $progress->setSubMessage(''); |
| 225 | $progress->setTotalSubItems($totalSize); |
| 226 | $progress->setCurrentSubItem($byteRead); |
| 227 | |
| 228 | $this->getQueueItem()->save(); |
| 229 | |
| 230 | $this->getTask()->checkExecutionTime(function() { |
| 231 | $this->getQueueItem()->getProgress()->setMessage('[ Gzip ] Waiting for next cron iteration'); |
| 232 | $this->getQueueItem()->save(); |
| 233 | }); |
| 234 | } |
| 235 | ); |
| 236 | |
| 237 | $this->getLogController()->logMessage('GZIP Compression done!'); |
| 238 | } |
| 239 | |
| 240 | abstract protected function getSnapshotItems():array; |
| 241 | |
| 242 | /** |
| 243 | * @throws \JetBackup\Exception\IOException |
| 244 | * @throws JBException |
| 245 | */ |
| 246 | private function _createSnapshot():Snapshot { |
| 247 | |
| 248 | $this->getLogController()->logDebug("[_createSnapshot] Creating snapshot"); |
| 249 | $multisite = []; |
| 250 | foreach(Wordpress::getMultisiteBlogs() as $blog) $multisite[] = $blog->getData(); |
| 251 | |
| 252 | $snapshot = new Snapshot(); |
| 253 | $snapshot->setCreated(time()); |
| 254 | $snapshot->setName($this->getQueueItemBackup()->getSnapshotName()); |
| 255 | $snapshot->setBackupType($this->getBackupJob()->getType()); |
| 256 | $snapshot->setContains($this->getBackupJob()->getContains()); |
| 257 | $snapshot->setStructure(Factory::getSettingsPerformance()->isGzipCompressArchive() ? BackupJob::STRUCTURE_COMPRESSED : BackupJob::STRUCTURE_ARCHIVED); |
| 258 | $snapshot->setJobIdentifier($this->getBackupJob()->getIdentifier()); |
| 259 | $snapshot->setDeleted(0); |
| 260 | $snapshot->setLocked(false); |
| 261 | $snapshot->setEngine(Engine::ENGINE_WP); |
| 262 | |
| 263 | $snapshot->addParam(Snapshot::PARAM_MULTISITE, $multisite); |
| 264 | $snapshot->addParam(Snapshot::PARAM_SITE_URL, Wordpress::getSiteURL()); |
| 265 | $snapshot->addParam(Snapshot::PARAM_DB_PREFIX, Wordpress::getDB()->getPrefix()); |
| 266 | |
| 267 | // Store whether this backup was created in a wp-content-only context (WP Cloud or setting enabled) |
| 268 | if (Init::isWpCloudAtomic() || Factory::getSettingsRestore()->isRestoreWpContentOnlyEnabled()) { |
| 269 | $snapshot->addParam(Snapshot::PARAM_WP_CONTENT_ONLY_BACKUP, true); |
| 270 | } |
| 271 | |
| 272 | $size = 0; |
| 273 | $items = $this->getSnapshotItems(); |
| 274 | foreach ($items as $item) $size += $item->getSize(); |
| 275 | |
| 276 | $snapshot->setItems($items); |
| 277 | $snapshot->setSize($size); |
| 278 | |
| 279 | |
| 280 | // Use schedule types captured when job was queued (before calculateNextRun advanced them) |
| 281 | foreach ($this->getQueueItemBackup()->getScheduleTypes() as $scheduleType) { |
| 282 | $snapshot->addScheduleByType($scheduleType); |
| 283 | } |
| 284 | if($this->getQueueItemBackup()->isManually()) $snapshot->addScheduleByType(Schedule::TYPE_MANUALLY); |
| 285 | if($this->getQueueItemBackup()->isAfterJobDone()) $snapshot->addScheduleByType(Schedule::TYPE_AFTER_JOB_DONE); |
| 286 | |
| 287 | return $snapshot; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * @return void |
| 292 | * @throws \JetBackup\Exception\IOException |
| 293 | */ |
| 294 | protected function _transferToDestination() { |
| 295 | |
| 296 | $this->getTask()->foreachCallable(function() { |
| 297 | $destinations = []; |
| 298 | |
| 299 | // Move local destination to be last |
| 300 | foreach($this->getQueueItemBackup()->getDestinations() as $destination_id) { |
| 301 | $destination = new Destination($destination_id); |
| 302 | if(!$destination->getId()) throw new BackupException("Invalid destination {$destination->getId()}"); |
| 303 | if($destination->getType() == Local::TYPE) $destinations[] = $destination_id; |
| 304 | else array_unshift($destinations, $destination_id); |
| 305 | } |
| 306 | |
| 307 | return $destinations; |
| 308 | |
| 309 | }, [], function($key, $destination_id) { |
| 310 | |
| 311 | $destination = new Destination($destination_id); |
| 312 | $destination->setLogController($this->getLogController()); |
| 313 | |
| 314 | $this->getLogController()->logMessage("Uploading backup to destination \"{$destination->getName()}\" (id: {$destination->getId()})"); |
| 315 | |
| 316 | $progress = $this->getQueueItem()->getProgress(); |
| 317 | $progress->setSubMessage('Transferring to destination "' . $destination->getName() . '"'); |
| 318 | $this->getQueueItem()->save(); |
| 319 | |
| 320 | // Create the snapshot object and dump it to the snapshot folder for upload |
| 321 | // Don't save this object yet, Only after upload is done |
| 322 | $snapshot = $this->_createSnapshot(); |
| 323 | $snapshot->setDestinationId($destination->getId()); |
| 324 | |
| 325 | $this->getTask()->func(function() use ($snapshot, $destination, $progress) { |
| 326 | |
| 327 | // if needed, add more snapshot details above this line |
| 328 | $snapshot->exportMeta($this->getSnapshotDirectory()); |
| 329 | |
| 330 | if($destination->getType() == Local::TYPE) { |
| 331 | |
| 332 | $source = $this->getSnapshotDirectory(); |
| 333 | $target = rtrim($destination->getInstance()->getPath(), JetBackup::SEP) . JetBackup::SEP . $this->getBackupJob()->getIdentifier() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName(); |
| 334 | |
| 335 | if (!file_exists(dirname($target))) { |
| 336 | if (!mkdir(dirname($target), 0700, true)) { |
| 337 | throw new BackupException("Failed to create directory: $target"); |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | // We don't need the real size, the `rename` will be very fast |
| 342 | $progress->setTotalSubItems(1); |
| 343 | $progress->setCurrentSubItem(0); |
| 344 | $this->getQueueItem()->save(); |
| 345 | |
| 346 | $this->getLogController()->logMessage("Moving snap data to local location: $source -> $target"); |
| 347 | if (!rename($source, $target)) { |
| 348 | throw new BackupException("Failed to move $source -> $target"); |
| 349 | } |
| 350 | |
| 351 | $progress->setCurrentSubItem(1); |
| 352 | $this->getQueueItem()->save(); |
| 353 | |
| 354 | } else { |
| 355 | |
| 356 | (new Tree($destination, $this->getQueueItem(), $this->getSnapshotDirectory()))->process(function($file) use ($destination) { |
| 357 | |
| 358 | $this->getTask()->checkExecutionTime(); |
| 359 | |
| 360 | $source = $this->getSnapshotDirectory() . $file; |
| 361 | $target = $this->getBackupJob()->getIdentifier() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName() . $file; |
| 362 | |
| 363 | if (is_dir($source)) { |
| 364 | $this->getLogController()->logMessage("Creating folder $target"); |
| 365 | $destination->createDir($target); |
| 366 | return; |
| 367 | } |
| 368 | |
| 369 | $destination->copyFileToRemote($source, $target, $this->getQueueItem(), $this->getTask()); |
| 370 | }); |
| 371 | } |
| 372 | |
| 373 | $progress->setTotalSubItems(0); |
| 374 | $progress->setCurrentSubItem(0); |
| 375 | $this->getQueueItem()->save(); |
| 376 | |
| 377 | }, [], '_uploadMetaDestination' . $destination->getId()); |
| 378 | |
| 379 | $this->getTask()->func(function() use ($destination) { |
| 380 | |
| 381 | $this->getLogController()->logMessage('Uploading log file to destination id ' . $destination->getId()); |
| 382 | $target = $this->getBackupJob()->getIdentifier() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName() . JetBackup::SEP . Snapshot::SKELETON_LOG_DIRNAME; |
| 383 | $destination->createDir($target); |
| 384 | |
| 385 | $logfile = $this->getTask()->getLogFile(); |
| 386 | $logfile_tmp = $logfile .'_tmp'; |
| 387 | // We cannot upload the original log since we continue to write to it during upload |
| 388 | // Some remote destination are doing hash calculations and will return error because of size mismatch |
| 389 | $this->getLogController()->logMessage('Preparing log file for upload'); |
| 390 | $this->getLogController()->logMessage('### Data after this line will not be updated in the snapshot log file ###'); |
| 391 | Util::cp($logfile, $logfile_tmp, 0400); |
| 392 | |
| 393 | Gzip::compress( |
| 394 | $logfile_tmp, |
| 395 | Gzip::DEFAULT_COMPRESS_CHUNK_SIZE, |
| 396 | Gzip::DEFAULT_COMPRESSION_LEVEL, |
| 397 | function ($byteRead, $totalSize) { |
| 398 | |
| 399 | $progress = $this->getTask()->getQueueItem()->getProgress(); |
| 400 | $progress->setSubMessage(''); |
| 401 | $progress->setTotalSubItems($totalSize); |
| 402 | $progress->setCurrentSubItem($byteRead); |
| 403 | |
| 404 | $this->getTask()->getQueueItem()->save(); |
| 405 | |
| 406 | $this->getTask()->checkExecutionTime(function () { |
| 407 | $this->getTask()->getQueueItem()->getProgress()->setMessage('[ Gzip ] Waiting for next cron iteration'); |
| 408 | $this->getTask()->getQueueItem()->save(); |
| 409 | }); |
| 410 | } |
| 411 | ); |
| 412 | |
| 413 | $logfile_tmp = $logfile_tmp . '.gz'; |
| 414 | $destination->copyFileToRemote($logfile_tmp, $target . JetBackup::SEP . Snapshot::SKELETON_LOG_FILENAME, $this->getQueueItem(), $this->getTask()); |
| 415 | $this->getLogController()->logDebug("Temporary log file uploaded [$logfile_tmp]"); |
| 416 | unlink($logfile_tmp); |
| 417 | $this->getLogController()->logDebug("Temporary log file deleted [$logfile_tmp]"); |
| 418 | |
| 419 | }, [], '_uploadLogDestination' . $destination->getId()); |
| 420 | |
| 421 | // after upload is done, save the snapshot object each destination |
| 422 | $snapshot->save(); |
| 423 | |
| 424 | }, '_transferToAllDestinations'); |
| 425 | |
| 426 | $this->getLogController()->logMessage('Sending backup to all destinations is complete'); |
| 427 | $this->getLogController()->logMessage('Removing temp folder ' . $this->getSnapshotDirectory()); |
| 428 | |
| 429 | Util::rm($this->getSnapshotDirectory()); |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * @return void |
| 434 | * @throws IOException |
| 435 | * @throws InvalidArgumentException |
| 436 | * @throws DBException |
| 437 | * @throws ScheduleException |
| 438 | */ |
| 439 | protected function _calculateAfterJobDone () { |
| 440 | |
| 441 | $schedule_details = Schedule::query() |
| 442 | ->select([JetBackup::ID_FIELD]) |
| 443 | ->where([Schedule::BACKUP_ID, '=', $this->getBackupJob()->getId()]) |
| 444 | ->getQuery() |
| 445 | ->first(); |
| 446 | |
| 447 | if(!$schedule_details) return; |
| 448 | |
| 449 | $schedule_id = $schedule_details[JetBackup::ID_FIELD]; |
| 450 | |
| 451 | $list = BackupJob::query() |
| 452 | ->select([JetBackup::ID_FIELD]) |
| 453 | ->getQuery() |
| 454 | ->fetch(); |
| 455 | |
| 456 | foreach($list as $config_details) { |
| 457 | $backup_config = new BackupJob($config_details[JetBackup::ID_FIELD]); |
| 458 | |
| 459 | if(!($schedule = $backup_config->getScheduleById($schedule_id))) continue; |
| 460 | |
| 461 | $schedule->setNextRun(time()); |
| 462 | $backup_config->updateSchedule($schedule); |
| 463 | $backup_config->save(); |
| 464 | } |
| 465 | } |
| 466 | |
| 467 | /** |
| 468 | * @return void |
| 469 | * @throws IOException |
| 470 | * @throws InvalidArgumentException |
| 471 | * @throws JBException |
| 472 | */ |
| 473 | protected function _retentionCleanup() { |
| 474 | $this->getLogController()->logMessage('Marking unneeded snapshots for delete'); |
| 475 | |
| 476 | $addToQueue = false; |
| 477 | |
| 478 | foreach($this->getBackupJob()->getSchedules() as $schedule) { |
| 479 | |
| 480 | $snapshots = Snapshot::query() |
| 481 | ->where([Snapshot::DESTINATION_ID, 'in', $this->getQueueItemBackup()->getDestinations()]) |
| 482 | ->where([Snapshot::JOB_IDENTIFIER, '=', $this->getBackupJob()->getIdentifier()]) |
| 483 | ->where([Snapshot::SCHEDULES, 'contains', $schedule->getType()]) |
| 484 | ->where([Snapshot::DELETED, '=', 0]) |
| 485 | ->orderBy([Snapshot::NAME => 'desc']) |
| 486 | ->skip($schedule->getRetain()) // skip the needed snapshots |
| 487 | ->getQuery() |
| 488 | ->fetch(); |
| 489 | |
| 490 | foreach($snapshots as $snapshot_details) { |
| 491 | |
| 492 | $snapshot = new Snapshot($snapshot_details[JetBackup::ID_FIELD]); |
| 493 | $this->getLogController()->logDebug('Marked snap ' . $snapshot->getName() . ' for delete, Destination ' . $snapshot->getDestinationName() . '[ ID ' . $snapshot->getDestinationId() . ']' ); |
| 494 | |
| 495 | $snapshot->removeSchedule($schedule->getType()); |
| 496 | |
| 497 | // if there is no more schedules assigned for this snapshot we need to delete it |
| 498 | if(!sizeof($snapshot->getSchedules())) { |
| 499 | $snapshot->setDeleted(time()); |
| 500 | $addToQueue = true; |
| 501 | } |
| 502 | |
| 503 | $snapshot->save(); |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | // There is nothing to delete, don't add cleanup to queue |
| 508 | if(!$addToQueue) return; |
| 509 | |
| 510 | $this->getLogController()->logMessage('Adding retention cleanup to queue'); |
| 511 | |
| 512 | $itemId = 0; |
| 513 | |
| 514 | // Singleton queue item: if already queued/running, do nothing (normal). |
| 515 | if (Queue::inQueue(Queue::QUEUE_TYPE_RETENTION_CLEANUP, $itemId)) { |
| 516 | $this->getLogController()->logDebug('Retention cleanup is already in the queue'); |
| 517 | return; |
| 518 | } |
| 519 | |
| 520 | try { |
| 521 | $queue_item = QueueItem::prepare(); |
| 522 | $queue_item->setType(Queue::QUEUE_TYPE_RETENTION_CLEANUP); |
| 523 | $queue_item->setItemId($itemId); |
| 524 | |
| 525 | Queue::addToQueue($queue_item); |
| 526 | |
| 527 | } catch (QueueException $e) { |
| 528 | // Race-safe: two processes can pass the pre-check simultaneously. |
| 529 | if (stripos($e->getMessage(), 'already in queue') !== false) { |
| 530 | $this->getLogController()->logDebug('Retention cleanup is already in the queue'); |
| 531 | return; |
| 532 | } |
| 533 | |
| 534 | $this->getLogController()->logMessage('[Backup] Adding retention cleanup failed: ' . $e->getMessage()); |
| 535 | // just logging without breaking |
| 536 | } |
| 537 | |
| 538 | } |
| 539 | } |
| 540 |