Passed
Push — develop ( ca4128...18e2e5 )
by Nikolay
04:24
created

Storage::hddExists()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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