PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
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 / FileBackupService.php
wp-staging / Backup / Service Last commit date
Compression 4 months ago Database 2 weeks ago AbstractBackupsFinder.php 1 year ago AbstractExtractor.php 1 day ago AbstractServiceProvider.php 2 years ago Archiver.php 2 weeks ago BackupAssets.php 1 month ago BackupContent.php 1 year ago BackupMetadataEditor.php 1 year ago BackupMetadataReader.php 5 months ago BackupSigner.php 7 months ago BackupsDirectoryResolver.php 2 weeks ago BackupsFinder.php 2 weeks ago Extractor.php 2 weeks ago FileBackupService.php 1 day ago FileBackupServiceProvider.php 2 years ago ServiceInterface.php 2 years ago TmpBackupCleaner.php 2 weeks ago ZlibCompressor.php 11 months ago
FileBackupService.php
372 lines
1 <?php
2
3 /**
4 * Manages the file backup process for adding files to backup archives
5 *
6 * Coordinates file archiving operations including reading files from disk,
7 * handling large files across multiple requests, and managing backup queues.
8 */
9
10 namespace WPStaging\Backup\Service;
11
12 use WPStaging\Backup\Dto\Job\JobBackupDataDto;
13 use WPStaging\Backup\Dto\Service\ArchiverDto;
14 use WPStaging\Framework\Job\Dto\StepsDto;
15 use WPStaging\Backup\Exceptions\BackupSkipItemException;
16 use WPStaging\Backup\Task\FileBackupTask;
17 use WPStaging\Framework\Adapter\Directory;
18 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
19 use WPStaging\Framework\Filesystem\Filesystem;
20 use WPStaging\Framework\Filesystem\FilesystemScanner;
21 use WPStaging\Framework\Job\Exception\ThresholdException;
22 use WPStaging\Framework\Queue\FinishedQueueException;
23 use WPStaging\Framework\Queue\SeekableQueueInterface;
24 use WPStaging\Framework\SiteInfo;
25 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
26 use WPStaging\Framework\Traits\ResourceTrait;
27 use WPStaging\Vendor\Psr\Log\LoggerInterface;
28
29 use function WPStaging\functions\debug_log;
30
31 class FileBackupService implements ServiceInterface
32 {
33 use ResourceTrait;
34 use EndOfLinePlaceholderTrait;
35
36 /** @var Archiver */
37 protected $archiver;
38
39 /** @var Directory */
40 protected $directory;
41
42 /** @var Filesystem */
43 protected $filesystem;
44
45 /** @var SeekableQueueInterface */
46 protected $taskQueue;
47
48 /** @var LoggerInterface */
49 protected $logger;
50
51 /** @var JobBackupDataDto */
52 protected $jobDataDto;
53
54 /** @var StepsDto */
55 protected $stepsDto;
56
57 /** @var int|ArchiverDto If a file couldn't be processed in a single request, this will be populated */
58 protected $bigFileBeingProcessed;
59
60 /** @var FileBackupTask */
61 protected $fileBackupTask;
62
63 /** @var bool */
64 protected $isWpContentOutsideAbspath = false;
65
66 /** @var bool */
67 protected $isGracefulShutdown = true;
68
69 /** @var float */
70 protected $start;
71
72 /** @var string */
73 protected $fileIdentifier;
74
75 public function __construct(Archiver $archiver, Directory $directory, Filesystem $filesystem, SiteInfo $siteInfo)
76 {
77 $this->archiver = $archiver;
78 $this->directory = $directory;
79 $this->filesystem = $filesystem;
80
81 $this->isWpContentOutsideAbspath = $siteInfo->isWpContentOutsideAbspath();
82 }
83
84 /**
85 * @param FileBackupTask $fileBackupTask
86 * @param SeekableQueueInterface $taskQueue
87 * @param LoggerInterface $logger
88 * @param JobBackupDataDto $jobDataDto
89 * @param StepsDto $stepsDto
90 * @return void
91 */
92 public function inject(FileBackupTask $fileBackupTask, SeekableQueueInterface $taskQueue, LoggerInterface $logger, JobBackupDataDto $jobDataDto, StepsDto $stepsDto)
93 {
94 $this->fileBackupTask = $fileBackupTask;
95 $this->taskQueue = $taskQueue;
96 $this->logger = $logger;
97 $this->jobDataDto = $jobDataDto;
98 $this->stepsDto = $stepsDto;
99 }
100
101 /**
102 * @param bool $isGracefulShutdown
103 * @return void
104 */
105 public function setIsGracefulShutdown(bool $isGracefulShutdown)
106 {
107 $this->isGracefulShutdown = $isGracefulShutdown;
108 }
109
110 /**
111 * @param string $fileIdentifier
112 * @param bool $isOtherWpRootFilesTask (Used in Pro)
113 * @return void
114 */
115 public function setupArchiver(string $fileIdentifier, bool $isOtherWpRootFilesTask = false)
116 {
117 $this->fileIdentifier = $fileIdentifier;
118 $this->archiver->createArchiveFile(Archiver::CREATE_BINARY_HEADER);
119 }
120
121 /**
122 * @return void
123 */
124 public function execute()
125 {
126 $this->archiver->setFileAppendTimeLimit($this->jobDataDto->getFileAppendTimeLimit());
127 $this->start = microtime(true);
128
129 while (!$this->isThreshold() && !$this->stepsDto->isFinished()) {
130 try {
131 $this->backup();
132 } catch (ThresholdException $exception) {
133 break;
134 } catch (FinishedQueueException $exception) {
135 $this->stepsDto->finish();
136 $this->logger->info(sprintf('Added %d/%d %s files to backup (%s)', $this->stepsDto->getCurrent(), $this->stepsDto->getTotal(), $this->getTranslatedFileIdentifier(), $this->getBackupSpeed()));
137 $this->updateMultipartInfo();
138
139 return;
140 } catch (DiskNotWritableException $exception) {
141 // Probably disk full. Should be handled in Job\AbstractJob::prepareAndExecute(). Let's stop the code here if it did not happen!
142 throw new \Exception('Disk is probably full. Error message: ' . $exception->getMessage());
143 } catch (\Throwable $th) {
144 throw new \Exception('Fail to create backup. Error message: ' . $th->getMessage());
145 }
146 }
147
148 $this->logExecution();
149
150 $this->updateMultipartInfo();
151 }
152
153 /**
154 * @throws DiskNotWritableException
155 * @throws FinishedQueueException
156 * @return void
157 */
158 public function backup()
159 {
160 $archiverDto = $this->archiver->getDto();
161 $archiverDto->setWrittenBytesTotal($this->jobDataDto->getFileBeingBackupWrittenBytes());
162 $archiverDto->setFileHeaderSizeInBytes($this->jobDataDto->getCurrentWrittenFileHeaderBytes());
163 $archiverDto->setStartOffset($this->jobDataDto->getCurrentFileStartOffset());
164
165 if ($archiverDto->getWrittenBytesTotal() !== 0) {
166 $archiverDto->setIndexPositionCreated(true);
167 $this->logger->debug('Resuming backup of a large file from previous request.');
168 }
169
170 $path = $this->taskQueue->dequeue();
171 $path = $this->replacePlaceholdersWithEOLs($path);
172
173 if (is_null($path)) {
174 debug_log("Backup error: no task to dequeue");
175 throw new FinishedQueueException();
176 }
177
178 if (empty($path)) {
179 return;
180 }
181
182 $indexPath = '';
183 if (strpos($path, FilesystemScanner::PATH_SEPARATOR) !== false) {
184 list($path, $indexPath) = explode(FilesystemScanner::PATH_SEPARATOR, $path);
185 }
186
187 if ($this->shouldPrependAbsPath()) {
188 $path = trailingslashit(ABSPATH) . $path;
189 }
190
191 try {
192 $isFileWrittenCompletely = $this->appendCurrentFileToBackup($path, $indexPath);
193 } catch (BackupSkipItemException $e) {
194 $isFileWrittenCompletely = true;
195 } catch (ThresholdException $e) {
196 $isFileWrittenCompletely = null;
197 } catch (\RuntimeException $e) {
198 // One unclassifiable/invalid file must not abort the whole backup: skip it, but
199 // make it obvious which file was skipped and why so it can be diagnosed. Prefer
200 // $indexPath — that is the path the classifier tried to shorten; $path may already
201 // have ABSPATH prepended and would not match. Collapse newlines so the warning
202 // stays a single log line.
203 $isFileWrittenCompletely = true;
204 $skippedFile = $indexPath !== '' ? $indexPath : $path;
205 $skippedFile = $skippedFile === '' ? '(empty path)' : $skippedFile;
206 $reason = str_replace(["\r", "\n"], ' ', $e->getMessage());
207 $this->logger->warning(sprintf('Skipped a file that could not be added to the backup: %s — %s', $skippedFile, $reason));
208 debug_log(sprintf('Backup: skipped file "%s" — %s', $skippedFile, $reason));
209 } catch (\Throwable $th) {
210 throw $th;
211 }
212
213 $this->jobDataDto->setCurrentWrittenFileHeaderBytes(0);
214 // Done processing this file
215 if ($isFileWrittenCompletely === true) {
216 $this->jobDataDto->setFileBeingBackupWrittenBytes(0);
217 $this->stepsDto->incrementCurrentStep();
218 $this->jobDataDto->setQueueOffset($this->taskQueue->getOffset());
219
220 if (!$this->jobDataDto->getIsMultipartBackup()) {
221 $this->jobDataDto->incrementFilesInPart($this->fileIdentifier);
222 }
223
224 $this->persistJobDataDto();
225 return;
226 }
227
228 // Processing a file that could not be finished in this request
229 $archiverDto = $this->archiver->getDto();
230 $this->jobDataDto->setFileBeingBackupWrittenBytes($archiverDto->getWrittenBytesTotal());
231 $this->jobDataDto->setCurrentFileStartOffset($archiverDto->getStartOffset());
232 $this->taskQueue->retry(false);
233
234 if ($archiverDto->getFileHeaderSizeInBytes() > 0) {
235 $this->jobDataDto->setCurrentWrittenFileHeaderBytes($archiverDto->getFileHeaderSizeInBytes());
236 }
237
238 if ($archiverDto->getWrittenBytesTotal() < $archiverDto->getFileSize() && $archiverDto->getFileSize() > 10 * MB_IN_BYTES) {
239 $this->bigFileBeingProcessed = $archiverDto;
240 }
241
242 $this->persistJobDataDto();
243 if ($isFileWrittenCompletely === null) {
244 throw new ThresholdException();
245 }
246 }
247
248 protected function getBackupSpeed(): string
249 {
250 $elapsed = microtime(true) - $this->start;
251 // Fixes a "division by zero fatal error" when $elapsed was 0. issue #2571
252 $elapsed = empty($elapsed) ? 1 : $elapsed;
253
254 $bytesPerSecond = min(10 * GB_IN_BYTES, absint($this->archiver->getBytesWrittenInThisRequest() / $elapsed));
255
256 if ($bytesPerSecond === 10 * GB_IN_BYTES) {
257 return '10GB/s+';
258 }
259
260 // Format with 2 decimal places if faster than 1MB/s
261 if ($bytesPerSecond >= MB_IN_BYTES) {
262 if ($bytesPerSecond >= GB_IN_BYTES) {
263 return number_format($bytesPerSecond / GB_IN_BYTES, 2) . 'GB/s';
264 }
265
266 return number_format($bytesPerSecond / MB_IN_BYTES, 2) . 'MB/s';
267 }
268
269 return size_format($bytesPerSecond) . '/s';
270 }
271
272 protected function shouldPrependAbsPath(): bool
273 {
274 return $this->isWpContentOutsideAbspath === false;
275 }
276
277 /**
278 * This method logs how many files processed in the current request.
279 * @return void
280 */
281 protected function logExecution()
282 {
283 if ($this->bigFileBeingProcessed instanceof ArchiverDto) {
284 $relativePathForLogging = str_replace($this->filesystem->normalizePath(ABSPATH, true), '', $this->filesystem->normalizePath($this->bigFileBeingProcessed->getFilePath()));
285 $percentProcessed = ceil(($this->bigFileBeingProcessed->getWrittenBytesTotal() / $this->bigFileBeingProcessed->getFileSize()) * 100);
286 $this->logger->info(sprintf(
287 'Adding big %s file: %s - %s/%s (%s%%) (%s)',
288 $this->getTranslatedFileIdentifier(),
289 $relativePathForLogging,
290 size_format($this->bigFileBeingProcessed->getWrittenBytesTotal(), 2),
291 size_format($this->bigFileBeingProcessed->getFileSize(), 2),
292 $percentProcessed,
293 $this->getBackupSpeed()
294 ));
295 return;
296 }
297
298 $this->logger->info(sprintf(
299 'Added %d/%d %s files to backup (%s)',
300 $this->stepsDto->getCurrent(),
301 $this->stepsDto->getTotal(),
302 $this->getTranslatedFileIdentifier(),
303 $this->getBackupSpeed()
304 ));
305 }
306
307 /**
308 * @return void
309 */
310 protected function updateMultipartInfo()
311 {
312 // Used in Pro
313 }
314
315 /**
316 * @return void
317 */
318 protected function maybeIncrementPartNo(string $path)
319 {
320 // Used in Pro
321 }
322
323 /**
324 * Drives the actual append of the current queue entry to the backup. The Pro subclass
325 * overrides this to route oversize files through the cross-part segmenter while keeping
326 * the existing single-shot path for files that fit inside the configured part size.
327 *
328 * Returns true when the file is finished, false when it still needs more work in the
329 * next request, and null when the caller should treat the current work as threshold-hit.
330 *
331 * @param string $path
332 * @param string $indexPath
333 * @return bool|null
334 * @throws BackupSkipItemException
335 * @throws ThresholdException
336 * @throws \RuntimeException
337 * @throws \Throwable
338 */
339 protected function appendCurrentFileToBackup(string $path, string $indexPath)
340 {
341 $this->maybeIncrementPartNo($path);
342 return $this->archiver->appendFileToBackup($path, $indexPath);
343 }
344
345 protected function getTranslatedFileIdentifier(): string
346 {
347 switch ($this->fileIdentifier) {
348 case 'muplugins':
349 return __('mu-plugin', 'wp-staging');
350 case 'plugins':
351 return __('plugin', 'wp-staging');
352 case 'themes':
353 return __('theme', 'wp-staging');
354 case 'otherfiles':
355 return __('other', 'wp-staging');
356 case 'rootfiles':
357 return __('root', 'wp-staging');
358 default:
359 return $this->fileIdentifier; // fallback
360 }
361 }
362
363 protected function persistJobDataDto()
364 {
365 if ($this->jobDataDto->getIsFastPerformanceMode()) {
366 return;
367 }
368
369 $this->fileBackupTask->persistJobDataDto();
370 }
371 }
372