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