Passed
Push — develop ( 89ed24...6b9531 )
by Nikolay
04:52
created

Storage::updateEnvironmentAfterChangeMountPoint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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