PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / trunk
WP STAGING – WordPress Backup, Restore, Migration & Clone vtrunk
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 1 week ago AbstractBackupsFinder.php 1 year ago AbstractExtractor.php 1 week ago AbstractServiceProvider.php 2 years ago Archiver.php 1 week ago BackupAssets.php 1 month ago BackupContent.php 1 year ago BackupMetadataEditor.php 1 year ago BackupMetadataReader.php 5 months ago BackupSigner.php 6 months ago BackupsDirectoryResolver.php 1 week ago BackupsFinder.php 1 week ago Extractor.php 1 week ago FileBackupService.php 1 week ago FileBackupServiceProvider.php 2 years ago ServiceInterface.php 2 years ago TmpBackupCleaner.php 1 week ago ZlibCompressor.php 11 months ago
FileBackupService.php
365 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 $isFileWrittenCompletely = true;
199 // Invalid file
200 $this->logger->warning("Invalid file. Could not add file to backup: $path");
201 debug_log("Backup error: cannot append file to backup: $path");
202 } catch (\Throwable $th) {
203 throw $th;
204 }
205
206 $this->jobDataDto->setCurrentWrittenFileHeaderBytes(0);
207 // Done processing this file
208 if ($isFileWrittenCompletely === true) {
209 $this->jobDataDto->setFileBeingBackupWrittenBytes(0);
210 $this->stepsDto->incrementCurrentStep();
211 $this->jobDataDto->setQueueOffset($this->taskQueue->getOffset());
212
213 if (!$this->jobDataDto->getIsMultipartBackup()) {
214 $this->jobDataDto->incrementFilesInPart($this->fileIdentifier);
215 }
216
217 $this->persistJobDataDto();
218 return;
219 }
220
221 // Processing a file that could not be finished in this request
222 $archiverDto = $this->archiver->getDto();
223 $this->jobDataDto->setFileBeingBackupWrittenBytes($archiverDto->getWrittenBytesTotal());
224 $this->jobDataDto->setCurrentFileStartOffset($archiverDto->getStartOffset());
225 $this->taskQueue->retry(false);
226
227 if ($archiverDto->getFileHeaderSizeInBytes() > 0) {
228 $this->jobDataDto->setCurrentWrittenFileHeaderBytes($archiverDto->getFileHeaderSizeInBytes());
229 }
230
231 if ($archiverDto->getWrittenBytesTotal() < $archiverDto->getFileSize() && $archiverDto->getFileSize() > 10 * MB_IN_BYTES) {
232 $this->bigFileBeingProcessed = $archiverDto;
233 }
234
235 $this->persistJobDataDto();
236 if ($isFileWrittenCompletely === null) {
237 throw new ThresholdException();
238 }
239 }
240
241 protected function getBackupSpeed(): string
242 {
243 $elapsed = microtime(true) - $this->start;
244 // Fixes a "division by zero fatal error" when $elapsed was 0. issue #2571
245 $elapsed = empty($elapsed) ? 1 : $elapsed;
246
247 $bytesPerSecond = min(10 * GB_IN_BYTES, absint($this->archiver->getBytesWrittenInThisRequest() / $elapsed));
248
249 if ($bytesPerSecond === 10 * GB_IN_BYTES) {
250 return '10GB/s+';
251 }
252
253 // Format with 2 decimal places if faster than 1MB/s
254 if ($bytesPerSecond >= MB_IN_BYTES) {
255 if ($bytesPerSecond >= GB_IN_BYTES) {
256 return number_format($bytesPerSecond / GB_IN_BYTES, 2) . 'GB/s';
257 }
258
259 return number_format($bytesPerSecond / MB_IN_BYTES, 2) . 'MB/s';
260 }
261
262 return size_format($bytesPerSecond) . '/s';
263 }
264
265 protected function shouldPrependAbsPath(): bool
266 {
267 return $this->isWpContentOutsideAbspath === false;
268 }
269
270 /**
271 * This method logs how many files processed in the current request.
272 * @return void
273 */
274 protected function logExecution()
275 {
276 if ($this->bigFileBeingProcessed instanceof ArchiverDto) {
277 $relativePathForLogging = str_replace($this->filesystem->normalizePath(ABSPATH, true), '', $this->filesystem->normalizePath($this->bigFileBeingProcessed->getFilePath()));
278 $percentProcessed = ceil(($this->bigFileBeingProcessed->getWrittenBytesTotal() / $this->bigFileBeingProcessed->getFileSize()) * 100);
279 $this->logger->info(sprintf(
280 'Adding big %s file: %s - %s/%s (%s%%) (%s)',
281 $this->getTranslatedFileIdentifier(),
282 $relativePathForLogging,
283 size_format($this->bigFileBeingProcessed->getWrittenBytesTotal(), 2),
284 size_format($this->bigFileBeingProcessed->getFileSize(), 2),
285 $percentProcessed,
286 $this->getBackupSpeed()
287 ));
288 return;
289 }
290
291 $this->logger->info(sprintf(
292 'Added %d/%d %s files to backup (%s)',
293 $this->stepsDto->getCurrent(),
294 $this->stepsDto->getTotal(),
295 $this->getTranslatedFileIdentifier(),
296 $this->getBackupSpeed()
297 ));
298 }
299
300 /**
301 * @return void
302 */
303 protected function updateMultipartInfo()
304 {
305 // Used in Pro
306 }
307
308 /**
309 * @return void
310 */
311 protected function maybeIncrementPartNo(string $path)
312 {
313 // Used in Pro
314 }
315
316 /**
317 * Drives the actual append of the current queue entry to the backup. The Pro subclass
318 * overrides this to route oversize files through the cross-part segmenter while keeping
319 * the existing single-shot path for files that fit inside the configured part size.
320 *
321 * Returns true when the file is finished, false when it still needs more work in the
322 * next request, and null when the caller should treat the current work as threshold-hit.
323 *
324 * @param string $path
325 * @param string $indexPath
326 * @return bool|null
327 * @throws BackupSkipItemException
328 * @throws ThresholdException
329 * @throws \RuntimeException
330 * @throws \Throwable
331 */
332 protected function appendCurrentFileToBackup(string $path, string $indexPath)
333 {
334 $this->maybeIncrementPartNo($path);
335 return $this->archiver->appendFileToBackup($path, $indexPath);
336 }
337
338 protected function getTranslatedFileIdentifier(): string
339 {
340 switch ($this->fileIdentifier) {
341 case 'muplugins':
342 return __('mu-plugin', 'wp-staging');
343 case 'plugins':
344 return __('plugin', 'wp-staging');
345 case 'themes':
346 return __('theme', 'wp-staging');
347 case 'otherfiles':
348 return __('other', 'wp-staging');
349 case 'rootfiles':
350 return __('root', 'wp-staging');
351 default:
352 return $this->fileIdentifier; // fallback
353 }
354 }
355
356 protected function persistJobDataDto()
357 {
358 if ($this->jobDataDto->getIsFastPerformanceMode()) {
359 return;
360 }
361
362 $this->fileBackupTask->persistJobDataDto();
363 }
364 }
365