PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.13.4
JetBackup – Backup, Restore & Migrate v3.1.13.4
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 9 months ago Backup.php 9 months ago BackupAccount.php 9 months ago BackupConfig.php 9 months ago index.html 9 months ago web.config 9 months ago
Backup.php
495 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 fclose($list_fd);
152 $this->getLogController()->logMessage('Archive Done');
153 //touch($directory_tree_file_done);
154
155 } catch ( Exception $e) {
156
157 // Handle the exception (e.g., log the error, display a message, etc.)
158 Alert::add('Error', 'Error during archive creation:' . $e->getMessage(), Alert::LEVEL_CRITICAL);
159
160 $this->getQueueItem()->updateStatus(Queue::STATUS_FAILED);
161 $this->getQueueItem()->updateProgress('Error occurred');
162
163 throw $e;
164 }
165 }
166
167 protected function _createWorkspace():void {
168 $directories = [
169 $this->getSnapshotDirectory(),
170 '%s' . Snapshot::SKELETON_DATABASE_DIRNAME,
171 '%s' . Snapshot::SKELETON_FILES_DIRNAME,
172 '%s' . Snapshot::SKELETON_CONFIG_DIRNAME,
173 '%s' . Snapshot::SKELETON_TEMP_DIRNAME,
174 '%s' . Snapshot::SKELETON_LOG_DIRNAME,
175 '%s' . Snapshot::SKELETON_META_DIRNAME,
176 ];
177
178 foreach($directories as $folder) {
179 $folder = sprintf($folder, $this->getSnapshotDirectory() . JetBackup::SEP);
180 $this->getLogController()->logDebug("Creating directory: $folder");
181 Util::secureFolder($folder);
182 }
183 }
184
185 protected function _compressFiles() {
186
187 $file_backup_archive = $this->getSnapshotDirectory() . JetBackup::SEP . Snapshot::SKELETON_FILES_DIRNAME . JetBackup::SEP . Snapshot::SKELETON_FILES_ARCHIVE_NAME;
188
189 $this->getLogController()->logMessage('Starting compression for: ' . $file_backup_archive);
190
191 Gzip::compress(
192 $file_backup_archive,
193 Gzip::DEFAULT_COMPRESS_CHUNK_SIZE,
194 Gzip::DEFAULT_COMPRESSION_LEVEL,
195 function($byteRead, $totalSize) {
196
197 $progress = $this->getQueueItem()->getProgress();
198 $progress->setSubMessage('');
199 $progress->setTotalSubItems($totalSize);
200 $progress->setCurrentSubItem($byteRead);
201
202 $this->getQueueItem()->save();
203
204 $this->getTask()->checkExecutionTime(function() {
205 $this->getQueueItem()->getProgress()->setMessage('[ Gzip ] Waiting for next cron iteration');
206 $this->getQueueItem()->save();
207 });
208 }
209 );
210
211 $this->getLogController()->logMessage('GZIP Compression done!');
212 }
213
214 abstract protected function getSnapshotItems():array;
215
216 /**
217 * @throws \JetBackup\Exception\IOException
218 * @throws JBException
219 */
220 private function _createSnapshot():Snapshot {
221
222 $this->getLogController()->logDebug("[_createSnapshot] Creating snapshot");
223 $multisite = [];
224 foreach(Wordpress::getMultisiteBlogs() as $blog) $multisite[] = $blog->getData();
225
226 $snapshot = new Snapshot();
227 $snapshot->setCreated(time());
228 $snapshot->setName($this->getQueueItemBackup()->getSnapshotName());
229 $snapshot->setBackupType($this->getBackupJob()->getType());
230 $snapshot->setContains($this->getBackupJob()->getContains());
231 $snapshot->setStructure(Factory::getSettingsPerformance()->isGzipCompressArchive() ? BackupJob::STRUCTURE_COMPRESSED : BackupJob::STRUCTURE_ARCHIVED);
232 $snapshot->setJobIdentifier($this->getBackupJob()->getIdentifier());
233 $snapshot->setDeleted(0);
234 $snapshot->setLocked(false);
235 $snapshot->setEngine(Engine::ENGINE_WP);
236
237 $snapshot->addParam(Snapshot::PARAM_MULTISITE, $multisite);
238 $snapshot->addParam(Snapshot::PARAM_SITE_URL, Wordpress::getSiteURL());
239 $snapshot->addParam(Snapshot::PARAM_DB_PREFIX, Wordpress::getDB()->getPrefix());
240
241 $size = 0;
242 $items = $this->getSnapshotItems();
243 foreach ($items as $item) $size += $item->getSize();
244
245 $snapshot->setItems($items);
246 $snapshot->setSize($size);
247
248
249 $schedules = $this->getBackupJob()->getRunningSchedules($this->getQueueItem()->getStarted());
250 foreach ($schedules as $schedule) $snapshot->addScheduleByType($schedule->getType());
251 if($this->getQueueItemBackup()->isManually()) $snapshot->addScheduleByType(Schedule::TYPE_MANUALLY);
252 if($this->getQueueItemBackup()->isAfterJobDone()) $snapshot->addScheduleByType(Schedule::TYPE_AFTER_JOB_DONE);
253
254 return $snapshot;
255 }
256
257 /**
258 * @throws IOVanishedException
259 * @throws DestinationException
260 * @throws BackupException
261 * @throws IOException
262 * @throws \JetBackup\Exception\IOException
263 * @throws InvalidArgumentException
264 * @throws Exception
265 */
266 protected function _transferToDestination() {
267
268 $this->getTask()->foreachCallable(function() {
269 $destinations = [];
270
271 // Move local destination to be last
272 foreach($this->getQueueItemBackup()->getDestinations() as $destination_id) {
273 $destination = new Destination($destination_id);
274 if(!$destination->getId()) throw new BackupException("Invalid destination {$destination->getId()}");
275 if($destination->getType() == Local::TYPE) $destinations[] = $destination_id;
276 else array_unshift($destinations, $destination_id);
277 }
278
279 return $destinations;
280
281 }, [], function($key, $destination_id) {
282
283 $destination = new Destination($destination_id);
284 $destination->setLogController($this->getLogController());
285
286 $this->getLogController()->logMessage("Uploading backup to destination \"{$destination->getName()}\" (id: {$destination->getId()})");
287
288 $progress = $this->getQueueItem()->getProgress();
289 $progress->setSubMessage('Transferring to destination "' . $destination->getName() . '"');
290 $this->getQueueItem()->save();
291
292 // Create the snapshot object and dump it to the snapshot folder for upload
293 // Don't save this object yet, Only after upload is done
294 $snapshot = $this->_createSnapshot();
295 $snapshot->setDestinationId($destination->getId());
296
297 $this->getTask()->func(function() use ($snapshot, $destination, $progress) {
298
299 // if needed, add more snapshot details above this line
300 $snapshot->exportMeta($this->getSnapshotDirectory());
301
302 if($destination->getType() == Local::TYPE) {
303
304 $source = $this->getSnapshotDirectory();
305 $target = rtrim($destination->getInstance()->getPath(), JetBackup::SEP) . JetBackup::SEP . $this->getBackupJob()->getIdentifier() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName();
306
307 if (!file_exists(dirname($target))) {
308 if (!mkdir(dirname($target), 0700, true)) {
309 throw new BackupException("Failed to create directory: $target");
310 }
311 }
312
313 // We don't need the real size, the `rename` will be very fast
314 $progress->setTotalSubItems(1);
315 $progress->setCurrentSubItem(0);
316 $this->getQueueItem()->save();
317
318 $this->getLogController()->logMessage("Moving snap data to local location: $source -> $target");
319 if (!rename($source, $target)) {
320 throw new BackupException("Failed to move $source -> $target");
321 }
322
323 $progress->setCurrentSubItem(1);
324 $this->getQueueItem()->save();
325
326 } else {
327
328 (new Tree($destination, $this->getQueueItem(), $this->getSnapshotDirectory()))->process(function($file) use ($destination) {
329
330 $this->getTask()->checkExecutionTime();
331
332 $source = $this->getSnapshotDirectory() . $file;
333 $target = $this->getBackupJob()->getIdentifier() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName() . $file;
334
335 if (is_dir($source)) {
336 $this->getLogController()->logMessage("Creating folder $target");
337 $destination->createDir($target);
338 return;
339 }
340
341 $destination->copyFileToRemote($source, $target, $this->getQueueItem(), $this->getTask());
342 });
343 }
344
345 $progress->setTotalSubItems(0);
346 $progress->setCurrentSubItem(0);
347 $this->getQueueItem()->save();
348
349 }, [], '_uploadMetaDestination' . $destination->getId());
350
351 $this->getTask()->func(function() use ($destination) {
352
353 $this->getLogController()->logMessage('Uploading log file to destination id ' . $destination->getId());
354 $target = $this->getBackupJob()->getIdentifier() . JetBackup::SEP . $this->getQueueItemBackup()->getSnapshotName() . JetBackup::SEP . Snapshot::SKELETON_LOG_DIRNAME;
355 $destination->createDir($target);
356
357 $logfile = $this->getTask()->getLogFile();
358 $logfile_tmp = $logfile .'_tmp';
359 // We cannot upload the original log since we continue to write to it during upload
360 // Some remote destination are doing hash calculations and will return error because of size mismatch
361 $this->getLogController()->logMessage('Preparing log file for upload');
362 $this->getLogController()->logMessage('### Data after this line will not be updated in the snapshot log file ###');
363 Util::cp($logfile, $logfile_tmp, 0400);
364
365 Gzip::compress(
366 $logfile_tmp,
367 Gzip::DEFAULT_COMPRESS_CHUNK_SIZE,
368 Gzip::DEFAULT_COMPRESSION_LEVEL,
369 function ($byteRead, $totalSize) {
370
371 $progress = $this->getTask()->getQueueItem()->getProgress();
372 $progress->setSubMessage('');
373 $progress->setTotalSubItems($totalSize);
374 $progress->setCurrentSubItem($byteRead);
375
376 $this->getTask()->getQueueItem()->save();
377
378 $this->getTask()->checkExecutionTime(function () {
379 $this->getTask()->getQueueItem()->getProgress()->setMessage('[ Gzip ] Waiting for next cron iteration');
380 $this->getTask()->getQueueItem()->save();
381 });
382 }
383 );
384
385 $logfile_tmp = $logfile_tmp . '.gz';
386 $destination->copyFileToRemote($logfile_tmp, $target . JetBackup::SEP . Snapshot::SKELETON_LOG_FILENAME, $this->getQueueItem(), $this->getTask());
387 $this->getLogController()->logDebug("Temporary log file uploaded [$logfile_tmp]");
388 unlink($logfile_tmp);
389 $this->getLogController()->logDebug("Temporary log file deleted [$logfile_tmp]");
390
391 }, [], '_uploadLogDestination' . $destination->getId());
392
393 // after upload is done, save the snapshot object each destination
394 $snapshot->save();
395
396 }, '_transferToAllDestinations');
397
398 $this->getLogController()->logMessage('Sending backup to all destinations is complete');
399 $this->getLogController()->logMessage('Removing temp folder ' . $this->getSnapshotDirectory());
400
401 Util::rm($this->getSnapshotDirectory());
402 }
403
404 /**
405 * @return void
406 * @throws IOException
407 * @throws InvalidArgumentException
408 * @throws DBException
409 * @throws ScheduleException
410 */
411 protected function _calculateAfterJobDone () {
412
413 $schedule_details = Schedule::query()
414 ->select([JetBackup::ID_FIELD])
415 ->where([Schedule::BACKUP_ID, '=', $this->getBackupJob()->getId()])
416 ->getQuery()
417 ->first();
418
419 if(!$schedule_details) return;
420
421 $schedule_id = $schedule_details[JetBackup::ID_FIELD];
422
423 $list = BackupJob::query()
424 ->select([JetBackup::ID_FIELD])
425 ->getQuery()
426 ->fetch();
427
428 foreach($list as $config_details) {
429 $backup_config = new BackupJob($config_details[JetBackup::ID_FIELD]);
430
431 if(!($schedule = $backup_config->getScheduleById($schedule_id))) continue;
432
433 $schedule->setNextRun(time());
434 $backup_config->updateSchedule($schedule);
435 $backup_config->save();
436 }
437 }
438
439 /**
440 * @return void
441 * @throws IOException
442 * @throws InvalidArgumentException
443 * @throws JBException
444 */
445 protected function _retentionCleanup() {
446 $this->getLogController()->logMessage('Marking unneeded snapshots for delete');
447
448 $addToQueue = false;
449
450 foreach($this->getBackupJob()->getSchedules() as $schedule) {
451
452 $snapshots = Snapshot::query()
453 ->where([Snapshot::DESTINATION_ID, 'in', $this->getQueueItemBackup()->getDestinations()])
454 ->where([Snapshot::JOB_IDENTIFIER, '=', $this->getBackupJob()->getIdentifier()])
455 ->where([Snapshot::SCHEDULES, 'contains', $schedule->getType()])
456 ->where([Snapshot::DELETED, '=', 0])
457 ->orderBy([Snapshot::NAME => 'desc'])
458 ->skip($schedule->getRetain()) // skip the needed snapshots
459 ->getQuery()
460 ->fetch();
461
462 foreach($snapshots as $snapshot_details) {
463
464 $snapshot = new Snapshot($snapshot_details[JetBackup::ID_FIELD]);
465 $this->getLogController()->logDebug('Marked snap ' . $snapshot->getName() . ' for delete, Destination ' . $snapshot->getDestinationName() . '[ ID ' . $snapshot->getDestinationId() . ']' );
466
467 $snapshot->removeSchedule($schedule->getType());
468
469 // if there is no more schedules assigned for this snapshot we need to delete it
470 if(!sizeof($snapshot->getSchedules())) {
471 $snapshot->setDeleted(time());
472 $addToQueue = true;
473 }
474
475 $snapshot->save();
476 }
477 }
478
479 // There is nothing to delete, don't add cleanup to queue
480 if(!$addToQueue) return;
481
482 $this->getLogController()->logMessage('Adding retention cleanup to queue');
483
484 try {
485 $queue_item = QueueItem::prepare();
486 $queue_item->setType(Queue::QUEUE_TYPE_RETENTION_CLEANUP);
487
488 Queue::addToQueue($queue_item);
489 } catch (QueueException $e) {
490 $this->getLogController()->logMessage('[Backup] Adding retention cleanup failed: ' . $e->getMessage());
491 // just logging without breaking
492 }
493
494 }
495 }