Passed
Branch develop (309f3f)
by Nikolay
10:15
created

Storage::statusMkfs()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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