Passed
Push — develop ( 68c59a...3b1482 )
by Nikolay
04:26
created

Storage::getStorageDev()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 23
rs 9.5222
cc 5
nc 5
nop 2
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use Error;
23
use JsonException;
24
use MikoPBX\Common\Config\ClassLoader;
25
use MikoPBX\Common\Models\PbxExtensionModules;
26
use MikoPBX\Common\Models\SoundFiles;
27
use MikoPBX\Common\Providers\MainDatabaseProvider;
28
use MikoPBX\Core\Asterisk\CdrDb;
29
use MikoPBX\Core\Config\RegisterDIServices;
30
use MikoPBX\Core\System\Configs\PHPConf;
31
use MikoPBX\Common\Models\Storage as StorageModel;
32
use MikoPBX\Common\Providers\ConfigProvider;
33
use MikoPBX\Core\System\Configs\SyslogConf;
34
use MikoPBX\Core\System\Upgrade\UpdateDatabase;
35
use MikoPBX\Modules\PbxExtensionUtils;
36
use MikoPBX\PBXCoreREST\Lib\SystemManagementProcessor;
37
use MikoPBX\PBXCoreREST\Workers\WorkerApiCommands;
38
use Phalcon\Di;
39
40
use function MikoPBX\Common\Config\appPath;
41
42
43
/**
44
 * Class Storage
45
 *
46
 * Manages storage-related operations.
47
 *
48
 * @package MikoPBX\Core\System
49
 * @property \Phalcon\Config config
50
 */
