Passed
Push — develop ( 2e4521...d91dd3 )
by Nikolay
05:38
created

Storage::saveDiskSettings()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 23
rs 9.0777
cc 6
nc 6
nop 2
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, 8 2020
7
 */
8
9
namespace MikoPBX\Core\System;
10
11
use MikoPBX\Core\System\Configs\PHPConf;
12
use MikoPBX\Core\Workers\WorkerRemoveOldRecords;
13
use MikoPBX\Common\Models\Storage as StorageModel;
14
use MikoPBX\Common\Providers\ConfigProvider;
15
use Phalcon\Di;
16
use function MikoPBX\Common\Config\appPath;
17
18
19
/**
20
 * Вспомогательные методы.
21
 */
22
class Storage extends Di\Injectable
23
{
24
    /**
25
     * @var \Phalcon\Config
26
     */
27
    private $config;
28
29
    /**
30
     * System constructor.
31
     */
32
    public function __construct()
33
    {
34
        $this->config = $this->di->getShared('config');
35
    }
36
37
    /**
38
     * Возвращает директорию для хранения файлов записей разговоров.
39
     *
40
     * @return string
41
     */
42
    public static function getMonitorDir(): string
43
    {
44
        $di = Di::getDefault();
45
        if ($di !== null) {
46
            return $di->getConfig()->path('asterisk.monitordir');
47
        }
48
49
        return '/tmp';
50
    }
51
52
    /**
53
     * Возвращает директорию для хранения media файлов.
54
     *
55
     * @return string
56
     */
57
    public static function getMediaDir(): string
58
    {
59
        $di = Di::getDefault();
60
        if ($di !== null) {
61
            return $di->getConfig()->path('core.mediaMountPoint');
62
        }
63
64
        return '/tmp';
65
    }
66
67
68
    /**
69
     * Прверяем является ли диск хранилищем.
70
     *
71
     * @param $device
72
     *
73
     * @return bool
74
     */
75
    public static function isStorageDisk($device): bool
76
    {
77
        $result = false;
78
        if ( ! file_exists("{$device}")) {
79
            return $result;
80
        }
81
82
        $tmp_dir = '/tmp/mnt_' . time();
83
        Util::mwMkdir($tmp_dir);
84
        $out = [];
85
86
        $storage  = new Storage();
87
        $uid_part = 'UUID=' . $storage->getUuid($device) . '';
88
        $format   = $storage->getFsType($device);
89
        if ($format === '') {
90
            return false;
91
        }
92
        $mountPath  = Util::which('mount');
93
        $umountPath = Util::which('umount');
94
        $rmPath     = Util::which('rm');
95
96
        Util::mwExec("{$mountPath} -t {$format} {$uid_part} {$tmp_dir}", $out);
97
        if (is_dir("{$tmp_dir}/mikopbx") && trim(implode('', $out)) === '') {
98
            // $out - пустая строка, ошибок нет
99
            // присутствует каталог mikopbx.
100
            $result = true;
101
        }
102
        if (self::isStorageDiskMounted($device)) {
103
            Util::mwExec("{$umountPath} {$device}");
104
        }
105
106
        if ( ! self::isStorageDiskMounted($device)) {
107
            Util::mwExec("{$rmPath} -rf '{$tmp_dir}'");
108
        }
109
110
        return $result;
111
    }
112
113
    /**
114
     * Получение идентификатора устройства.
115
     *
116
     * @param $device
117
     *
118
     * @return string
119
     */
120
    public function getUuid($device): string
121
    {
122
        if (empty($device)) {
123
            return '';
124
        }
125
        $blkidPath   = Util::which('blkid');
126
        $busyboxPath = Util::which('busybox');
127
        $sedPath     = Util::which('sed');
128
        $grepPath    = Util::which('grep');
129
        $awkPath     = Util::which('awk');
130
        $headPath    = Util::which('head');
131
132
        $res = Util::mwExec(
133
            "{$blkidPath} -ofull {$device} | {$busyboxPath} {$sedPath} -r 's/[[:alnum:]]+=/\\n&/g' | {$busyboxPath} {$grepPath} \"^UUID\" | {$busyboxPath} {$awkPath} -F \"\\\"\" '{print $2}' | {$headPath} -n 1",
134
            $output
135
        );
136
        if ($res == 0 && count($output) > 0) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $res of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
137
            $result = $output[0];
138
        } else {
139
            $result = '';
140
        }
141
142
        return $result;
143
    }
144
145
    /**
146
     * Возвращает тип файловой системы блочного устройства.
147
     *
148
     * @param $device
149
     *
150
     * @return string
151
     */
152
    public function getFsType($device): string
153
    {
154
        $blkidPath   = Util::which('blkid');
155
        $busyboxPath = Util::which('busybox');
156
        $sedPath     = Util::which('sed');
157
        $grepPath    = Util::which('grep');
158
        $awkPath     = Util::which('awk');
159
160
        $device = str_replace('/dev/', '', $device);
161
        $out    = [];
162
        Util::mwExec(
163
            "{$blkidPath} -ofull /dev/{$device} | {$busyboxPath} {$sedPath} -r 's/[[:alnum:]]+=/\\n&/g' | {$busyboxPath} {$grepPath} \"^TYPE=\" | {$busyboxPath} {$awkPath} -F \"\\\"\" '{print $2}'",
164
            $out
165
        );
166
        $format = implode('', $out);
167
        if ($format == 'msdosvfat') {
168
            $format = 'msdos';
169
        }
170
171
        return $format;
172
    }
