Passed
Push — develop ( 1c76c7...abee22 )
by Портнов
32:44
created

Storage   F

Complexity

Total Complexity 182

Size/Duplication

Total Lines 1273
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 182
eloc 626
c 8
b 0
f 0
dl 0
loc 1273
rs 1.974

41 Methods

Rating   Name   Duplication   Size   Complexity  
A statusMkfs() 0 12 3
B isStorageDiskMounted() 0 32 6
A mountSftpDisk() 0 21 2
A mkfs_disk() 0 22 5
B mountFtp() 0 34 6
A umountDisk() 0 15 4
A clearSessionsFiles() 0 11 3
A getMonitorDir() 0 8 2
A getMediaDir() 0 8 2
A getLsBlkDiskInfo() 0 10 2
A diskIsMounted() 0 14 3
A getFreeSpace() 0 17 3
A getFsType() 0 20 2
A getUuid() 0 17 4
A formatDiskLocalPart2() 0 20 3
A formatDiskLocal() 0 12 2
B diskGetDevices() 0 25 7
A getVendorDisk() 0 14 4
A cdromGetDevices() 0 13 4
C determineFormatFs() 0 72 14
A mountDisk() 0 24 4
C getAllHdd() 0 88 13
B configure() 0 44 8
A saveFstab() 0 34 3
A hddExists() 0 8 3
A updateConfigWithNewMountPoint() 0 22 5
B isStorageDisk() 0 36 7
A updateEnvironmentAfterChangeMountPoint() 0 11 1
A getDiskSettings() 0 14 3
A createWorkDirsAfterDBUpgrade() 0 7 1
C createWorkDirs() 0 58 13
A createModulesCacheSymlinks() 0 6 2
A clearCacheFiles() 0 21 5
A saveDiskSettings() 0 23 6
A createAssetsSymlinks() 0 10 1
A getStorageFreeSpaceMb() 0 16 4
A getDevPartName() 0 10 1
B applyFolderRights() 0 51 8
A mountSwap() 0 15 2
A makeSwapFile() 0 28 5
A getRecoverDiskName() 0 18 6

How to fix   Complexity   

Complex Class

Complex classes like Storage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Storage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use Error;
23
use JsonException;
24
use MikoPBX\Common\Config\ClassLoader;
25
use MikoPBX\Common\Models\PbxExtensionModules;
26
use MikoPBX\Core\Config\RegisterDIServices;
27
use MikoPBX\Core\System\Configs\PHPConf;
28
use MikoPBX\Core\Workers\WorkerRemoveOldRecords;
29
use MikoPBX\Common\Models\Storage as StorageModel;
30
use MikoPBX\Common\Providers\ConfigProvider;
31
use MikoPBX\Modules\PbxExtensionUtils;
32
use Phalcon\Di;
33
34
use function MikoPBX\Common\Config\appPath;
35
36
37
/**
38
 * Class Storage
39
 *
40
 * @package MikoPBX\Core\System
41
 * @property \Phalcon\Config config
42
 */
