PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 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 / Staging / Service / FileCopier.php
wp-staging / Staging / Service Last commit date
Database 1 day ago AbstractStagingSetup.php 1 day ago DirectoryScanner.php 1 day ago FileCopier.php 1 day ago LegacyOptionsCache.php 1 day ago StagingEngine.php 1 day ago StagingSetup.php 1 day ago TableScanner.php 1 day ago
FileCopier.php
475 lines
1 <?php
2
3 namespace WPStaging\Staging\Service;
4
5 use WPStaging\Framework\Adapter\Directory;
6 use WPStaging\Framework\Facades\Hooks;
7 use WPStaging\Framework\Job\Dto\StepsDto;
8 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
9 use WPStaging\Framework\Filesystem\Filesystem;
10 use WPStaging\Framework\Filesystem\FilesystemScanner;
11 use WPStaging\Framework\Filesystem\Permissions;
12 use WPStaging\Framework\Queue\FinishedQueueException;
13 use WPStaging\Framework\Queue\SeekableQueueInterface;
14 use WPStaging\Framework\SiteInfo;
15 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
16 use WPStaging\Framework\Traits\ResourceTrait;
17 use WPStaging\Framework\Utils\Strings;
18 use WPStaging\Staging\Dto\Service\BigFileDto;
19 use WPStaging\Vendor\Psr\Log\LoggerInterface;
20
21 use function WPStaging\functions\debug_log;
22
23
24
25
26 class FileCopier
27 {
28 use ResourceTrait;
29 use EndOfLinePlaceholderTrait;
30
31
32
33
34 const BATCH_SIZE = 512 * 1024;
35
36
37
38
39 const FILTER_COPY_BATCH_SIZE = 'wpstg.clone.copy_batch_size';
40
41
42 protected $filesystem;
43
44
45 protected $directory;
46
47
48 protected $siteInfo;
49
50
51 protected $permissions;
52
53
54 protected $strings;
55
56
57 protected $taskQueue;
58
59
60 protected $logger;
61
62
63 protected $stepsDto;
64
65
66 protected $bigFileDto = null;
67
68
69 protected $isWpContentOutsideAbspath = false;
70
71
72 protected $fileIdentifier;
73
74
75 protected $batchSize = 0;
76
77
78 protected $stagingSitePath = '';
79
80
81 protected $absPath = ABSPATH;
82
83
84 protected $wpContentDir = WP_CONTENT_DIR;
85
86
87
88
89 protected $isWpContent = false;
90
91 public function __construct(Filesystem $filesystem, Directory $directory, SiteInfo $siteInfo, Permissions $permissions, Strings $strings)
92 {
93 $this->filesystem = $filesystem;
94 $this->permissions = $permissions;
95 $this->strings = $strings;
96 $this->siteInfo = $siteInfo;
97 $this->directory = $directory;
98
99 $this->isWpContentOutsideAbspath = $this->siteInfo->isWpContentOutsideAbspath();
100 $this->absPath = $this->filesystem->normalizePath($this->directory->getAbsPath(), true);
101 $this->wpContentDir = $this->filesystem->normalizePath($this->directory->getWpContentDirectory(), true);
102 }
103
104
105
106
107
108
109
110 public function inject(SeekableQueueInterface $taskQueue, LoggerInterface $logger, StepsDto $stepsDto)
111 {
112 $this->taskQueue = $taskQueue;
113 $this->logger = $logger;
114 $this->stepsDto = $stepsDto;
115 }
116
117
118
119
120
121 public function setupBigFileBeingCopied(BigFileDto $bigFileDto)
122 {
123 if (empty($bigFileDto->getFilePath()) || $bigFileDto->getFileSize() <= 0) {
124 $this->bigFileDto = null;
125 return;
126 }
127
128 $this->bigFileDto = $bigFileDto;
129 }
130
131
132
133
134 public function getBigFileDto()
135 {
136 return $this->bigFileDto;
137 }
138
139
140
141
142
143
144
145 public function setup(string $stagingSitePath, string $fileIdentifier, bool $isWpContent = false)
146 {
147 $this->stagingSitePath = $this->filesystem->normalizePath($stagingSitePath, true);
148 $this->fileIdentifier = $fileIdentifier;
149 $this->isWpContent = $isWpContent;
150
151
152 $this->batchSize = Hooks::applyFilters(self::FILTER_COPY_BATCH_SIZE, self::BATCH_SIZE);
153 }
154
155
156
157
158 public function execute()
159 {
160 while (!$this->isThreshold() && !$this->stepsDto->isFinished()) {
161 try {
162 $this->copy();
163 } catch (FinishedQueueException $exception) {
164 $this->stepsDto->finish();
165 $this->logger->info(sprintf('Copied %d/%d %s files', $this->stepsDto->getCurrent(), $this->stepsDto->getTotal(), $this->fileIdentifier));
166
167 return;
168 } catch (DiskNotWritableException $exception) {
169
170 throw new \Exception('Disk is probably full. Error message: ' . $exception->getMessage());
171 } catch (\Throwable $th) {
172 throw new \Exception('Fail to copy file. Error message: ' . $th->getMessage());
173 }
174 }
175
176 if ($this->bigFileDto instanceof BigFileDto) {
177 $relativePathForLogging = str_replace($this->filesystem->normalizePath(ABSPATH, true), '', $this->filesystem->normalizePath($this->bigFileDto->getFilePath(), true));
178 $percentProcessed = ceil(($this->bigFileDto->getWrittenBytesTotal() / $this->bigFileDto->getFileSize()) * 100);
179 $this->logger->info(sprintf('Copying big %s file: %s - %s/%s (%s%%)', $this->fileIdentifier, $relativePathForLogging, size_format($this->bigFileDto->getWrittenBytesTotal(), 2), size_format($this->bigFileDto->getFileSize(), 2), $percentProcessed));
180 } else {
181 $this->logger->info(sprintf('Copied %d/%d %s files', $this->stepsDto->getCurrent(), $this->stepsDto->getTotal(), $this->fileIdentifier));
182 }
183 }
184
185
186
187
188
189
190 public function copy()
191 {
192 $path = $this->taskQueue->dequeue();
193 $path = $this->replacePlaceholdersWithEOLs($path);
194
195 if (is_null($path)) {
196 throw new FinishedQueueException();
197 }
198
199 if (empty($path)) {
200 return;
201 }
202
203 $indexPath = '';
204 if (strpos($path, FilesystemScanner::PATH_SEPARATOR) !== false) {
205 list($path, $indexPath) = explode(FilesystemScanner::PATH_SEPARATOR, $path);
206 }
207
208
209 $path = $this->maybePrependSitePath($path);
210
211 try {
212
213
214 if (is_dir($path)) {
215 $isFileWrittenCompletely = $this->processEmptyDirectory($path, $indexPath);
216 } else {
217 $isFileWrittenCompletely = $this->processFile($path, $indexPath);
218 }
219 } catch (\RuntimeException $e) {
220 $this->logger->warning($e->getMessage());
221 debug_log($e->getMessage());
222 $isFileWrittenCompletely = true;
223 } catch (\Throwable $th) {
224 throw $th;
225 }
226
227
228 if ($isFileWrittenCompletely === true) {
229 $this->stepsDto->incrementCurrentStep();
230 $this->bigFileDto = null;
231
232 return;
233 }
234
235
236 $this->taskQueue->retry(false);
237 }
238
239 protected function maybePrependSitePath(string $filePath): string
240 {
241 return $this->shouldPrependAbsPath() ? $this->absPath . $filePath : $filePath;
242 }
243
244
245
246
247
248
249
250 protected function shouldPrependAbsPath(): bool
251 {
252 return !$this->isWpContentOutsideAbspath || !$this->isWpContent;
253 }
254
255
256
257
258
259
260
261
262
263
264 protected function processEmptyDirectory(string $sourcePath, string $indexPath): bool
265 {
266 $staging = empty($indexPath) ? $sourcePath : $indexPath;
267 $staging = $this->filesystem->normalizePath($staging);
268
269 if ($this->isWpContentOutsideAbspath && $this->isWpContent) {
270 $relStagingPath = $this->strings->replaceStartWith($this->wpContentDir, '', $staging);
271 $destinationPath = $this->stagingSitePath . 'wp-content/' . $relStagingPath;
272 } else {
273 $relStagingPath = $this->strings->replaceStartWith($this->absPath, '', $staging);
274 $destinationPath = $this->stagingSitePath . $relStagingPath;
275 }
276
277 $destinationPath = $this->filesystem->normalizePath($destinationPath);
278
279 if (!is_dir($destinationPath) && !$this->filesystem->mkdir($destinationPath)) {
280 return false;
281 }
282
283 $this->chmod($destinationPath, $this->permissions->getDirectoryOctal());
284 return true;
285 }
286
287 protected function processFile(string $filePath, string $indexPath): bool
288 {
289
290 if (!is_file($filePath)) {
291 throw new \RuntimeException("Invalid file. Could not copy file: $filePath");
292 }
293
294
295 if (!$this->filesystem->isReadableFile($filePath)) {
296 throw new \RuntimeException("Can't read file {$filePath}");
297 }
298
299 $destinationPath = $this->getDestinationPath($filePath, $indexPath);
300
301
302 $fileSize = filesize($filePath);
303
304 $result = false;
305
306 if ($fileSize > $this->batchSize) {
307 $result = $this->copyBigFile($filePath, $destinationPath, $this->batchSize);
308 } else {
309 $result = $this->filesystem->copyFile($filePath, $destinationPath);
310 }
311
312 if (!$result) {
313 return false;
314 }
315
316
317 $this->chmod($destinationPath, $this->permissions->getFilePermission($destinationPath));
318
319 $this->setDirPermissions($destinationPath);
320
321 return true;
322 }
323
324 protected function copyBigFile(string $sourcePath, string $destinationPath, int $batchSize): bool
325 {
326 $this->setupBigFileCopy($sourcePath, $destinationPath);
327
328 if ($this->bigFileDto->isFinished()) {
329 return true;
330 }
331
332 $srcFile = fopen($sourcePath, 'rb');
333 $destFile = fopen($destinationPath, $this->getBigFileWriteMode($destinationPath));
334
335 if ($srcFile === false || $destFile === false) {
336 throw new \RuntimeException('Could not open file for reading or writing');
337 }
338
339 fseek($srcFile, $this->bigFileDto->getWrittenBytesTotal());
340
341 do {
342 $bytesWritten = fwrite($destFile, fread($srcFile, $batchSize));
343 if ($bytesWritten === false) {
344 throw new \RuntimeException('Could not write to file');
345 }
346
347 $this->bigFileDto->appendWrittenBytes($bytesWritten);
348 } while (!$this->isThreshold() && !$this->bigFileDto->isFinished());
349
350 fclose($srcFile);
351 fclose($destFile);
352 $srcFile = null;
353 $destFile = null;
354
355 return $this->bigFileDto->getWrittenBytesTotal() === $this->bigFileDto->getFileSize();
356 }
357
358 protected function setupBigFileCopy(string $sourcePath, string $destinationPath)
359 {
360 if ($this->bigFileDto instanceof BigFileDto && $this->isSameBigFileCopy($sourcePath, $destinationPath)) {
361 return;
362 }
363
364 $this->bigFileDto = new BigFileDto();
365 $this->bigFileDto->setFilePath($sourcePath);
366 $this->bigFileDto->setDestinationPath($destinationPath);
367 $this->bigFileDto->setFileSize(filesize($sourcePath));
368 $this->bigFileDto->setWrittenBytesTotal(0);
369 }
370
371 protected function isSameBigFileCopy(string $sourcePath, string $destinationPath): bool
372 {
373 return $this->bigFileDto->getFilePath() === wp_normalize_path($sourcePath)
374 && $this->bigFileDto->getDestinationPath() === wp_normalize_path($destinationPath)
375 && $this->bigFileDto->getFileSize() === filesize($sourcePath);
376 }
377
378 protected function getBigFileWriteMode(string $destinationPath): string
379 {
380 if ($this->bigFileDto->getWrittenBytesTotal() <= 0) {
381 return 'wb';
382 }
383
384 if (!is_file($destinationPath) || filesize($destinationPath) !== $this->bigFileDto->getWrittenBytesTotal()) {
385 $this->bigFileDto->setWrittenBytesTotal(0);
386 return 'wb';
387 }
388
389 return 'ab';
390 }
391
392
393
394
395
396
397
398
399 protected function getDestinationPath(string $filePath, string $indexPath): string
400 {
401 if (empty($indexPath)) {
402 $stagingPath = $filePath;
403 } else {
404 $stagingPath = $indexPath;
405 }
406
407 $stagingPath = $this->filesystem->normalizePath($stagingPath);
408 if ($this->isWpContentOutsideAbspath && $this->isWpContent) {
409 $relStagingPath = $this->strings->replaceStartWith($this->wpContentDir, '', $stagingPath);
410 $destinationPath = $this->stagingSitePath . 'wp-content/' . $relStagingPath;
411 } else {
412 $relStagingPath = $this->strings->replaceStartWith($this->absPath, '', $stagingPath);
413 $destinationPath = $this->stagingSitePath . $relStagingPath;
414 }
415
416 $destinationDirectory = dirname($destinationPath);
417
418 if (is_dir($destinationDirectory)) {
419 return $this->filesystem->normalizePath($destinationPath);
420 }
421
422
423 if ($this->filesystem->mkdir($destinationDirectory)) {
424 return $this->filesystem->normalizePath($destinationPath);
425 }
426
427
428 if (!is_dir($destinationDirectory)) {
429 throw new \RuntimeException("Can not create directory {$destinationDirectory}." . $this->filesystem->getLogs()[0]);
430 }
431
432 return $this->filesystem->normalizePath($destinationPath);
433 }
434
435 private function setDirPermissions(string $file): bool
436 {
437 $dir = dirname($file);
438 if (is_dir($dir)) {
439 return $this->chmod($dir, $this->permissions->getDirectoryOctal());
440 }
441
442 return false;
443 }
444
445
446
447
448
449
450 protected function chmod(string $path, int $mode): bool
451 {
452 $lastWarning = null;
453 set_error_handler(function ($severity, $message, $file, $line) use (&$lastWarning) {
454 if (($severity & (E_WARNING | E_NOTICE)) === 0 || $file !== __FILE__ || strpos($message, 'chmod') !== 0) {
455 return false;
456 }
457
458 $lastWarning = $message;
459 return true;
460 });
461
462 try {
463 $result = \chmod($path, $mode);
464 } finally {
465 restore_error_handler();
466 }
467
468 if ($lastWarning !== null) {
469 debug_log(sprintf('chmod warning: %s', $lastWarning));
470 }
471
472 return $result !== false;
473 }
474 }
475