173
174
    /**
175
     * Проверка, смонтирован ли диск - хранилище.
176
     *
177
     * @param string $filter
178
     * @param string $mount_dir
179
     *
180
     * @return bool
181
     */
182
    public static function isStorageDiskMounted($filter = '', &$mount_dir = ''): bool
183
    {
184
        if (Util::isSystemctl() && file_exists('/storage/usbdisk1/')) {
185
            $mount_dir = '/storage/usbdisk1/';
186
187
            return true;
188
        }
189
        if ('' === $filter) {
190
            $di = Di::getDefault();
191
            if ($di !== null) {
192
                $varEtcDir = $di->getConfig()->path('core.varEtcDir');
193
            } else {
194
                $varEtcDir = '/var/etc';
195
            }
196
197
            $filename = "{$varEtcDir}/storage_device";
198
            if (file_exists($filename)) {
199
                $filter = file_get_contents($filename);
200
            } else {
201
                $filter = 'usbdisk1';
202
            }
203
        }
204
        $filter = escapeshellarg($filter);
205
206
        $out       = [];
207
        $grepPath  = Util::which('grep');
208
        $mountPath = Util::which('mount');
209
        $awkPath   = Util::which('awk');
210
        Util::mwExec("{$mountPath} | {$grepPath} {$filter} | {$awkPath} '{print $3}'", $out);
211
        $mount_dir = trim(implode('', $out));
212
213
        return ($mount_dir !== '');
214
    }
215
216
    /**
217
     * Монитирование каталога с удаленного сервера SFTP.
218
     *
219
     * @param        $host
220
     * @param int    $port
221
     * @param string $user
222
     * @param string $pass
223
     * @param string $remout_dir
224
     * @param string $local_dir
225
     *
226
     * @return bool
227
     */
228
    public static function mountSftpDisk($host, $port, $user, $pass, $remout_dir, $local_dir): bool
229
    {
230
        Util::mwMkdir($local_dir);
231
232
        $out         = [];
233
        $timeoutPath = Util::which('timeout');
234
        $sshfsPath   = Util::which('sshfs');
235
236
        $command = "{$timeoutPath} -t 3 {$sshfsPath} -p {$port} -o nonempty -o password_stdin -o 'StrictHostKeyChecking=no' " .
237
            "{$user}@{$host}:{$remout_dir} {$local_dir} << EOF\n" .
238
            "{$pass}\n" .
239
            "EOF\n";
240
        // file_put_contents('/tmp/sshfs_'.$host, $command);
241
        Util::mwExec($command, $out);
242
        $response = trim(implode('', $out));
243
        if ('Terminated' == $response) {
244
            // Удаленный сервер не ответил / или не корректно указан пароль.
245
            unset($response);
246
        }
247
248
        return self::isStorageDiskMounted("$local_dir ");
249
    }
250
251
    /**
252
     * Монитирование каталога с удаленного сервера FTP.
253
     *
254
     * @param        $host
255
     * @param        $port
256
     * @param        $user
257
     * @param        $pass
258
     * @param string $remout_dir
259
     * @param        $local_dir
260
     *
261
     * @return bool
262
     */
263
    public static function mountFtp($host, $port, $user, $pass, $remout_dir, $local_dir): bool
264
    {
265
        Util::mwMkdir($local_dir);
266
        $out = [];
267
268
        // Собираем строку подключения к ftp.
269
        $auth_line = '';
270
        if ( ! empty($user)) {
271
            $auth_line .= 'user="' . $user;
272
            if ( ! empty($pass)) {
273
                $auth_line .= ":{$pass}";
274
            }
275
            $auth_line .= '",';
276
        }
277
278
        $connect_line = 'ftp://' . $host;
279
        if ( ! empty($port)) {
280
            $connect_line .= ":{$port}";
281
        }
282
        if ( ! empty($remout_dir)) {
283
            $connect_line .= "$remout_dir";
284
        }
285
286
        $timeoutPath   = Util::which('timeout');
287
        $curlftpfsPath = Util::which('curlftpfs');
288
        $command       = "{$timeoutPath} -t 3 {$curlftpfsPath}  -o allow_other -o {$auth_line}fsname={$host} {$connect_line} {$local_dir}";
289
        Util::mwExec($command, $out);
290
        $response = trim(implode('', $out));
291
        if ('Terminated' === $response) {
292
            // Удаленный сервер не ответил / или не корректно указан пароль.
293
            unset($response);
294
        }
295
296
        return self::isStorageDiskMounted("$local_dir ");
297
    }
298
299
    /**
300
     * Запускает процесс форматирования диска.
301
     *
302
     * @param $dev
303
     *
304
     * @return array|bool
305
     */
306
    public static function mkfs_disk($dev)
307
    {
308
        if ( ! file_exists($dev)) {
309
            $dev = "/dev/{$dev}";
310
        }
311
        if ( ! file_exists($dev)) {
312
            return false;
313
        }
314
        $dir = '';
315
        self::isStorageDiskMounted("$dev", $dir);
316
317
        if (empty($dir) || self::umountDisk($dir)) {
318
            // Диск размонтирован.
319
            $st = new Storage();
320
            // Будет запущен процесс:
321
            $st->formatDiskLocal($dev, true);
322
            sleep(1);
323
324
            return (self::statusMkfs($dev) == 'inprogress');
325
        } else {
326
            // Ошибка размонтирования диска.
327
            return false;
328
        }
329
    }
