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