Passed
Pull Request — master (#16)
by Nikolay
13:10 queued 02:12
created

Storage::getFreeSpace()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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