330
331
    /**
332
     * Размонтирует диск. Удаляет каталог в случае успеха.
333
     *
334
     * @param $dir
335
     *
336
     * @return bool
337
     */
338
    public static function umountDisk($dir): bool
339
    {
340
        $umountPath = Util::which('umount');
341
        $rmPath     = Util::which('rm');
342
        if (self::isStorageDiskMounted($dir)) {
343
            Util::mwExec("/sbin/shell_functions.sh 'killprocesses' '$dir' -TERM 0");
344
            Util::mwExec("{$umountPath} {$dir}");
345
        }
346
        $result = ! self::isStorageDiskMounted($dir);
347
        if ($result && file_exists($dir)) {
348
            // Если диск не смонтирован, то удаляем каталог.
349
            Util::mwExec("{$rmPath} -rf '{$dir}'");
350
        }
351
352
        return $result;
353
    }
354
355
    /**
356
     * Разметка диска.
357
     *
358
     * @param string $device
359
     * @param bool   $bg
360
     *
361
     * @return mixed
362
     */
363
    public function formatDiskLocal($device, $bg = false)
364
    {
365
        $partedPath = Util::which('parted');
366
        $retVal     = Util::mwExec(
367
            "{$partedPath} --script --align optimal '{$device}' 'mklabel msdos mkpart primary ext4 0% 100%'"
368
        );
369
        Util::sysLogMsg(__CLASS__, "{$partedPath} returned {$retVal}");
370
        if (false === $bg) {
371
            sleep(1);
372
        }
373
374
        return $this->formatDiskLocalPart2($device, $bg);
375
    }
376
377
    /**
378
     * Форматирование диска.
379
     *
380
     * @param string $device
381
     * @param bool   $bg
382
     *
383
     * @return mixed
384
     */
385
    private function formatDiskLocalPart2($device, $bg = false)
386
    {
387
        if (is_numeric(substr($device, -1))) {
388
            $device_id = "";
389
        } else {
390
            $device_id = "1";
391
        }
392
        $format   = 'ext4';
393
        $mkfsPath = Util::which("mkfs.{$format}");
394
        $cmd      = "{$mkfsPath} {$device}{$device_id}";
395
        if ($bg === false) {
396
            $retVal = Util::mwExec("{$cmd} 2>&1");
397
            Util::sysLogMsg(__CLASS__, "{$mkfsPath} returned {$retVal}");
398
        } else {
399
            usleep(200000);
400
            Util::mwExecBg($cmd);
401
            $retVal = true;
402
        }
403
404
        return $retVal;
405
    }
406
407
    /**
408
     * Возвращает текущий статус форматирования диска.
409
     *
410
     * @param $dev
411
     *
412
     * @return string
413
     */
414
    public static function statusMkfs($dev): string
415
    {
416
        if ( ! file_exists($dev)) {
417
            $dev = "/dev/{$dev}";
418
        }
419
        $out      = [];
420
        $psPath   = Util::which('ps');
421
        $grepPath = Util::which('grep');
422
        Util::mwExec("{$psPath} -A -f | {$grepPath} {$dev} | {$grepPath} mkfs | {$grepPath} -v grep", $out);
423
        $mount_dir = trim(implode('', $out));
424
425
        return empty($mount_dir) ? 'ended' : 'inprogress';
426
    }
427
428
    /**
429
     * Clear cache folders from PHP sessions files
430
     */
431
    public static function clearSessionsFiles()
432
    {
433
        $di = Di::getDefault();
434
        if ($di === null) {
435
            return;
436
        }
437
        $config         = $di->getShared('config');
438
        $phpSessionDir = $config->path('www.phpSessionDir');
439
        if ( ! empty($phpSessionDir)) {
440
            $rmPath = Util::which('rm');
441
            Util::mwExec("{$rmPath} -rf {$phpSessionDir}/*");
442
        }
443
    }
444
445
    /**
446
     * Проверка свободного места на дисках. Уведомление в случае проблем.
447
     */
448
    public function checkFreeSpace(): void
449
    {
450
        $util = new Util();
451
        $hdd  = $this->getAllHdd(true);
452
        // Создание больщого файла для тестов.
453
        // head -c 1500MB /dev/urandom > /storage/usbdisk1/big_file.mp3
454
        foreach ($hdd as $disk) {
455
            if ($disk['sys_disk'] === true && ! self::isStorageDiskMounted("{$disk['id']}4")) {
456
                // Это системный диск (4ый раздел). Он не смонтирован.
457
                continue;
458
            }
459
460
            $free       = ($disk['free_space'] / $disk['size'] * 100);
461
            $need_alert = false;
462
            $test_alert = '';
463
            if ($free < 5) {
464
                $need_alert = true;
465
                $test_alert = "The {$disk['id']} has less than 5% of free space available.";
466
            }
467
468
            if ($disk['free_space'] < 500) {
469
                $need_alert = true;
470
                $test_alert = "The {$disk['id']} has less than 500MB of free space available.";
471
            }
472
473
            if ($disk['free_space'] < 100) {
474
                $need_alert = true;
475
                $test_alert = "The {$disk['id']} has less than 100MB of free space available. Old call records will be deleted.";
476
                Util::restartPHPWorker(WorkerRemoveOldRecords::class);
477
            }
478
479
            if ( ! $need_alert) {
480
                continue;
481
            }
482
483
            Util::sysLogMsg("STORAGE", $test_alert);
484
            $data = [
485
                'Device     - ' => "/dev/{$disk['id']}",
486
                'Directoire - ' => "{$disk['mounted']}",
487
                'Desciption - ' => $test_alert,
488
            ];
489
            // Добавляем задачу на уведомление.
490
            $util->addJobToBeanstalk('WorkerNotifyError_storage', $data);
491
        }
492
    }
