Passed
Push — develop ( 8c4a8c...ad4266 )
by Портнов
04:45
created

Storage::moveReadOnlySoundsToStorage()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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