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