493
494
    /**
495
     * Возвращает все подключенные HDD.
496
     *
497
     * @param bool $mounted_only
498
     *
499
     * @return array
500
     */
501
    public function getAllHdd($mounted_only = false): array
502
    {
503
        $res_disks = [];
504
505
        if (Util::isSystemctl()) {
506
            $out      = [];
507
            $grepPath = Util::which('grep');
508
            $dfPath   = Util::which('df');
509
            $awkPath  = Util::which('awk');
510
            Util::mwExec(
511
                "{$dfPath} -k /storage/usbdisk1 | {$awkPath}  '{ print $1 \"|\" $3 \"|\" $4} ' | {$grepPath} -v 'Available'",
512
                $out
513
            );
514
            $disk_data = explode('|', implode(" ", $out));
515
            if (count($disk_data) === 3) {
516
                $m_size      = round(($disk_data[1] + $disk_data[2]) / 1024, 1);
517
                $res_disks[] = [
518
                    'id'         => $disk_data[0],
519
                    'size'       => "" . $m_size,
520
                    'size_text'  => "" . $m_size . " Mb",
521
                    'vendor'     => 'Debian',
522
                    'mounted'    => '/storage/usbdisk1',
523
                    'free_space' => round($disk_data[2] / 1024, 1),
524
                    'partitions' => [],
525
                    'sys_disk'   => true,
526
                ];
527
            }
528
529
            return $res_disks;
530
        }
531
532
        $cd_disks = $this->cdromGetDevices();
533
        $cd_disks = array_unique($cd_disks);
534
535
        // TODO Получение данных о дисках в формате JSON:
536
        // lsblk -J -b -o VENDOR,MODEL,SERIAL,LABEL,TYPE,FSTYPE,MOUNTPOINT,SUBSYSTEMS,NAME,UUID
537
        $disks = $this->diskGetDevices();
538
        $disks = array_unique($disks);
539
540
        $cf_disk    = '';
541
        $varEtcDir = $this->config->path('core.varEtcDir');
542
        if (file_exists($varEtcDir . '/cfdevice')) {
543
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
544
        }
545
546
        foreach ($disks as $disk) {
547
            if (in_array($disk, $cd_disks)) {
548
                // Это CD-ROM.
549
                continue;
550
            }
551
            unset($temp_vendor, $temp_size, $original_size);
552
            $mounted = self::diskIsMounted("{$disk}");
553
            if ($mounted_only === true && $mounted === false) {
554
                continue;
555
            }
556
            $sys_disk = ($cf_disk == $disk);
557
558
            $mb_size = 0;
559
            if (is_file("/sys/block/" . $disk . "/size")) {
560
                $original_size = trim(file_get_contents("/sys/block/" . $disk . "/size"));
561
                $original_size = ($original_size * 512 / 1024 / 1024);
562
                $mb_size       = $original_size;
563
            }
564
            if ($mb_size > 100) {
565
                $temp_size   = sprintf("%.0f MB", $mb_size);
566
                $temp_vendor = $this->getVendorDisk($disk);
567
                $free_space  = $this->getFreeSpace($disk);
568
569
                $arr_disk_info = $this->determineFormatFs($disk);
570
                if (count($arr_disk_info) > 0) {
571
                    $used = 0;
572
                    foreach ($arr_disk_info as $disk_info) {
573
                        $used += $disk_info['used_space'];
574
                    }
575
                    if ($used > 0) {
576
                        $free_space = $mb_size - $used;
577
                    }
578
                }
579
580
                $res_disks[] = [
581
                    'id'         => $disk,
582
                    'size'       => $mb_size,
583
                    'size_text'  => $temp_size,
584
                    'vendor'     => $temp_vendor,
585
                    'mounted'    => $mounted,
586
                    'free_space' => $free_space,
587
                    'partitions' => $arr_disk_info,
588
                    'sys_disk'   => $sys_disk,
589
                ];
590
            }
591
        }
592
593
        return $res_disks;
594
    }
595
596
    /**
597
     * Получение массива подключенныйх cdrom.
598
     *
599
     * @return array
600
     */
601
    private function cdromGetDevices(): array
602
    {
603
        $grepPath    = Util::which('grep');
604
        $sysctlPath  = Util::which('sysctl');
605
        $busyboxPath = Util::which('busybox');
606
        $cutPath     = Util::which('cut');
607
608
        return explode(
609
            " ",
610
            trim(
611
                exec(
612
                    "{$sysctlPath} -n dev.cdrom.info | {$busyboxPath} {$grepPath} 'drive name' | {$busyboxPath} {$cutPath} -f 3"
613
                )
614
            )
615
        );
616
    }
