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