43
class Storage extends Di\Injectable
44
{
45
46
    /**
47
     * Возвращает директорию для хранения файлов записей разговоров.
48
     *
49
     * @return string
50
     */
51
    public static function getMonitorDir(): string
52
    {
53
        $di = Di::getDefault();
54
        if ($di !== null) {
55
            return $di->getConfig()->path('asterisk.monitordir');
56
        }
57
58
        return '/tmp';
59
    }
60
61
    /**
62
     * Возвращает директорию для хранения media файлов.
63
     *
64
     * @return string
65
     */
66
    public static function getMediaDir(): string
67
    {
68
        $di = Di::getDefault();
69
        if ($di !== null) {
70
            return $di->getConfig()->path('core.mediaMountPoint');
71
        }
72
73
        return '/tmp';
74
    }
75
76
77
    /**
78
     * Прверяем является ли диск хранилищем.
79
     *
80
     * @param $device
81
     *
82
     * @return bool
83
     */
84
    public static function isStorageDisk($device): bool
85
    {
86
        $result = false;
87
        if ( ! file_exists($device)) {
88
            return $result;
89
        }
90
91
        $tmp_dir = '/tmp/mnt_' . time();
92
        Util::mwMkdir($tmp_dir);
93
        $out = [];
94
95
        $storage  = new self();
96
        $uid_part = 'UUID=' . $storage->getUuid($device) . '';
97
        $format   = $storage->getFsType($device);
98
        if ($format === '') {
99
            return false;
100
        }
101
        $mountPath  = Util::which('mount');
102
        $umountPath = Util::which('umount');
103
        $rmPath     = Util::which('rm');
104
105
        Processes::mwExec("{$mountPath} -t {$format} {$uid_part} {$tmp_dir}", $out);
106
        if (is_dir("{$tmp_dir}/mikopbx") && trim(implode('', $out)) === '') {
107
            // $out - пустая строка, ошибок нет
108
            // присутствует каталог mikopbx.
109
            $result = true;
110
        }
111
        if (self::isStorageDiskMounted($device)) {
112
            Processes::mwExec("{$umountPath} {$device}");
113
        }
114
115
        if ( ! self::isStorageDiskMounted($device)) {
116
            Processes::mwExec("{$rmPath} -rf '{$tmp_dir}'");
117
        }
118
119
        return $result;
120
    }
121
122
    /**
123
     * Получение идентификатора устройства.
124
     *
125
     * @param $device
126
     *
127
     * @return string
128
     */
129
    public function getUuid($device): string
130
    {
131
        if (empty($device)) {
132
            return '';
133
        }
134
        $lsBlkPath   = Util::which('lsblk');
135
        $busyboxPath = Util::which('busybox');
136
137
        $cmd = "{$lsBlkPath} -r -o NAME,UUID | {$busyboxPath} grep ".basename($device)." | {$busyboxPath} cut -d ' ' -f 2";
138
        $res = Processes::mwExec($cmd, $output);
139
        if ($res === 0 && count($output) > 0) {
140
            $result = $output[0];
141
        } else {
142
            $result = '';
143
        }
144
145
        return $result;
146
    }
147
148
    /**
149
     * Возвращает тип файловой системы блочного устройства.
150
     *
151
     * @param $device
152
     *
153
     * @return string
154
     */
155
    public function getFsType($device): string
156
    {
157
        $blkidPath   = Util::which('blkid');
158
        $busyboxPath = Util::which('busybox');
159
        $sedPath     = Util::which('sed');
160
        $grepPath    = Util::which('grep');
161
        $awkPath     = Util::which('awk');
162
163
        $device = str_replace('/dev/', '', $device);
164
        $out    = [];
165
        Processes::mwExec(
166
            "{$blkidPath} -ofull /dev/{$device} | {$busyboxPath} {$sedPath} -r 's/[[:alnum:]]+=/\\n&/g' | {$busyboxPath} {$grepPath} \"^TYPE=\" | {$busyboxPath} {$awkPath} -F \"\\\"\" '{print $2}'",
167
            $out
168
        );
169
        $format = implode('', $out);
170
        if ($format === 'msdosvfat') {
171
            $format = 'msdos';
172
        }
173
174
        return $format;
175
    }
176
177
    /**
178
     * Проверка, смонтирован ли диск - хранилище.
179
     *
180
     * @param string $filter
181
     * @param string $mount_dir
182
     *
183
     * @return bool
184
     */
185
    public static function isStorageDiskMounted($filter = '', &$mount_dir = ''): bool
186
    {
187
        if (Util::isSystemctl() && file_exists('/storage/usbdisk1/')) {
188
            $mount_dir = '/storage/usbdisk1/';
189
190
            return true;
191
        }
192
        if ('' === $filter) {
193
            $di = Di::getDefault();
194
            if ($di !== null) {
195
                $varEtcDir = $di->getConfig()->path('core.varEtcDir');
196
            } else {
197
                $varEtcDir = '/var/etc';
198
            }
199
200
            $filename = "{$varEtcDir}/storage_device";
201
            if (file_exists($filename)) {
202
                $filter = file_get_contents($filename);
203
            } else {
204
                $filter = 'usbdisk1';
205
            }
206
        }
207
        $filter = escapeshellarg($filter);
208
209
        $out       = [];
210
        $grepPath  = Util::which('grep');
211
        $mountPath = Util::which('mount');
212
        $awkPath   = Util::which('awk');
213
        Processes::mwExec("{$mountPath} | {$grepPath} {$filter} | {$awkPath} '{print $3}'", $out);
214
        $mount_dir = trim(implode('', $out));
215
216
        return ($mount_dir !== '');
217
    }
218
219
    /**
220
     * Монитирование каталога с удаленного сервера SFTP.
221
     *
222
     * @param        $host
223
     * @param int    $port
224
     * @param string $user
225
     * @param string $pass
226
     * @param string $remout_dir
227
     * @param string $local_dir
228
     *
229
     * @return bool
230
     */
231
    public static function mountSftpDisk($host, $port, $user, $pass, $remout_dir, $local_dir): bool
232
    {
233
        Util::mwMkdir($local_dir);
234
235
        $out         = [];
236
        $timeoutPath = Util::which('timeout');
237
        $sshfsPath   = Util::which('sshfs');
238
239
        $command = "{$timeoutPath} 3 {$sshfsPath} -p {$port} -o nonempty -o password_stdin -o 'StrictHostKeyChecking=no' " .
240
            "{$user}@{$host}:{$remout_dir} {$local_dir} << EOF\n" .
241
            "{$pass}\n" .
242
            "EOF\n";
243
        // file_put_contents('/tmp/sshfs_'.$host, $command);
244
        Processes::mwExec($command, $out);
245
        $response = trim(implode('', $out));
246
        if ('Terminated' == $response) {
247
            // Удаленный сервер не ответил / или не корректно указан пароль.
248
            unset($response);
249
        }
250
251
        return self::isStorageDiskMounted("$local_dir ");
252
    }
253
254
    /**
255
     * Монитирование каталога с удаленного сервера FTP.
256
     *
257
     * @param        $host
258
     * @param        $port
259
     * @param        $user
260
     * @param        $pass
261
     * @param string $remout_dir
262
     * @param        $local_dir
263
     *
264
     * @return bool
265
     */
266
    public static function mountFtp($host, $port, $user, $pass, $remout_dir, $local_dir): bool
267
    {
268
        Util::mwMkdir($local_dir);
269
        $out = [];
270
271
        // Собираем строку подключения к ftp.
272
        $auth_line = '';
273
        if ( ! empty($user)) {
274
            $auth_line .= 'user="' . $user;
275
            if ( ! empty($pass)) {
276
                $auth_line .= ":{$pass}";
277
            }
278
            $auth_line .= '",';
279
        }
280
281
        $connect_line = 'ftp://' . $host;
282
        if ( ! empty($port)) {
283
            $connect_line .= ":{$port}";
284
        }
285
        if ( ! empty($remout_dir)) {
286
            $connect_line .= "$remout_dir";
287
        }
288
289
        $timeoutPath   = Util::which('timeout');
290
        $curlftpfsPath = Util::which('curlftpfs');
291
        $command       = "{$timeoutPath} 3 {$curlftpfsPath}  -o allow_other -o {$auth_line}fsname={$host} {$connect_line} {$local_dir}";
292
        Processes::mwExec($command, $out);
293
        $response = trim(implode('', $out));
294
        if ('Terminated' === $response) {
295
            // Удаленный сервер не ответил / или не корректно указан пароль.
296
            unset($response);
297
        }
298
299
        return self::isStorageDiskMounted("$local_dir ");
300
    }
301
302
    /**
303
     * Запускает процесс форматирования диска.
304
     *
305
     * @param $dev
306
     *
307
     * @return array|bool
308
     */
309
    public static function mkfs_disk($dev)
310
    {
311
        if ( ! file_exists($dev)) {
312
            $dev = "/dev/{$dev}";
313
        }
314
        if ( ! file_exists($dev)) {
315
            return false;
316
        }
317
        $dir = '';
318
        self::isStorageDiskMounted("$dev", $dir);
319
320
        if (empty($dir) || self::umountDisk($dir)) {
321
            // Диск размонтирован.
322
            $st = new Storage();
323
            // Будет запущен процесс:
324
            $st->formatDiskLocal($dev, true);
325
            sleep(1);
326
327
            return (self::statusMkfs($dev) == 'inprogress');
328
        } else {
329
            // Ошибка размонтирования диска.
330
            return false;
331
        }
332
    }
333
334
    /**
335
     * Размонтирует диск. Удаляет каталог в случае успеха.
336
     *
337
     * @param $dir
338
     *
339
     * @return bool
340
     */
341
    public static function umountDisk($dir): bool
342
    {
343
        $umountPath = Util::which('umount');
344
        $rmPath     = Util::which('rm');
345
        if (self::isStorageDiskMounted($dir)) {
346
            Processes::mwExec("/sbin/shell_functions.sh 'killprocesses' '$dir' -TERM 0");
347
            Processes::mwExec("{$umountPath} {$dir}");
348
        }
349
        $result = ! self::isStorageDiskMounted($dir);
350
        if ($result && file_exists($dir)) {
351
            // Если диск не смонтирован, то удаляем каталог.
352
            Processes::mwExec("{$rmPath} -rf '{$dir}'");
353
        }
354
355
        return $result;
356
    }
357
358
    /**
359
     * Разметка диска.
360
     *
361
     * @param string $device
362
     * @param bool   $bg
363
     *
364
     * @return mixed
365
     */
366
    public function formatDiskLocal($device, $bg = false)
367
    {
368
        $partedPath = Util::which('parted');
369
        $retVal     = Processes::mwExec(
370
            "{$partedPath} --script --align optimal '{$device}' 'mklabel msdos mkpart primary ext4 0% 100%'"
371
        );
372
        Util::sysLogMsg(__CLASS__, "{$partedPath} returned {$retVal}");
373
        if (false === $bg) {
374
            sleep(1);
375
        }
376
377
        return $this->formatDiskLocalPart2($device, $bg);
378
    }
379
380
    /**
381
     * Форматирование диска.
382
     *
383
     * @param string $device
384
     * @param bool   $bg
385
     *
386
     * @return mixed
387
     */
388
    private function formatDiskLocalPart2($device, $bg = false):bool
389
    {
390
        if (is_numeric(substr($device, -1))) {
391
            $device_id = "";
392
        } else {
393
            $device_id = "1";
394
        }
395
        $format   = 'ext4';
396
        $mkfsPath = Util::which("mkfs.{$format}");
397
        $cmd      = "{$mkfsPath} {$device}{$device_id}";
398
        if ($bg === false) {
399
            $retVal = (Processes::mwExec("{$cmd} 2>&1") === 0);
400
            Util::sysLogMsg(__CLASS__, "{$mkfsPath} returned {$retVal}");
401
        } else {
402
            usleep(200000);
403
            Processes::mwExecBg($cmd);
404
            $retVal = true;
405
        }
406
407
        return $retVal;
408
    }
409
410
    /**
411
     * Возвращает текущий статус форматирования диска.
412
     *
413
     * @param $dev
414
     *
415
     * @return string
416
     */
417
    public static function statusMkfs($dev): string
418
    {
419
        if ( ! file_exists($dev)) {
420
            $dev = "/dev/{$dev}";
421
        }
422
        $out      = [];
423
        $psPath   = Util::which('ps');
424
        $grepPath = Util::which('grep');
425
        Processes::mwExec("{$psPath} -A -f | {$grepPath} {$dev} | {$grepPath} mkfs | {$grepPath} -v grep", $out);
426
        $mount_dir = trim(implode('', $out));
427
428
        return empty($mount_dir) ? 'ended' : 'inprogress';
429
    }
430
431
    /**
432
     * Clear cache folders from PHP sessions files
433
     */
434
    public static function clearSessionsFiles():void
435
    {
436
        $di = Di::getDefault();
437
        if ($di === null) {
438
            return;
439
        }
440
        $config        = $di->getShared('config');
441
        $phpSessionDir = $config->path('www.phpSessionDir');
442
        if ( ! empty($phpSessionDir)) {
443
            $rmPath = Util::which('rm');
444
            Processes::mwExec("{$rmPath} -rf {$phpSessionDir}/*");
445
        }
446
    }
447
448
    /**
449
     * Возвращает все подключенные HDD.
450
     *
451
     * @param bool $mounted_only
452
     *
453
     * @return array
454
     */
455
    public function getAllHdd($mounted_only = false): array
456
    {
457
        $res_disks = [];
458
459
        if (Util::isSystemctl()) {
460
            $out      = [];
461
            $grepPath = Util::which('grep');
462
            $dfPath   = Util::which('df');
463
            $awkPath  = Util::which('awk');
464
            Processes::mwExec(
465
                "{$dfPath} -k /storage/usbdisk1 | {$awkPath}  '{ print $1 \"|\" $3 \"|\" $4} ' | {$grepPath} -v 'Available'",
466
                $out
467
            );
468
            $disk_data = explode('|', implode(" ", $out));
469
            if (count($disk_data) === 3) {
470
                $m_size      = round(($disk_data[1] + $disk_data[2]) / 1024, 1);
471
                $res_disks[] = [
472
                    'id'         => $disk_data[0],
473
                    'size'       => "" . $m_size,
474
                    'size_text'  => "" . $m_size . " Mb",
475
                    'vendor'     => 'Debian',
476
                    'mounted'    => '/storage/usbdisk1',
477
                    'free_space' => round($disk_data[2] / 1024, 1),
478
                    'partitions' => [],
479
                    'sys_disk'   => true,
480
                ];
481
            }
482
483
            return $res_disks;
484
        }
485
486
        $cd_disks   = $this->cdromGetDevices();
487
        $disks      = $this->diskGetDevices();
488
        $cf_disk    = '';
489
        $varEtcDir  = $this->config->path('core.varEtcDir');
490
491
        if (file_exists($varEtcDir . '/cfdevice')) {
492
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
493
        }
494
495
        foreach ($disks as $disk => $diskInfo) {
496
            if (in_array($disk, $cd_disks, true)) {
497
                // Это CD-ROM.
498
                continue;
499
            }
500
            unset($temp_vendor, $temp_size, $original_size);
501
            $mounted = self::diskIsMounted($disk);
502
            if ($mounted_only === true && $mounted === false) {
503
                continue;
504
            }
505
            $sys_disk = ($cf_disk === $disk);
506
507
            $mb_size = 0;
508
            if (is_file("/sys/block/" . $disk . "/size")) {
509
                $original_size = trim(file_get_contents("/sys/block/" . $disk . "/size"));
510
                $original_size = ($original_size * 512 / 1024 / 1024);
511
                $mb_size       = $original_size;
512
            }
513
            if ($mb_size > 100) {
514
                $temp_size   = sprintf("%.0f MB", $mb_size);
515
                $temp_vendor = $this->getVendorDisk($diskInfo);
516
                $free_space  = $this->getFreeSpace($disk);
517
518
                $arr_disk_info = $this->determineFormatFs($diskInfo);
519
                if (count($arr_disk_info) > 0) {
520
                    $used = 0;
521
                    foreach ($arr_disk_info as $disk_info) {
522
                        $used += $disk_info['used_space'];
523
                    }
524
                    if ($used > 0) {
525
                        $free_space = $mb_size - $used;
526
                    }
527
                }
528
529
                $res_disks[] = [
530
                    'id'         => $disk,
531
                    'size'       => $mb_size,
532
                    'size_text'  => $temp_size,
533
                    'vendor'     => $temp_vendor,
534
                    'mounted'    => $mounted,
535
                    'free_space' => $free_space,
536
                    'partitions' => $arr_disk_info,
537
                    'sys_disk'   => $sys_disk,
538
                ];
539
            }
540
        }
541
542
        return $res_disks;
543
    }
544
545
    /**
546
     * Получение массива подключенныйх cdrom.
547
     *
548
     * @return array
549
     */
550
    private function cdromGetDevices(): array
551
    {
552
        $disks = [];
553
        $blockDevices = $this->getLsBlkDiskInfo();
554
        foreach ($blockDevices as $diskData){
555
            $type = $diskData['type']??'';
556
            $name = $diskData['name']??'';
557
            if($type !== 'rom' || $name === ''){
558
                continue;
559
            }
560
            $disks[]    = $name;
561
        }
562
        return $disks;
563
    }
564
565
    /**
566
     * Получение массива подключенныйх HDD.
567
     * @param false $diskOnly
568
     * @return array
569
     */
570
    public function diskGetDevices($diskOnly=false): array
571
    {
572
        $disks = [];
573
        $blockDevices = $this->getLsBlkDiskInfo();
574
        foreach ($blockDevices as $diskData){
575
            $type = $diskData['type']??'';
576
            $name = $diskData['name']??'';
577
            if($type !== 'disk' || $name === ''){
578
                continue;
579
            }
580
            $disks[$name]    = $diskData;
581
            if($diskOnly === true){
582
                continue;
583
            }
584
            $children   = $diskData['children']??[];
585
            foreach ($children as $child){
586
                $childName = $child['name']??'';
587
                if($childName === ''){
588
                    continue;
589
                }
590
                $disks[$childName]    = $child;
591
            }
592
593
        }
594
        return $disks;
595
    }
596
597
    /**
598
     * Возвращает информацию о дисках.
599
     * @return array
600
     */
601
    private function getLsBlkDiskInfo():array{
602
        $lsBlkPath = Util::which('lsblk');
603
        Processes::mwExec("{$lsBlkPath} -J -b -o VENDOR,MODEL,SERIAL,LABEL,TYPE,FSTYPE,MOUNTPOINT,SUBSYSTEMS,NAME,UUID", $out);
604
        try {
605
            $data = json_decode(implode(PHP_EOL, $out), true, 512, JSON_THROW_ON_ERROR);
606
            $data = $data['blockdevices']??[];
607
        } catch (JsonException $e) {
608
            $data = [];
609
        }
610
        return $data;
611
    }
612
613
    /**
614
     * Проверка, смонтирован ли диск.
615
     *
616
     * @param $disk
617
     * @param $filter
618
     *
619
     * @return string|bool
620
     */
621
    public static function diskIsMounted($disk, $filter = '/dev/')
622
    {
623
        $out       = [];
624
        $grepPath  = Util::which('grep');
625
        $mountPath = Util::which('mount');
626
        Processes::mwExec("{$mountPath} | {$grepPath} '{$filter}{$disk}'", $out);
627
        if (count($out) > 0) {
628
            $res_out = end($out);
629
        } else {
630
            $res_out = implode('', $out);
631
        }
632
        $data = explode(' ', trim($res_out));
633
634
        return (count($data) > 2) ? $data[2] : false;
635
    }
636
637
    /**
638
     * Получение сведений по диску.
639
     *
640
     * @param $diskInfo
641
     *
642
     * @return string
643
     */
644
    private function getVendorDisk($diskInfo): string
645
    {
646
        $temp_vendor = [];
647
        $keys = ['vendor', 'model', 'type'];
648
        foreach ($keys as $key){
649
            $data = $diskInfo[$key]??'';
650
            if ($data !== '') {
651
                $temp_vendor[] = trim(str_replace(',', '', $data));
652
            }
653
        }
654
        if (count($temp_vendor) === 0) {
655
            $temp_vendor[] = $diskInfo['name']??'ERROR: NoName';
656
        }
657
        return implode(', ', $temp_vendor);
658
    }
659
660
    /**
661
     * Получаем свободное место на диске в Mb.
662
     *
663
     * @param $hdd
664
     *
665
     * @return mixed
666
     */
667
    public function getFreeSpace($hdd)
668
    {
669
        $out      = [];
670
        $hdd      = escapeshellarg($hdd);
671
        $grepPath = Util::which('grep');
672
        $awkPath  = Util::which('awk');
673
        $dfPath   = Util::which('df');
674
        Processes::mwExec("{$dfPath} -m | {$grepPath} {$hdd} | {$awkPath} '{print $4}'", $out);
675
        $result = 0;
676
        foreach ($out as $res) {
677
            if ( ! is_numeric($res)) {
678
                continue;
679
            }
680
            $result += (1 * $res);
681
        }
682
683
        return $result;
684
    }
685
686
    /**
687
     * Определить формат файловой системы и размер дисков.
688
     *
689
     * @param $deviceInfo
690
     *
691
     * @return array|bool
692
     */
693
    public function determineFormatFs($deviceInfo)
694
    {
695
        $grepPath      = Util::which('grep');
696
        $lsPath        = Util::which('ls');
697
        $trPath        = Util::which('tr');
698
        $allow_formats = ['ext2', 'ext4', 'fat', 'ntfs', 'msdos'];
699
        $device        = str_replace('/dev/', '', $deviceInfo['name']??'');
700
        $devices       = explode(" ", trim(exec("{$lsPath} /dev | {$grepPath} '{$device}' | {$trPath} \"\n\" \" \"")));
701
702
        $result_data = [];
703
        foreach ($devices as $dev) {
704
            if (empty($dev) || (count($devices) > 1 && $device === $dev) || is_dir("/sys/block/{$dev}")) {
705
                continue;
706
            }
707
            $mb_size        = 0;
708
            $path_size_info = '';
709
            $tmp_path       = "/sys/block/{$device}/{$dev}/size";
710
            if (file_exists($tmp_path)) {
711
                $path_size_info = $tmp_path;
712
            }
713
            if (empty($path_size_info)) {
714
                $tmp_path = "/sys/block/" . substr($dev, 0, 3) . "/{$dev}/size";
715
                if (file_exists($tmp_path)) {
716
                    $path_size_info = $tmp_path;
717
                }
718
            }
719
720
            if ( ! empty($path_size_info)) {
721
                $original_size = trim(file_get_contents($path_size_info));
722
                $original_size = ($original_size * 512 / 1024 / 1024);
723
                $mb_size       = $original_size;
724
            }
725
726
            $tmp_dir = "/tmp/{$dev}_" . time();
727
            $out     = [];
728
729
            $fs           = null;
730
            $need_unmount = false;
731
            $mount_dir    = '';
732
            if (self::isStorageDiskMounted("/dev/{$dev} ", $mount_dir)) {
733
                $grepPath  = Util::which('grep');
734
                $awkPath   = Util::which('awk');
735
                $mountPath = Util::which('mount');
736
                Processes::mwExec("{$mountPath} | {$grepPath} '/dev/{$dev}' | {$awkPath} '{print $5}'", $out);
737
                $fs         = trim(implode("", $out));
738
                $fs         = ($fs === 'fuseblk') ? 'ntfs' : $fs;
739
                $free_space = $this->getFreeSpace("/dev/{$dev} ");
740
                $used_space = $mb_size - $free_space;
741
            } else {
742
                $format = $this->getFsType($device);
743
                if (in_array($format, $allow_formats)) {
744
                    $fs = $format;
745
                }
746
                self::mountDisk($dev, $format, $tmp_dir);
747
748
                $need_unmount = true;
749
                $used_space   = Util::getSizeOfFile($tmp_dir);
750
            }
751
            $result_data[] = [
752
                "dev"        => $dev,
753
                'size'       => round($mb_size, 2),
754
                "used_space" => round($used_space, 2),
755
                "free_space" => round($mb_size - $used_space, 2),
756
                "uuid"       => $this->getUuid("/dev/{$dev} "),
757
                "fs"         => $fs,
758
            ];
759
            if ($need_unmount) {
760
                self::umountDisk($tmp_dir);
761
            }
762
        }
763
764
        return $result_data;
765
    }
766
767
    /**
768
     * Монтирует диск в указанный каталог.
769
     *
770
     * @param $dev
771
     * @param $format
772
     * @param $dir
773
     *
774
     * @return bool
775
     */
776
    public static function mountDisk($dev, $format, $dir): bool
777
    {
778
        if (self::isStorageDiskMounted("/dev/{$dev} ")) {
779
            return true;
780
        }
781
        Util::mwMkdir($dir);
782
783
        if ( ! file_exists($dir)) {
784
            Util::sysLogMsg('Storage', "Unable mount $dev $format to $dir. Unable create dir.");
785
786
            return false;
787
        }
788
        $dev = str_replace('/dev/', '', $dev);
789
        if ('ntfs' === $format) {
790
            $mountNtfs3gPath = Util::which('mount.ntfs-3g');
791
            Processes::mwExec("{$mountNtfs3gPath} /dev/{$dev} {$dir}", $out);
792
        } else {
793
            $storage   = new self();
794
            $uid_part  = 'UUID=' . $storage->getUuid("/dev/{$dev}") . '';
795
            $mountPath = Util::which('mount');
796
            Processes::mwExec("{$mountPath} -t {$format} {$uid_part} {$dir}", $out);
797
        }
798
799
        return self::isStorageDiskMounted("/dev/{$dev} ");
800
    }
801
802
    /**
803
     * Монтирование разделов диска с базой данных настроек.
804
     */
805
    public function configure(): void
806
    {
807
        $cf_disk          = '';
808
        $varEtcDir        = $this->config->path('core.varEtcDir');
809
        $storage_dev_file = "{$varEtcDir}/storage_device";
810
        if (file_exists($storage_dev_file)) {
811
            unlink($storage_dev_file);
812
        }
813
814
        if (file_exists($varEtcDir . '/cfdevice')) {
815
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
816
        }
817
818
        $disks = $this->getDiskSettings();
819
        $conf  = '';
820
        foreach ($disks as $disk) {
821
            clearstatcache();
822
            if ($disk['device'] !== "/dev/{$cf_disk}") {
823
                // Если это обычный диск, то раздел 1.
824
                $part = "1";
825
            } else {
826
                // Если это системный диск, то пытаемся подключить раздел 4.
827
                $part = "4";
828
            }
829
            $devName = self::getDevPartName($disk['device'], $part);
830
            $dev     = '/dev/'.$devName;
831
            if ( ! $this->hddExists($dev)) {
832
                // Диск не существует.
833
                continue;
834
            }
835
            if ($disk['media'] === '1' || ! file_exists($storage_dev_file)) {
836
                file_put_contents($storage_dev_file, "/storage/usbdisk{$disk['id']}");
837
                $this->updateConfigWithNewMountPoint("/storage/usbdisk{$disk['id']}");
838
            }
839
840
            $str_uid     = 'UUID=' . $this->getUuid($dev) . '';
841
            $format_p4   = $this->getFsType($dev);
842
            $conf        .= "{$str_uid} /storage/usbdisk{$disk['id']} {$format_p4} async,rw 0 0\n";
843
            $mount_point = "/storage/usbdisk{$disk['id']}";
844
            Util::mwMkdir($mount_point);
845
        }
846
        $this->saveFstab($conf);
847
        $this->createWorkDirs();
848
        PHPConf::setupLog();
849
    }
850
851
    /**
852
     * Получаем настройки диска из базы данных.
853
     *
854
     * @param string $id
855
     *
856
     * @return array
857
     */
858
    public function getDiskSettings($id = ''): array
859
    {
860
        $data = [];
861
        if ('' === $id) {
862
            // Возвращаем данные до модификации.
863
            $data = StorageModel::find()->toArray();
864
        } else {
865
            $pbxSettings = StorageModel::findFirst("id='$id'");
866
            if ($pbxSettings !== null) {
867
                $data = $pbxSettings->toArray();
868
            }
869
        }
870
871
        return $data;
872
    }
873
874
    /**
875
     * Проверяет, существует ли диск в массиве.
876
     *
877
     * @param $disk
878
     *
879
     * @return bool
880
     */
881
    private function hddExists($disk): bool
882
    {
883
        $result = false;
884
        $uid    = $this->getUuid($disk);
885
        if ($uid !== false && file_exists($disk)) {
886
            $result = true;
887
        }
888
        return $result;
889
    }
890
891
    /**
892
     * After mount storage we will change /mountpoint/ to new $mount_point value
893
     *
894
     * @param string $mount_point
895
     *
896
     */
897
    private function updateConfigWithNewMountPoint(string $mount_point): void
898
    {
899
        $staticSettingsFile     = '/etc/inc/mikopbx-settings.json';
900
        $staticSettingsFileOrig = appPath('config/mikopbx-settings.json');
901
902
        $jsonString = file_get_contents($staticSettingsFileOrig);
903
        try {
904
            $data = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
905
        } catch (JsonException $exception) {
906
            throw new Error("{$staticSettingsFileOrig} has broken format");
907
        }
908
        foreach ($data as $rootKey => $rootEntry) {
909
            foreach ($rootEntry as $nestedKey => $entry) {
910
                if (stripos($entry, '/mountpoint') !== false) {
911
                    $data[$rootKey][$nestedKey] = str_ireplace('/mountpoint', $mount_point, $entry);
912
                }
913
            }
914
        }
915
916
        $newJsonString = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
917
        file_put_contents($staticSettingsFile, $newJsonString);
918
        $this->updateEnvironmentAfterChangeMountPoint();
919
    }
920
921
922
    /**
923
     * Recreates DI services and reloads config from JSON file
924
     *
925
     */
926
    private function updateEnvironmentAfterChangeMountPoint(): void
927
    {
928
        // Update config variable
929
        ConfigProvider::recreateConfigProvider();
930
        $this->config = $this->di->get('config');
931
932
        // Reload classes from system and storage disks
933
        ClassLoader::init();
934
935
        // Reload all providers
936
        RegisterDIServices::init();
937
938
    }
939
940
    /**
941
     * Generates fstab file
942
     * Mounts volumes
943
     *
944
     * @param string $conf
945
     */
946
    public function saveFstab($conf = ''): void
947
    {
948
        $varEtcDir = $this->config->path('core.varEtcDir');
949
        // Точка монтирования доп. дисков.
950
        Util::mwMkdir('/storage');
951
        $chmodPath = Util::which('chmod');
952
        Processes::mwExec("{$chmodPath} 755 /storage");
953
        if ( ! file_exists($varEtcDir . '/cfdevice')) {
954
            return;
955
        }
956
        $fstab     = '';
957
        $file_data = file_get_contents($varEtcDir . '/cfdevice');
958
        $cf_disk   = trim($file_data);
959
        if ('' === $cf_disk) {
960
            return;
961
        }
962
        $part2 = self::getDevPartName($cf_disk, '2');
963
        $part3 = self::getDevPartName($cf_disk, '3');
964
965
        $uid_part2 = 'UUID=' . $this->getUuid("/dev/{$part2}");
966
        $format_p2 = $this->getFsType($part2);
967
        $uid_part3 = 'UUID=' . $this->getUuid("/dev/{$part3}");
968
        $format_p3 = $this->getFsType($part3);
969
970
        $fstab .= "{$uid_part2} /offload {$format_p2} ro 0 0\n";
971
        $fstab .= "{$uid_part3} /cf {$format_p3} rw 1 1\n";
972
        $fstab .= $conf;
973
974
        file_put_contents("/etc/fstab", $fstab);
975
        // Дублируем для работы vmtoolsd.
976
        file_put_contents("/etc/mtab", $fstab);
977
        $mountPath = Util::which('mount');
978
        Processes::mwExec("{$mountPath} -a 2> /dev/null");
979
        Util::addRegularWWWRights('/cf');
980
    }
981
982
    /**
983
     * Возвращает имя раздела диска по имени и номеру.
984
     * @param string $dev
985
     * @param string $part
986
     * @return string
987
     */
988
    public static function getDevPartName(string $dev, string $part):string{
989
        $lsBlkPath  = Util::which('lsblk');
990
        $cutPath    = Util::which('cut');
991
        $grepPath   = Util::which('grep');
992
        $sortPath   = Util::which('sort');
993
994
        $command = "{$lsBlkPath} -r | {$grepPath} ' part' | {$sortPath} -u | {$cutPath} -d ' ' -f 1 | {$grepPath} \"".basename($dev)."\" | {$grepPath} \"{$part}\$\"";
995
        Processes::mwExec($command, $out);
996
        $devName = trim(implode('', $out));
997
        return trim($devName);
998
    }
999
1000
    /**
1001
     * Creates system folders according to config file
1002
     *
1003
     * @return void
1004
     */
1005
    private function createWorkDirs(): void
1006
    {
1007
        $path      = '';
1008
        $mountPath = Util::which('mount');
1009
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1010
1011
        $isLiveCd = file_exists('/offload/livecd');
1012
        // Create dirs
1013
        $arrConfig = $this->config->toArray();
1014
        foreach ($arrConfig as $rootEntry) {
1015
            foreach ($rootEntry as $key => $entry) {
1016
                if (stripos($key, 'path') === false && stripos($key, 'dir') === false) {
1017
                    continue;
1018
                }
1019
                if (file_exists($entry)) {
1020
                    continue;
1021
                }
1022
                if ($isLiveCd && strpos($entry, '/offload/') === 0) {
1023
                    continue;
1024
                }
1025
                $path .= " $entry";
1026
            }
1027
        }
1028
1029
        if ( ! empty($path)) {
1030
            Util::mwMkdir($path);
1031
        }
1032
1033
        $downloadCacheDir = appPath('sites/pbxcore/files/cache');
1034
        if ( ! $isLiveCd) {
1035
            Util::mwMkdir($downloadCacheDir);
1036
            Util::createUpdateSymlink($this->config->path('www.downloadCacheDir'), $downloadCacheDir);
1037
        }
1038
1039
        $this->createAssetsSymlinks();
1040
1041
        Util::createUpdateSymlink($this->config->path('www.phpSessionDir'), '/var/lib/php/session');
1042
        Util::createUpdateSymlink($this->config->path('www.uploadDir'), '/ultmp');
1043
1044
        $filePath = appPath('src/Core/Asterisk/Configs/lua/extensions.lua');
1045
        Util::createUpdateSymlink($filePath, '/etc/asterisk/extensions.lua');
1046
1047
        // Create symlinks to AGI-BIN
1048
        $agiBinDir = $this->config->path('asterisk.astagidir');
1049
        if ($isLiveCd && strpos($agiBinDir, '/offload/') !== 0) {
1050
            Util::mwMkdir($agiBinDir);
1051
        }
1052
1053
        $roAgiBinFolder = appPath('src/Core/Asterisk/agi-bin');
1054
        $files          = glob("{$roAgiBinFolder}/*.{php}", GLOB_BRACE);
1055
        foreach ($files as $file) {
1056
            $fileInfo    = pathinfo($file);
1057
            $newFilename = "{$agiBinDir}/{$fileInfo['filename']}.{$fileInfo['extension']}";
1058
            Util::createUpdateSymlink($file, $newFilename);
1059
        }
1060
        $this->clearCacheFiles();
1061
        $this->applyFolderRights();
1062
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1063
    }
1064
1065
    /**
1066
     * Creates JS, CSS, IMG cache folders and links
1067
     *
1068
     */
1069
    public function createAssetsSymlinks(): void
1070
    {
1071
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
1072
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/js', $jsCacheDir);
1073
1074
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
1075
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/css', $cssCacheDir);
1076
1077
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
1078
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/img', $imgCacheDir);
1079
    }
1080
1081
    /**
1082
     * Clears cache folders from old and orphaned files
1083
     */
1084
    public function clearCacheFiles(): void
1085
    {
1086
        $cacheDirs   = [];
1087
        $cacheDirs[] = $this->config->path('www.uploadDir');
1088
        $cacheDirs[] = $this->config->path('www.downloadCacheDir');
1089
        $cacheDirs[] = $this->config->path('www.managedCacheDir');
1090
        $cacheDirs[] = $this->config->path('www.modelsCacheDir');
1091
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/js';
1092
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/css';
1093
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/img';
1094
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
1095
        $rmPath      = Util::which('rm');
1096
        foreach ($cacheDirs as $cacheDir) {
1097
            if ( ! empty($cacheDir)) {
1098
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
1099
            }
1100
        }
1101
1102
        // Delete boot cache folders
1103
        if (is_dir('/mountpoint') && self::isStorageDiskMounted()) {
1104
            Processes::mwExec("{$rmPath} -rf /mountpoint");
1105
        }
1106
    }
1107
1108
    /**
1109
     * Create system folders and links after upgrade and connect config DB
1110
     */
1111
    public function createWorkDirsAfterDBUpgrade(): void
1112
    {
1113
        $mountPath = Util::which('mount');
1114
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1115
        $this->createModulesCacheSymlinks();
1116
        $this->applyFolderRights();
1117
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1118
    }
1119
1120
    /**
1121
     * Restore modules cache folders and symlinks
1122
     */
1123
    public function createModulesCacheSymlinks(): void
1124
    {
1125
        $modules = PbxExtensionModules::getModulesArray();
1126
        foreach ($modules as $module) {
1127
            PbxExtensionUtils::createAssetsSymlinks($module['uniqid']);
1128
            PbxExtensionUtils::createAgiBinSymlinks($module['uniqid']);
1129
        }
1130
    }
1131
1132
    /**
1133
     * Fixes permissions for Folder and Files
1134
     */
1135
    private function applyFolderRights(): void
1136
    {
1137
        // Add Rights to the WWW dirs plus some core dirs
1138
        $www_dirs  = [];
1139
        $exec_dirs = [];
1140
1141
        $arrConfig = $this->config->toArray();
1142
        foreach ($arrConfig as $key => $entry) {
1143
            if (in_array($key, ['www','adminApplication'])){
1144
                foreach ($entry as $subKey => $subEntry) {
1145
                    if (stripos($subKey, 'path') === false
1146
                        && stripos($subKey, 'dir') === false
1147
                    ) {
1148
                        continue;
1149
                    }
1150
                    $www_dirs[] = $subEntry;
1151
                }
1152
            }
1153
        }
1154
1155
        $www_dirs[] = $this->config->path('core.tempDir');
1156
        $www_dirs[] = $this->config->path('core.logsDir');
1157
1158
        // Create empty log files with www rights
1159
        $logFiles = [
1160
            $this->config->path('database.debugLogFile'),
1161
            $this->config->path('cdrDatabase.debugLogFile'),
1162
            $this->config->path('eventsLogDatabase.debugLogFile')
1163
        ];
1164
1165
        foreach ($logFiles as $logFile){
1166
            $filename = (string)$logFile;
1167
            if (!file_exists($filename)){
1168
                file_put_contents($filename, '');
1169
            }
1170
            $www_dirs[] = $filename;
1171
        }
1172
1173
        $www_dirs[] = '/etc/version';
1174
        $www_dirs[] = appPath('/');
1175
1176
        // Add read rights
1177
        Util::addRegularWWWRights(implode(' ', $www_dirs));
1178
1179
        // Add executable rights
1180
        $exec_dirs[] = appPath('src/Core/Asterisk/agi-bin');
1181
        $exec_dirs[] = appPath('src/Core/Rc');
1182
        Util::addExecutableRights(implode(' ', $exec_dirs));
1183
1184
        $mountPath = Util::which('mount');
1185
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1186
    }
1187
1188
    /**
1189
     * Creates swap file on storage
1190
     */
1191
    public function mountSwap(): void
1192
    {
1193
        $tempDir    = $this->config->path('core.tempDir');
1194
        $swapFile   = "{$tempDir}/swapfile";
1195
1196
        $swapOffCmd = Util::which('swapoff');
1197
        Processes::mwExec("{$swapOffCmd} {$swapFile}");
1198
1199
        $this->makeSwapFile($swapFile);
1200
        if (!file_exists($swapFile)) {
1201
            return;
1202
        }
1203
        $swapOnCmd = Util::which('swapon');
1204
        $result    = Processes::mwExec("{$swapOnCmd} {$swapFile}");
1205
        Util::sysLogMsg('Swap', 'connect swap result: ' . $result, LOG_INFO);
1206
    }
1207
1208
    /**
1209
     * Создает swap файл на storage.
1210
     * @param $swapFile
1211
     */
1212
    private function makeSwapFile($swapFile):void{
1213
        $swapLabel  = Util::which('swaplabel');
1214
        if(Processes::mwExec("{$swapLabel} {$swapFile}") === 0){
1215
            // Файл уже существует.
1216
            return;
1217
        }
1218
        if (file_exists($swapFile)) {
1219
            unlink($swapFile);
1220
        }
1221
1222
        $size     = $this->getStorageFreeSpaceMb();
1223
        if ($size > 2000) {
1224
            $swapSize = 1024;
1225
        } elseif ($size > 1000) {
1226
            $swapSize = 512;
1227
        }else{
1228
            // Не достаточно свободного места.
1229
            return;
1230
        }
1231
        $bs         = 1024;
1232
        $countBlock = $swapSize * $bs;
1233
        $ddCmd      = Util::which('dd');
1234
1235
        Util::sysLogMsg('Swap', 'make swap ' . $swapFile, LOG_INFO);
1236
        Processes::mwExec("{$ddCmd} if=/dev/zero of={$swapFile} bs={$bs} count={$countBlock}");
1237
1238
        $mkSwapCmd = Util::which('mkswap');
1239
        Processes::mwExec("{$mkSwapCmd} {$swapFile}");
1240
    }
1241
1242
    /**
1243
     * Returns free space on mounted storage disk
1244
     *
1245
     * @return int size in megabytes
1246
     */
1247
    public function getStorageFreeSpaceMb(): int
1248
    {
1249
        $size    = 0;
1250
        $mntDir  = '';
1251
        $mounted = self::isStorageDiskMounted('', $mntDir);
1252
        if ( ! $mounted) {
1253
            return 0;
1254
        }
1255
        $hd = $this->getAllHdd(true);
1256
        foreach ($hd as $disk) {
1257
            if ($disk['mounted'] === $mntDir) {
1258
                $size = $disk['free_space'];
1259
                break;
1260
            }
1261
        }
1262
        return $size;
1263
    }
1264
1265
    /**
1266
     * Сохраняем новые данные диска.
1267
     *
1268
     * @param        $data
1269
     * @param string $id
1270
     */
1271
    public function saveDiskSettings($data, $id = '1'): void
1272
    {
1273
        if ( ! is_array($data)) {
1274
            return;
1275
        }
1276
        $disk_data = $this->getDiskSettings($id);
1277
        if (count($disk_data) === 0) {
1278
            $uniqid           = strtoupper('STORAGE-DISK-' . md5(time()));
1279
            $storage_settings = new StorageModel();
1280
            foreach ($data as $key => $val) {
1281
                $storage_settings->writeAttribute($key, $val);
1282
            }
1283
            $storage_settings->writeAttribute('uniqid', $uniqid);
1284
            $storage_settings->save();
1285
        } else {
1286
            $storage_settings = StorageModel::findFirst("id = '$id'");
1287
            if ($storage_settings === null) {
1288
                return;
1289
            }
1290
            foreach ($data as $key => $value) {
1291
                $storage_settings->writeAttribute($key, $value);
1292
            }
1293
            $storage_settings->save();
1294
        }
1295
    }
1296
1297
1298
    public function getRecoverDiskName():string{
1299
        $disks   = $this->diskGetDevices(true);
1300
        foreach ($disks as $disk => $diskInfo) {
1301
            // RAID содержит вложенный массив "children"
1302
            if (isset($diskInfo['children'][0]['children'])) {
1303
                $diskInfo = $diskInfo['children'][0];
1304
                // Корректируем имя диска. Это RAID или иной виртуальный device.
1305
                $disk = $diskInfo['name'];
1306
            }
1307
            foreach ($diskInfo['children'] as $child){
1308
                $mountpoint = $child['mountpoint']??'';
1309
                $diskPath   = "/dev/{$disk}";
1310
                if($mountpoint === '/conf.recover' && file_exists($diskPath)){
1311
                    return "/dev/{$disk}";
1312
                }
1313
            }
1314
        }
1315
        return '';
1316
    }
1317
}