617
618
    /**
619
     * Получение массива подключенныйх HDD.
620
     *
621
     * @return array
622
     */
623
    private function diskGetDevices(): array
624
    {
625
        //  TODO // Переписать через использование lsblk.
626
        $grepPath = Util::which('grep');
627
        $lsPath   = Util::which('ls');
628
        $trPath   = Util::which('tr');
629
630
        return explode(" ", trim(exec("{$lsPath} /dev | {$grepPath} '^[a-z]d[a-z]' | {$trPath} \"\n\" \" \"")));
631
    }
632
633
    /**
634
     * Проверка, смонтирован ли диск.
635
     *
636
     * @param $disk
637
     * @param $filter
638
     *
639
     * @return string|bool
640
     */
641
    public static function diskIsMounted($disk, $filter = '/dev/')
642
    {
643
        $out       = [];
644
        $grepPath  = Util::which('grep');
645
        $mountPath = Util::which('mount');
646
        Util::mwExec("{$mountPath} | {$grepPath} '{$filter}{$disk}'", $out);
647
        if (count($out) > 0) {
648
            $res_out = end($out);
649
        } else {
650
            $res_out = implode('', $out);
651
        }
652
        $data = explode(' ', trim($res_out));
653
654
        return (count($data) > 2) ? $data[2] : false;
655
    }
656
657
    /**
658
     * Получение сведений по диску.
659
     *
660
     * @param $disk
661
     *
662
     * @return string
663
     */
664
    private function getVendorDisk($disk): string
665
    {
666
        $temp_vendor = [];
667
        if (is_file("/sys/block/" . $disk . "/device/vendor")) {
668
            $data = trim(file_get_contents("/sys/block/" . $disk . "/device/vendor"));
669
            if ($data != '') {
670
                $temp_vendor[] = trim(str_replace(',', ' ', $data));
671
            }
672
        }
673
        if (is_file("/sys/block/" . $disk . "/device/model")) {
674
            $data = trim(file_get_contents("/sys/block/" . $disk . "/device/model"));
675
            if ($data != '') {
676
                $temp_vendor[] = trim(str_replace(',', ' ', $data));
677
            }
678
        }
679
        if (count($temp_vendor) == 0) {
680
            $temp_vendor[] = $disk;
681
        }
682
        if (is_file("/sys/block/" . $disk . "/device/type")) {
683
            $data = trim(file_get_contents("/sys/block/" . $disk . "/device/type"));
684
            if ($data != '') {
685
                $temp_vendor[] = trim(str_replace(',', ' ', $data));
686
            }
687
        }
688
689
        return implode(', ', $temp_vendor);
690
    }
691
692
    /**
693
     * Получаем свободное место на диске в Mb.
694
     *
695
     * @param $hdd
696
     *
697
     * @return mixed
698
     */
699
    public function getFreeSpace($hdd)
700
    {
701
        $out      = [];
702
        $hdd      = escapeshellarg($hdd);
703
        $grepPath = Util::which('grep');
704
        $awkPath  = Util::which('awk');
705
        $dfPath   = Util::which('df');
706
        Util::mwExec("{$dfPath} -m | {$grepPath} {$hdd} | {$awkPath} '{print $4}'", $out);
707
        $result = 0;
708
        foreach ($out as $res) {
709
            if ( ! is_numeric($res)) {
710
                continue;
711
            }
712
            $result += (1 * $res);
713
        }
714
715
        return $result;
716
    }
717
718
    /**
719
     * Определить формат файловой системы и размер дисков.
720
     *
721
     * @param $device
722
     *
723
     * @return array|bool
724
     */
725
    public function determineFormatFs($device)
726
    {
727
        $grepPath      = Util::which('grep');
728
        $lsPath        = Util::which('ls');
729
        $trPath        = Util::which('tr');
730
        $allow_formats = ['ext2', 'ext4', 'fat', 'ntfs', 'msdos'];
731
        $device        = str_replace('/dev/', '', $device);
732
        $devices       = explode(" ", trim(exec("{$lsPath} /dev | {$grepPath} '{$device}' | {$trPath} \"\n\" \" \"")));
733
734
        $result_data = [];
735
        foreach ($devices as $dev) {
736
            if (empty($dev) || (count($devices) > 1 && $device == $dev) || is_dir("/sys/block/{$dev}")) {
737
                continue;
738
            }
739
            $mb_size        = 0;
740
            $path_size_info = '';
741
            $tmp_path       = "/sys/block/{$device}/{$dev}/size";
742
            if (file_exists($tmp_path)) {
743
                $path_size_info = $tmp_path;
744
            }
745
            if (empty($path_size_info)) {
746
                $tmp_path = "/sys/block/" . substr($dev, 0, 3) . "/{$dev}/size";
747
                if (file_exists($tmp_path)) {
748
                    $path_size_info = $tmp_path;
749
                }
750
            }
751
752
            if ( ! empty($path_size_info)) {
753
                $original_size = trim(file_get_contents($path_size_info));
754
                $original_size = ($original_size * 512 / 1024 / 1024);
755
                $mb_size       = $original_size;
756
            }
757
758
            $tmp_dir = "/tmp/{$dev}_" . time();
759
            $out     = [];
760
761
            $fs           = null;
762
            $need_unmount = false;
763
            $mount_dir    = '';
764
            if (self::isStorageDiskMounted("/dev/{$dev} ", $mount_dir)) {
765
                $grepPath  = Util::which('grep');
766
                $awkPath   = Util::which('awk');
767
                $mountPath = Util::which('mount');
768
                Util::mwExec("{$mountPath} | {$grepPath} '/dev/{$dev}' | {$awkPath} '{print $5}'", $out);
769
                $fs         = trim(implode("", $out));
770
                $fs         = ($fs == 'fuseblk') ? 'ntfs' : $fs;
771
                $free_space = $this->getFreeSpace("/dev/{$dev} ");
772
                $used_space = $mb_size - $free_space;
773
            } else {
774
                $format = $this->getFsType($device);
775
                if (in_array($format, $allow_formats)) {
776
                    $fs = $format;
777
                }
778
                self::mountDisk($dev, $format, $tmp_dir);
779
780
                $need_unmount = true;
781
                $used_space   = Util::getSizeOfFile("$tmp_dir");
782
            }
783
            $result_data[] = [
784
                "dev"        => $dev,
785
                'size'       => round($mb_size, 2),
786
                "used_space" => round($used_space, 2),
787
                "free_space" => round($mb_size - $used_space, 2),
788
                "uuid"       => $this->getUuid("/dev/{$dev} "),
789
                "fs"         => $fs,
790
            ];
791
            if ($need_unmount) {
792
                self::umountDisk($tmp_dir);
793
            }
794
        }
795
796
        return $result_data;
797
    }
