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