PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.8.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.8.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 / Staging / Service / FileCopier.php
wp-staging / Staging / Service Last commit date
Database 2 months ago AbstractStagingSetup.php 4 months ago DirectoryScanner.php 4 months ago FileCopier.php 2 months ago StagingSetup.php 4 months ago TableScanner.php 8 months ago
FileCopier.php
372 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 * This class is responsible for copying files from the current site to the staging site in batches.
25 */
26 class FileCopier
27 {
28 use ResourceTrait;
29 use EndOfLinePlaceholderTrait;
30
31 /**
32 * @var int 512KB
33 */
34 const BATCH_SIZE = 512 * 1024;
35
36 /**
37 * @var string
38 */
39 const FILTER_COPY_BATCH_SIZE = 'wpstg.clone.copy_batch_size';
40
41 /** @var Filesystem */
42 protected $filesystem;
43
44 /** @var Directory */
45 protected $directory;
46
47 /** @var SiteInfo */
48 protected $siteInfo;
49
50 /** @var Permissions */
51 protected $permissions;
52
53 /** @var Strings */
54 protected $strings;
55
56 /** @var SeekableQueueInterface */
57 protected $taskQueue;
58
59 /** @var LoggerInterface */
60 protected $logger;
61
62 /** @var StepsDto */
63 protected $stepsDto;
64
65 /** @var ?BigFileDto If a file couldn't be processed in a single request, this will be populated */
66 protected $bigFileDto = null;
67
68 /** @var bool */
69 protected $isWpContentOutsideAbspath = false;
70
71 /** @var string */
72 protected $fileIdentifier;
73
74 /** @var int */
75 protected $batchSize = 0;
76
77 /** @var string */
78 protected $stagingSitePath = '';
79
80 /** @var string */
81 protected $absPath = ABSPATH;
82
83 /** @var string */
84 protected $wpContentDir = WP_CONTENT_DIR;
85
86 /**
87 * @var bool
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 * @param SeekableQueueInterface $taskQueue
106 * @param LoggerInterface $logger
107 * @param StepsDto $stepsDto
108 * @return void
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 * @param BigFileDto $bigFileDto
119 * @return void
120 */
121 public function setupBigFileBeingCopied(BigFileDto $bigFileDto)
122 {
123 $this->bigFileDto = $bigFileDto;
124 }
125
126 /**
127 * @return ?BigFileDto
128 */
129 public function getBigFileDto()
130 {
131 return $this->bigFileDto;
132 }
133
134 /**
135 * @param string $stagingSitePath
136 * @param string $fileIdentifier
137 * @param bool $isWpContent
138 * @return void
139 */
140 public function setup(string $stagingSitePath, string $fileIdentifier, bool $isWpContent = false)
141 {
142 $this->stagingSitePath = $this->filesystem->normalizePath($stagingSitePath, true);
143 $this->fileIdentifier = $fileIdentifier;
144 $this->isWpContent = $isWpContent;
145
146 // Default batch size is 512KB
147 $this->batchSize = Hooks::applyFilters(self::FILTER_COPY_BATCH_SIZE, self::BATCH_SIZE);
148 }
149
150 /**
151 * @return void
152 */
153 public function execute()
154 {
155 while (!$this->isThreshold() && !$this->stepsDto->isFinished()) {
156 try {
157 $this->copy();
158 } catch (FinishedQueueException $exception) {
159 $this->stepsDto->finish();
160 $this->logger->info(sprintf('Copied %d/%d %s files', $this->stepsDto->getCurrent(), $this->stepsDto->getTotal(), $this->fileIdentifier));
161
162 return;
163 } catch (DiskNotWritableException $exception) {
164 // Probably disk full. Should be handled in Job\AbstractJob::prepareAndExecute(). Let's stop the code here if it did not happen!
165 throw new \Exception('Disk is probably full. Error message: ' . $exception->getMessage());
166 } catch (\Throwable $th) {
167 throw new \Exception('Fail to copy file. Error message: ' . $th->getMessage());
168 }
169 }
170
171 if ($this->bigFileDto instanceof BigFileDto) {
172 $relativePathForLogging = str_replace($this->filesystem->normalizePath(ABSPATH, true), '', $this->filesystem->normalizePath($this->bigFileDto->getFilePath(), true));
173 $percentProcessed = ceil(($this->bigFileDto->getWrittenBytesTotal() / $this->bigFileDto->getFileSize()) * 100);
174 $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));
175 } else {
176 $this->logger->info(sprintf('Copied %d/%d %s files', $this->stepsDto->getCurrent(), $this->stepsDto->getTotal(), $this->fileIdentifier));
177 }
178 }
179
180 /**
181 * @throws DiskNotWritableException
182 * @throws FinishedQueueException
183 * @return void
184 */
185 public function copy()
186 {
187 $path = $this->taskQueue->dequeue();
188 $path = $this->replacePlaceholdersWithEOLs($path);
189
190 if (is_null($path)) {
191 throw new FinishedQueueException();
192 }
193
194 if (empty($path)) {
195 return;
196 }
197
198 $indexPath = '';
199 if (strpos($path, FilesystemScanner::PATH_SEPARATOR) !== false) {
200 list($path, $indexPath) = explode(FilesystemScanner::PATH_SEPARATOR, $path);
201 }
202
203 // When wp-content is inside of ABSPATH, we need to prepend ABSPATH to the file path, as it was removed while scanning
204 $path = $this->maybePrependSitePath($path);
205
206 try {
207 $isFileWrittenCompletely = $this->processFile($path, $indexPath);
208 } catch (\RuntimeException $e) {
209 $this->logger->warning($e->getMessage());
210 debug_log($e->getMessage());
211 $isFileWrittenCompletely = true;
212 } catch (\Throwable $th) {
213 throw $th;
214 }
215
216 // Done processing this file
217 if ($isFileWrittenCompletely === true) {
218 $this->stepsDto->incrementCurrentStep();
219 $this->bigFileDto = null;
220
221 return;
222 }
223
224 // Processing a file that could not be finished in this request
225 $this->taskQueue->retry(false);
226 }
227
228 protected function maybePrependSitePath(string $filePath): string
229 {
230 if ($this->shouldPrependAbsPath()) {
231 return $this->absPath . $filePath;
232 }
233
234 return $filePath;
235 }
236
237 protected function shouldPrependAbsPath(): bool
238 {
239 return $this->isWpContentOutsideAbspath === false;
240 }
241
242 protected function processFile(string $filePath, string $indexPath): bool
243 {
244 // Invalid file
245 if (!is_file($filePath)) {
246 throw new \RuntimeException("Invalid file. Could not copy file: $filePath");
247 }
248
249 // If file is unreadable, skip it as if succeeded
250 if (!$this->filesystem->isReadableFile($filePath)) {
251 throw new \RuntimeException("Can't read file {$filePath}");
252 }
253
254 $destinationPath = $this->getDestinationPath($filePath, $indexPath);
255
256 // Get file size
257 $fileSize = filesize($filePath);
258
259 $result = false;
260 // File is over batch size
261 if ($fileSize >= $this->batchSize) {
262 $result = $this->copyBigFile($filePath, $destinationPath, $this->batchSize);
263 } else {
264 $result = $this->filesystem->copyFile($filePath, $destinationPath);
265 }
266
267 if (!$result) {
268 return false;
269 }
270
271 // Set file permissions
272 @chmod($destinationPath, $this->permissions->getFilesOctal());
273
274 $this->setDirPermissions($destinationPath);
275
276 return true;
277 }
278
279 protected function copyBigFile(string $sourcePath, string $destinationPath, int $batchSize): bool
280 {
281 if ($this->bigFileDto === null) {
282 $this->bigFileDto = new BigFileDto();
283 $this->bigFileDto->setFilePath($sourcePath);
284 $this->bigFileDto->setFileSize(filesize($sourcePath));
285
286 $this->bigFileDto->setWrittenBytesTotal(0);
287 }
288
289 if ($this->bigFileDto->isFinished()) {
290 return true;
291 }
292
293 $srcFile = fopen($sourcePath, 'rb');
294 $destFile = fopen($destinationPath, 'ab');
295
296 if ($srcFile === false || $destFile === false) {
297 throw new \RuntimeException('Could not open file for reading or writing');
298 }
299
300 fseek($srcFile, $this->bigFileDto->getWrittenBytesTotal());
301
302 do {
303 $bytesWritten = fwrite($destFile, fread($srcFile, $batchSize));
304 if ($bytesWritten === false) {
305 throw new \RuntimeException('Could not write to file');
306 }
307
308 $this->bigFileDto->appendWrittenBytes($bytesWritten);
309 } while (!$this->isThreshold() && !$this->bigFileDto->isFinished());
310
311 fclose($srcFile);
312 fclose($destFile);
313 $srcFile = null;
314 $destFile = null;
315
316 return $this->bigFileDto->getWrittenBytesTotal() === $this->bigFileDto->getFileSize();
317 }
318
319 /**
320 * Gets destination file and checks if the directory exists, if it does not attempts to create it.
321 * If creating destination directory fails, it will throw exception.
322 * @param string $filePath
323 * @param string $indexPath
324 * @return string
325 */
326 protected function getDestinationPath(string $filePath, string $indexPath): string
327 {
328 if (empty($indexPath)) {
329 $stagingPath = $filePath;
330 } else {
331 $stagingPath = $indexPath;
332 }
333
334 $stagingPath = $this->filesystem->normalizePath($stagingPath);
335 if ($this->isWpContentOutsideAbspath && $this->isWpContent) {
336 $relStagingPath = $this->strings->replaceStartWith($this->wpContentDir, '', $stagingPath);
337 $destinationPath = $this->stagingSitePath . 'wp-content/' . $relStagingPath;
338 } else {
339 $relStagingPath = $this->strings->replaceStartWith($this->absPath, '', $stagingPath);
340 $destinationPath = $this->stagingSitePath . $relStagingPath;
341 }
342
343 $destinationDirectory = dirname($destinationPath);
344 // If directory already exists, return the destination path
345 if (is_dir($destinationDirectory)) {
346 return $this->filesystem->normalizePath($destinationPath);
347 }
348
349 // If directory does not exist, create it
350 if ($this->filesystem->mkdir($destinationDirectory)) {
351 return $this->filesystem->normalizePath($destinationPath);
352 }
353
354 // If directory still does not exist, throw an exception
355 if (!is_dir($destinationDirectory)) {
356 throw new \RuntimeException("Can not create directory {$destinationDirectory}." . $this->filesystem->getLogs()[0]);
357 }
358
359 return $this->filesystem->normalizePath($destinationPath);
360 }
361
362 private function setDirPermissions(string $file): bool
363 {
364 $dir = dirname($file);
365 if (is_dir($dir)) {
366 return @chmod($dir, $this->permissions->getDirectoryOctal());
367 }
368
369 return false;
370 }
371 }
372