PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.7.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.7.0
4.11.2 4.11.1 4.11.0 4.10.0 4.9.5 4.9.4 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 / Archiver.php
wp-staging / Backup / Service Last commit date
Compression 6 months ago Database 6 months ago AbstractBackupsFinder.php 1 year ago AbstractExtractor.php 5 months ago AbstractServiceProvider.php 2 years ago Archiver.php 7 months ago BackupAssets.php 2 years ago BackupContent.php 1 year ago BackupMetadataEditor.php 1 year ago BackupMetadataReader.php 7 months ago BackupSigner.php 8 months ago BackupsFinder.php 6 months ago Extractor.php 5 months ago FileBackupService.php 8 months ago FileBackupServiceProvider.php 2 years ago ServiceInterface.php 2 years ago ZlibCompressor.php 1 year ago
Archiver.php
843 lines
1 <?php
2
3 // TODO PHP7.1; constant visibility
4
5 namespace WPStaging\Backup\Service;
6
7 use Exception;
8 use LogicException;
9 use RuntimeException;
10 use WPStaging\Backup\BackupFileIndex;
11 use WPStaging\Backup\BackupHeader;
12 use WPStaging\Backup\Dto\Job\JobBackupDataDto;
13 use WPStaging\Backup\Dto\Service\ArchiverDto;
14 use WPStaging\Backup\Entity\BackupMetadata;
15 use WPStaging\Backup\Exceptions\BackupSkipItemException;
16 use WPStaging\Backup\FileHeader;
17 use WPStaging\Core\WPStaging;
18 use WPStaging\Framework\Adapter\Directory;
19 use WPStaging\Framework\Adapter\PhpAdapter;
20 use WPStaging\Framework\Facades\Hooks;
21 use WPStaging\Framework\Filesystem\Filesystem;
22 use WPStaging\Framework\Filesystem\PartIdentifier;
23 use WPStaging\Framework\Filesystem\PathIdentifier;
24 use WPStaging\Framework\Job\Dto\JobDataDto;
25 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
26 use WPStaging\Framework\Job\Exception\NotFinishedException;
27 use WPStaging\Framework\Job\Exception\ThresholdException;
28 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
29 use WPStaging\Framework\Utils\Cache\BufferedCache;
30 use WPStaging\Vendor\lucatume\DI52\NotFoundException;
31
32 use function WPStaging\functions\debug_log;
33
34 /**
35 * This class is responsible for archiving files and creating backups.
36 */
37 class Archiver
38 {
39 use EndOfLinePlaceholderTrait;
40
41 /**
42 * @var string
43 */
44 const BACKUP_EXTENSION = 'wpstg';
45
46 /**
47 * Used during push and pull jobs
48 * @var string
49 */
50 const TMP_BACKUP_EXTENSION = 'wpstgtmp';
51
52 /**
53 * After this number of failed requests, we will extend the execution time by 5s for file append.
54 * @var int
55 */
56 const MAX_RETRIES_BEFORE_EXTENDING_TIME_LIMIT = 1;
57
58 /**
59 * The maximum execution time limit allowed (in seconds), even if PHP is configured
60 * * with a higher value or unlimited (0 or -1).
61 */
62 const MAX_ALLOWED_PHP_TIME_LIMIT = 60;
63
64 /**
65 * The minimum execution time limit (in seconds) enforced when the server configuration
66 * * sets a very low value for max_execution_time below this threshold.
67 */
68 const MIN_ALLOWED_PHP_TIME_LIMIT = 10;
69
70 /**
71 * The fraction (percentage) of the allowed PHP time limit to use.
72 * This ensures some buffer time remains before reaching the actual limit.
73 * @var float
74 */
75 const PHP_TIME_LIMIT_IN_FRACTION = 0.8;
76
77 /** @var string */
78 const BACKUP_DIR_NAME = 'backups';
79
80 /** @var bool */
81 const CREATE_BINARY_HEADER = true;
82
83 /** @var BufferedCache */
84 protected $tempBackupIndex;
85
86 /** @var BufferedCache */
87 protected $tempBackup;
88
89 /** @var ArchiverDto */
90 protected $archiverDto;
91
92 /** @var PathIdentifier */
93 protected $pathIdentifier;
94
95 /** @var int */
96 protected $archivedFileSize = 0;
97
98 /** @var JobDataDto */
99 protected $jobDataDto;
100
101 /** @var PhpAdapter */
102 protected $phpAdapter;
103
104 /** @var bool */
105 protected $isLocalBackup = false;
106
107 /** @var int */
108 protected $bytesWrittenInThisRequest = 0;
109
110 /** @var FileHeader */
111 protected $fileHeader;
112
113 /** @var BackupHeader */
114 protected $backupHeader;
115
116 /** @var BackupFileIndex */
117 protected $backupFileIndex;
118
119 /** @var Filesystem */
120 protected $filesystem;
121
122 /** @var bool */
123 protected $isTempBackup = false;
124
125 public function __construct(
126 BufferedCache $cacheIndex,
127 BufferedCache $tempBackup,
128 PathIdentifier $pathIdentifier,
129 JobDataDto $jobDataDto,
130 ArchiverDto $archiverDto,
131 PhpAdapter $phpAdapter,
132 BackupFileIndex $backupFileIndex,
133 FileHeader $fileHeader,
134 BackupHeader $backupHeader,
135 Filesystem $filesystem
136 ) {
137 $this->jobDataDto = $jobDataDto;
138 $this->archiverDto = $archiverDto;
139 $this->tempBackupIndex = $cacheIndex;
140 $this->tempBackup = $tempBackup;
141 $this->pathIdentifier = $pathIdentifier;
142 $this->phpAdapter = $phpAdapter;
143 $this->backupFileIndex = $backupFileIndex;
144 $this->fileHeader = $fileHeader;
145 $this->backupHeader = $backupHeader;
146 $this->filesystem = $filesystem;
147 }
148
149 /**
150 * @param int $fileAppendTimeLimit
151 * @return void
152 */
153 public function setFileAppendTimeLimit(int $fileAppendTimeLimit)
154 {
155 $this->tempBackup->setFileAppendTimeLimit($fileAppendTimeLimit);
156 $this->tempBackupIndex->setFileAppendTimeLimit($fileAppendTimeLimit);
157 }
158
159 /**
160 * @param bool $isTempBackup
161 * @return void
162 */
163 public function setIsTempBackup(bool $isTempBackup)
164 {
165 $this->isTempBackup = $isTempBackup;
166 }
167
168 /**
169 * @param bool $isCreateBinaryHeader
170 * @return void
171 */
172 public function createArchiveFile(bool $isCreateBinaryHeader = false)
173 {
174 $this->setupTmpBackupFile();
175
176 if ($isCreateBinaryHeader && !$this->tempBackup->isValid()) {
177 // Create temp file with binary header
178 $this->tempBackup->save($this->isBackupFormatV1() ? $this->backupHeader->getV1FormatHeader() : $this->backupHeader->getHeader() . "\n");
179 }
180 }
181
182 /**
183 * Setup temp backup file and temp files index file for the given job id,
184 * @return void
185 */
186 public function setupTmpBackupFile()
187 {
188 $this->tempBackup->setFilename('temp_wpstg_backup_' . $this->jobDataDto->getId());
189 $this->tempBackup->setLifetime(DAY_IN_SECONDS);
190
191 $tempBackupIndexFilePrefix = 'temp_backup_index_';
192 $this->tempBackupIndex->setFilename($tempBackupIndexFilePrefix . $this->jobDataDto->getId());
193 $this->tempBackupIndex->setLifetime(DAY_IN_SECONDS);
194 }
195
196 /**
197 * @var bool $isLocalBackup
198 */
199 public function setIsLocalBackup(bool $isLocalBackup)
200 {
201 $this->isLocalBackup = $isLocalBackup;
202 }
203
204 /**
205 * @return ArchiverDto
206 */
207 public function getDto(): ArchiverDto
208 {
209 return $this->archiverDto;
210 }
211
212 /**
213 * @return int
214 */
215 public function getBytesWrittenInThisRequest(): int
216 {
217 return $this->bytesWrittenInThisRequest;
218 }
219
220 /**
221 * @return BufferedCache
222 */
223 public function getTempBackupIndex(): BufferedCache
224 {
225 return $this->tempBackupIndex;
226 }
227
228 /**
229 * @return BufferedCache
230 */
231 public function getTempBackup(): BufferedCache
232 {
233 return $this->tempBackup;
234 }
235
236 /**
237 * @param string $fullFilePath
238 * @param string $indexPath
239 *
240 * `true` -> finished
241 * `false` -> not finished
242 *
243 * @throws DiskNotWritableException
244 * @throws RuntimeException
245 * @throws BackupSkipItemException Skip this file don't do anything
246 * @throws ThresholdException
247 *
248 * @return bool
249 */
250 public function appendFileToBackup(string $fullFilePath, string $indexPath = ''): bool
251 {
252 // We can use evil '@' as we don't check is_file || file_exists to speed things up.
253 // Since in this case speed > anything else
254 // However if @ is not used, depending on if file exists or not this can throw E_WARNING.
255 $resource = @fopen($fullFilePath, 'rb');
256 if (!$resource) {
257 debug_log("appendFileToBackup(): Can't open file {$fullFilePath} for reading");
258 throw new BackupSkipItemException();
259 }
260
261 if (empty($indexPath)) {
262 $indexPath = $fullFilePath;
263 }
264
265 $indexPath = $this->replaceEOLsWithPlaceholders($indexPath);
266 $fileStats = fstat($resource);
267 $this->initiateDtoByFilePath($fullFilePath, $fileStats);
268 $this->archiverDto->setIndexPath($indexPath);
269 $fileHeaderSizeInBytes = 0;
270 if (!$this->isBackupFormatV1() && !$this->archiverDto->isFileHeaderWritten()) {
271 $fileHeaderSizeInBytes = $this->writeFileHeader($fullFilePath, $indexPath);
272 $this->archiverDto->setFileHeaderSizeInBytes($fileHeaderSizeInBytes);
273 } elseif (!$this->isBackupFormatV1()) {
274 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->filesystem->maybeNormalizePath($indexPath));
275 $this->fileHeader->readFile($fullFilePath, $identifiablePath);
276 }
277
278 $writtenBytesBefore = $this->archiverDto->getWrittenBytesTotal();
279 try {
280 $writtenBytesTotal = $this->appendToArchiveFile($resource, $fullFilePath);
281 } catch (ThresholdException $ex) {
282 // Let close the file resource before re-throwing the exception
283 fclose($resource);
284 $resource = null;
285 $this->maybeIncrementFileAppendTimeLimit();
286
287 throw $ex;
288 }
289
290 $newBytesWritten = $writtenBytesTotal + $fileHeaderSizeInBytes - $writtenBytesBefore;
291 $writtenBytesIncludingFileHeader = $writtenBytesTotal + $this->archiverDto->getFileHeaderSizeInBytes();
292
293 if (!$this->isBackupFormatV1() && empty($this->fileHeader->getFilePath())) {
294 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->filesystem->maybeNormalizePath($indexPath));
295 $this->fileHeader->readFile($fullFilePath, $identifiablePath);
296 }
297
298 $retries = 0;
299
300 if (!$this->isBackupFormatV1() && empty($this->fileHeader->getFilePath())) {
301 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->filesystem->maybeNormalizePath($indexPath));
302 $this->fileHeader->readFile($fullFilePath, $identifiablePath);
303 }
304
305 do {
306 if ($retries > 0) {
307 usleep((int)$this->getDelayForRetry($retries));
308 }
309
310 $bytesAddedForIndex = $this->addIndex($writtenBytesIncludingFileHeader, $newBytesWritten);
311 $retries++;
312 } while ($bytesAddedForIndex === 0 && $retries < 3);
313
314 $this->archiverDto->setWrittenBytesTotal($writtenBytesTotal);
315
316 $this->bytesWrittenInThisRequest += $newBytesWritten;
317
318 $isFinished = $this->archiverDto->isFinished();
319 if ($isFinished) {
320 $this->resetFileAppendTimeLimitAndRetries();
321 }
322
323 $this->archiverDto->resetIfFinished();
324
325 return $isFinished;
326 }
327
328 /**
329 * @param string $filePath
330 * @param array $fileStats
331 * @param bool
332 */
333 public function initiateDtoByFilePath(string $filePath, array $fileStats = []): bool
334 {
335 if (empty($filePath) || ($filePath === $this->archiverDto->getFilePath() && $fileStats['size'] === $this->archiverDto->getFileSize())) {
336 return false;
337 }
338
339 $this->archiverDto->setFilePath($filePath);
340 $this->archiverDto->setFileSize($fileStats['size']);
341 return true;
342 }
343
344 /**
345 * Combines index and archive file, renames / moves it to destination
346 *
347 * This function is called only once, so performance improvements has no impact here.
348 *
349 * @param int $backupSizeBeforeAddingIndex
350 * @param string $finalFileNameOnRename
351 *
352 * @return string
353 */
354 public function generateBackupMetadata(int $backupSizeBeforeAddingIndex = 0, string $finalFileNameOnRename = ''): string
355 {
356 clearstatcache();
357 $backupSizeAfterAddingIndex = filesize($this->tempBackup->getFilePath());
358
359 $backupMetadata = $this->archiverDto->getBackupMetadata();
360 $backupMetadata->setHeaderStart($backupSizeBeforeAddingIndex);
361 $backupMetadata->setHeaderEnd($backupSizeAfterAddingIndex);
362
363 if ($this->jobDataDto instanceof JobBackupDataDto) {
364 /** @var JobBackupDataDto */
365 $jobDataDto = $this->jobDataDto;
366 $this->setBackupMetadataCategoryInfo($backupMetadata, $jobDataDto);
367 }
368
369 $this->tempBackup->append(json_encode($backupMetadata));
370 if (!$this->isBackupFormatV1()) {
371 $this->backupHeader->readFromPath($this->tempBackup->getFilePath());
372 $this->backupHeader->setMetadataStartOffset($backupSizeAfterAddingIndex);
373 $this->backupHeader->setMetadataEndOffset($backupSizeAfterAddingIndex);
374 $this->backupHeader->updateHeader($this->tempBackup->getFilePath());
375 }
376
377 return $this->renameBackup($finalFileNameOnRename);
378 }
379
380 /** @return int */
381 public function addFileIndex(): int
382 {
383 clearstatcache();
384 $indexResource = fopen($this->tempBackupIndex->getFilePath(), 'rb');
385
386 if (!$indexResource) {
387 debug_log('[Add File Index] Nothing to backup, no index resource! File Index: ' . $this->tempBackupIndex->getFilePath());
388 throw new NotFoundException('Nothing to backup, no index resource found!');
389 }
390
391 static $isFirstInsert = false;
392 $insertSeparator = '';
393 if ($isFirstInsert === false) {
394 $lastLine = $this->tempBackup->readLastLine();
395 if (!empty($lastLine) && preg_match('@^INSERT\sINTO\s@', $lastLine)) {
396 $isFirstInsert = true;
397 $insertSeparator = "\n--\n-- SQL DATA END\n--\n";
398 $this->tempBackup->append($insertSeparator);
399 $this->tempBackup->deleteBottomBytes(strlen(PHP_EOL));
400 }
401 }
402
403 $indexStats = fstat($indexResource);
404 $this->initiateDtoByFilePath($this->tempBackupIndex->getFilePath(), $indexStats);
405
406 $lastLine = $this->tempBackup->readLastLine();
407 $writtenBytes = $this->archiverDto->getWrittenBytesTotal();
408 if ($lastLine !== PHP_EOL && $writtenBytes === 0) {
409 $this->tempBackup->append(''); // ensure that file index start from new line. See https://github.com/wp-staging/wp-staging-pro/issues/2861
410 }
411
412 clearstatcache();
413 $backupSizeBeforeAddingIndex = filesize($this->tempBackup->getFilePath());
414 $backupIndexFileSize = filesize($this->tempBackupIndex->getFilePath());
415
416 // Write the index to the backup file, regardless of resource limits threshold
417 // @throws Exception
418 $writtenBytes = $this->appendToArchiveFile($indexResource, $this->tempBackupIndex->getFilePath());
419 $this->archiverDto->setWrittenBytesTotal($writtenBytes);
420
421 if ($writtenBytes === 0) {
422 $this->jobDataDto->setRetries($this->jobDataDto->getRetries() + 1);
423 } else {
424 $this->jobDataDto->setRetries(0);
425 }
426
427 // close the index file handle to make it deletable for Windows where PHP < 7.3
428 fclose($indexResource);
429
430 if ($this->jobDataDto->getRetries() > 3) {
431 $indexSize = $backupIndexFileSize === false ? 0 : size_format($backupIndexFileSize, 3);
432 debug_log(sprintf('[Add File Index] Failed to write files-index to backup file! Tmp Size: %s. Index Size: %s', size_format($backupSizeBeforeAddingIndex, 3), $indexSize));
433 throw new Exception(sprintf('Failed to write files-index to backup file! Tmp Size: %s. Index Size: %s', size_format($backupSizeBeforeAddingIndex, 3), $indexSize));
434 } elseif ($writtenBytes === 0) {
435 debug_log('[Add File Index] Failed to write any byte to files-index! Retrying...');
436 }
437
438 if (!$this->archiverDto->isFinished()) {
439 throw new NotFinishedException('File backup is not finished yet!');
440 }
441
442 $this->tempBackupIndex->delete();
443 $this->archiverDto->reset();
444
445 $backupSizeAfterAddingIndex = filesize($this->tempBackup->getFilePath());
446 if (!$this->isBackupFormatV1()) {
447 $this->backupHeader->setFilesIndexStartOffset($backupSizeBeforeAddingIndex);
448 $this->backupHeader->setFilesIndexEndOffset($backupSizeAfterAddingIndex);
449 $this->backupHeader->updateHeader($this->tempBackup->getFilePath());
450 }
451
452 $this->tempBackup->append(PHP_EOL);
453
454 return $backupSizeBeforeAddingIndex;
455 }
456
457 /**
458 * @return string
459 */
460 public function getDestinationPath(): string
461 {
462 return sprintf(
463 '%s_%s_%s.%s',
464 parse_url(get_home_url())['host'],
465 current_time('Ymd-His'),
466 $this->jobDataDto->getId(),
467 self::BACKUP_EXTENSION
468 );
469 }
470
471 /**
472 * @param string $renameFileTo
473 * @param bool $isLocalBackup
474 * @return string
475 */
476 public function getFinalPath(string $renameFileTo = '', bool $isLocalBackup = true): string
477 {
478 $backupsDirectory = $this->getFinalBackupParentDirectory($isLocalBackup);
479 if ($renameFileTo === '') {
480 $renameFileTo = $this->getDestinationPath();
481 }
482
483 return $backupsDirectory . $renameFileTo;
484 }
485
486 public function getFinalBackupParentDirectory(bool $isLocalBackup = true): string
487 {
488 if ($isLocalBackup) {
489 return WPStaging::make(BackupsFinder::class)->getBackupsDirectory();
490 }
491
492 return WPStaging::make(Directory::class)->getCacheDirectory();
493 }
494
495 /**
496 * @param string $filePath
497 * @param string $indexPath
498 * @return int
499 */
500 protected function writeFileHeader(string $filePath, string $indexPath): int
501 {
502 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->filesystem->maybeNormalizePath($indexPath));
503 $this->fileHeader->readFile($filePath, $identifiablePath);
504
505 return $this->tempBackup->append($this->fileHeader->getFileHeader());
506 }
507
508 /**
509 * Get delay in milliseconds for retry according to retry number
510 *
511 * @param int $retry
512 * @return float
513 */
514 protected function getDelayForRetry(int $retry): float
515 {
516 $delay = 0.1;
517 for ($i = 0; $i < $retry; $i++) {
518 $delay *= 2;
519 }
520
521 return $delay * 1000;
522 }
523
524 /**
525 * @param BackupMetadata $backupMetadata
526 * @param JobBackupDataDto $jobBackupDataDto
527 * @return void
528 */
529 protected function setBackupMetadataCategoryInfo(BackupMetadata $backupMetadata, JobBackupDataDto $jobBackupDataDto)
530 {
531 $backupMetadata->setIndexPartSize($jobBackupDataDto->getCategorySizes());
532 }
533
534 /**
535 * @param JobBackupDataDto $jobBackupDataDto
536 * @return void
537 */
538 protected function incrementFilesCount(JobBackupDataDto $jobBackupDataDto)
539 {
540 $jobBackupDataDto->setTotalFiles($jobBackupDataDto->getTotalFiles() + 1);
541 }
542
543 /**
544 * @return void
545 */
546 protected function setIndexPositionCreated()
547 {
548 $this->archiverDto->setIndexPositionCreated(true);
549 }
550
551 /**
552 * @return bool
553 */
554 protected function isIndexPositionCreated(): bool
555 {
556 return $this->archiverDto->isIndexPositionCreated();
557 }
558
559 /**
560 * @return void
561 * @throws RuntimeException
562 */
563 protected function maybeIncrementFileAppendTimeLimit()
564 {
565 $this->jobDataDto->incrementNumberOfRetries();
566 if ($this->jobDataDto->getNumberOfRetries() > self::MAX_RETRIES_BEFORE_EXTENDING_TIME_LIMIT) {
567 return;
568 }
569
570 /** @var JobBackupDataDto */
571 $jobDataDto = $this->jobDataDto;
572 $jobDataDto->incrementFileAppendTimeLimit();
573 if ($jobDataDto->getFileAppendTimeLimit() > $this->getMaxPhpTimeLimitAllowed()) {
574 throw new RuntimeException('Maximum file append time limit exceeded. Please increase your PHP max execution time to proceed.');
575 }
576 }
577
578 protected function getMaxPhpTimeLimitAllowed(): int
579 {
580 $maxAllowedPhpTimeLimit = (int)ini_get('max_execution_time');
581 if ($maxAllowedPhpTimeLimit === 0 || $maxAllowedPhpTimeLimit === -1) {
582 $maxAllowedPhpTimeLimit = self::MAX_ALLOWED_PHP_TIME_LIMIT * self::PHP_TIME_LIMIT_IN_FRACTION;
583 return (int)Hooks::applyFilters(JobDataDto::FILTER_RESOURCES_EXECUTION_TIME_LIMIT, $maxAllowedPhpTimeLimit);
584 }
585
586 $maxAllowedPhpTimeLimit = max(self::MIN_ALLOWED_PHP_TIME_LIMIT, $maxAllowedPhpTimeLimit);
587 $maxAllowedPhpTimeLimit = min(self::MAX_ALLOWED_PHP_TIME_LIMIT, $maxAllowedPhpTimeLimit);
588 $maxAllowedPhpTimeLimit = $maxAllowedPhpTimeLimit * self::PHP_TIME_LIMIT_IN_FRACTION;
589
590 return (int)Hooks::applyFilters(JobDataDto::FILTER_RESOURCES_EXECUTION_TIME_LIMIT, $maxAllowedPhpTimeLimit);
591 }
592
593 /**
594 * @return void
595 */
596 protected function resetFileAppendTimeLimitAndRetries()
597 {
598 /** @var JobBackupDataDto */
599 $jobDataDto = $this->jobDataDto;
600
601 $jobDataDto->resetFileAppendTimeLimit();
602 $jobDataDto->resetNumberOfRetries();
603 }
604
605 protected function addNewFileHeaderToIndex(int $writtenBytes, int $startOffset): int
606 {
607 if ($this->isIndexPositionCreated()) {
608 return 0;
609 }
610
611 $this->fileHeader->setStartOffset($startOffset);
612 return $this->tempBackupIndex->append($this->fileHeader->getIndexHeader());
613 }
614
615 /**
616 * @param resource $resource
617 * @param string $filePath
618 *
619 * @return int Bytes written
620 * @throws DiskNotWritableException
621 * @throws RuntimeException
622 * @throws ThresholdException
623 */
624 protected function appendToArchiveFile($resource, string $filePath): int
625 {
626 try {
627 return $this->tempBackup->appendFile(
628 $resource,
629 $this->archiverDto->getWrittenBytesTotal()
630 );
631 } catch (DiskNotWritableException $e) {
632 debug_log('Failed to write to file: ' . $filePath);
633 // Re-throw for readability
634 throw $e;
635 }
636 }
637
638 /**
639 * @throws RuntimeException
640 *
641 * @param string $renameFileTo
642 * @return string
643 */
644 private function renameBackup(string $renameFileTo = ''): string
645 {
646 if ($renameFileTo === '') {
647 $renameFileTo = $this->getDestinationPath();
648 }
649
650 $destination = trailingslashit(dirname($this->tempBackup->getFilePath())) . $renameFileTo;
651 if ($this->isLocalBackup) {
652 $destination = $this->getFinalPath($renameFileTo);
653 }
654
655 if (!rename($this->tempBackup->getFilePath(), $destination)) {
656 throw new RuntimeException('Failed to generate destination');
657 }
658
659 return $destination;
660 }
661
662 /**
663 * @param int $writtenBytesTotal
664 * @param int $newBytesAdded
665 * @return int
666 * @throws \WPStaging\Framework\Exceptions\IOException
667 * @throws LogicException
668 * @throws RuntimeException
669 */
670 private function addIndex(int $writtenBytesTotal, int $newBytesAdded = 0): int
671 {
672 clearstatcache();
673 if (file_exists($this->tempBackup->getFilePath())) {
674 $this->archivedFileSize = filesize($this->tempBackup->getFilePath());
675 }
676
677 $start = max($this->archivedFileSize - $writtenBytesTotal, 0);
678
679 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->archiverDto->getIndexPath());
680
681 // Old backup format
682 if ($this->isBackupFormatV1() && $this->isIndexPositionCreated()) {
683 return $this->updateIndexInformationForAlreadyAddedIndex($writtenBytesTotal);
684 }
685
686 if ($this->isBackupFormatV1()) {
687 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->archiverDto->getIndexPath());
688 $backupFileIndex = $this->backupFileIndex->createIndex($identifiablePath, $start, $writtenBytesTotal, false);
689 $bytesWritten = $this->tempBackupIndex->append($backupFileIndex->getIndex());
690 }
691
692 if (!$this->isBackupFormatV1()) {
693 $bytesWritten = $this->addNewFileHeaderToIndex($newBytesAdded, $start);
694 if ($this->isIndexPositionCreated()) {
695 $this->addIndexPartSize($identifiablePath, $newBytesAdded);
696 return $newBytesAdded;
697 }
698 }
699
700 $this->archiverDto->setIndexPositionCreated(true);
701
702 $this->addIndexPartSize($identifiablePath, $writtenBytesTotal);
703
704 /**
705 * We require JobDataDto in the constructor because it is wired in the DI container
706 * to the current job DTO instance. However, here we need to make sure this DTO
707 * is the jobBackupDataDto.
708 */
709 if (!$this->phpAdapter->isCallable([$this->jobDataDto, 'setTotalFiles']) || !$this->phpAdapter->isCallable([$this->jobDataDto, 'getTotalFiles'])) {
710 debug_log('This method can only be called from the context of Backup');
711 throw new LogicException('This method can only be called from the context of Backup');
712 }
713
714 /** @var JobBackupDataDto $jobBackupDataDto */
715 $jobBackupDataDto = $this->jobDataDto;
716 if ($this->archiverDto->getFileSize() >= 2 * GB_IN_BYTES) {
717 $jobBackupDataDto->setIsContaining2GBFile(true);
718 }
719
720 $this->incrementFilesCount($jobBackupDataDto);
721
722 return $bytesWritten;
723 }
724
725 /**
726 * @param string $identifiablePath
727 * @param int $newBytesWritten
728 *
729 * @return void
730 */
731 private function addIndexPartSize(string $identifiablePath, int $newBytesWritten)
732 {
733 // Early bail if jobDataDto is not instance of jobBackupDataDto
734 if (!$this->jobDataDto instanceof JobBackupDataDto) {
735 return;
736 }
737
738 /** @var JobBackupDataDto $jobDataDto */
739 $jobDataDto = $this->jobDataDto;
740
741 $collectPartSize = $jobDataDto->getCategorySizes();
742
743 $partName = '';
744 switch ($identifiablePath) {
745 case ($this->pathIdentifier::IDENTIFIER_WP_CONTENT === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_WP_CONTENT))):
746 $partName = PartIdentifier::WP_CONTENT_PART_SIZE_IDENTIFIER;
747
748 if ($this->pathIdentifier->hasDropinsFile($identifiablePath)) {
749 $dropinsPartName = PartIdentifier::DROPIN_PART_SIZE_IDENTIFIER;
750 if (!isset($collectPartSize[$dropinsPartName])) {
751 $collectPartSize[$dropinsPartName] = 0;
752 }
753
754 $collectPartSize[$dropinsPartName] += $newBytesWritten;
755 }
756
757 break;
758 case ($this->pathIdentifier::IDENTIFIER_PLUGINS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_PLUGINS))):
759 $partName = PartIdentifier::PLUGIN_PART_SIZE_IDENTIFIER;
760 break;
761 case ($this->pathIdentifier::IDENTIFIER_THEMES === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_THEMES))):
762 $partName = PartIdentifier::THEME_PART_SIZE_IDENTIFIER;
763 break;
764 case ($this->pathIdentifier::IDENTIFIER_MUPLUGINS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_MUPLUGINS))):
765 $partName = PartIdentifier::MU_PLUGIN_PART_SIZE_IDENTIFIER;
766 break;
767 case ($this->pathIdentifier::IDENTIFIER_UPLOADS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_UPLOADS))):
768 $partName = PartIdentifier::UPLOAD_PART_SIZE_IDENTIFIER;
769 if (substr($identifiablePath, -4) === '.sql') {
770 $partName = PartIdentifier::DATABASE_PART_SIZE_IDENTIFIER;
771 }
772
773 break;
774 case ($this->pathIdentifier::IDENTIFIER_LANG === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_LANG))):
775 $partName = PartIdentifier::LANGUAGE_PART_SIZE_IDENTIFIER;
776 break;
777 case ($this->pathIdentifier::IDENTIFIER_ABSPATH === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_ABSPATH))):
778 $partName = PartIdentifier::WP_ROOT_PART_SIZE_IDENTIFIER;
779 break;
780 }
781
782 if (empty($partName)) {
783 return;
784 }
785
786 // If identifier not in array yet
787 if (!isset($collectPartSize[$partName])) {
788 $collectPartSize[$partName] = 0;
789 }
790
791 $collectPartSize[$partName] += $newBytesWritten;
792 $jobDataDto->setCategorySizes($collectPartSize);
793 }
794
795 /**
796 * Used in v1 Backup Format
797 * At the moment this is used when processing adding of big file which is not done in a single request
798 * @param int $writtenBytesTotal
799 * @return int
800 * @throws RuntimeException
801 */
802 private function updateIndexInformationForAlreadyAddedIndex(int $writtenBytesTotal): int
803 {
804 $lastLine = $this->tempBackupIndex->readLines(1, null, BufferedCache::POSITION_BOTTOM);
805 if (!is_array($lastLine)) {
806 debug_log('Failed to read backup metadata file index information. Error: The last line is no array. Last line: ' . $lastLine);
807 throw new RuntimeException('Failed to read backup metadata file index information. Error: The last line is no array.');
808 }
809
810 $lastLine = array_filter($lastLine, [$this->backupFileIndex, 'isIndexLine']);
811
812 if (count($lastLine) !== 1) {
813 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, true));
814 throw new RuntimeException('Failed to read backup metadata file index information. Error: The last line is not an array or element with countable interface.');
815 }
816
817 $lastLine = array_shift($lastLine);
818
819 $backupFileIndex = $this->backupFileIndex->readIndex($lastLine);
820 $writtenPreviously = $backupFileIndex->bytesEnd;
821
822 $this->tempBackupIndex->deleteBottomBytes(strlen($lastLine));
823
824 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->archiverDto->getIndexPath());
825 $backupFileIndex = $this->backupFileIndex->createIndex($identifiablePath, $backupFileIndex->bytesStart, $writtenBytesTotal, false);
826 $bytesWritten = $this->tempBackupIndex->append($backupFileIndex->getIndex());
827
828 $this->setIndexPositionCreated();
829
830 // We only need to increment newly added bytes
831 $this->addIndexPartSize($identifiablePath, $writtenBytesTotal - (int)$writtenPreviously);
832
833 return $bytesWritten;
834 }
835
836 private function isBackupFormatV1(): bool
837 {
838 /** @var JobBackupDataDto */
839 $jobDataDto = $this->jobDataDto;
840 return $jobDataDto->getIsBackupFormatV1();
841 }
842 }
843