PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.6.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.6.0
4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backup / Service / Compressor.php
wp-staging / Backup / Service Last commit date
Compression 2 years ago Database 2 years ago Multipart 2 years ago BackupAssets.php 3 years ago BackupMetadataEditor.php 3 years ago BackupsFinder.php 2 years ago Compressor.php 2 years ago Extractor.php 2 years ago ZlibCompressor.php 2 years ago
Compressor.php
649 lines
1 <?php
2
3 // TODO PHP7.x; declare(strict_types=1);
4 // TODO PHP7.x; return types && type-hints
5 // TODO PHP7.1; constant visibility
6
7 namespace WPStaging\Backup\Service;
8
9 use Exception;
10 use LogicException;
11 use RuntimeException;
12 use WPStaging\Backup\BackupFileIndex;
13 use WPStaging\Backup\Dto\Job\JobBackupDataDto;
14 use WPStaging\Backup\Dto\JobDataDto;
15 use WPStaging\Backup\Dto\Service\CompressorDto;
16 use WPStaging\Backup\Exceptions\DiskNotWritableException;
17 use WPStaging\Backup\Service\Multipart\MultipartSplitInterface;
18 use WPStaging\Core\WPStaging;
19 use WPStaging\Framework\Adapter\Directory;
20 use WPStaging\Framework\Adapter\PhpAdapter;
21 use WPStaging\Framework\Filesystem\PathIdentifier;
22 use WPStaging\Framework\Utils\Cache\BufferedCache;
23 use WPStaging\Vendor\lucatume\DI52\NotFoundException;
24
25 use function WPStaging\functions\debug_log;
26
27 class Compressor
28 {
29 const BACKUP_DIR_NAME = 'backups';
30
31 /** @var BufferedCache */
32 private $tempBackupIndex;
33
34 /** @var BufferedCache */
35 private $tempBackup;
36
37 /** @var CompressorDto */
38 private $compressorDto;
39
40 /** @var PathIdentifier */
41 private $pathIdentifier;
42
43 /** @var int */
44 private $compressedFileSize = 0;
45
46 /** @var JobDataDto */
47 private $jobDataDto;
48
49 /** @var PhpAdapter */
50 private $phpAdapter;
51
52 /** @var MultipartSplitInterface */
53 private $multipartSplit;
54
55 /**
56 * Category can be: empty string|null|false, plugins, mu-plugins, themes, uploads, other, database
57 * Where empty string|null|false is used for single file backup,
58 * And other is for files from wp-content not including plugins, mu-plugins, themes, uploads
59 * @var string
60 */
61 private $category = '';
62
63 /**
64 * The current index of category in which appending files
65 * Not used in single file backup
66 * @var int
67 */
68 private $categoryIndex = 0;
69
70 /** @var bool */
71 private $isLocalBackup = false;
72
73 /** @var int */
74 protected $bytesWrittenInThisRequest = 0;
75
76
77 /** @var BackupFileIndex */
78 private $backupFileIndex;
79
80 // TODO telescoped
81 public function __construct(
82 BufferedCache $cacheIndex,
83 BufferedCache $tempBackup,
84 PathIdentifier $pathIdentifier,
85 JobDataDto $jobDataDto,
86 CompressorDto $compressorDto,
87 PhpAdapter $phpAdapter,
88 MultipartSplitInterface $multipartSplit,
89 BackupFileIndex $backupFileIndex
90 ) {
91 $this->jobDataDto = $jobDataDto;
92 $this->compressorDto = $compressorDto;
93 $this->tempBackupIndex = $cacheIndex;
94 $this->tempBackup = $tempBackup;
95 $this->pathIdentifier = $pathIdentifier;
96 $this->phpAdapter = $phpAdapter;
97 $this->multipartSplit = $multipartSplit;
98 $this->backupFileIndex = $backupFileIndex;
99
100 $this->setCategory('');
101 }
102
103 /**
104 * @param int $index
105 * @param bool $isCreateBinaryHeader
106 */
107 public function setCategoryIndex($index, $isCreateBinaryHeader = true)
108 {
109 if (empty($index)) {
110 $index = 0;
111 }
112
113 $this->categoryIndex = $index;
114 $this->setCategory($this->category, $isCreateBinaryHeader);
115 }
116
117 /**
118 * @param string $category
119 * @param bool $isCreateBinaryHeader
120 */
121 public function setCategory($category = '', $isCreateBinaryHeader = false)
122 {
123 $this->category = $category;
124 $this->setupTmpBackupFile();
125
126 if ($isCreateBinaryHeader && !$this->tempBackup->isValid()) {
127 // Create temp file with binary header
128 $this->tempBackup->save(file_get_contents(WPSTG_PLUGIN_DIR . 'Backup/wpstgBackupHeader.txt'));
129 }
130 }
131
132 /**
133 * Setup temp backup file and temp files index file for the given job id,
134 * If multipart backup category and category index are given, then they are used to create unique file names
135 */
136 public function setupTmpBackupFile()
137 {
138 $additionalInfo = empty($this->category) ? '' : $this->category . '_' . $this->categoryIndex . '_';
139
140 $postFix = $additionalInfo . $this->jobDataDto->getId();
141
142 //debug_log("[Set Tmp Backup Files] File name postfix: " . $postFix);
143
144 $this->tempBackup->setFilename('temp_wpstg_backup_' . $postFix);
145 $this->tempBackup->setLifetime(DAY_IN_SECONDS);
146
147 $tempBackupIndexFilePrefix = 'temp_backup_index_';
148 $this->tempBackupIndex->setFilename($tempBackupIndexFilePrefix . $postFix);
149 $this->tempBackupIndex->setLifetime(DAY_IN_SECONDS);
150 }
151
152 /**
153 * @param int $fileSize
154 * @param int $maxPartSize
155 * @return bool
156 */
157 public function doExceedMaxPartSize($fileSize, $maxPartSize)
158 {
159 $allowedSize = $fileSize - $this->compressorDto->getWrittenBytesTotal();
160 $sizeAfterAdding = $allowedSize + filesize($this->tempBackup->getFilePath());
161 return $sizeAfterAdding >= $maxPartSize;
162 }
163
164 /**
165 * @var bool $isLocalBackup
166 */
167 public function setIsLocalBackup($isLocalBackup)
168 {
169 $this->isLocalBackup = $isLocalBackup;
170 }
171
172 /**
173 * @return CompressorDto
174 */
175 public function getDto()
176 {
177 return $this->compressorDto;
178 }
179
180 /**
181 * @return int
182 */
183 public function getBytesWrittenInThisRequest()
184 {
185 return $this->bytesWrittenInThisRequest;
186 }
187
188 /**
189 * @param string $fullFilePath
190 * @param string $indexPath
191 *
192 * `true` -> finished
193 * `false` -> not finished
194 * `null` -> skip / didn't do anything
195 *
196 * @throws DiskNotWritableException
197 * @throws RuntimeException
198 *
199 * @return bool|null
200 */
201 public function appendFileToBackup(string $fullFilePath, string $indexPath = '')
202 {
203 // We can use evil '@' as we don't check is_file || file_exists to speed things up.
204 // Since in this case speed > anything else
205 // However if @ is not used, depending on if file exists or not this can throw E_WARNING.
206 $resource = @fopen($fullFilePath, 'rb');
207 if (!$resource) {
208 debug_log("appendFileToBackup(): Can't open file {$fullFilePath} for reading");
209 return null;
210 }
211
212 if (empty($indexPath)) {
213 $indexPath = $fullFilePath;
214 }
215
216 $fileStats = fstat($resource);
217 $this->initiateDtoByFilePath($fullFilePath, $fileStats);
218 $this->compressorDto->setIndexPath($indexPath);
219 $writtenBytesBefore = $this->compressorDto->getWrittenBytesTotal();
220 $writtenBytesTotal = $this->appendToCompressedFile($resource, $fullFilePath);
221
222 $retries = 0;
223
224 do {
225 if ($retries > 0) {
226 usleep($this->getDelayForRetry($retries));
227 }
228
229 $bytesAddedForIndex = $this->addIndex($writtenBytesTotal);
230 $retries++;
231 } while ($bytesAddedForIndex === 0 && $retries < 3);
232
233 $this->compressorDto->setWrittenBytesTotal($writtenBytesTotal);
234
235 $this->bytesWrittenInThisRequest += $writtenBytesTotal - $writtenBytesBefore;
236
237 $isFinished = $this->compressorDto->isFinished();
238
239 $this->compressorDto->resetIfFinished();
240
241 return $isFinished;
242 }
243
244 /**
245 * @param string $filePath
246 * @param array $fileStats
247 */
248 public function initiateDtoByFilePath($filePath, array $fileStats = [])
249 {
250 if ($filePath === null || ($filePath === $this->compressorDto->getFilePath() && $fileStats['size'] === $this->compressorDto->getFileSize())) {
251 return;
252 }
253
254 $this->compressorDto->setFilePath($filePath);
255 $this->compressorDto->setFileSize($fileStats['size']);
256 }
257
258 /**
259 * @param int $sizeBeforeAddingIndex
260 * @param string $category
261 * @param string $partName
262 * @param int $categoryIndex
263 */
264 public function generateBackupMetadataForBackupPart($sizeBeforeAddingIndex, $category, $partName, $categoryIndex)
265 {
266 $this->category = $category;
267 $this->categoryIndex = $categoryIndex;
268 $this->setupTmpBackupFile();
269 $this->generateBackupMetadata($sizeBeforeAddingIndex, $partName, $isBackupPart = true);
270 }
271
272 /**
273 * Combines index and compressed file, renames / moves it to destination
274 *
275 * This function is called only once, so performance improvements has no impact here.
276 *
277 * @param int $backupSizeBeforeAddingIndex
278 * @param string $finalFileNameOnRename
279 * @param bool $isBackupPart
280 *
281 * @return string|null
282 */
283 public function generateBackupMetadata($backupSizeBeforeAddingIndex = 0, $finalFileNameOnRename = '', $isBackupPart = false)
284 {
285 clearstatcache();
286 $backupSizeAfterAddingIndex = filesize($this->tempBackup->getFilePath());
287
288 $backupMetadata = $this->compressorDto->getBackupMetadata();
289 $backupMetadata->setHeaderStart($backupSizeBeforeAddingIndex);
290 $backupMetadata->setHeaderEnd($backupSizeAfterAddingIndex);
291
292 if ($isBackupPart) {
293 $this->multipartSplit->updateMultipartMetadata($this->jobDataDto, $backupMetadata, $this->category, $this->categoryIndex);
294 }
295
296 if ($this->jobDataDto instanceof JobBackupDataDto) {
297 /** @var JobBackupDataDto */
298 $jobDataDto = $this->jobDataDto;
299 $backupMetadata->setIndexPartSize($jobDataDto->getCategorySizes());
300 }
301
302 $this->tempBackup->append(json_encode($backupMetadata));
303
304 return $this->renameBackup($finalFileNameOnRename);
305 }
306
307 /**
308 * @return array
309 */
310 public function getFinalizeBackupInfo()
311 {
312 return [
313 'category' => $this->category,
314 'index' => $this->categoryIndex,
315 'filePath' => $this->tempBackup->getFilePath(),
316 'destination' => $this->getDestinationPath(),
317 'status' => 'Pending',
318 'sizeBeforeAddingIndex' => 0
319 ];
320 }
321
322 /** @return int|null */
323 public function addFileIndex()
324 {
325 clearstatcache();
326 $indexResource = fopen($this->tempBackupIndex->getFilePath(), 'rb');
327
328 if (!$indexResource) {
329 debug_log('[Add File Index] Nothing to backup, no index resource! File Index: ' . $this->tempBackupIndex->getFilePath());
330 throw new NotFoundException('Nothing to backup, no index resource found!');
331 }
332
333 static $isFirstInsert = false;
334 $insertSeparator = '';
335 if ($isFirstInsert === false) {
336 $lastLine = $this->tempBackup->readLastLine();
337 if (!empty($lastLine) && preg_match('@^INSERT\sINTO\s@', $lastLine)) {
338 $isFirstInsert = true;
339 $insertSeparator = "\n--\n-- SQL DATA END\n--\n";
340 $this->tempBackup->append($insertSeparator);
341 $this->tempBackup->deleteBottomBytes(strlen(PHP_EOL));
342 }
343 }
344
345 $indexStats = fstat($indexResource);
346 $this->initiateDtoByFilePath($this->tempBackupIndex->getFilePath(), $indexStats);
347
348 $lastLine = $this->tempBackup->readLastLine();
349 $writtenBytes = $this->compressorDto->getWrittenBytesTotal();
350 if ($lastLine !== PHP_EOL && $writtenBytes === 0) {
351 $this->tempBackup->append(''); // ensure that file index start from new line. See https://github.com/wp-staging/wp-staging-pro/issues/2861
352 }
353
354 clearstatcache();
355 $backupSizeBeforeAddingIndex = filesize($this->tempBackup->getFilePath());
356
357 // Write the index to the backup file, regardless of resource limits threshold
358 // @throws Exception
359 $writtenBytes = $this->appendToCompressedFile($indexResource, $this->tempBackupIndex->getFilePath());
360 $this->compressorDto->setWrittenBytesTotal($writtenBytes);
361
362 if ($writtenBytes === 0) {
363 $this->jobDataDto->setRetries($this->jobDataDto->getRetries() + 1);
364 } else {
365 $this->jobDataDto->setRetries(0);
366 }
367
368 // close the index file handle to make it deletable for Windows where PHP < 7.3
369 fclose($indexResource);
370
371 if ($this->jobDataDto->getRetries() > 3) {
372 debug_log('[Add File Index] Failed to write files-index to backup file!');
373 throw new Exception('Failed to write files-index to backup file!');
374 } elseif ($writtenBytes === 0) {
375 debug_log('[Add File Index] Failed to write any byte to files-index! Retrying...');
376 }
377
378 if (!$this->compressorDto->isFinished()) {
379 return null;
380 }
381
382 $this->tempBackupIndex->delete();
383 $this->compressorDto->reset();
384
385 $this->tempBackup->append(PHP_EOL);
386
387 return $backupSizeBeforeAddingIndex;
388 }
389
390 /**
391 * @return string
392 */
393 private function getDestinationPath()
394 {
395 $extension = "wpstg";
396
397 if ($this->category !== '') {
398 $index = $this->categoryIndex === 0 ? '' : ($this->categoryIndex . '.');
399 $extension = $this->category . '.' . $index . $extension;
400 }
401
402 return sprintf(
403 '%s_%s_%s.%s',
404 parse_url(get_home_url())['host'],
405 current_time('Ymd-His'),
406 $this->jobDataDto->getId(),
407 $extension
408 );
409 }
410
411 /**
412 * @param string $renameFileTo
413 * @param bool $isLocalBackup
414 * @return string
415 */
416 public function getFinalPath($renameFileTo = '', $isLocalBackup = true)
417 {
418 $backupsDirectory = $this->getFinalBackupParentDirectory($isLocalBackup);
419 if ($renameFileTo === '') {
420 $renameFileTo = $this->getDestinationPath();
421 }
422
423 return $backupsDirectory . $renameFileTo;
424 }
425
426 /**
427 * @return string
428 */
429 public function getFinalBackupParentDirectory($isLocalBackup = true)
430 {
431 if ($isLocalBackup) {
432 return WPStaging::make(BackupsFinder::class)->getBackupsDirectory();
433 }
434
435 return WPStaging::make(Directory::class)->getCacheDirectory();
436 }
437
438 /**
439 * Get delay in milliseconds for retry according to retry number
440 *
441 * @param int $retry
442 * @return float
443 */
444 protected function getDelayForRetry($retry)
445 {
446 $delay = 0.1;
447 for ($i = 0; $i < $retry; $i++) {
448 $delay *= 2;
449 }
450
451 return $delay * 1000;
452 }
453
454 /** @var string $renameFileTo */
455 private function renameBackup($renameFileTo = '')
456 {
457 if ($renameFileTo === '') {
458 $renameFileTo = $this->getDestinationPath();
459 }
460
461 $destination = trailingslashit(dirname($this->tempBackup->getFilePath())) . $renameFileTo;
462 if ($this->isLocalBackup) {
463 $destination = $this->getFinalPath($renameFileTo);
464 }
465
466 if (!rename($this->tempBackup->getFilePath(), $destination)) {
467 throw new RuntimeException('Failed to generate destination');
468 }
469
470 return $destination;
471 }
472
473 /**
474 * @param int $writtenBytesTotal
475 * @return int
476 * @throws \WPStaging\Framework\Exceptions\IOException
477 * @throws LogicException
478 * @throws RuntimeException
479 */
480 private function addIndex(int $writtenBytesTotal)
481 {
482 clearstatcache();
483 if (file_exists($this->tempBackup->getFilePath())) {
484 $this->compressedFileSize = filesize($this->tempBackup->getFilePath());
485 }
486
487 $start = max($this->compressedFileSize - $writtenBytesTotal, 0);
488
489 if ($this->compressorDto->isIndexPositionCreated($this->category, $this->categoryIndex)) {
490 return $this->updateIndexInformationForAlreadyAddedIndex($writtenBytesTotal);
491 }
492
493 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->compressorDto->getIndexPath());
494 $backupFileIndex = $this->backupFileIndex->createIndex($identifiablePath, $start, $writtenBytesTotal, false);
495 $bytesWritten = $this->tempBackupIndex->append($backupFileIndex->getIndex());
496
497 $this->compressorDto->setIndexPositionCreated(true);
498
499 $this->addIndexPartSize($identifiablePath, $writtenBytesTotal);
500
501 /**
502 * We require JobDataDto in the constructor because it is wired in the DI container
503 * to the current job DTO instance. However, here we need to make sure this DTO
504 * is the jobBackupDataDto.
505 */
506 if (!$this->phpAdapter->isCallable([$this->jobDataDto, 'setTotalFiles']) || !$this->phpAdapter->isCallable([$this->jobDataDto, 'getTotalFiles'])) {
507 debug_log('This method can only be called from the context of Backup');
508 throw new LogicException('This method can only be called from the context of Backup');
509 }
510
511 /** @var JobBackupDataDto $jobBackupDataDto */
512 $jobBackupDataDto = $this->jobDataDto;
513 $jobBackupDataDto->setTotalFiles($jobBackupDataDto->getTotalFiles() + 1);
514
515 $this->multipartSplit->incrementFileCountInPart($jobBackupDataDto, $this->category, $this->categoryIndex);
516
517 return $bytesWritten;
518 }
519
520 /**
521 * At the moment this is used when processing adding of big file which is not done in a single request
522 * @param int $writtenBytesTotal
523 * @return int
524 * @throws RuntimeException
525 */
526 private function updateIndexInformationForAlreadyAddedIndex($writtenBytesTotal)
527 {
528 $lastLine = $this->tempBackupIndex->readLines(1, null, BufferedCache::POSITION_BOTTOM);
529 if (!is_array($lastLine)) {
530 debug_log('Failed to read backup metadata file index information. Error: The last line is no array. Last line: ' . $lastLine);
531 throw new RuntimeException('Failed to read backup metadata file index information. Error: The last line is no array.');
532 }
533
534 $lastLine = array_filter($lastLine, [$this->backupFileIndex, 'isIndexLine']);
535
536 if (count($lastLine) !== 1) {
537 debug_log('Failed to read backup metadata file index information. Error: The last line is not an array or element with countable interface. Last line: ' . print_r($lastLine, 1));
538 throw new RuntimeException('Failed to read backup metadata file index information. Error: The last line is not an array or element with countable interface.');
539 }
540
541 $lastLine = array_shift($lastLine);
542
543 $backupFileIndex = $this->backupFileIndex->readIndex($lastLine);
544 $writtenPreviously = $backupFileIndex->bytesEnd;
545
546 $this->tempBackupIndex->deleteBottomBytes(strlen($lastLine));
547
548 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->compressorDto->getIndexPath());
549 $backupFileIndex = $this->backupFileIndex->createIndex($identifiablePath, $backupFileIndex->bytesStart, $writtenBytesTotal, false);
550 $bytesWritten = $this->tempBackupIndex->append($backupFileIndex->getIndex());
551
552 $this->compressorDto->setIndexPositionCreated(true, $this->category, $this->categoryIndex);
553
554 // We only need to increment newly added bytes
555 $this->addIndexPartSize($identifiablePath, $writtenBytesTotal - (int)$writtenPreviously);
556
557 return $bytesWritten;
558 }
559
560 /**
561 * @param $resource
562 * @param $filePath
563 *
564 * @return int Bytes written
565 * @throws DiskNotWritableException
566 * @throws RuntimeException
567 */
568 private function appendToCompressedFile($resource, $filePath)
569 {
570 try {
571 return $this->tempBackup->appendFile(
572 $resource,
573 $this->compressorDto->getWrittenBytesTotal()
574 );
575 } catch (DiskNotWritableException $e) {
576 debug_log('Failed to write to file: ' . $filePath);
577 // Re-throw for readability
578 throw $e;
579 }
580 }
581
582 /**
583 * @param string $identifiablePath
584 * @param int $newBytesWritten
585 */
586 private function addIndexPartSize($identifiablePath, $newBytesWritten)
587 {
588 // Early bail if jobDataDto is not instance of jobBackupDataDto
589 if (!$this->jobDataDto instanceof JobBackupDataDto) {
590 return;
591 }
592
593 /** @var JobBackupDataDto $jobDataDto */
594 $jobDataDto = $this->jobDataDto;
595
596 $collectPartsize = $jobDataDto->getCategorySizes();
597
598 $partName = 'unknownSize';
599 switch ($identifiablePath) {
600 case ($this->pathIdentifier::IDENTIFIER_WP_CONTENT === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_WP_CONTENT))):
601 $partName = 'wpcontentSize';
602 break;
603 case ($this->pathIdentifier::IDENTIFIER_PLUGINS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_PLUGINS))):
604 $partName = 'pluginsSize';
605 break;
606 case ($this->pathIdentifier::IDENTIFIER_THEMES === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_THEMES))):
607 $partName = 'themesSize';
608 break;
609 case ($this->pathIdentifier::IDENTIFIER_MUPLUGINS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_MUPLUGINS))):
610 $partName = 'mupluginsSize';
611 break;
612 case ($this->pathIdentifier::IDENTIFIER_UPLOADS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_UPLOADS))):
613 $partName = 'uploadsSize';
614 if (substr($identifiablePath, -4) === '.sql') {
615 $partName = 'sqlSize';
616 }
617
618 break;
619 case ($this->pathIdentifier::IDENTIFIER_LANG === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_LANG))):
620 $partName = 'langSize';
621 break;
622 }
623
624 // TODO: This should never happen. Log this when we have our own Logger, see https://github.com/wp-staging/wp-staging-pro/pull/2440#discussion_r1247951548
625 if (!isset($collectPartsize[$partName])) {
626 $collectPartsize[$partName] = 0;
627 }
628
629 $collectPartsize[$partName] += $newBytesWritten;
630 $jobDataDto->setCategorySizes($collectPartsize);
631 }
632
633 /**
634 * @return BufferedCache
635 */
636 public function getTempBackupIndex(): BufferedCache
637 {
638 return $this->tempBackupIndex;
639 }
640
641 /**
642 * @return BufferedCache
643 */
644 public function getTempBackup(): BufferedCache
645 {
646 return $this->tempBackup;
647 }
648 }
649