798
799
    /**
800
     * Монтирует диск в указанный каталог.
801
     *
802
     * @param $dev
803
     * @param $format
804
     * @param $dir
805
     *
806
     * @return bool
807
     */
808
    public static function mountDisk($dev, $format, $dir): bool
809
    {
810
        if (self::isStorageDiskMounted("/dev/{$dev} ")) {
811
            return true;
812
        }
813
        Util::mwMkdir($dir);
814
815
        if ( ! file_exists($dir)) {
816
            Util::sysLogMsg('Storage', "Unable mount $dev $format to $dir. Unable create dir.");
817
818
            return false;
819
        }
820
        $dev = str_replace('/dev/', '', $dev);
821
        if ('ntfs' == $format) {
822
            $mountNtfs3gPath = Util::which('mount.ntfs-3g');
823
            Util::mwExec("{$mountNtfs3gPath} /dev/{$dev} {$dir}", $out);
824
        } else {
825
            $storage   = new Storage();
826
            $uid_part  = 'UUID=' . $storage->getUuid("/dev/{$dev}") . '';
827
            $mountPath = Util::which('mount');
828
            Util::mwExec("{$mountPath} -t {$format} {$uid_part} {$dir}", $out);
829
        }
830
831
        return self::isStorageDiskMounted("/dev/{$dev} ");
832
    }
833
834
    /**
835
     * Монтирование разделов диска с базой данных настроек.
836
     */
837
    public function configure(): void
838
    {
839
        $cf_disk          = '';
840
        $varEtcDir       = $this->config->path('core.varEtcDir');
841
        $storage_dev_file = "{$varEtcDir}/storage_device";
842
        if (file_exists($storage_dev_file)) {
843
            unlink($storage_dev_file);
844
        }
845
846
        if (file_exists($varEtcDir . '/cfdevice')) {
847
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
848
        }
849
850
        $disks = $this->getDiskSettings();
851
        $conf  = '';
852
        foreach ($disks as $disk) {
853
            clearstatcache();
854
            if ($disk['device'] !== "/dev/{$cf_disk}") {
855
                // Если это обычный диск, то раздел 1.
856
                $dev = "{$disk['device']}1";
857
            } else {
858
                // Если это системный диск, то пытаемся подключить раздел 4.
859
                $dev = "{$disk['device']}4";
860
            }
861
            if ( ! $this->hddExists($dev)) {
862
                // Диск не существует.
863
                continue;
864
            }
865
            if ($disk['media'] === '1' || ! file_exists($storage_dev_file)) {
866
                file_put_contents($storage_dev_file, "/storage/usbdisk{$disk['id']}");
867
                $this->updateConfigWithNewMountPoint("/storage/usbdisk{$disk['id']}");
868
            }
869
870
            $str_uid     = 'UUID=' . $this->getUuid($dev) . '';
871
            $format_p4   = $this->getFsType($dev);
872
            $conf        .= "{$str_uid} /storage/usbdisk{$disk['id']} {$format_p4} async,rw 0 0\n";
873
            $mount_point = "/storage/usbdisk{$disk['id']}";
874
            Util::mwMkdir($mount_point);
875
        }
876
        $this->saveFstab($conf);
877
        $this->createWorkDirs();
878
        PHPConf::setupLog();
879
    }
880
881
    /**
882
     * Получаем настройки диска из базы данных.
883
     *
884
     * @param string $id
885
     *
886
     * @return array
887
     */
888
    public function getDiskSettings($id = ''): array
889
    {
890
        $data = [];
891
        if ('' === $id) {
892
            $pbxSettings = StorageModel::find();
893
            if ($pbxSettings) {
0 ignored issues
show
introduced by
$pbxSettings is of type Phalcon\Mvc\Model\ResultsetInterface, thus it always evaluated to true.
Loading history...
894
                // Возвращаем данные до модификации.
895
                $data = $pbxSettings->toArray();
896
            }
897
        } else {
898
            $pbxSettings = StorageModel::findFirst("id='$id'");
899
            if ($pbxSettings !== null) {
900
                $data = $pbxSettings->toArray();
901
            }
902
        }
903
904
        return $data;
905
    }