51
class Storage extends Di\Injectable
52
{
53
    /**
54
     * Returns the monitor directory path.
55
     *
56
     * @return string The monitor directory path.
57
     */
58
    public static function getMonitorDir(): string
59
    {
60
        $di = Di::getDefault();
61
        if ($di !== null) {
62
            return $di->getConfig()->path('asterisk.monitordir');
63
        }
64
65
        return '/tmp';
66
    }
67
68
    /**
69
     * Get the media directory path.
70
     *
71
     * @return string The media directory path.
72
     */
73
    public static function getMediaDir(): string
74
    {
75
        $di = Di::getDefault();
76
        if ($di !== null) {
77
            return $di->getConfig()->path('core.mediaMountPoint');
78
        }
79
80
        // If the Dependency Injection container is not available or the configuration option is not set,
81
        // return the default media directory path '/tmp'
82
        return '/tmp';
83
    }
84
85
86
    /**
87
     * Check if a storage disk is valid.
88
     *
89
     * @param string $device The device path of the storage disk.
90
     * @return bool Returns true if the storage disk is valid, false otherwise.
91
     */
92
    public static function isStorageDisk(string $device): bool
93
    {
94
        $result = false;
95
        // Check if the device path exists
96
        if (!file_exists($device)) {
97
            return $result;
98
        }
99
100
        $tmp_dir = '/tmp/mnt_' . time();
101
        Util::mwMkdir($tmp_dir);
102
        $out = [];
103
104
        $storage = new self();
105
        $uid_part = 'UUID=' . $storage->getUuid($device) . '';
106
        $format = $storage->getFsType($device);
107
        // If the file system type is not available, return false
108
        if ($format === '') {
109
            return false;
110
        }
111
        $mountPath = Util::which('mount');
112
        $umountPath = Util::which('umount');
113
        $rmPath = Util::which('rm');
114
115
        Processes::mwExec("{$mountPath} -t {$format} {$uid_part} {$tmp_dir}", $out);
116
        if (is_dir("{$tmp_dir}/mikopbx") && trim(implode('', $out)) === '') {
117
            // $out - empty string, no errors
118
            // mikopbx directory exists
119
            $result = true;
120
        }
121
122
        // Check if the storage disk is mounted, and unmount if necessary
123
        if (self::isStorageDiskMounted($device)) {
124
            Processes::mwExec("{$umountPath} {$device}");
125
        }
126
127
        // Check if the storage disk is unmounted, and remove the temporary directory
128
        if (!self::isStorageDiskMounted($device)) {
129
            Processes::mwExec("{$rmPath} -rf '{$tmp_dir}'");
130
        }
131
132
        return $result;
133
    }
134
135
    /**
136
     * Get the UUID (Universally Unique Identifier) of a device.
137
     *
138
     * @param string $device The device path.
139
     * @return string The UUID of the device.
140
     */
141
    public function getUuid(string $device): string
142
    {
143
        if (empty($device)) {
144
            return '';
145
        }
146
        $lsBlkPath = Util::which('lsblk');
147
        $busyboxPath = Util::which('busybox');
148
149
        // Build the command to retrieve the UUID of the device
150
        $cmd = "{$lsBlkPath} -r -o NAME,UUID | {$busyboxPath} grep " . basename($device) . " | {$busyboxPath} cut -d ' ' -f 2";
151
        $res = Processes::mwExec($cmd, $output);
152
        if ($res === 0 && count($output) > 0) {
153
            $result = $output[0];
154
        } else {
155
            $result = '';
156
        }
157
158
        return $result;
159
    }
160
161
    /**
162
     * Get the file system type of a device.
163
     *
164
     * @param string $device The device path.
165
     * @return string The file system type of the device.
166
     */
167
    public function getFsType(string $device): string
168
    {
169
        $blkidPath = Util::which('blkid');
170
        $busyboxPath = Util::which('busybox');
171
        $sedPath = Util::which('sed');
172
        $grepPath = Util::which('grep');
173
        $awkPath = Util::which('awk');
174
175
        // Remove '/dev/' from the device path
176
        $device = str_replace('/dev/', '', $device);
177
        $out = [];
178
179
        // Execute the command to retrieve the file system type of the device
180
        Processes::mwExec(
181
            "{$blkidPath} -ofull /dev/{$device} | {$busyboxPath} {$sedPath} -r 's/[[:alnum:]]+=/\\n&/g' | {$busyboxPath} {$grepPath} \"^TYPE=\" | {$busyboxPath} {$awkPath} -F \"\\\"\" '{print $2}'",
182
            $out
183
        );
184
        $format = implode('', $out);
185
186
        // Check if the format is 'msdosvfat' and replace it with 'msdos'
187
        if ($format === 'msdosvfat') {
188
            $format = 'msdos';
189
        }
190
191
        return $format;
192
    }
193
194
    /**
195
     * Move read-only sounds to the storage.
196
     * This function assumes a storage disk is mounted.
197
     *
198
     * @return void
199
     */
200
    public static function moveReadOnlySoundsToStorage(): void
201
    {
202
        // Check if a storage disk is mounted
203
        if (!self::isStorageDiskMounted()) {
204
            return;
205
        }
206
        $di = Di::getDefault();
207
208
        // Check if the Dependency Injection container is available
209
        if ($di === null) {
210
            return;
211
        }
212
213
        // Create the current media directory if it doesn't exist
214
        $currentMediaDir = $di->getConfig()->path('asterisk.customSoundDir') . '/';
215
        if (!file_exists($currentMediaDir)) {
216
            Util::mwMkdir($currentMediaDir);
217
        }
218
219
        $soundFiles = SoundFiles::find();
220
221
        // Iterate through each sound file
222
        foreach ($soundFiles as $soundFile) {
223
            if (stripos($soundFile->path, '/offload/asterisk/sounds/other/') === 0) {
224
                $newPath = $currentMediaDir . pathinfo($soundFile->path)['basename'];
225
226
                // Copy the sound file to the new path
227
                if (copy($soundFile->path, $newPath)) {
228
                    SystemManagementProcessor::convertAudioFile($newPath);
229
230
                    // Update the sound file path and extension
231
                    $soundFile->path = Util::trimExtensionForFile($newPath) . ".mp3";
232
233
                    // Update the sound file if the new path exists
234
                    if (file_exists($soundFile->path)) {
235
                        $soundFile->update();
236
                    }
237
                }
238
            }
239
        }
240
        unset($soundFiles);
241
    }
242
243
    /**
244
     * Copy MOH (Music on Hold) files to the storage.
245
     * This function assumes a storage disk is mounted.
246
     *
247
     * @return void
248
     */
249
    public static function copyMohFilesToStorage(): void
250
    {
251
252
        // Check if a storage disk is mounted
253
        if (!self::isStorageDiskMounted()) {
254
            return;
255
        }
256
257
        // Check if the Dependency Injection container is available
258
        $di = Di::getDefault();
259
        if ($di === null) {
260
            return;
261
        }
262
        $config = $di->getConfig();
263
        $oldMohDir = $config->path('asterisk.astvarlibdir') . '/sounds/moh';
264
        $currentMohDir = $config->path('asterisk.mohdir');
265
266
        // If the old MOH directory doesn't exist or unable to create the current MOH directory, return
267
        if (!file_exists($oldMohDir) || Util::mwMkdir($currentMohDir)) {
268
            return;
269
        }
270
271
272
        $files = scandir($oldMohDir);
273
274
        // Iterate through each file in the old MOH directory
275
        foreach ($files as $file) {
276
            if (in_array($file, ['.', '..'])) {
277
                continue;
278
            }
279
280
            // Copy the file from the old MOH directory to the current MOH directory
281
            if (copy($oldMohDir . '/' . $file, $currentMohDir . '/' . $file)) {
282
                $sound_file = new SoundFiles();
283
                $sound_file->path = $currentMohDir . '/' . $file;
284
                $sound_file->category = SoundFiles::CATEGORY_MOH;
285
                $sound_file->name = $file;
286
                $sound_file->save();
287
            }
288
        }
289
    }
290
291
    /**
292
     * Check if a storage disk is mounted.
293
     *
294
     * @param string $filter Optional filter for the storage disk.
295
     * @param string $mount_dir If the disk is mounted, the mount directory will be stored in this variable.
296
     * @return bool Returns true if the storage disk is mounted, false otherwise.
297
     */
298
    public static function isStorageDiskMounted(string $filter = '', string &$mount_dir = ''): bool
299
    {
300
301
        // Check if it's a T2Sde Linux and /storage/usbdisk1/ exists
302
        if (!Util::isT2SdeLinux()
303
            && file_exists('/storage/usbdisk1/')
304
        ) {
305
            $mount_dir = '/storage/usbdisk1/';
306
            return true;
307
        }
308
        if ('' === $filter) {
309
310
311
            $di = Di::getDefault();
312
313
            // Check if the Dependency Injection container is available
314
            if ($di !== null) {
315
                $varEtcDir = $di->getConfig()->path('core.varEtcDir');
316
            } else {
317
                $varEtcDir = '/var/etc';
318
            }
319
320
            $filename = "{$varEtcDir}/storage_device";
321
322
            // If the storage_device file exists, read its contents as the filter, otherwise use 'usbdisk1' as the filter
323
            if (file_exists($filename)) {
324
                $filter = file_get_contents($filename);
325
            } else {
326
                $filter = 'usbdisk1';
327
            }
328
        }
329
        $grepPath = Util::which('grep');
330
        $mountPath = Util::which('mount');
331
        $awkPath = Util::which('awk');
332
333
        $filter = escapeshellarg($filter);
334
335
        // Execute the command to filter the mount points based on the filter
336
        $out = shell_exec("$mountPath | $grepPath $filter | {$awkPath} '{print $3}'");
337
        $mount_dir = trim($out);
338
        return ($mount_dir !== '');
339
    }
340
341
    /**
342
     * Mount an SFTP disk.
343
     *
344
     * @param string $host The SFTP server host.
345
     * @param int $port The SFTP server port.
346
     * @param string $user The SFTP server username.
347
     * @param string $pass The SFTP server password.
348
     * @param string $remote_dir The remote directory on the SFTP server.
349
     * @param string $local_dir The local directory to mount the SFTP disk.
350
     * @return bool Returns true if the SFTP disk is successfully mounted, false otherwise.
351
     */
352
    public static function mountSftpDisk(string $host, string $port, string $user, string $pass, string $remout_dir, string $local_dir): bool
353
    {
354
355
        // Create the local directory if it doesn't exist
356
        Util::mwMkdir($local_dir);
357
358
        $out = [];
359
        $timeoutPath = Util::which('timeout');
360
        $sshfsPath = Util::which('sshfs');
361
362
        // Build the command to mount the SFTP disk
363
        $command = "{$timeoutPath} 3 {$sshfsPath} -p {$port} -o nonempty -o password_stdin -o 'StrictHostKeyChecking=no' " . "{$user}@{$host}:{$remout_dir} {$local_dir} << EOF\n" . "{$pass}\n" . "EOF\n";
364
365
        // Execute the command to mount the SFTP disk
366
        Processes::mwExec($command, $out);
367
        $response = trim(implode('', $out));
368
369
        if ('Terminated' === $response) {
370
            // The remote server did not respond or an incorrect password was provided.
371
            unset($response);
372
        }
373
374
        return self::isStorageDiskMounted("$local_dir ");
375
    }
376
377
    /**
378
     * Mount an FTP disk.
379
     *
380
     * @param string $host The FTP server host.
381
     * @param string $port The FTP server port.
382
     * @param string $user The FTP server username.
383
     * @param string $pass The FTP server password.
384
     * @param string $remote_dir The remote directory on the FTP server.
385
     * @param string $local_dir The local directory to mount the FTP disk.
386
     * @return bool Returns true if the FTP disk is successfully mounted, false otherwise.
387
     */
388
    public static function mountFtp(string $host, string $port, string $user, string $pass, string $remout_dir, string $local_dir): bool
389
    {
390
391
        // Create the local directory if it doesn't exist
392
        Util::mwMkdir($local_dir);
393
        $out = [];
394
395
        // Build the authentication line for the FTP connection
396
        $auth_line = '';
397
        if (!empty($user)) {
398
            $auth_line .= 'user="' . $user;
399
            if (!empty($pass)) {
400
                $auth_line .= ":{$pass}";
401
            }
402
            $auth_line .= '",';
403
        }
404
405
        // Build the connect line for the FTP connection
406
        $connect_line = 'ftp://' . $host;
407
        if (!empty($port)) {
408
            $connect_line .= ":{$port}";
409
        }
410
        if (!empty($remout_dir)) {
411
            $connect_line .= "$remout_dir";
412
        }
413
414
        $timeoutPath = Util::which('timeout');
415
        $curlftpfsPath = Util::which('curlftpfs');
416
417
        // Build the command to mount the FTP disk
418
        $command = "{$timeoutPath} 3 {$curlftpfsPath}  -o allow_other -o {$auth_line}fsname={$host} {$connect_line} {$local_dir}";
419
420
        // Execute the command to mount the FTP disk
421
        Processes::mwExec($command, $out);
422
        $response = trim(implode('', $out));
423
        if ('Terminated' === $response) {
424
            // The remote server did not respond or an incorrect password was provided.
425
            unset($response);
426
        }
427
428
        return self::isStorageDiskMounted("$local_dir ");
429
    }
430
431
    /**
432
     * Mount a WebDAV disk.
433
     *
434
     * @param string $host The WebDAV server host.
435
     * @param string $user The WebDAV server username.
436
     * @param string $pass The WebDAV server password.
437
     * @param string $dstDir The destination directory on the WebDAV server.
438
     * @param string $local_dir The local directory to mount the WebDAV disk.
439
     * @return bool Returns true if the WebDAV disk is successfully mounted, false otherwise.
440
     */
441
    public static function mountWebDav(string $host, string $user, string $pass, string $dstDir, string $local_dir): bool
442
    {
443
        $host = trim($host);
444
        $dstDir = trim($dstDir);
445
446
        // Remove trailing slash from host if present
447
        if (substr($host, -1) === '/') {
448
            $host = substr($host, 0, -1);
449
        }
450
451
        // Remove leading slash from destination directory if present
452
        if ($dstDir[0] === '/') {
453
            $dstDir = substr($dstDir, 1);
454
        }
455
456
        // Create the local directory if it doesn't exist
457
        Util::mwMkdir($local_dir);
458
        $out = [];
459
        $conf = 'dav_user www' . PHP_EOL .
460
            'dav_group www' . PHP_EOL;
461
462
463
        // Write WebDAV credentials to secrets file
464
        file_put_contents('/etc/davfs2/secrets', "{$host}{$dstDir} $user $pass");
465
        file_put_contents('/etc/davfs2/davfs2.conf', $conf);
466
        $timeoutPath = Util::which('timeout');
467
        $mount = Util::which('mount.davfs');
468
469
        // Build the command to mount the WebDAV disk
470
        $command = "$timeoutPath 3 yes | $mount {$host}{$dstDir} {$local_dir}";
471
472
        // Execute the command to mount the WebDAV disk
473
        Processes::mwExec($command, $out);
474
        $response = trim(implode('', $out));
475
        if ('Terminated' === $response) {
476
            // The remote server did not respond or an incorrect password was provided.
477
            unset($response);
478
        }
479
        return self::isStorageDiskMounted("$local_dir ");
480
    }
481
482
    /**
483
     * Create a file system on a disk.
484
     *
485
     * @param string $dev The device path of the disk.
486
     * @return bool Returns true if the file system creation process is initiated, false otherwise.
487
     */
488
    public static function mkfs_disk(string $dev)
489
    {
490
        if (!file_exists($dev)) {
491
            $dev = "/dev/{$dev}";
492
        }
493
        if (!file_exists($dev)) {
494
            return false;
495
        }
496
        $dir = '';
497
        self::isStorageDiskMounted($dev, $dir);
498
499
        // If the disk is not mounted or successfully unmounted, proceed with the file system creation
500
        if (empty($dir) || self::umountDisk($dir)) {
501
            $st = new Storage();
502
            // Initiate the file system creation process
503
            $st->formatDiskLocal($dev, true);
504
            sleep(1);
505
506
            return (self::statusMkfs($dev) === 'inprogress');
507
        }
508
509
        // Error occurred during disk unmounting
510
        return false;
511
    }
512
513
    /**
514
     * Unmount a disk.
515
     *
516
     * @param string $dir The mount directory of the disk.
517
     * @return bool Returns true if the disk is successfully unmounted, false otherwise.
518
     */
519
    public static function umountDisk(string $dir): bool
520
    {
521
        $umountPath = Util::which('umount');
522
        $rmPath = Util::which('rm');
523
524
        // If the disk is mounted, terminate processes using the disk and unmount it
525
        if (self::isStorageDiskMounted($dir)) {
526
            Processes::mwExec("/sbin/shell_functions.sh 'killprocesses' '$dir' -TERM 0");
527
            Processes::mwExec("{$umountPath} {$dir}");
528
        }
529
        $result = !self::isStorageDiskMounted($dir);
530
531
        // If the disk is successfully unmounted and the directory exists, remove the directory
532
        if ($result && file_exists($dir)) {
533
            Processes::mwExec("{$rmPath} -rf '{$dir}'");
534
        }
535
536
        return $result;
537
    }
538
539
    /**
540
     * Format a disk locally using parted command.
541
     *
542
     * @param string $device The device path of the disk.
543
     * @param bool $bg Whether to run the command in the background.
544
     * @return bool Returns true if the disk formatting process is initiated, false otherwise.
545
     */
546
    public function formatDiskLocal(string $device, bool $bg = false)
547
    {
548
        $partedPath = Util::which('parted');
549
550
        // Execute the parted command to format the disk with msdos partition table and ext4 partition
551
        $retVal = Processes::mwExec(
552
            "{$partedPath} --script --align optimal '{$device}' 'mklabel msdos mkpart primary ext4 0% 100%'"
553
        );
554
555
        // Log the result of the parted command
556
        Util::sysLogMsg(__CLASS__, "{$partedPath} returned {$retVal}");
557
        if (false === $bg) {
558
            sleep(1);
559
        }
560
561
        return $this->formatDiskLocalPart2($device, $bg);
562
    }
563
564
    /**
565
     * Format a disk locally (part 2) using mkfs command.
566
     *
567
     * @param string $device The device path of the disk.
568
     * @param bool $bg Whether to run the command in the background.
569
     * @return bool Returns true if the disk formatting process is successfully completed, false otherwise.
570
     */
571
    private function formatDiskLocalPart2(string $device, bool $bg = false): bool
572
    {
573
574
        // Determine the device ID based on the last character of the device path
575
        if (is_numeric(substr($device, -1))) {
576
            $device_id = "";
577
        } else {
578
            $device_id = "1";
579
        }
580
        $format = 'ext4';
581
        $mkfsPath = Util::which("mkfs.{$format}");
582
        $cmd = "{$mkfsPath} {$device}{$device_id}";
583
        if ($bg === false) {
584
            // Execute the mkfs command and check the return value
585
            $retVal = (Processes::mwExec("{$cmd} 2>&1") === 0);
586
            Util::sysLogMsg(__CLASS__, "{$cmd} returned {$retVal}");
587
        } else {
588
            usleep(200000);
589
            // Execute the mkfs command in the background
590
            Processes::mwExecBg($cmd);
591
            $retVal = true;
592
        }
593
594
        return $retVal;
595
    }
596
597
    /**
598
     * Get the status of mkfs process on a disk.
599
     *
600
     * @param string $dev The device path of the disk.
601
     * @return string Returns the status of mkfs process ('inprogress' or 'ended').
602
     */
603
    public static function statusMkfs(string $dev): string
604
    {
605
        if (!file_exists($dev)) {
606
            $dev = "/dev/{$dev}";
607
        }
608
        $out = [];
609
        $psPath = Util::which('ps');
610
        $grepPath = Util::which('grep');
611
612
        // Execute the command to check the status of mkfs process
613
        Processes::mwExec("{$psPath} -A -f | {$grepPath} {$dev} | {$grepPath} mkfs | {$grepPath} -v grep", $out);
614
        $mount_dir = trim(implode('', $out));
615
616
        return empty($mount_dir) ? 'ended' : 'inprogress';
617
    }
618
619
    /**
620
     * Get information about all HDD devices.
621
     *
622
     * @param bool $mounted_only Whether to include only mounted devices.
623
     * @return array An array of HDD device information.
624
     */
625
    public function getAllHdd(bool $mounted_only = false): array
626
    {
627
        $res_disks = [];
628
629
        if (Util::isDocker()) {
630
631
            // Get disk information for /storage directory
632
            $out = [];
633
            $grepPath = Util::which('grep');
634
            $dfPath = Util::which('df');
635
            $awkPath = Util::which('awk');
636
637
            // Execute the command to get disk information for /storage directory
638
            Processes::mwExec(
639
                "{$dfPath} -k /storage | {$awkPath}  '{ print \$1 \"|\" $3 \"|\" \$4} ' | {$grepPath} -v 'Available'",
640
                $out
641
            );
642
            $disk_data = explode('|', implode(" ", $out));
643
            if (count($disk_data) === 3) {
644
                $m_size = round(($disk_data[1] + $disk_data[2]) / 1024, 1);
645
646
                // Add Docker disk information to the result
647
                $res_disks[] = [
648
                    'id' => $disk_data[0],
649
                    'size' => "" . $m_size,
650
                    'size_text' => "" . $m_size . " Mb",
651
                    'vendor' => 'Debian',
652
                    'mounted' => '/storage/usbdisk',
653
                    'free_space' => round($disk_data[2] / 1024, 1),
654
                    'partitions' => [],
655
                    'sys_disk' => true,
656
                ];
657
            }
658
659
            return $res_disks;
660
        }
661
662
        // Get CD-ROM and HDD devices
663
        $cd_disks = $this->cdromGetDevices();
664
        $disks = $this->diskGetDevices();
665
666
        $cf_disk = '';
667
        $varEtcDir = $this->config->path('core.varEtcDir');
668
669
        if (file_exists($varEtcDir . '/cfdevice')) {
670
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
671
        }
672
673
        foreach ($disks as $disk => $diskInfo) {
674
            $type = $diskInfo['fstype'] ?? '';
675
676
            // Skip Linux RAID member disks
677
            if ($type === 'linux_raid_member') {
678
                continue;
679
            }
680
681
            // Skip CD-ROM disks
682
            if (in_array($disk, $cd_disks, true)) {
683
                continue;
684
            }
685
            unset($temp_vendor, $temp_size, $original_size);
686
            $mounted = self::diskIsMounted($disk);
687
            if ($mounted_only === true && $mounted === false) {
688
                continue;
689
            }
690
            $sys_disk = ($cf_disk === $disk);
691
692
            $mb_size = 0;
693
            if (is_file("/sys/block/" . $disk . "/size")) {
694
                $original_size = trim(file_get_contents("/sys/block/" . $disk . "/size"));
695
                $original_size = ($original_size * 512 / 1024 / 1024);
696
                $mb_size = round($original_size, 1);
697
            }
698
            if ($mb_size > 100) {
699
                $temp_size = sprintf("%.0f MB", $mb_size);
700
                $temp_vendor = $this->getVendorDisk($diskInfo);
701
                $free_space = $this->getFreeSpace($disk);
702
703
                $arr_disk_info = $this->determineFormatFs($diskInfo);
704
705
                if (count($arr_disk_info) > 0) {
706
                    $used = 0;
707
                    foreach ($arr_disk_info as $disk_info) {
708
                        $used += $disk_info['used_space'];
709
                    }
710
                    if ($used > 0) {
711
                        $free_space = $mb_size - $used;
712
                    }
713
                }
714
715
                // Add HDD device information to the result
716
                $res_disks[] = [
717
                    'id' => $disk,
718
                    'size' => $mb_size,
719
                    'size_text' => $temp_size,
720
                    'vendor' => $temp_vendor,
721
                    'mounted' => $mounted,
722
                    'free_space' => round($free_space, 1),
723
                    'partitions' => $arr_disk_info,
724
                    'sys_disk' => $sys_disk,
725
                ];
726
            }
727
        }
728
        return $res_disks;
729
    }
730
731
    /**
732
     * Get CD-ROM devices.
733
     *
734
     * @return array An array of CD-ROM device names.
735
     */
736
    private function cdromGetDevices(): array
737
    {
738
        $disks = [];
739
        $blockDevices = $this->getLsBlkDiskInfo();
740
        foreach ($blockDevices as $diskData) {
741
            $type = $diskData['type'] ?? '';
742
            $name = $diskData['name'] ?? '';
743
744
            // Skip devices that are not CD-ROM
745
            if ($type !== 'rom' || $name === '') {
746
                continue;
747
            }
748
            $disks[] = $name;
749
        }
750
        return $disks;
751
    }
752
753
    /**
754
     * Get disk devices.
755
     *
756
     * @param bool $diskOnly Whether to include only disk devices.
757
     * @return array An array of disk device information.
758
     */
759
    public function diskGetDevices(bool $diskOnly = false): array
760
    {
761
        $disks = [];
762
        $blockDevices = $this->getLsBlkDiskInfo();
763
764
        foreach ($blockDevices as $diskData) {
765
            $type = $diskData['type'] ?? '';
766
            $name = $diskData['name'] ?? '';
767
            if ($type !== 'disk' || $name === '') {
768
                continue;
769
            }
770
            $disks[$name] = $diskData;
771
            if ($diskOnly === true) {
772
                continue;
773
            }
774
            $children = $diskData['children'] ?? [];
775
776
            foreach ($children as $child) {
777
                $childName = $child['name'] ?? '';
778
                if ($childName === '') {
779
                    continue;
780
                }
781
                $disks[$childName] = $child;
782
            }
783
        }
784
        return $disks;
785
    }
786
787
    /**
788
     * Get disk information using lsblk command.
789
     *
790
     * @return array An array containing disk information.
791
     */
792
    private function getLsBlkDiskInfo(): array
793
    {
794
        $lsBlkPath = Util::which('lsblk');
795
796
        // Execute lsblk command to get disk information in JSON format
797
        Processes::mwExec(
798
            "{$lsBlkPath} -J -b -o VENDOR,MODEL,SERIAL,LABEL,TYPE,FSTYPE,MOUNTPOINT,SUBSYSTEMS,NAME,UUID",
799
            $out
800
        );
801
        try {
802
            $data = json_decode(implode(PHP_EOL, $out), true, 512, JSON_THROW_ON_ERROR);
803
            $data = $data['blockdevices'] ?? [];
804
        } catch (JsonException $e) {
805
            $data = [];
806
        }
807
        return $data;
808
    }
809
810
    /**
811
     * Check if a disk is mounted.
812
     *
813
     * @param string $disk The name of the disk.
814
     * @param string $filter The filter to match the disk name.
815
     * @return string|bool The mount point if the disk is mounted, or false if not mounted.
816
     */
817
    public static function diskIsMounted(string $disk, string $filter = '/dev/')
818
    {
819
        $out = [];
820
        $grepPath = Util::which('grep');
821
        $mountPath = Util::which('mount');
822
823
        // Execute mount command and grep the output for the disk name
824
        Processes::mwExec("{$mountPath} | {$grepPath} '{$filter}{$disk}'", $out);
825
        if (count($out) > 0) {
826
            $res_out = end($out);
827
        } else {
828
            $res_out = implode('', $out);
829
        }
830
        $data = explode(' ', trim($res_out));
831
832
        return (count($data) > 2) ? $data[2] : false;
833
    }
834
835
    /**
836
     * Get the vendor name for a disk.
837
     *
838
     * @param array $diskInfo The disk information.
839
     * @return string The vendor name.
840
     */
841
    private function getVendorDisk(array $diskInfo): string
842
    {
843
        $temp_vendor = [];
844
        $keys = ['vendor', 'model', 'type'];
845
846
        // Iterate through the keys to retrieve vendor-related data
847
        foreach ($keys as $key) {
848
            $data = $diskInfo[$key] ?? '';
849
            if ($data !== '') {
850
                $temp_vendor[] = trim(str_replace(',', '', $data));
851
            }
852
        }
853
854
        // If no vendor-related data is found, use the disk name
855
        if (count($temp_vendor) === 0) {
856
            $temp_vendor[] = $diskInfo['name'] ?? 'ERROR: NoName';
857
        }
858
        return implode(', ', $temp_vendor);
859
    }
860
861
    /**
862
     * Get the free space in megabytes for a given HDD.
863
     *
864
     * @param string $hdd The name of the HDD.
865
     * @return int The free space in megabytes.
866
     */
867
    public function getFreeSpace(string $hdd)
868
    {
869
        $out = [];
870
        $hdd = escapeshellarg($hdd);
871
        $grepPath = Util::which('grep');
872
        $awkPath = Util::which('awk');
873
        $dfPath = Util::which('df');
874
875
        // Execute df command to get the free space for the HDD
876
        Processes::mwExec("{$dfPath} -m | {$grepPath} {$hdd} | {$awkPath} '{print $4}'", $out);
877
        $result = 0;
878
879
        // Sum up the free space values
880
        foreach ($out as $res) {
881
            if (!is_numeric($res)) {
882
                continue;
883
            }
884
            $result += (1 * $res);
885
        }
886
887
        return $result;
888
    }
889
890
    /**
891
     * Get the disk partitions using the lsblk command.
892
     *
893
     * @param string $diskName The name of the disk.
894
     * @return array An array of disk partition names.
895
     */
896
    private function getDiskParted(string $diskName): array
897
    {
898
        $result = [];
899
        $lsBlkPath = Util::which('lsblk');
900
901
        // Execute lsblk command to get disk partition information in JSON format
902
        Processes::mwExec("{$lsBlkPath} -J -b -o NAME,TYPE {$diskName}", $out);
903
904
        try {
905
            $data = json_decode(implode(PHP_EOL, $out), true, 512, JSON_THROW_ON_ERROR);
906
            $data = $data['blockdevices'][0] ?? [];
907
        } catch (\JsonException $e) {
908
            $data = [];
909
        }
910
911
        $type = $data['children'][0]['type'] ?? '';
912
913
        // Check if the disk is not a RAID type
914
        if (strpos($type, 'raid') === false) {
915
            $children = $data['children'] ?? [];
916
            foreach ($children as $child) {
917
                $result[] = $child['name'];
918
            }
919
        }
920
921
        return $result;
922
    }
923
924
    /**
925
     * Determine the format and file system information for a device.
926
     *
927
     * @param array $deviceInfo The device information.
928
     * @return array An array containing format and file system information for each device partition.
929
     */
930
    public function determineFormatFs(array $deviceInfo)
931
    {
932
        $allow_formats = ['ext2', 'ext4', 'fat', 'ntfs', 'msdos'];
933
        $device = basename($deviceInfo['name'] ?? '');
934
935
        $devices = $this->getDiskParted('/dev/' . $deviceInfo['name'] ?? '');
936
        $result_data = [];
937
938
        // Iterate through each device partition
939
        foreach ($devices as $dev) {
940
            if (empty($dev) || (count($devices) > 1 && $device === $dev) || is_dir("/sys/block/{$dev}")) {
941
                continue;
942
            }
943
            $mb_size = 0;
944
            $path_size_info = '';
945
            $tmp_path = "/sys/block/{$device}/{$dev}/size";
946
            if (file_exists($tmp_path)) {
947
                $path_size_info = $tmp_path;
948
            }
949
950
            // If the size path is not found, try an alternate path
951
            if (empty($path_size_info)) {
952
                $tmp_path = "/sys/block/" . substr($dev, 0, 3) . "/{$dev}/size";
953
                if (file_exists($tmp_path)) {
954
                    $path_size_info = $tmp_path;
955
                }
956
            }
957
958
            // Calculate the size in megabytes
959
            if (!empty($path_size_info)) {
960
                $original_size = trim(file_get_contents($path_size_info));
961
                $original_size = ($original_size * 512 / 1024 / 1024);
962
                $mb_size = $original_size;
963
            }
964
965
            $tmp_dir = "/tmp/{$dev}_" . time();
966
            $out = [];
967
968
            $fs = null;
969
            $need_unmount = false;
970
            $mount_dir = '';
971
972
            // Check if the device is currently mounted
973
            if (self::isStorageDiskMounted("/dev/{$dev} ", $mount_dir)) {
974
                $grepPath = Util::which('grep');
975
                $awkPath = Util::which('awk');
976
                $mountPath = Util::which('mount');
977
978
                // Get the file system type and free space of the mounted device
979
                Processes::mwExec("{$mountPath} | {$grepPath} '/dev/{$dev}' | {$awkPath} '{print $5}'", $out);
980
                $fs = trim(implode("", $out));
981
                $fs = ($fs === 'fuseblk') ? 'ntfs' : $fs;
982
                $free_space = $this->getFreeSpace("/dev/{$dev} ");
983
                $used_space = $mb_size - $free_space;
984
            } else {
985
                $format = $this->getFsType($device);
986
987
                // Check if the detected format is allowed
988
                if (in_array($format, $allow_formats)) {
989
                    $fs = $format;
990
                }
991
992
                // Mount the device and determine the used space
993
                self::mountDisk($dev, $format, $tmp_dir);
994
995
                $need_unmount = true;
996
                $used_space = Util::getSizeOfFile($tmp_dir);
997
            }
998
999
            // Store the partition information in the result array
1000
            $result_data[] = [
1001
                "dev" => $dev,
1002
                'size' => round($mb_size, 2),
1003
                "used_space" => round($used_space, 2),
1004
                "free_space" => round($mb_size - $used_space, 2),
1005
                "uuid" => $this->getUuid("/dev/{$dev} "),
1006
                "fs" => $fs,
1007
            ];
1008
1009
            // Unmount the temporary mount point if needed
1010
            if ($need_unmount) {
1011
                self::umountDisk($tmp_dir);
1012
            }
1013
        }
1014
1015
        return $result_data;
1016
    }
1017
1018
    /**
1019
     * Mount a disk to a directory.
1020
     *
1021
     * @param string $dev The device name.
1022
     * @param string $format The file system format.
1023
     * @param string $dir The directory to mount the disk.
1024
     * @return bool True if the disk was successfully mounted, false otherwise.
1025
     */
1026
    public static function mountDisk(string $dev, string $format, string $dir): bool
1027
    {
1028
        // Check if the disk is already mounted
1029
        if (self::isStorageDiskMounted("/dev/{$dev} ")) {
1030
            return true;
1031
        }
1032
1033
        // Create the directory if it doesn't exist
1034
        Util::mwMkdir($dir);
1035
1036
        // Check if the directory was created successfully
1037
        if (!file_exists($dir)) {
1038
            Util::sysLogMsg('Storage', "Unable mount $dev $format to $dir. Unable create dir.");
1039
1040
            return false;
1041
        }
1042
1043
        // Remove the '/dev/' prefix from the device name
1044
        $dev = str_replace('/dev/', '', $dev);
1045
1046
        if ('ntfs' === $format) {
1047
            // Mount NTFS disk using 'mount.ntfs-3g' command
1048
            $mountNtfs3gPath = Util::which('mount.ntfs-3g');
1049
            Processes::mwExec("{$mountNtfs3gPath} /dev/{$dev} {$dir}", $out);
1050
        } else {
1051
            // Mount disk using specified file system format and UUID
1052
            $storage = new self();
1053
            $uid_part = 'UUID=' . $storage->getUuid("/dev/{$dev}") . '';
1054
            $mountPath = Util::which('mount');
1055
            Processes::mwExec("{$mountPath} -t {$format} {$uid_part} {$dir}", $out);
1056
        }
1057
1058
        // Check if the disk is now mounted
1059
        return self::isStorageDiskMounted("/dev/{$dev} ");
1060
    }
1061
1062
    /**
1063
     * Configures the storage settings.
1064
     */
1065
    public function configure(): void
1066
    {
1067
        $varEtcDir = $this->config->path('core.varEtcDir');
1068
        $storage_dev_file = "{$varEtcDir}/storage_device";
1069
        if (!Util::isT2SdeLinux()) {
1070
            // Configure for non-T2Sde Linux
1071
            file_put_contents($storage_dev_file, "/storage/usbdisk1");
1072
            $this->updateConfigWithNewMountPoint("/storage/usbdisk1");
1073
            $this->createWorkDirs();
1074
            PHPConf::setupLog();
1075
            return;
1076
        }
1077
1078
        $cf_disk = '';
1079
1080
        // Remove the storage_dev_file if it exists
1081
        if (file_exists($storage_dev_file)) {
1082
            unlink($storage_dev_file);
1083
        }
1084
1085
        // Check if cfdevice file exists and get its content
1086
        if (file_exists($varEtcDir . '/cfdevice')) {
1087
            $cf_disk = trim(file_get_contents($varEtcDir . '/cfdevice'));
1088
        }
1089
1090
        $disks = $this->getDiskSettings();
1091
        $conf = '';
1092
1093
        // Loop through each disk
1094
        foreach ($disks as $disk) {
1095
            clearstatcache();
1096
            $dev = $this->getStorageDev($disk, $cf_disk);
1097
            // Check if the disk exists
1098
            if (!$this->hddExists($dev)) {
1099
                continue;
1100
            }
1101
1102
            // Check if the disk is marked as media or storage_dev_file doesn't exist
1103
            if ($disk['media'] === '1' || !file_exists($storage_dev_file)) {
1104
                // Update the storage_dev_file and the mount point configuration
1105
                file_put_contents($storage_dev_file, "/storage/usbdisk{$disk['id']}");
1106
                $this->updateConfigWithNewMountPoint("/storage/usbdisk{$disk['id']}");
1107
            }
1108
1109
            $formatFs = $this->getFsType($dev);
1110
1111
            // Check if the file system type matches the expected type
1112
            if ($formatFs !== $disk['filesystemtype'] && !($formatFs === 'ext4' && $disk['filesystemtype'] === 'ext2')) {
1113
                Util::sysLogMsg('Storage', "The file system type has changed {$disk['filesystemtype']} -> {$formatFs}. The disk will not be connected.");
1114
                continue;
1115
            }
1116
            $str_uid = 'UUID=' . $this->getUuid($dev);
1117
            $conf .= "{$str_uid} /storage/usbdisk{$disk['id']} {$formatFs} async,rw 0 0\n";
1118
            $mount_point = "/storage/usbdisk{$disk['id']}";
1119
            Util::mwMkdir($mount_point);
1120
        }
1121
1122
        // Save the configuration to the fstab file
1123
        $this->saveFstab($conf);
1124
1125
        // Create necessary working directories
1126
        $this->createWorkDirs();
1127
1128
        // Setup the PHP log configuration
1129
        PHPConf::setupLog();
1130
    }
1131
1132
    /**
1133
     * Retrieves the storage device for the given disk.
1134
     *
1135
     * @param array $disk The disk information.
1136
     * @param string $cfDisk The cf_disk information.
1137
     * @return string The storage device path.
1138
     */
1139
    private function getStorageDev($disk, $cf_disk): string
1140
    {
1141
        if (!empty($disk['uniqid']) && strpos($disk['uniqid'], 'STORAGE-DISK') === false) {
1142
            // Find the partition name by UID.
1143
            $lsBlkPath = Util::which('lsblk');
1144
            $busyboxPath = Util::which('busybox');
1145
            $cmd = "{$lsBlkPath} -r -o NAME,UUID | {$busyboxPath} grep {$disk['uniqid']} | {$busyboxPath} cut -d ' ' -f 1";
1146
            $dev = '/dev/' . trim(shell_exec($cmd));
1147
            if ($this->hddExists($dev)) {
1148
                // Disk exists.
1149
                return $dev;
1150
            }
1151
        }
1152
        // Determine the disk by its name.
1153
        if ($disk['device'] !== "/dev/{$cf_disk}") {
1154
            // If it's a regular disk, use partition 1.
1155
            $part = "1";
1156
        } else {
1157
            // If it's a system disk, attempt to connect partition 4.
1158
            $part = "4";
1159
        }
1160
        $devName = self::getDevPartName($disk['device'], $part);
1161
        return '/dev/' . $devName;
1162
    }
1163
1164
    /**
1165
     * Retrieves the disk settings from the database.
1166
     *
1167
     * @param string $id The ID of the disk (optional).
1168
     * @return array The disk settings.
1169
     */
1170
    public function getDiskSettings(string $id = ''): array
1171
    {
1172
        $data = [];
1173
        if ('' === $id) {
1174
            // Return all disk settings.
1175
            $data = StorageModel::find()->toArray();
1176
        } else {
1177
            // Return disk settings for the specified ID.
1178
            $pbxSettings = StorageModel::findFirst("id='$id'");
1179
            if ($pbxSettings !== null) {
1180
                $data = $pbxSettings->toArray();
1181
            }
1182
        }
1183
1184
        return $data;
1185
    }
1186
1187
    /**
1188
     * Returns candidates to storage
1189
     * @return array
1190
     */
1191
    public function getStorageCandidate():array
1192
    {
1193
        $disks = $this->getLsBlkDiskInfo();
1194
        $storages = [];
1195
        foreach ($disks as $disk){
1196
            if($disk['type'] !== 'disk'){
1197
                continue;
1198
            }
1199
            $children = $disk['children']??[];
1200
            if(count($children) === 0){
1201
                continue;
1202
            }
1203
            if(count($children) === 1){
1204
                $part = '1';
1205
            }else{
1206
                $part = '4';
1207
            }
1208
1209
            $dev = ''; $fs = '';
1210
            foreach ($children as $child){
1211
                if( $child['fstype'] !== 'ext4' && $child['fstype'] !== 'ext2') {
1212
                    continue;
1213
                }
1214
                if( $disk['name'].$part === $child['name']){
1215
                    $dev = '/dev/'.$child['name'];
1216
                    $fs = $child['fstype'];
1217
                    break;
1218
                }
1219
            }
1220
            if(!empty($dev) && !empty($fs)){
1221
                $storages[$dev] = $fs;
1222
            }
1223
        }
1224
1225
        return $storages;
1226
    }
1227
1228
    /**
1229
     * Checks if the HDD exists.
1230
     *
1231
     * @param string $disk The HDD device.
1232
     * @return bool True if the HDD exists, false otherwise.
1233
     */
1234
    private function hddExists(string $disk): bool
1235
    {
1236
        if (is_dir($disk)) {
1237
            return false;
1238
        }
1239
        $result = false;
1240
        $uid = $this->getUuid($disk);
1241
        if (!empty($uid) && file_exists($disk)) {
1242
            $result = true;
1243
        }
1244
        return $result;
1245
    }
1246
1247
    /**
1248
     * Updates the configuration file with the new mount point.
1249
     *
1250
     * After mount storage we will change /mountpoint/ to new $mount_point value
1251
     *
1252
     * @param string $mount_point The new mount point.
1253
     * @throws Error If the original configuration file has a broken format.
1254
     */
1255
    private function updateConfigWithNewMountPoint(string $mount_point): void
1256
    {
1257
        $settingsFile = '/etc/inc/mikopbx-settings.json';
1258
        $staticSettingsFileOrig = '/etc/inc/mikopbx-settings-orig.json';
1259
        if (!file_exists($staticSettingsFileOrig)){
1260
            copy($settingsFile, $staticSettingsFileOrig);
1261
        }
1262
1263
        $jsonString = file_get_contents($staticSettingsFileOrig);
1264
        try {
1265
            $data = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
1266
        } catch (JsonException $exception) {
1267
            throw new Error("{$staticSettingsFileOrig} has broken format");
1268
        }
1269
        foreach ($data as $rootKey => $rootEntry) {
1270
            foreach ($rootEntry as $nestedKey => $entry) {
1271
                if (stripos($entry, '/mountpoint') !== false) {
1272
                    $data[$rootKey][$nestedKey] = str_ireplace('/mountpoint', $mount_point, $entry);
1273
                }
1274
            }
1275
        }
1276
1277
        $newJsonString = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1278
        file_put_contents($settingsFile, $newJsonString);
1279
        $this->updateEnvironmentAfterChangeMountPoint();
1280
    }
1281
1282
1283
    /**
1284
     * Updates the environment after changing the mount point.
1285
     * - Recreates the config provider and updates the config variable.
1286
     * - Reloads classes from system and storage disks.
1287
     * - Reloads all providers.
1288
     */
1289
    private function updateEnvironmentAfterChangeMountPoint(): void
1290
    {
1291
        // Update config variable
1292
        ConfigProvider::recreateConfigProvider();
1293
        $this->config = $this->di->get('config');
1294
1295
        // Reload classes from system and storage disks
1296
        ClassLoader::init();
1297
1298
        // Reload all providers
1299
        RegisterDIServices::init();
1300
    }
1301
1302
    /**
1303
     * Saves the fstab configuration.
1304
     *
1305
     * @param string $conf Additional configuration to append to fstab
1306
     * @return void
1307
     */
1308
    public function saveFstab(string $conf = ''): void
1309
    {
1310
        $varEtcDir = $this->config->path('core.varEtcDir');
1311
1312
        // Create the mount point directory for additional disks
1313
        Util::mwMkdir('/storage');
1314
        $chmodPath = Util::which('chmod');
1315
        Processes::mwExec("{$chmodPath} 755 /storage");
1316
1317
        // Check if cfdevice file exists
1318
        if (!file_exists($varEtcDir . '/cfdevice')) {
1319
            return;
1320
        }
1321
        $fstab = '';
1322
1323
        // Read cfdevice file
1324
        $file_data = file_get_contents($varEtcDir . '/cfdevice');
1325
        $cf_disk = trim($file_data);
1326
        if ('' === $cf_disk) {
1327
            return;
1328
        }
1329
        $part2 = self::getDevPartName($cf_disk, '2');
1330
        $part3 = self::getDevPartName($cf_disk, '3');
1331
1332
        $uid_part2 = 'UUID=' . $this->getUuid("/dev/{$part2}");
1333
        $format_p2 = $this->getFsType($part2);
1334
        $uid_part3 = 'UUID=' . $this->getUuid("/dev/{$part3}");
1335
        $format_p3 = $this->getFsType($part3);
1336
1337
        $fstab .= "{$uid_part2} /offload {$format_p2} ro 0 0\n";
1338
        $fstab .= "{$uid_part3} /cf {$format_p3} rw 1 1\n";
1339
        $fstab .= $conf;
1340
1341
        // Write fstab file
1342
        file_put_contents("/etc/fstab", $fstab);
1343
1344
        // Duplicate for vmtoolsd
1345
        file_put_contents("/etc/mtab", $fstab);
1346
1347
        // Mount the file systems
1348
        $mountPath = Util::which('mount');
1349
        Processes::mwExec("{$mountPath} -a 2> /dev/null");
1350
1351
        // Add regular www rights to /cf directory
1352
        Util::addRegularWWWRights('/cf');
1353
    }
1354
1355
    /**
1356
     * Retrieves the partition name of a device.
1357
     *
1358
     * @param string $dev The device name
1359
     * @param string $part The partition number
1360
     * @return string The partition name
1361
     */
1362
    public static function getDevPartName(string $dev, string $part): string
1363
    {
1364
        $lsBlkPath = Util::which('lsblk');
1365
        $cutPath = Util::which('cut');
1366
        $grepPath = Util::which('grep');
1367
        $sortPath = Util::which('sort');
1368
1369
        $command = "{$lsBlkPath} -r | {$grepPath} ' part' | {$sortPath} -u | {$cutPath} -d ' ' -f 1 | {$grepPath} \"" . basename(
1370
                $dev
1371
            ) . "\" | {$grepPath} \"{$part}\$\"";
1372
        Processes::mwExec($command, $out);
1373
        $devName = trim(implode('', $out));
1374
        return trim($devName);
1375
    }
1376
1377
    /**
1378
     * Creates the necessary working directories and symlinks.
1379
     *
1380
     * @return void
1381
     */
1382
    private function createWorkDirs(): void
1383
    {
1384
        $path = '';
1385
        $mountPath = Util::which('mount');
1386
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1387
1388
        $isLiveCd = file_exists('/offload/livecd');
1389
1390
        // Create directories
1391
        $arrConfig = $this->config->toArray();
1392
        foreach ($arrConfig as $rootEntry) {
1393
            foreach ($rootEntry as $key => $entry) {
1394
                if (stripos($key, 'path') === false && stripos($key, 'dir') === false) {
1395
                    continue;
1396
                }
1397
                if (file_exists($entry)) {
1398
                    continue;
1399
                }
1400
                if ($isLiveCd && strpos($entry, '/offload/') === 0) {
1401
                    continue;
1402
                }
1403
                $path .= " $entry";
1404
            }
1405
        }
1406
1407
        if (!empty($path)) {
1408
            Util::mwMkdir($path);
1409
        }
1410
1411
        $downloadCacheDir = appPath('sites/pbxcore/files/cache');
1412
        if (!$isLiveCd) {
1413
            Util::mwMkdir($downloadCacheDir);
1414
            Util::createUpdateSymlink($this->config->path('www.downloadCacheDir'), $downloadCacheDir);
1415
        }
1416
1417
        $this->createAssetsSymlinks();
1418
        $this->createViewSymlinks();
1419
        $this->createAGIBINSymlinks($isLiveCd);
1420
1421
        Util::createUpdateSymlink($this->config->path('www.uploadDir'), '/ultmp');
1422
1423
        $filePath = appPath('src/Core/Asterisk/Configs/lua/extensions.lua');
1424
        Util::createUpdateSymlink($filePath, '/etc/asterisk/extensions.lua');
1425
1426
        $this->clearCacheFiles();
1427
        $this->clearTmpFiles();
1428
        $this->applyFolderRights();
1429
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1430
    }
1431
1432
    /**
1433
     * Creates symlinks for asset cache directories.
1434
     *
1435
     * @return void
1436
     */
1437
    public function createAssetsSymlinks(): void
1438
    {
1439
        // Create symlink for JS cache directory
1440
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
1441
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/js', $jsCacheDir);
1442
1443
        // Create symlink for CSS cache directory
1444
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
1445
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/css', $cssCacheDir);
1446
1447
        // Create symlink for image cache directory
1448
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
1449
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/img', $imgCacheDir);
1450
1451
    }
1452
1453
    /**
1454
     * Creates symlinks for modules view.
1455
     *
1456
     * @return void
1457
     */
1458
    public function createViewSymlinks(): void
1459
    {
1460
        $viewCacheDir = appPath('src/AdminCabinet/Views/Modules');
1461
        Util::createUpdateSymlink($this->config->path('adminApplication.viewCacheDir'), $viewCacheDir);
1462
    }
1463
1464
    /**
1465
     * Creates AGI bin symlinks for extension modules.
1466
     *
1467
     * @param bool $isLiveCd Whether the system loaded on LiveCD mode.
1468
     * @return void
1469
     */
1470
    public function createAGIBINSymlinks(bool $isLiveCd): void
1471
    {
1472
        $agiBinDir = $this->config->path('asterisk.astagidir');
1473
        if ($isLiveCd && strpos($agiBinDir, '/offload/') !== 0) {
1474
            Util::mwMkdir($agiBinDir);
1475
        }
1476
1477
        $roAgiBinFolder = appPath('src/Core/Asterisk/agi-bin');
1478
        $files = glob("{$roAgiBinFolder}/*.{php}", GLOB_BRACE);
1479
        foreach ($files as $file) {
1480
            $fileInfo = pathinfo($file);
1481
            $newFilename = "{$agiBinDir}/{$fileInfo['filename']}.{$fileInfo['extension']}";
1482
            Util::createUpdateSymlink($file, $newFilename);
1483
        }
1484
    }
1485
1486
    /**
1487
     * Clears the cache files for various directories.
1488
     *
1489
     * @return void
1490
     */
1491
    public function clearCacheFiles(): void
1492
    {
1493
        $cacheDirs = [];
1494
        $cacheDirs[] = $this->config->path('www.uploadDir');
1495
        $cacheDirs[] = $this->config->path('www.downloadCacheDir');
1496
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/js';
1497
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/css';
1498
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/img';
1499
        $cacheDirs[] = $this->config->path('adminApplication.viewCacheDir');
1500
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
1501
        $rmPath = Util::which('rm');
1502
1503
        // Clear cache files for each directory
1504
        foreach ($cacheDirs as $cacheDir) {
1505
            if (!empty($cacheDir)) {
1506
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
1507
            }
1508
        }
1509
1510
        // Delete boot cache folders if storage disk is mounted
1511
        if (is_dir('/mountpoint') && self::isStorageDiskMounted()) {
1512
            Processes::mwExec("{$rmPath} -rf /mountpoint");
1513
        }
1514
    }
1515
1516
    /**
1517
     * @return void
1518
     */
1519
    private function clearTmpFiles(): void
1520
    {
1521
        $busyboxPath = Util::which('busybox');
1522
        $tmpDir = $this->config->path('core.tempDir');
1523
        if(!file_exists($tmpDir)){
1524
            return;
1525
        }
1526
        // Trying to get a list of files
1527
        Processes::mwExec("$busyboxPath timeout 10 $busyboxPath find $tmpDir -type f", $out, $ret);
1528
        if($ret !== 0){
1529
            // there are too many files in the temporary directory, we will clear them
1530
            // it may cause a failure when setting access rights (chown)
1531
            $resDirForRm = "$tmpDir-".time();
1532
            shell_exec("$busyboxPath mv '$tmpDir' '$resDirForRm'");
1533
            if(file_exists("$resDirForRm/swapfile")){
1534
                // Saving only the swap file
1535
                shell_exec("$busyboxPath mv '$resDirForRm/swapfile' '$tmpDir/swapfile'");
1536
            }
1537
            // Let's start deleting temporary files
1538
            Processes::mwExecBg("/usr/bin/nice -n 19 $busyboxPath rm -rf $resDirForRm");
1539
        }
1540
        Util::mwMkdir($tmpDir, true);
1541
    }
1542
1543
    /**
1544
     * Create system folders and links after upgrade and connect config DB
1545
     *
1546
     * @return void
1547
     */
1548
    public function createWorkDirsAfterDBUpgrade(): void
1549
    {
1550
        // Remount /offload directory as read-write
1551
        $mountPath = Util::which('mount');
1552
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1553
1554
        // Create symlinks for module caches
1555
        $this->createModulesCacheSymlinks();
1556
1557
        // Apply folder rights
1558
        $this->applyFolderRights();
1559
1560
        // Remount /offload directory as read-only
1561
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1562
    }
1563
1564
1565
    /**
1566
     * Creates symlinks for module caches.
1567
     *
1568
     * @return void
1569
     */
1570
    public function createModulesCacheSymlinks(): void
1571
    {
1572
        $modules = PbxExtensionModules::getModulesArray();
1573
        foreach ($modules as $module) {
1574
            // Create cache links for JS, CSS, IMG folders
1575
            PbxExtensionUtils::createAssetsSymlinks($module['uniqid']);
1576
1577
            // Create links for the module view templates
1578
            PbxExtensionUtils::createViewSymlinks($module['uniqid']);
1579
1580
            // Create AGI bin symlinks for the module
1581
            PbxExtensionUtils::createAgiBinSymlinks($module['uniqid']);
1582
        }
1583
    }
1584
1585
    /**
1586
     * Applies folder rights to the appropriate directories.
1587
     *
1588
     * @return void
1589
     */
1590
    private function applyFolderRights(): void
1591
    {
1592
1593
        $www_dirs = []; // Directories with WWW rights
1594
        $exec_dirs = []; // Directories with executable rights
1595
1596
        $arrConfig = $this->config->toArray();
1597
1598
        // Get the directories for WWW rights from the configuration
1599
        foreach ($arrConfig as $key => $entry) {
1600
            if (in_array($key, ['www', 'adminApplication'])) {
1601
                foreach ($entry as $subKey => $subEntry) {
1602
                    if (stripos($subKey, 'path') === false && stripos($subKey, 'dir') === false) {
1603
                        continue;
1604
                    }
1605
                    $www_dirs[] = $subEntry;
1606
                }
1607
            }
1608
        }
1609
1610
        // Add additional directories with WWW rights
1611
        $www_dirs[] = $this->config->path('core.tempDir');
1612
        $www_dirs[] = $this->config->path('core.logsDir');
1613
1614
        // Create empty log files with WWW rights
1615
        $logFiles = [
1616
            $this->config->path('database.debugLogFile'),
1617
            $this->config->path('cdrDatabase.debugLogFile'),
1618
        ];
1619
1620
        foreach ($logFiles as $logFile) {
1621
            $filename = (string)$logFile;
1622
            if (!file_exists($filename)) {
1623
                file_put_contents($filename, '');
1624
            }
1625
            $www_dirs[] = $filename;
1626
        }
1627
1628
        $www_dirs[] = '/etc/version';
1629
        $www_dirs[] = appPath('/');
1630
1631
        // Add read rights to the directories
1632
        Util::addRegularWWWRights(implode(' ', $www_dirs));
1633
1634
        // Add executable rights to the directories
1635
        $exec_dirs[] = appPath('src/Core/Asterisk/agi-bin');
1636
        $exec_dirs[] = appPath('src/Core/Rc');
1637
        Util::addExecutableRights(implode(' ', $exec_dirs));
1638
1639
        $mountPath = Util::which('mount');
1640
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1641
    }
1642
1643
    /**
1644
     * Mounts the swap file.
1645
     */
1646
    public function mountSwap(): void
1647
    {
1648
        $tempDir = $this->config->path('core.tempDir');
1649
        $swapFile = "{$tempDir}/swapfile";
1650
1651
        $swapOffCmd = Util::which('swapoff');
1652
        Processes::mwExec("{$swapOffCmd} {$swapFile}");
1653
1654
        $this->makeSwapFile($swapFile);
1655
        if (!file_exists($swapFile)) {
1656
            return;
1657
        }
1658
        $swapOnCmd = Util::which('swapon');
1659
        $result = Processes::mwExec("{$swapOnCmd} {$swapFile}");
1660
        Util::sysLogMsg('Swap', 'connect swap result: ' . $result, LOG_INFO);
1661
    }
1662
1663
    /**
1664
     * Creates a swap file.
1665
     *
1666
     * @param string $swapFile The path to the swap file.
1667
     */
1668
    private function makeSwapFile(string $swapFile): void
1669
    {
1670
        $swapLabel = Util::which('swaplabel');
1671
1672
        // Check if swap file already exists
1673
        if (Processes::mwExec("{$swapLabel} {$swapFile}") === 0) {
1674
            return;
1675
        }
1676
        if (file_exists($swapFile)) {
1677
            unlink($swapFile);
1678
        }
1679
1680
        $size = $this->getStorageFreeSpaceMb();
1681
        if ($size > 2000) {
1682
            $swapSize = 1024;
1683
        } elseif ($size > 1000) {
1684
            $swapSize = 512;
1685
        } else {
1686
            // Not enough free space.
1687
            return;
1688
        }
1689
        $bs = 1024;
1690
        $countBlock = $swapSize * $bs;
1691
        $ddCmd = Util::which('dd');
1692
1693
        Util::sysLogMsg('Swap', 'make swap ' . $swapFile, LOG_INFO);
1694
1695
        // Create swap file using dd command
1696
        Processes::mwExec("{$ddCmd} if=/dev/zero of={$swapFile} bs={$bs} count={$countBlock}");
1697
1698
        $mkSwapCmd = Util::which('mkswap');
1699
1700
        // Set up swap space on the file
1701
        Processes::mwExec("{$mkSwapCmd} {$swapFile}");
1702
    }
1703
1704
    /**
1705
     * Retrieves the amount of free storage space in megabytes.
1706
     *
1707
     * @return int The amount of free storage space in megabytes.
1708
     */
1709
    public function getStorageFreeSpaceMb(): int
1710
    {
1711
        $size = 0;
1712
        $mntDir = '';
1713
        $mounted = self::isStorageDiskMounted('', $mntDir);
1714
        if (!$mounted) {
1715
            return 0;
1716
        }
1717
        $hd = $this->getAllHdd(true);
1718
        foreach ($hd as $disk) {
1719
            if ($disk['mounted'] === $mntDir) {
1720
                $size = $disk['free_space'];
1721
                break;
1722
            }
1723
        }
1724
        return $size;
1725
    }
1726
1727
    /**
1728
     * Saves the disk settings to the database.
1729
     *
1730
     * @param array $data The disk settings data to be saved.
1731
     * @param string $id The ID of the disk settings to be updated (default: '1').
1732
     * @return void
1733
     */
1734
    public function saveDiskSettings(array $data, string $id = '1'): void
1735
    {
1736
        $disk_data = $this->getDiskSettings($id);
1737
        if (count($disk_data) === 0) {
1738
            $storage_settings = new StorageModel();
1739
        } else {
1740
            $storage_settings = StorageModel::findFirst("id = '$id'");
1741
        }
1742
        foreach ($data as $key => $value) {
1743
            $storage_settings->writeAttribute($key, $value);
1744
        }
1745
        $storage_settings->save();
1746
    }
1747
1748
    /**
1749
     * Retrieves the name of the disk used for recovery. (conf.recover.)
1750
     *
1751
     * @return string The name of the recovery disk (e.g., '/dev/sda').
1752
     */
1753
    public function getRecoverDiskName(): string
1754
    {
1755
        $disks = $this->diskGetDevices(true);
1756
        foreach ($disks as $disk => $diskInfo) {
1757
            // Check if the disk is a RAID or virtual device
1758
            if (isset($diskInfo['children'][0]['children'])) {
1759
                $diskInfo = $diskInfo['children'][0];
1760
                // Adjust the disk name for RAID or other virtual devices
1761
                $disk = $diskInfo['name'];
1762
            }
1763
            foreach ($diskInfo['children'] as $child) {
1764
                $mountpoint = $child['mountpoint'] ?? '';
1765
                $diskPath = "/dev/{$disk}";
1766
                if ($mountpoint === '/conf.recover' && file_exists($diskPath)) {
1767
                    return "/dev/{$disk}";
1768
                }
1769
            }
1770
        }
1771
        return '';
1772
    }
1773
1774
    /**
1775
     * Selects the storage disk and performs the necessary configuration.
1776
     *
1777
     * @param string $automatic Flag to determine if the disk should be selected automatically
1778
     * @return bool Returns true on success, false otherwise
1779
     */
1780
    static function selectAndConfigureStorageDisk(string $automatic): bool
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1781
    {
1782
        // Open standard input in binary mode for interactive reading
1783
        $fp = fopen('php://stdin', 'rb');
1784
        $storage = new Storage();
1785
1786
        // Check if the storage disk is already mounted
1787
        if(Storage::isStorageDiskMounted()){
1788
            echo "\n ".Util::translate('Storage disk is already mounted...')." \n\n";
1789
            sleep(2);
1790
            return true;
1791
        }
1792
1793
        $validDisks = [];
1794
1795
        // Get all available hard drives
1796
        $all_hdd = $storage->getAllHdd();
1797
1798
        $system_disk   = '';
1799
1800
        $selected_disk = ['size' => 0, 'id' => ''];
1801
1802
        // Iterate through all available hard drives
1803
        foreach ($all_hdd as $disk) {
1804
            $additional       = '';
1805
            $devName          = Storage::getDevPartName($disk['id'], '4');
1806
            $isLiveCd         = ( $disk['sys_disk'] && file_exists('/offload/livecd') );
1807
            $isMountedSysDisk = (!empty($disk['mounted']) && $disk['sys_disk'] && file_exists("/dev/$devName"));
1808
1809
            // Check if the disk is a system disk and is mounted
1810
            if($isMountedSysDisk  ||  $isLiveCd){
1811
                $system_disk = $disk['id'];
1812
                $additional.= "\033[31;1m [SYSTEM]\033[0m";
1813
            }elseif ($disk['mounted']){
1814
                // If disk is mounted but not a system disk, continue to the next iteration
1815
                continue;
1816
            }
1817
1818
            // Check if the current disk is larger than the previously selected disk
1819
            if($selected_disk['size'] === 0 || $disk['size'] > $selected_disk['size'] ){
1820
                $selected_disk = $disk;
1821
            }
1822
1823
            $part = $disk['sys_disk']?'4':'1';
1824
            $devName = Storage::getDevPartName($disk['id'], $part);
1825
            $devFour = '/dev/'.$devName;
1826
            if(Storage::isStorageDisk($devFour)){
1827
                $additional.= "\033[33;1m [STORAGE] \033[0m";
1828
            }
1829
1830
            // Check if the disk is a system disk and has a valid partition
1831
            if($disk['sys_disk']){
1832
                $part4_found = false;
1833
                foreach ($disk['partitions'] as $partition){
1834
                    if($partition['dev'] === $devName && $partition['size'] > 1000){
1835
                        $part4_found = true;
1836
                    }
1837
                }
1838
                if($part4_found === false){
1839
                    continue;
1840
                }
1841
            } elseif($disk['size'] < 1024){
1842
                // If disk size is less than 1024, continue to the next iteration
1843
                continue;
1844
            }
1845
1846
            // Add the valid disk to the validDisks array
1847
            $validDisks[$disk['id']] = "  - {$disk['id']}, {$disk['size_text']}, {$disk['vendor']}$additional\n";
1848
        }
1849
1850
        if(count($validDisks) === 0) {
1851
            // If no valid disks were found, log a message and return 0
1852
            echo "\n " . Util::translate('Valid disks not found...') . " \n";
1853
            sleep(3);
1854
            return false;
1855
        }
1856
1857
        echo "\n ".Util::translate('Select the drive to store the data.');
1858
        echo "\n ".Util::translate('Selected disk:')."\033[33;1m [{$selected_disk['id']}] \033[0m \n\n";
1859
        Util::echoWithSyslog("\n ".Util::translate('Valid disks are:')." \n\n");
1860
        foreach ($validDisks as $disk) {
1861
            Util::echoWithSyslog($disk);
1862
        }
1863
        echo "\n";
1864
1865
        // Check if the disk selection should be automatic
1866
        if($automatic === 'auto'){
1867
            $target_disk_storage = $selected_disk['id'];
1868
            Util::echoWithSyslog("Automatically selected disk is $target_disk_storage");
1869
        }else{
1870
            // Otherwise, prompt the user to enter a disk
1871
            do {
1872
                echo "\n".Util::translate('Enter the device name:').Util::translate('(default value = ').$selected_disk['id'].') :';
1873
                $target_disk_storage = trim(fgets($fp));
1874
                if ($target_disk_storage === '') {
1875
                    $target_disk_storage = $selected_disk['id'];
1876
                }
1877
            } while (!array_key_exists($target_disk_storage, $validDisks));
1878
        }
1879
1880
        // Determine the disk partition and format if necessary
1881
        $dev_disk  = "/dev/$target_disk_storage";
1882
        if(!empty($system_disk) && $system_disk === $target_disk_storage){
1883
            $part = "4";
1884
        }else{
1885
            $part = "1";
1886
        }
1887
        $partName = Storage::getDevPartName($target_disk_storage, $part);
1888
        $part_disk = "/dev/$partName";
1889
        if($part === '1' && !Storage::isStorageDisk($part_disk)){
1890
            $storage->formatDiskLocal($dev_disk);
1891
            $partName = Storage::getDevPartName($target_disk_storage, $part);
1892
            $part_disk = "/dev/$partName";
1893
        }
1894
1895
        // Create an array of disk data
1896
        $data=[
1897
            'device'         => $dev_disk,
1898
            'uniqid'         => $storage->getUuid($part_disk),
1899
            'filesystemtype' => 'ext4',
1900
            'name'           => 'Storage №1'
1901
        ];
1902
1903
        // Save the disk settings
1904
        $storage->saveDiskSettings($data);
1905
        if(file_exists('/offload/livecd')) {
1906
            // Do not need to start the PBX, it's the station installation in LiveCD mode.
1907
            return true;
1908
        }
1909
        MainDatabaseProvider::recreateDBConnections();
1910
1911
        // Configure the storage
1912
        $storage->configure();
1913
        MainDatabaseProvider::recreateDBConnections();
1914
        $success = Storage::isStorageDiskMounted();
1915
        if($success === true && $automatic === 'auto'){
1916
            System::rebootSync();
1917
            return true;
1918
        }
1919
1920
        fclose(STDERR);
1921
        Util::echoWithSyslog(' - Update database ... '. PHP_EOL);
1922
1923
        // Update the database
1924
        $dbUpdater = new UpdateDatabase();
1925
        $dbUpdater->updateDatabaseStructure();
1926
1927
        $STDERR = fopen('php://stderr', 'wb');
1928
        CdrDb::checkDb();
1929
1930
        // Restart syslog
1931
        $sysLog = new SyslogConf();
1932
        $sysLog->reStart();
1933
1934
        // Configure PBX
1935
        $pbx = new PBX();
1936
        $pbx->configure();
1937
1938
        // Restart processes related to storage
1939
        Processes::processPHPWorker(WorkerApiCommands::class);
1940
1941
        // Check if the disk was mounted successfully
1942
        if($success === true){
1943
            echo "\n ".Util::translate('Storage disk was mounted successfully...')." \n\n";
1944
        }else{
1945
            echo "\n ".Util::translate('Failed to mount the disc...')." \n\n";
1946
        }
1947
1948
        sleep(3);
1949
        fclose($STDERR);
1950
1951
        return $success;
1952
    }
1953
}