PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / Backup / Backup.php
backup / src / JetBackup / Backup Last commit date
.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