906
907
    /**
908
     * Проверяет, существует ли диск в массиве.
909
     *
910
     * @param $disk
911
     *
912
     * @return bool
913
     */
914
    private function hddExists($disk): bool
915
    {
916
        $result = false;
917
        $uid    = $this->getUuid("{$disk}");
918
        if (file_exists("{$disk}") && $uid !== false) {
919
            $result = true;
920
        }
921
922
        return $result;
923
    }
924
925
    /**
926
     * After mount storage we will change /mountpoint/ to new $mount_point value
927
     *
928
     * @param string $mount_point
929
     *
930
     * @throws \JsonException
931
     */
932
    private function updateConfigWithNewMountPoint(string $mount_point): void
933
    {
934
        $staticSettingsFile     = '/etc/inc/mikopbx-settings.json';
935
        $staticSettingsFileOrig = appPath('config/mikopbx-settings.json');
936
937
        $jsonString = file_get_contents($staticSettingsFileOrig);
938
        $data       = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
939
        foreach ($data as $rootKey => $rootEntry) {
940
            foreach ($rootEntry as $nestedKey => $entry) {
941
                if (stripos($entry, '/mountpoint') !== false) {
942
                    $data[$rootKey][$nestedKey] = str_ireplace('/mountpoint', $mount_point, $entry);
943
                }
944
            }
945
        }
946
        $newJsonString = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
947
        file_put_contents($staticSettingsFile, $newJsonString);
948
949
        // Update config variable
950
        $this->di->remove('config');
951
        $this->di->register(new ConfigProvider());
952
        $this->config = $this->di->getShared('config');
953
954
    }
955
956
    /**
957
     * Генерация файла fstab. Монтирование разделов.
958
     *
959
     * @param string $conf
960
     */
961
    public function saveFstab($conf = ''): void
962
    {
963
        $varEtcDir = $this->config->path('core.varEtcDir');
964
        // Точка монтирования доп. дисков.
965
        Util::mwMkdir('/storage');
966
        $chmodPath = Util::which('chmod');
967
        Util::mwExec("{$chmodPath} 755 /storage");
968
        if ( ! file_exists($varEtcDir . '/cfdevice')) {
969
            return;
970
        }
971
        $fstab     = '';
972
        $file_data = file_get_contents($varEtcDir . '/cfdevice');
973
        $cf_disk   = trim($file_data);
974
        if ('' == $cf_disk) {
975
            return;
976
        }
977
        // $part1 	 = (strpos($cf_disk, "mmcblk") !== false)?"{$cf_disk}p1":"{$cf_disk}1"; // Boot
978
        $part2 = (strpos($cf_disk, 'mmcblk') !== false) ? "{$cf_disk}p2" : "{$cf_disk}2"; // Offload
979
        $part3 = (strpos($cf_disk, 'mmcblk') !== false) ? "{$cf_disk}p3" : "{$cf_disk}3"; // Conf
980
981
982
        $uid_part2 = 'UUID=' . $this->getUuid("/dev/{$part2}") . '';
983
        $format_p2 = $this->getFsType($part2);
984
        $uid_part3 = 'UUID=' . $this->getUuid("/dev/{$part3}") . '';
985
        $format_p3 = $this->getFsType($part3);
986
987
        // $fstab .= "/dev/{$part1} /cf msdos ro 1 1\n"; // НЕ МОНТИРУЕМ!
988
        $fstab .= "{$uid_part2} /offload {$format_p2} ro 0 0\n";
989
        $fstab .= "{$uid_part3} /cf {$format_p3} rw 1 1\n";
990
        $fstab .= $conf;
991
992
        file_put_contents("/etc/fstab", $fstab);
993
        // Дублируем для работы vmtoolsd.
994
        file_put_contents("/etc/mtab", $fstab);
995
        $mountPath = Util::which('mount');
996
        Util::mwExec("{$mountPath} -a 2> /dev/null");
997
        Util::addRegularWWWRights('/cf');
998
    }
999
1000
    /**
1001
     * Create system folders
1002
     *
1003
     * @return void
1004
     */
1005
    private function createWorkDirs(): void
