PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.3.1
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 / Compressor.php
wp-staging / Backup / Service Last commit date
Database 2 years ago Multipart 3 years ago BackupAssets.php 3 years ago BackupMetadataEditor.php 3 years ago BackupsFinder.php 2 years ago Compressor.php 2 years ago Extractor.php 2 years ago
Compressor.php
633 lines
1 <?php
2
3 // TODO PHP7.x; declare(strict_types=1);
4 // TODO PHP7.x; return types && type-hints
5 // TODO PHP7.1; constant visibility
6
7 namespace WPStaging\Backup\Service;
8
9 use Exception;
10 use LogicException;
11 use RuntimeException;
12 use WPStaging\Backup\Dto\Job\JobBackupDataDto;
13 use WPStaging\Backup\Dto\JobDataDto;
14 use WPStaging\Backup\Dto\Service\CompressorDto;
15 use WPStaging\Backup\Exceptions\DiskNotWritableException;
16 use WPStaging\Backup\Service\Multipart\MultipartSplitInterface;
17 use WPStaging\Core\WPStaging;
18 use WPStaging\Framework\Adapter\Directory;
19 use WPStaging\Framework\Adapter\PhpAdapter;
20 use WPStaging\Framework\Filesystem\PathIdentifier;
21 use WPStaging\Framework\Utils\Cache\BufferedCache;
22 use WPStaging\Vendor\lucatume\DI52\NotFoundException;
23
24 use function WPStaging\functions\debug_log;
25
26 class Compressor
27 {
28 const BACKUP_DIR_NAME = 'backups';
29
30 /** @var BufferedCache */
31 private $tempBackupIndex;
32
33 /** @var BufferedCache */
34 private $tempBackup;
35
36 /** @var CompressorDto */
37 private $compressorDto;
38
39 /** @var PathIdentifier */
40 private $pathIdentifier;
41
42 /** @var int */
43 private $compressedFileSize = 0;
44
45 /** @var JobDataDto */
46 private $jobDataDto;
47
48 /** @var PhpAdapter */
49 private $phpAdapter;
50
51 /** @var MultipartSplitInterface */
52 private $multipartSplit;
53
54 /**
55 * Category can be: empty string|null|false, plugins, mu-plugins, themes, uploads, other, database
56 * Where empty string|null|false is used for single file backup,
57 * And other is for files from wp-content not including plugins, mu-plugins, themes, uploads
58 * @var string
59 */
60 private $category = '';
61
62 /**
63 * The current index of category in which appending files
64 * Not used in single file backup
65 * @var int
66 */
67 private $categoryIndex = 0;
68
69 /** @var bool */
70 private $isLocalBackup = false;
71
72 /** @var int */
73 protected $bytesWrittenInThisRequest = 0;
74
75 // TODO telescoped
76 public function __construct(BufferedCache $cacheIndex, BufferedCache $tempBackup, PathIdentifier $pathIdentifier, JobDataDto $jobDataDto, CompressorDto $compressorDto, PhpAdapter $phpAdapter, MultipartSplitInterface $multipartSplit)
77 {
78 $this->jobDataDto = $jobDataDto;
79 $this->compressorDto = $compressorDto;
80 $this->tempBackupIndex = $cacheIndex;
81 $this->tempBackup = $tempBackup;
82 $this->pathIdentifier = $pathIdentifier;
83 $this->phpAdapter = $phpAdapter;
84 $this->multipartSplit = $multipartSplit;
85
86 $this->setCategory('');
87 }
88
89 /**
90 * @param int $index
91 * @param bool $isCreateBinaryHeader
92 */
93 public function setCategoryIndex($index, $isCreateBinaryHeader = true)
94 {
95 if (empty($index)) {
96 $index = 0;
97 }
98
99 $this->categoryIndex = $index;
100 $this->setCategory($this->category, $isCreateBinaryHeader);
101 }
102
103 /**
104 * @param string $category
105 * @param bool $isCreateBinaryHeader
106 */
107 public function setCategory($category = '', $isCreateBinaryHeader = false)
108 {
109 $this->category = $category;
110 $this->setupTmpBackupFile();
111
112 if ($isCreateBinaryHeader && !$this->tempBackup->isValid()) {
113 // Create temp file with binary header
114 $this->tempBackup->save(file_get_contents(WPSTG_PLUGIN_DIR . 'Backup/wpstgBackupHeader.txt'));
115 }
116 }
117
118 /**
119 * Setup temp backup file and temp files index file for the given job id,
120 * If multipart backup category and category index are given, then they are used to create unique file names
121 */
122 public function setupTmpBackupFile()
123 {
124 $additionalInfo = empty($this->category) ? '' : $this->category . '_' . $this->categoryIndex . '_';
125
126 $postFix = $additionalInfo . $this->jobDataDto->getId();
127
128 //debug_log("[Set Tmp Backup Files] File name postfix: " . $postFix);
129
130 $this->tempBackup->setFilename('temp_wpstg_backup_' . $postFix);
131 $this->tempBackup->setLifetime(DAY_IN_SECONDS);
132
133 $tempBackupIndexFilePrefix = 'temp_backup_index_';
134 $this->tempBackupIndex->setFilename($tempBackupIndexFilePrefix . $postFix);
135 $this->tempBackupIndex->setLifetime(DAY_IN_SECONDS);
136 }
137
138 /**
139 * @param int $fileSize
140 * @param int $maxPartSize
141 * @return bool
142 */
143 public function doExceedMaxPartSize($fileSize, $maxPartSize)
144 {
145 $allowedSize = $fileSize - $this->compressorDto->getWrittenBytesTotal();
146 $sizeAfterAdding = $allowedSize + filesize($this->tempBackup->getFilePath());
147 return $sizeAfterAdding >= $maxPartSize;
148 }
149
150 /**
151 * @var bool $isLocalBackup
152 */
153 public function setIsLocalBackup($isLocalBackup)
154 {
155 $this->isLocalBackup = $isLocalBackup;
156 }
157
158 /**
159 * @return CompressorDto
160 */
161 public function getDto()
162 {
163 return $this->compressorDto;
164 }
165
166 /**
167 * @return int
168 */
169 public function getBytesWrittenInThisRequest()
170 {
171 return $this->bytesWrittenInThisRequest;
172 }
173
174 /**
175 * @param string $fullFilePath
176 * @param string $indexPath
177 *
178 * `true` -> finished
179 * `false` -> not finished
180 * `null` -> skip / didn't do anything
181 *
182 * @throws DiskNotWritableException
183 * @throws RuntimeException
184 *
185 * @return bool|null
186 */
187 public function appendFileToBackup(string $fullFilePath, string $indexPath = '')
188 {
189 // We can use evil '@' as we don't check is_file || file_exists to speed things up.
190 // Since in this case speed > anything else
191 // However if @ is not used, depending on if file exists or not this can throw E_WARNING.
192 $resource = @fopen($fullFilePath, 'rb');
193 if (!$resource) {
194 debug_log("appendFileToBackup(): Can't open file {$fullFilePath} for reading");
195 return null;
196 }
197
198 if (empty($indexPath)) {
199 $indexPath = $fullFilePath;
200 }
201
202 $fileStats = fstat($resource);
203 $this->initiateDtoByFilePath($fullFilePath, $fileStats);
204 $this->compressorDto->setIndexPath($indexPath);
205 $writtenBytesBefore = $this->compressorDto->getWrittenBytesTotal();
206 $writtenBytesTotal = $this->appendToCompressedFile($resource, $fullFilePath);
207 $bytesAddedForIndex = $this->addIndex($writtenBytesTotal);
208 $retries = 0;
209 while ($bytesAddedForIndex === 0 && $retries < 3) {
210 $delayInMs = $this->getDelayForRetry($retries);
211 // sleep in ms
212 usleep($delayInMs);
213 $bytesAddedForIndex = $this->addIndex($writtenBytesTotal);
214 $retries++;
215 }
216
217 $this->compressorDto->setWrittenBytesTotal($writtenBytesTotal);
218
219 $this->bytesWrittenInThisRequest += $writtenBytesTotal - $writtenBytesBefore;
220
221 $isFinished = $this->compressorDto->isFinished();
222
223 $this->compressorDto->resetIfFinished();
224
225 return $isFinished;
226 }
227
228 /**
229 * @param string $filePath
230 * @param array $fileStats
231 */
232 public function initiateDtoByFilePath($filePath, array $fileStats = [])
233 {
234 if ($filePath === null || ($filePath === $this->compressorDto->getFilePath() && $fileStats['size'] === $this->compressorDto->getFileSize())) {
235 return;
236 }
237
238 $this->compressorDto->setFilePath($filePath);
239 $this->compressorDto->setFileSize($fileStats['size']);
240 }
241
242 /**
243 * @param int $sizeBeforeAddingIndex
244 * @param string $category
245 * @param string $partName
246 * @param int $categoryIndex
247 */
248 public function generateBackupMetadataForBackupPart($sizeBeforeAddingIndex, $category, $partName, $categoryIndex)
249 {
250 $this->category = $category;
251 $this->categoryIndex = $categoryIndex;
252 $this->setupTmpBackupFile();
253 $this->generateBackupMetadata($sizeBeforeAddingIndex, $partName, $isBackupPart = true);
254 }
255
256 /**
257 * Combines index and compressed file, renames / moves it to destination
258 *
259 * This function is called only once, so performance improvements has no impact here.
260 *
261 * @param int $backupSizeBeforeAddingIndex
262 * @param string $finalFileNameOnRename
263 * @param bool $isBackupPart
264 *
265 * @return string|null
266 */
267 public function generateBackupMetadata($backupSizeBeforeAddingIndex = 0, $finalFileNameOnRename = '', $isBackupPart = false)
268 {
269 clearstatcache();
270 $backupSizeAfterAddingIndex = filesize($this->tempBackup->getFilePath());
271
272 $backupMetadata = $this->compressorDto->getBackupMetadata();
273 $backupMetadata->setHeaderStart($backupSizeBeforeAddingIndex);
274 $backupMetadata->setHeaderEnd($backupSizeAfterAddingIndex);
275
276 if ($isBackupPart) {
277 $this->multipartSplit->updateMultipartMetadata($this->jobDataDto, $backupMetadata, $this->category, $this->categoryIndex);
278 }
279
280 if ($this->jobDataDto instanceof JobBackupDataDto) {
281 /** @var JobBackupDataDto */
282 $jobDataDto = $this->jobDataDto;
283 $backupMetadata->setIndexPartSize($jobDataDto->getCategorySizes());
284 }
285
286 $this->tempBackup->append(json_encode($backupMetadata));
287
288 return $this->renameBackup($finalFileNameOnRename);
289 }
290
291 /**
292 * @return array
293 */
294 public function getFinalizeBackupInfo()
295 {
296 return [
297 'category' => $this->category,
298 'index' => $this->categoryIndex,
299 'filePath' => $this->tempBackup->getFilePath(),
300 'destination' => $this->getDestinationPath(),
301 'status' => 'Pending',
302 'sizeBeforeAddingIndex' => 0
303 ];
304 }
305
306 /** @return int|null */
307 public function addFileIndex()
308 {
309 clearstatcache();
310 $indexResource = fopen($this->tempBackupIndex->getFilePath(), 'rb');
311
312 if (!$indexResource) {
313 debug_log('[Add File Index] Nothing to backup, no index resource! File Index: ' . $this->tempBackupIndex->getFilePath());
314 throw new NotFoundException('Nothing to backup, no index resource found!');
315 }
316
317 static $isFirstInsert = false;
318 $insertSeparator = '';
319 if ($isFirstInsert === false) {
320 $lastLine = $this->tempBackup->readLastLine();
321 if (!empty($lastLine) && preg_match('@^INSERT\sINTO\s@', $lastLine)) {
322 $isFirstInsert = true;
323 $insertSeparator = "\n--\n-- SQL DATA END\n--\n";
324 $this->tempBackup->append($insertSeparator);
325 $this->tempBackup->deleteBottomBytes(strlen(PHP_EOL));
326 }
327 }
328
329 $indexStats = fstat($indexResource);
330 $this->initiateDtoByFilePath($this->tempBackupIndex->getFilePath(), $indexStats);
331
332 $lastLine = $this->tempBackup->readLastLine();
333 $writtenBytes = $this->compressorDto->getWrittenBytesTotal();
334 if ($lastLine !== PHP_EOL && $writtenBytes === 0) {
335 $this->tempBackup->append(''); // ensure that file index start from new line. See https://github.com/wp-staging/wp-staging-pro/issues/2861
336 }
337
338 clearstatcache();
339 $backupSizeBeforeAddingIndex = filesize($this->tempBackup->getFilePath());
340
341 // Write the index to the backup file, regardless of resource limits threshold
342 // @throws Exception
343 $writtenBytes = $this->appendToCompressedFile($indexResource, $this->tempBackupIndex->getFilePath());
344 $this->compressorDto->setWrittenBytesTotal($writtenBytes);
345
346 if ($writtenBytes === 0) {
347 $this->jobDataDto->setRetries($this->jobDataDto->getRetries() + 1);
348 } else {
349 $this->jobDataDto->setRetries(0);
350 }
351
352 // close the index file handle to make it deletable for Windows where PHP < 7.3
353 fclose($indexResource);
354
355 if ($this->jobDataDto->getRetries() > 3) {
356 debug_log('[Add File Index] Failed to write files-index to backup file!');
357 throw new Exception('Failed to write files-index to backup file!');
358 } elseif ($writtenBytes === 0) {
359 debug_log('[Add File Index] Failed to write any byte to files-index! Retrying...');
360 }
361
362 if (!$this->compressorDto->isFinished()) {
363 return null;
364 }
365
366 $this->tempBackupIndex->delete();
367 $this->compressorDto->reset();
368
369 $this->tempBackup->append(PHP_EOL);
370
371 return $backupSizeBeforeAddingIndex;
372 }
373
374 /**
375 * @return string
376 */
377 private function getDestinationPath()
378 {
379 $extension = "wpstg";
380
381 if ($this->category !== '') {
382 $index = $this->categoryIndex === 0 ? '' : ($this->categoryIndex . '.');
383 $extension = $this->category . '.' . $index . $extension;
384 }
385
386 return sprintf(
387 '%s_%s_%s.%s',
388 parse_url(get_home_url())['host'],
389 current_time('Ymd-His'),
390 $this->jobDataDto->getId(),
391 $extension
392 );
393 }
394
395 /**
396 * @param string $renameFileTo
397 * @param bool $isLocalBackup
398 * @return string
399 */
400 public function getFinalPath($renameFileTo = '', $isLocalBackup = true)
401 {
402 $backupsDirectory = $this->getFinalBackupParentDirectory($isLocalBackup);
403 if ($renameFileTo === '') {
404 $renameFileTo = $this->getDestinationPath();
405 }
406
407 return $backupsDirectory . $renameFileTo;
408 }
409
410 /**
411 * @return string
412 */
413 public function getFinalBackupParentDirectory($isLocalBackup = true)
414 {
415 if ($isLocalBackup) {
416 return WPStaging::make(BackupsFinder::class)->getBackupsDirectory();
417 }
418
419 return WPStaging::make(Directory::class)->getCacheDirectory();
420 }
421
422 /**
423 * Get delay in milliseconds for retry according to retry number
424 *
425 * @param int $retry
426 * @return float
427 */
428 protected function getDelayForRetry($retry)
429 {
430 $delay = 0.1;
431 for ($i = 0; $i < $retry; $i++) {
432 $delay *= 2;
433 }
434
435 return $delay * 1000;
436 }
437
438 /**
439 * Convert PIPE character (|) to {WPSTG_PIPE}
440 * Convert COLON character (:) to {WPSTG_COLON}
441 * @param string $filePath
442 * @return string
443 */
444 protected function filterPathForFileIndex(string $filePath): string
445 {
446 return str_replace(['|', ':'], ['{WPSTG_PIPE}', '{WPSTG_COLON}'], $filePath);
447 }
448
449 /** @var string $renameFileTo */
450 private function renameBackup($renameFileTo = '')
451 {
452 if ($renameFileTo === '') {
453 $renameFileTo = $this->getDestinationPath();
454 }
455
456 $destination = trailingslashit(dirname($this->tempBackup->getFilePath())) . $renameFileTo;
457 if ($this->isLocalBackup) {
458 $destination = $this->getFinalPath($renameFileTo);
459 }
460
461 if (!rename($this->tempBackup->getFilePath(), $destination)) {
462 throw new RuntimeException('Failed to generate destination');
463 }
464
465 return $destination;
466 }
467
468 /**
469 * @param int $writtenBytesTotal
470 * @return int
471 * @throws \WPStaging\Framework\Exceptions\IOException
472 * @throws LogicException
473 * @throws RuntimeException
474 */
475 private function addIndex($writtenBytesTotal)
476 {
477 clearstatcache();
478 if (file_exists($this->tempBackup->getFilePath())) {
479 $this->compressedFileSize = filesize($this->tempBackup->getFilePath());
480 }
481
482 $start = max($this->compressedFileSize - $writtenBytesTotal, 0);
483
484 if ($this->compressorDto->isIndexPositionCreated($this->category, $this->categoryIndex)) {
485 return $this->updateIndexInformationForAlreadyAddedIndex($writtenBytesTotal);
486 }
487
488 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->compressorDto->getIndexPath());
489 $identifiablePath = $this->filterPathForFileIndex($identifiablePath);
490 $info = $identifiablePath . '|' . $start . ':' . $writtenBytesTotal;
491 $bytesWritten = $this->tempBackupIndex->append($info);
492 $this->compressorDto->setIndexPositionCreated(true);
493
494 $this->addIndexPartSize($identifiablePath, $writtenBytesTotal);
495
496 /**
497 * We require JobDataDto in the constructor because it is wired in the DI container
498 * to the current job DTO instance. However, here we need to make sure this DTO
499 * is the jobBackupDataDto.
500 */
501 if (!$this->phpAdapter->isCallable([$this->jobDataDto, 'setTotalFiles']) || !$this->phpAdapter->isCallable([$this->jobDataDto, 'getTotalFiles'])) {
502 debug_log('This method can only be called from the context of Backup');
503 throw new LogicException('This method can only be called from the context of Backup');
504 }
505
506 /** @var JobBackupDataDto $jobBackupDataDto */
507 $jobBackupDataDto = $this->jobDataDto;
508 $jobBackupDataDto->setTotalFiles($jobBackupDataDto->getTotalFiles() + 1);
509
510 $this->multipartSplit->incrementFileCountInPart($jobBackupDataDto, $this->category, $this->categoryIndex);
511
512 return $bytesWritten;
513 }
514
515 /**
516 * At the moment this is used when processing adding of big file which is not done in a single request
517 * @param int $writtenBytesTotal
518 * @return int
519 * @throws RuntimeException
520 */
521 private function updateIndexInformationForAlreadyAddedIndex($writtenBytesTotal)
522 {
523 $lastLine = $this->tempBackupIndex->readLines(1, null, BufferedCache::POSITION_BOTTOM);
524 if (!is_array($lastLine)) {
525 debug_log('Failed to read backup metadata file index information. Error: The last line is no array. Last line: ' . $lastLine);
526 throw new RuntimeException('Failed to read backup metadata file index information. Error: The last line is no array.');
527 }
528
529 $lastLine = array_filter($lastLine, function ($item) {
530 return !empty($item) && strpos($item, ':') !== false && strpos($item, '|') !== false;
531 });
532
533 if (count($lastLine) !== 1) {
534 debug_log('Failed to read backup metadata file index information. Error: The last line is not an array or element with countable interface. Last line: ' . print_r($lastLine, 1));
535 throw new RuntimeException('Failed to read backup metadata file index information. Error: The last line is not an array or element with countable interface.');
536 }
537
538 $lastLine = array_shift($lastLine);
539
540 list($relativePath, $indexPosition) = explode('|', trim($lastLine));
541
542 // ['9378469', '4491']
543 list($offsetStart, $writtenPreviously) = explode(':', trim($indexPosition));
544
545 // @todo Should we use mb_strlen($_writtenBytes, '8bit') instead of strlen?
546 $this->tempBackupIndex->deleteBottomBytes(strlen($lastLine));
547
548 $identifiablePath = $this->pathIdentifier->transformPathToIdentifiable($this->compressorDto->getIndexPath());
549 $identifiablePath = $this->filterPathForFileIndex($identifiablePath);
550 $info = $identifiablePath . '|' . $offsetStart . ':' . $writtenBytesTotal;
551 $bytesWritten = $this->tempBackupIndex->append($info);
552 $this->compressorDto->setIndexPositionCreated(true, $this->category, $this->categoryIndex);
553
554 // We only need to increment newly added bytes
555 $this->addIndexPartSize($identifiablePath, $writtenBytesTotal - (int)$writtenPreviously);
556
557 return $bytesWritten;
558 }
559
560 /**
561 * @param $resource
562 * @param $filePath
563 *
564 * @return int
565 * @throws DiskNotWritableException
566 * @throws RuntimeException
567 */
568 private function appendToCompressedFile($resource, $filePath)
569 {
570 try {
571 return $this->tempBackup->appendFile(
572 $resource,
573 $this->compressorDto->getWrittenBytesTotal()
574 );
575 } catch (DiskNotWritableException $e) {
576 debug_log('Failed to write to file: ' . $filePath);
577 // Re-throw for readability
578 throw $e;
579 }
580 }
581
582 /**
583 * @param string $identifiablePath
584 * @param int $newBytesWritten
585 */
586 private function addIndexPartSize($identifiablePath, $newBytesWritten)
587 {
588 // Early bail if jobDataDto is not instance of jobBackupDataDto
589 if (!$this->jobDataDto instanceof JobBackupDataDto) {
590 return;
591 }
592
593 /** @var JobBackupDataDto $jobDataDto */
594 $jobDataDto = $this->jobDataDto;
595
596 $collectPartsize = $jobDataDto->getCategorySizes();
597
598 $partName = 'unknownSize';
599 switch ($identifiablePath) {
600 case ($this->pathIdentifier::IDENTIFIER_WP_CONTENT === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_WP_CONTENT))):
601 $partName = 'wpcontentSize';
602 break;
603 case ($this->pathIdentifier::IDENTIFIER_PLUGINS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_PLUGINS))):
604 $partName = 'pluginsSize';
605 break;
606 case ($this->pathIdentifier::IDENTIFIER_THEMES === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_THEMES))):
607 $partName = 'themesSize';
608 break;
609 case ($this->pathIdentifier::IDENTIFIER_MUPLUGINS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_MUPLUGINS))):
610 $partName = 'mupluginsSize';
611 break;
612 case ($this->pathIdentifier::IDENTIFIER_UPLOADS === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_UPLOADS))):
613 $partName = 'uploadsSize';
614 if (substr($identifiablePath, -4) === '.sql') {
615 $partName = 'sqlSize';
616 }
617
618 break;
619 case ($this->pathIdentifier::IDENTIFIER_LANG === substr($identifiablePath, 0, strlen($this->pathIdentifier::IDENTIFIER_LANG))):
620 $partName = 'langSize';
621 break;
622 }
623
624 // TODO: This should never happen. Log this when we have our own Logger, see https://github.com/wp-staging/wp-staging-pro/pull/2440#discussion_r1247951548
625 if (!isset($collectPartsize[$partName])) {
626 $collectPartsize[$partName] = 0;
627 }
628
629 $collectPartsize[$partName] += $newBytesWritten;
630 $jobDataDto->setCategorySizes($collectPartsize);
631 }
632 }
633