Passed
Push — develop ( bef097...351c48 )
by Nikolay
12:23
created

Storage::umountDisk()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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