1006
    {
1007
        $path      = '';
1008
        $mountPath = Util::which('mount');
1009
        Util::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1010
1011
        $isLiveCd = file_exists('/offload/livecd');
1012
        // Create dirs
1013
        $arrConfig = $this->config->toArray();
1014
        foreach ($arrConfig as $rootEntry) {
1015
            foreach ($rootEntry as $key => $entry) {
1016
                if (stripos($key, 'path') === false && stripos($key, 'dir') === false) {
1017
                    continue;
1018
                }
1019
                if (file_exists($entry)) {
1020
                    continue;
1021
                }
1022
                if ($isLiveCd && strpos($entry, '/offload/') === 0) {
1023
                    continue;
1024
                }
1025
                $path .= " $entry";
1026
            }
1027
        }
1028
1029
        if ( ! empty($path)) {
1030
            Util::mwMkdir($path);
1031
        }
1032
1033
        $downloadCacheDir = appPath('sites/pbxcore/files/cache');
1034
        if ( ! $isLiveCd ) {
1035
            Util::mwMkdir($downloadCacheDir);
1036
            Util::createUpdateSymlink($this->config->path('www.downloadCacheDir'), $downloadCacheDir);
1037
        }
1038
1039
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
1040
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/js', $jsCacheDir);
1041
1042
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
1043
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/css', $cssCacheDir);
1044
1045
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
1046
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/img', $imgCacheDir);
1047
1048
        Util::createUpdateSymlink($this->config->path('www.phpSessionDir'), '/var/lib/php/session');
1049
        Util::createUpdateSymlink($this->config->path('www.uploadDir'), '/ultmp');
1050
1051
        $filePath = appPath('src/Core/Asterisk/Configs/lua/extensions.lua');
1052
        Util::createUpdateSymlink($filePath, '/etc/asterisk/extensions.lua');
1053
1054
        // Create symlinks to AGI-BIN
1055
        $agiBinDir = $this->config->path('asterisk.astagidir');
1056
        if ($isLiveCd && strpos($agiBinDir, '/offload/') !== 0) {
1057
            Util::mwMkdir($agiBinDir);
1058
        }
1059
1060
        $roAgiBinFolder = appPath('src/Core/Asterisk/agi-bin');
1061
        $files          = glob("{$roAgiBinFolder}/*.{php}", GLOB_BRACE);
1062
        foreach ($files as $file) {
1063
            $fileInfo    = pathinfo($file);
1064
            $newFilename = "{$agiBinDir}/{$fileInfo['filename']}.{$fileInfo['extension']}";
1065
            Util::createUpdateSymlink($file, $newFilename);
1066
        }
1067
        $this->clearCacheFiles();
1068
        $this->applyFolderRights();
1069
    }
1070
1071
    /**
1072
     * Clear cache folders from old and orphaned files
1073
     */
1074
    public function clearCacheFiles()
1075
    {
1076
        $cacheDirs   = [];
1077
        $cacheDirs[] = $this->config->path('www.uploadDir');
1078
        $cacheDirs[] = $this->config->path('www.downloadCacheDir');
1079
        $cacheDirs[] = $this->config->path('www.managedCacheDir');
1080
        $cacheDirs[] = $this->config->path('www.modelsCacheDir');
1081
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/js';
1082
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/css';
1083
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/img';
1084
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
1085
        $rmPath      = Util::which('rm');
1086
        foreach ($cacheDirs as $cacheDir) {
1087
            if ( ! empty($cacheDir)) {
1088
                Util::mwExec("{$rmPath} -rf {$cacheDir}/*");
1089
            }
1090
        }
1091
    }
1092
1093
    /**
1094
     * Fix permissions for Folder and Files
1095
     */
1096
    private function applyFolderRights(): void
1097
    {
1098
        // Add Rights to the WWW dirs plus some core dirs
1099
        $www_dirs  = [];
1100
        $exec_dirs = [];
1101
        $arrConfig = $this->config->adminApplication->toArray();
1102
        foreach ($arrConfig as $key => $entry) {
1103
            if (stripos($key, 'path') === false
1104
                && stripos($key, 'dir') === false
1105
            ) {
1106
                continue;
1107
            }
1108
            $www_dirs[] = $entry;
1109
        }
1110
1111
        $arrConfig = $this->config->www->toArray();
1112
        foreach ($arrConfig as $key => $entry) {
1113
            if (stripos($key, 'path') === false
1114
                && stripos($key, 'dir') === false
1115
            ) {
1116
                continue;
1117
            }
1118
            $www_dirs[] = $entry;
1119
        }
1120
1121
        $www_dirs[] = $this->config->path('core.tempDir');
1122
        $www_dirs[] = $this->config->path('database.logsDir');
1123
        $www_dirs[] = '/etc/version';
1124
        $www_dirs[] = appPath('/');
1125
1126
        // Add read rights
1127
        Util::addRegularWWWRights(implode(' ', $www_dirs));
1128
1129
        // Add executable rights
1130
        $exec_dirs[] = appPath('src/Core/Asterisk/agi-bin');
1131
        $exec_dirs[] = appPath('src/Core/Rc');
1132
        Util::addExecutableRights(implode(' ', $exec_dirs));
1133
1134
        $mountPath = Util::which('mount');
1135
        Util::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1136
    }
1137
1138
    /**
1139
     * Сохраняем новые данные диска.
1140
     *
1141
     * @param        $data
1142
     * @param string $id
1143
     */
1144
    public function saveDiskSettings($data, $id = '1'): void
1145
    {
1146
        if ( ! is_array($data)) {
1147
            return;
1148
        }
1149
        $disk_data = $this->getDiskSettings($id);
1150
        if (count($disk_data) === 0) {
1151
            $uniqid           = strtoupper('STORAGE-DISK-' . md5(time()));
1152
            $storage_settings = new StorageModel();
1153
            foreach ($data as $key => $val) {
1154
                $storage_settings->writeAttribute($key, $val);
1155
            }
1156
            $storage_settings->writeAttribute('uniqid', $uniqid);
1157
            $storage_settings->save();
1158
        } else {
1159
            $storage_settings = StorageModel::findFirst("id = '$id'");
1160
            if ($storage_settings === null) {
1161
                return;
1162
            }
1163
            foreach ($data as $key => $value) {
1164
                $storage_settings->writeAttribute($key, $value);
1165
            }
1166
            $storage_settings->save();
1167
        }
1168
    }
1169
}