Passed
Push — develop ( a48897...a3169f )
by Nikolay
04:47
created

Storage::getDiskParted()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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