PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.3.1
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 / Extractor.php
wp-staging / Backup / Service Last commit date
Compression 1 year ago Database 10 months ago AbstractBackupsFinder.php 1 year ago AbstractExtractor.php 11 months ago AbstractServiceProvider.php 2 years ago Archiver.php 11 months ago BackupAssets.php 2 years ago BackupContent.php 1 year ago BackupMetadataEditor.php 1 year ago BackupMetadataReader.php 1 year ago BackupSigner.php 1 year ago BackupsFinder.php 1 year ago Extractor.php 11 months ago FileBackupService.php 11 months ago FileBackupServiceProvider.php 2 years ago ServiceInterface.php 2 years ago ZlibCompressor.php 11 months ago
Extractor.php
346 lines
1 <?php
2
3 namespace WPStaging\Backup\Service;
4
5 use Exception;
6 use OutOfRangeException;
7 use RuntimeException;
8 use WPStaging\Backup\BackupFileIndex;
9 use WPStaging\Backup\BackupHeader;
10 use WPStaging\Backup\BackupValidator;
11 use WPStaging\Backup\Exceptions\EmptyChunkException;
12 use WPStaging\Backup\FileHeader;
13 use WPStaging\Core\WPStaging;
14 use WPStaging\Framework\Adapter\Directory;
15 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
16 use WPStaging\Framework\Job\Exception\FileValidationException;
17 use WPStaging\Framework\Facades\Hooks;
18 use WPStaging\Framework\Filesystem\FileObject;
19 use WPStaging\Framework\Filesystem\DiskWriteCheck;
20 use WPStaging\Framework\Filesystem\MissingFileException;
21 use WPStaging\Framework\Filesystem\PathIdentifier;
22 use WPStaging\Framework\Filesystem\Permissions;
23 use WPStaging\Framework\Queue\FinishedQueueException;
24 use WPStaging\Framework\Traits\ResourceTrait;
25 use WPStaging\Framework\Traits\RestoreFileExclusionTrait;
26 use WPStaging\Vendor\Psr\Log\LoggerInterface;
27
28 class Extractor extends AbstractExtractor
29 {
30 use ResourceTrait;
31 use RestoreFileExclusionTrait;
32
33 /** @var LoggerInterface */
34 protected $logger;
35
36 /** @var DiskWriteCheck */
37 protected $diskWriteCheck;
38
39 /** @var BackupValidator */
40 protected $backupValidator;
41
42 /** @var ZlibCompressor */
43 protected $zlibCompressor;
44
45 /** @var bool */
46 protected $isRepairMultipleHeadersIssue = false;
47
48 public function __construct(
49 PathIdentifier $pathIdentifier,
50 Directory $directory,
51 DiskWriteCheck $diskWriteCheck,
52 ZlibCompressor $zlibCompressor,
53 BackupValidator $backupValidator,
54 BackupHeader $backupHeader,
55 Permissions $permissions
56 ) {
57 parent::__construct($pathIdentifier, $directory, $backupHeader, $permissions);
58 $this->zlibCompressor = $zlibCompressor;
59 $this->backupValidator = $backupValidator;
60 $this->diskWriteCheck = $diskWriteCheck;
61 }
62
63 /**
64 * @param bool $isBackupFormatV1
65 * @return void
66 */
67 public function setIsBackupFormatV1(bool $isBackupFormatV1)
68 {
69 $this->isBackupFormatV1 = $isBackupFormatV1;
70 if ($isBackupFormatV1) {
71 $this->indexLineDto = new BackupFileIndex();
72 } else {
73 $this->indexLineDto = WPStaging::make(FileHeader::class);
74 }
75 }
76
77 /**
78 * @param bool $isRepairMultipleHeadersIssue
79 * @return void
80 */
81 public function setIsRepairMultipleHeadersIssue(bool $isRepairMultipleHeadersIssue)
82 {
83 $this->isRepairMultipleHeadersIssue = $isRepairMultipleHeadersIssue;
84 }
85
86 /**
87 * @param LoggerInterface $logger
88 * @return void
89 */
90 public function setLogger(LoggerInterface $logger)
91 {
92 $this->logger = $logger;
93 }
94
95 /**
96 * @param bool $isValidateOnly
97 * @return void
98 */
99 public function setIsValidateOnly(bool $isValidateOnly)
100 {
101 $this->isValidateOnly = $isValidateOnly;
102 if ($isValidateOnly) {
103 $this->throwExceptionOnValidationFailure = true;
104 }
105 }
106
107 /**
108 * @return void
109 * @throws DiskNotWritableException
110 */
111 public function execute()
112 {
113 while (!$this->isThreshold()) {
114 try {
115 $this->findFileToExtract();
116 } catch (OutOfRangeException $e) {
117 // Done processing, or failed
118 $this->logger->warning('OutOfRangeException. Error: ' . $e->getMessage());
119 return;
120 } catch (RuntimeException $e) {
121 $this->logger->warning($e->getMessage());
122 continue;
123 } catch (MissingFileException $e) {
124 $this->logger->warning('MissingFileException. Error: ' . $e->getMessage());
125 continue;
126 } catch (Exception $e) {
127 if ($e->getCode() === self::FILE_FILTERED_EXCEPTION_CODE) {
128 continue;
129 }
130
131 if ($e->getCode() === self::FINISHED_QUEUE_EXCEPTION_CODE) {
132 throw new FinishedQueueException();
133 }
134
135 if ($e->getCode() === self::ITEM_SKIP_EXCEPTION_CODE) {
136 continue;
137 }
138
139 throw $e;
140 }
141
142 try {
143 $this->processCurrentFile();
144 } catch (FileValidationException $e) {
145 if ($this->isValidateOnly || $this->throwExceptionOnValidationFailure) {
146 throw $e;
147 }
148
149 $this->logger->warning('Unable to validate file. Error: ' . $e->getMessage());
150 }
151 }
152 }
153
154 /**
155 * @param Exception $ex
156 * @param string $filePath
157 * @return void
158 */
159 protected function throwMissingFileException(Exception $ex, string $filePath)
160 {
161 throw new MissingFileException(sprintf("Following backup part missing: %s", $filePath), 0, $ex);
162 }
163
164 protected function isBigFile(): bool
165 {
166 $sizeToConsiderAsBigFile = Hooks::applyFilters('wpstg.tests.restore.bigFileSize', 10 * MB_IN_BYTES);
167
168 return $this->extractingFile->getTotalBytes() > $sizeToConsiderAsBigFile;
169 }
170
171 protected function cleanExistingFile(string $identifier)
172 {
173 if ($this->isValidateOnly) {
174 return;
175 }
176
177 if ($identifier !== PathIdentifier::IDENTIFIER_UPLOADS || $this->extractingFile->getWrittenBytes() > 0) {
178 return;
179 }
180
181 if (file_exists($this->extractingFile->getBackupPath())) {
182 // Delete the original upload file
183 if (!unlink($this->extractingFile->getBackupPath())) {
184 throw new \RuntimeException(sprintf(__('Could not delete original media library file %s. Skipping restore of it...', 'wp-staging'), $this->extractingFile->getRelativePath()));
185 }
186 }
187 }
188
189 /**
190 * Fixes issue https://github.com/wp-staging/wp-staging-pro/issues/2861
191 * @return void
192 */
193 protected function maybeRemoveLastAccidentalCharFromLastExtractedFile()
194 {
195 if ($this->backupMetadata->getTotalFiles() !== $this->extractorDto->getTotalFilesExtracted()) {
196 return;
197 }
198
199 if ($this->backupValidator->validateFileIndexFirstLine($this->wpstgFile, $this->backupMetadata)) {
200 return;
201 }
202
203 $this->removeLastCharInExtractedFile();
204 }
205
206 protected function getExtractFolder(string $identifier): string
207 {
208 if ($this->isValidateOnly) {
209 return trailingslashit($this->dirRestore . self::VALIDATE_DIRECTORY);
210 }
211
212 if ($identifier === PathIdentifier::IDENTIFIER_UPLOADS) {
213 return $this->directory->getUploadsDirectory();
214 }
215
216 return $this->dirRestore . $identifier;
217 }
218
219 /**
220 * @return void
221 * @throws DiskNotWritableException
222 */
223 private function processCurrentFile()
224 {
225 $destinationFilePath = $this->extractingFile->getBackupPath();
226 if ($this->currentIdentifier === PathIdentifier::IDENTIFIER_UPLOADS && $this->isExcludedFile($destinationFilePath)) {
227 $this->extractorDto->incrementTotalFilesSkipped();
228 $this->extractorDto->setCurrentIndexOffset($this->wpstgIndexOffsetForNextFile);
229 return;
230 }
231
232 try {
233 if ($this->isThreshold()) {
234 // Prevent considering a file as big just because we start extracting at the threshold
235 return;
236 }
237
238 $this->fileBatchWrite();
239
240 $isFileExtracted = $this->isExtractingFileExtracted(function ($message) {
241 $this->logger->info($message);
242 });
243
244 if (!$isFileExtracted) {
245 return;
246 }
247 } catch (DiskNotWritableException $e) {
248 // Re-throw
249 throw $e;
250 } catch (OutOfRangeException $e) {
251 // Backup header, should be ignored silently
252 $this->extractingFile->setWrittenBytes($this->extractingFile->getTotalBytes());
253 } catch (Exception $e) {
254 // Set this file as "written", so that we can skip to the next file.
255 $this->extractingFile->setWrittenBytes($this->extractingFile->getTotalBytes());
256
257 if (defined('WPSTG_DEBUG') && WPSTG_DEBUG) {
258 $this->logger->warning(sprintf('Skipped file %s. Reason: %s', $this->extractingFile->getRelativePath(), $e->getMessage()));
259 }
260 }
261
262 $this->validateExtractedFileAndMoveNext();
263 }
264
265 /**
266 * @return void
267 * @throws DiskNotWritableException
268 * @throws \WPStaging\Framework\Filesystem\FilesystemExceptions
269 */
270 private function fileBatchWrite()
271 {
272 $destinationFilePath = $this->extractingFile->getBackupPath();
273
274 if (strpos($destinationFilePath, '.sql') !== false) {
275 $this->logger->debug(sprintf('DEBUG: Restoring SQL file %s', $destinationFilePath));
276 }
277
278 wp_mkdir_p(dirname($destinationFilePath));
279
280 /**
281 * On some servers, it is required to create empty file first, so we will create empty files.
282 * On some servers, touch doesn't work consistently, so we will use fwrite, see the reason below.
283 * On sites hosted on SiteGround, creating files using file_puts_contents uses a lot of memory,
284 * so by default we will use fwrite to create the empty file.
285 * If creating the empty file using fwrite fails, let try creating it using file_put_contents
286 * @see https://github.com/wp-staging/wp-staging-pro/issues/3272 why it was needed.
287 */
288 if (!$this->createEmptyFile($destinationFilePath)) {
289 file_put_contents($destinationFilePath, '');
290 }
291
292 $destinationFileResource = @fopen($destinationFilePath, FileObject::MODE_APPEND);
293
294 if (!$destinationFileResource) {
295 $this->diskWriteCheck->testDiskIsWriteable();
296 throw new Exception("Can not extract file $destinationFilePath");
297 }
298
299 $lastDebugMessage = '';
300 while (!$this->extractingFile->isFinished() && !$this->isThreshold()) {
301 $readBytesBefore = $this->wpstgFile->ftell();
302
303 $chunk = null;
304 try {
305 $chunk = $this->zlibCompressor->getService()->readChunk($this->wpstgFile, $this->extractingFile, function ($currentChunkNumber) use (&$lastDebugMessage) {
306 // Log every 200 chunks to provide progress updates without overwhelming the logs.
307 if ($currentChunkNumber % 200 === 0 || $currentChunkNumber === $this->extractorDto->getTotalChunks()) {
308 $lastDebugMessage = sprintf('DEBUG: Extracting chunk %d/%d', $currentChunkNumber, $this->extractorDto->getTotalChunks());
309 }
310 });
311 } catch (DiskNotWritableException $ex) {
312 $this->diskWriteCheck->testDiskIsWriteable();
313 // If empty chunk, it is an empty file, so we can skip it
314 throw new Exception("Unable to extract file to $destinationFilePath. Please check if there is enough disk space available.");
315 } catch (EmptyChunkException $ex) {
316 // If empty chunk, it is an empty file, so we can skip it
317 continue;
318 }
319
320 if ($this->isRepairMultipleHeadersIssue) {
321 $chunk = $this->maybeRepairMultipleHeadersIssue($chunk);
322 }
323
324 $writtenBytes = fwrite($destinationFileResource, $chunk, (int)$this->getScriptMemoryLimit());
325
326 if ($writtenBytes === false || $writtenBytes <= 0) {
327 fclose($destinationFileResource);
328 $destinationFileResource = null;
329 throw DiskNotWritableException::diskNotWritable();
330 }
331
332 $readBytesAfter = $this->wpstgFile->ftell() - $readBytesBefore;
333
334 $this->extractingFile->addReadBytes($readBytesAfter);
335 $this->extractingFile->addWrittenBytes($writtenBytes);
336 }
337
338 if (!empty($lastDebugMessage)) {
339 $this->logger->debug($lastDebugMessage);
340 }
341
342 fclose($destinationFileResource);
343 $destinationFileResource = null;
344 }
345 }
346