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