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