Passed
Push — develop ( a0e63f...b8e0a8 )
by Портнов
05:18
created

Storage::makeSwapFile()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 34
rs 9.3222
c 0
b 0
f 0
cc 5
nc 7
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 = round($original_size, 1);
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' => round($free_space, 1),
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
        $settingsFile = '/etc/inc/mikopbx-settings.json';
1253
        $staticSettingsFileOrig = '/etc/inc/mikopbx-settings-orig.json';
1254
        if (!file_exists($staticSettingsFileOrig)){
1255
            copy($settingsFile, $staticSettingsFileOrig);
1256
        }
1257
1258
        $jsonString = file_get_contents($staticSettingsFileOrig);
1259
        try {
1260
            $data = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
1261
        } catch (JsonException $exception) {
1262
            throw new Error("{$staticSettingsFileOrig} has broken format");
1263
        }
1264
        foreach ($data as $rootKey => $rootEntry) {
1265
            foreach ($rootEntry as $nestedKey => $entry) {
1266
                if (stripos($entry, '/mountpoint') !== false) {
1267
                    $data[$rootKey][$nestedKey] = str_ireplace('/mountpoint', $mount_point, $entry);
1268
                }
1269
            }
1270
        }
1271
1272
        $newJsonString = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1273
        file_put_contents($settingsFile, $newJsonString);
1274
        $this->updateEnvironmentAfterChangeMountPoint();
1275
    }
1276
1277
1278
    /**
1279
     * Updates the environment after changing the mount point.
1280
     * - Recreates the config provider and updates the config variable.
1281
     * - Reloads classes from system and storage disks.
1282
     * - Reloads all providers.
1283
     */
1284
    private function updateEnvironmentAfterChangeMountPoint(): void
1285
    {
1286
        // Update config variable
1287
        ConfigProvider::recreateConfigProvider();
1288
        $this->config = $this->di->get('config');
1289
1290
        // Reload classes from system and storage disks
1291
        ClassLoader::init();
1292
1293
        // Reload all providers
1294
        RegisterDIServices::init();
1295
    }
1296
1297
    /**
1298
     * Saves the fstab configuration.
1299
     *
1300
     * @param string $conf Additional configuration to append to fstab
1301
     * @return void
1302
     */
1303
    public function saveFstab(string $conf = ''): void
1304
    {
1305
        $varEtcDir = $this->config->path('core.varEtcDir');
1306
1307
        // Create the mount point directory for additional disks
1308
        Util::mwMkdir('/storage');
1309
        $chmodPath = Util::which('chmod');
1310
        Processes::mwExec("{$chmodPath} 755 /storage");
1311
1312
        // Check if cfdevice file exists
1313
        if (!file_exists($varEtcDir . '/cfdevice')) {
1314
            return;
1315
        }
1316
        $fstab = '';
1317
1318
        // Read cfdevice file
1319
        $file_data = file_get_contents($varEtcDir . '/cfdevice');
1320
        $cf_disk = trim($file_data);
1321
        if ('' === $cf_disk) {
1322
            return;
1323
        }
1324
        $part2 = self::getDevPartName($cf_disk, '2');
1325
        $part3 = self::getDevPartName($cf_disk, '3');
1326
1327
        $uid_part2 = 'UUID=' . $this->getUuid("/dev/{$part2}");
1328
        $format_p2 = $this->getFsType($part2);
1329
        $uid_part3 = 'UUID=' . $this->getUuid("/dev/{$part3}");
1330
        $format_p3 = $this->getFsType($part3);
1331
1332
        $fstab .= "{$uid_part2} /offload {$format_p2} ro 0 0\n";
1333
        $fstab .= "{$uid_part3} /cf {$format_p3} rw 1 1\n";
1334
        $fstab .= $conf;
1335
1336
        // Write fstab file
1337
        file_put_contents("/etc/fstab", $fstab);
1338
1339
        // Duplicate for vmtoolsd
1340
        file_put_contents("/etc/mtab", $fstab);
1341
1342
        // Mount the file systems
1343
        $mountPath = Util::which('mount');
1344
        Processes::mwExec("{$mountPath} -a 2> /dev/null");
1345
1346
        // Add regular www rights to /cf directory
1347
        Util::addRegularWWWRights('/cf');
1348
    }
1349
1350
    /**
1351
     * Retrieves the partition name of a device.
1352
     *
1353
     * @param string $dev The device name
1354
     * @param string $part The partition number
1355
     * @return string The partition name
1356
     */
1357
    public static function getDevPartName(string $dev, string $part): string
1358
    {
1359
        $lsBlkPath = Util::which('lsblk');
1360
        $cutPath = Util::which('cut');
1361
        $grepPath = Util::which('grep');
1362
        $sortPath = Util::which('sort');
1363
1364
        $command = "{$lsBlkPath} -r | {$grepPath} ' part' | {$sortPath} -u | {$cutPath} -d ' ' -f 1 | {$grepPath} \"" . basename(
1365
                $dev
1366
            ) . "\" | {$grepPath} \"{$part}\$\"";
1367
        Processes::mwExec($command, $out);
1368
        $devName = trim(implode('', $out));
1369
        return trim($devName);
1370
    }
1371
1372
    /**
1373
     * Creates the necessary working directories and symlinks.
1374
     *
1375
     * @return void
1376
     */
1377
    private function createWorkDirs(): void
1378
    {
1379
        $path = '';
1380
        $mountPath = Util::which('mount');
1381
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1382
1383
        $isLiveCd = file_exists('/offload/livecd');
1384
1385
        // Create directories
1386
        $arrConfig = $this->config->toArray();
1387
        foreach ($arrConfig as $rootEntry) {
1388
            foreach ($rootEntry as $key => $entry) {
1389
                if (stripos($key, 'path') === false && stripos($key, 'dir') === false) {
1390
                    continue;
1391
                }
1392
                if (file_exists($entry)) {
1393
                    continue;
1394
                }
1395
                if ($isLiveCd && strpos($entry, '/offload/') === 0) {
1396
                    continue;
1397
                }
1398
                $path .= " $entry";
1399
            }
1400
        }
1401
1402
        if (!empty($path)) {
1403
            Util::mwMkdir($path);
1404
        }
1405
1406
        $downloadCacheDir = appPath('sites/pbxcore/files/cache');
1407
        if (!$isLiveCd) {
1408
            Util::mwMkdir($downloadCacheDir);
1409
            Util::createUpdateSymlink($this->config->path('www.downloadCacheDir'), $downloadCacheDir);
1410
        }
1411
1412
        $this->createAssetsSymlinks();
1413
        $this->createViewSymlinks();
1414
        $this->createAGIBINSymlinks($isLiveCd);
1415
1416
        Util::createUpdateSymlink($this->config->path('www.uploadDir'), '/ultmp');
1417
1418
        $filePath = appPath('src/Core/Asterisk/Configs/lua/extensions.lua');
1419
        Util::createUpdateSymlink($filePath, '/etc/asterisk/extensions.lua');
1420
1421
        $this->clearCacheFiles();
1422
        $this->clearTmpFiles();
1423
        $this->applyFolderRights();
1424
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1425
    }
1426
1427
    /**
1428
     * Creates symlinks for asset cache directories.
1429
     *
1430
     * @return void
1431
     */
1432
    public function createAssetsSymlinks(): void
1433
    {
1434
        // Create symlink for JS cache directory
1435
        $jsCacheDir = appPath('sites/admin-cabinet/assets/js/cache');
1436
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/js', $jsCacheDir);
1437
1438
        // Create symlink for CSS cache directory
1439
        $cssCacheDir = appPath('sites/admin-cabinet/assets/css/cache');
1440
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/css', $cssCacheDir);
1441
1442
        // Create symlink for image cache directory
1443
        $imgCacheDir = appPath('sites/admin-cabinet/assets/img/cache');
1444
        Util::createUpdateSymlink($this->config->path('adminApplication.assetsCacheDir') . '/img', $imgCacheDir);
1445
1446
    }
1447
1448
    /**
1449
     * Creates symlinks for modules view.
1450
     *
1451
     * @return void
1452
     */
1453
    public function createViewSymlinks(): void
1454
    {
1455
        $viewCacheDir = appPath('src/AdminCabinet/Views/Modules');
1456
        Util::createUpdateSymlink($this->config->path('adminApplication.viewCacheDir'), $viewCacheDir);
1457
    }
1458
1459
    /**
1460
     * Creates AGI bin symlinks for extension modules.
1461
     *
1462
     * @param bool $isLiveCd Whether the system loaded on LiveCD mode.
1463
     * @return void
1464
     */
1465
    public function createAGIBINSymlinks(bool $isLiveCd): void
1466
    {
1467
        $agiBinDir = $this->config->path('asterisk.astagidir');
1468
        if ($isLiveCd && strpos($agiBinDir, '/offload/') !== 0) {
1469
            Util::mwMkdir($agiBinDir);
1470
        }
1471
1472
        $roAgiBinFolder = appPath('src/Core/Asterisk/agi-bin');
1473
        $files = glob("{$roAgiBinFolder}/*.{php}", GLOB_BRACE);
1474
        foreach ($files as $file) {
1475
            $fileInfo = pathinfo($file);
1476
            $newFilename = "{$agiBinDir}/{$fileInfo['filename']}.{$fileInfo['extension']}";
1477
            Util::createUpdateSymlink($file, $newFilename);
1478
        }
1479
    }
1480
1481
    /**
1482
     * Clears the cache files for various directories.
1483
     *
1484
     * @return void
1485
     */
1486
    public function clearCacheFiles(): void
1487
    {
1488
        $cacheDirs = [];
1489
        $cacheDirs[] = $this->config->path('www.uploadDir');
1490
        $cacheDirs[] = $this->config->path('www.downloadCacheDir');
1491
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/js';
1492
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/css';
1493
        $cacheDirs[] = $this->config->path('adminApplication.assetsCacheDir') . '/img';
1494
        $cacheDirs[] = $this->config->path('adminApplication.viewCacheDir');
1495
        $cacheDirs[] = $this->config->path('adminApplication.voltCacheDir');
1496
        $rmPath = Util::which('rm');
1497
1498
        // Clear cache files for each directory
1499
        foreach ($cacheDirs as $cacheDir) {
1500
            if (!empty($cacheDir)) {
1501
                Processes::mwExec("{$rmPath} -rf {$cacheDir}/*");
1502
            }
1503
        }
1504
1505
        // Delete boot cache folders if storage disk is mounted
1506
        if (is_dir('/mountpoint') && self::isStorageDiskMounted()) {
1507
            Processes::mwExec("{$rmPath} -rf /mountpoint");
1508
        }
1509
    }
1510
1511
    /**
1512
     * @return void
1513
     */
1514
    private function clearTmpFiles(): void
1515
    {
1516
        $busyboxPath = Util::which('busybox');
1517
        $tmpDir = $this->config->path('core.tempDir');
1518
        if(!file_exists($tmpDir)){
1519
            return;
1520
        }
1521
        // Trying to get a list of files
1522
        Processes::mwExec("$busyboxPath timeout 10 $busyboxPath find $tmpDir -type f", $out, $ret);
1523
        if($ret !== 0){
1524
            // there are too many files in the temporary directory, we will clear them
1525
            // it may cause a failure when setting access rights (chown)
1526
            $resDirForRm = "$tmpDir-".time();
1527
            shell_exec("$busyboxPath mv '$tmpDir' '$resDirForRm'");
1528
            if(file_exists("$resDirForRm/swapfile")){
1529
                // Saving only the swap file
1530
                shell_exec("$busyboxPath mv '$resDirForRm/swapfile' '$tmpDir/swapfile'");
1531
            }
1532
            // Let's start deleting temporary files
1533
            Processes::mwExecBg("/usr/bin/nice -n 19 $busyboxPath rm -rf $resDirForRm");
1534
        }
1535
        Util::mwMkdir($tmpDir, true);
1536
    }
1537
1538
    /**
1539
     * Create system folders and links after upgrade and connect config DB
1540
     *
1541
     * @return void
1542
     */
1543
    public function createWorkDirsAfterDBUpgrade(): void
1544
    {
1545
        // Remount /offload directory as read-write
1546
        $mountPath = Util::which('mount');
1547
        Processes::mwExec("{$mountPath} -o remount,rw /offload 2> /dev/null");
1548
1549
        // Create symlinks for module caches
1550
        $this->createModulesCacheSymlinks();
1551
1552
        // Apply folder rights
1553
        $this->applyFolderRights();
1554
1555
        // Remount /offload directory as read-only
1556
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1557
    }
1558
1559
1560
    /**
1561
     * Creates symlinks for module caches.
1562
     *
1563
     * @return void
1564
     */
1565
    public function createModulesCacheSymlinks(): void
1566
    {
1567
        $modules = PbxExtensionModules::getModulesArray();
1568
        foreach ($modules as $module) {
1569
            // Create cache links for JS, CSS, IMG folders
1570
            PbxExtensionUtils::createAssetsSymlinks($module['uniqid']);
1571
1572
            // Create links for the module view templates
1573
            PbxExtensionUtils::createViewSymlinks($module['uniqid']);
1574
1575
            // Create AGI bin symlinks for the module
1576
            PbxExtensionUtils::createAgiBinSymlinks($module['uniqid']);
1577
        }
1578
    }
1579
1580
    /**
1581
     * Applies folder rights to the appropriate directories.
1582
     *
1583
     * @return void
1584
     */
1585
    private function applyFolderRights(): void
1586
    {
1587
1588
        $www_dirs = []; // Directories with WWW rights
1589
        $exec_dirs = []; // Directories with executable rights
1590
1591
        $arrConfig = $this->config->toArray();
1592
1593
        // Get the directories for WWW rights from the configuration
1594
        foreach ($arrConfig as $key => $entry) {
1595
            if (in_array($key, ['www', 'adminApplication'])) {
1596
                foreach ($entry as $subKey => $subEntry) {
1597
                    if (stripos($subKey, 'path') === false && stripos($subKey, 'dir') === false) {
1598
                        continue;
1599
                    }
1600
                    $www_dirs[] = $subEntry;
1601
                }
1602
            }
1603
        }
1604
1605
        // Add additional directories with WWW rights
1606
        $www_dirs[] = $this->config->path('core.tempDir');
1607
        $www_dirs[] = $this->config->path('core.logsDir');
1608
1609
        // Create empty log files with WWW rights
1610
        $logFiles = [
1611
            $this->config->path('database.debugLogFile'),
1612
            $this->config->path('cdrDatabase.debugLogFile'),
1613
        ];
1614
1615
        foreach ($logFiles as $logFile) {
1616
            $filename = (string)$logFile;
1617
            if (!file_exists($filename)) {
1618
                file_put_contents($filename, '');
1619
            }
1620
            $www_dirs[] = $filename;
1621
        }
1622
1623
        $www_dirs[] = '/etc/version';
1624
        $www_dirs[] = appPath('/');
1625
1626
        // Add read rights to the directories
1627
        Util::addRegularWWWRights(implode(' ', $www_dirs));
1628
1629
        // Add executable rights to the directories
1630
        $exec_dirs[] = appPath('src/Core/Asterisk/agi-bin');
1631
        Util::addExecutableRights(implode(' ', $exec_dirs));
1632
1633
        $mountPath = Util::which('mount');
1634
        Processes::mwExec("{$mountPath} -o remount,ro /offload 2> /dev/null");
1635
    }
1636
1637
    /**
1638
     * Mounts the swap file.
1639
     */
1640
    public function mountSwap(): void
1641
    {
1642
        $tempDir = $this->config->path('core.tempDir');
1643
        $swapFile = "{$tempDir}/swapfile";
1644
1645
        $swapOffCmd = Util::which('swapoff');
1646
        Processes::mwExec("{$swapOffCmd} {$swapFile}");
1647
1648
        $this->makeSwapFile($swapFile);
1649
        if (!file_exists($swapFile)) {
1650
            return;
1651
        }
1652
        $swapOnCmd = Util::which('swapon');
1653
        $result = Processes::mwExec("{$swapOnCmd} {$swapFile}");
1654
        Util::sysLogMsg('Swap', 'connect swap result: ' . $result, LOG_INFO);
1655
    }
1656
1657
    /**
1658
     * Creates a swap file.
1659
     *
1660
     * @param string $swapFile The path to the swap file.
1661
     */
1662
    private function makeSwapFile(string $swapFile): void
1663
    {
1664
        $swapLabel = Util::which('swaplabel');
1665
1666
        // Check if swap file already exists
1667
        if (Processes::mwExec("{$swapLabel} {$swapFile}") === 0) {
1668
            return;
1669
        }
1670
        if (file_exists($swapFile)) {
1671
            unlink($swapFile);
1672
        }
1673
1674
        $size = $this->getStorageFreeSpaceMb();
1675
        if ($size > 2000) {
1676
            $swapSize = 1024;
1677
        } elseif ($size > 1000) {
1678
            $swapSize = 512;
1679
        } else {
1680
            // Not enough free space.
1681
            return;
1682
        }
1683
        $bs = 1024;
1684
        $countBlock = $swapSize * $bs;
1685
        $ddCmd = Util::which('dd');
1686
1687
        Util::sysLogMsg('Swap', 'make swap ' . $swapFile, LOG_INFO);
1688
1689
        // Create swap file using dd command
1690
        Processes::mwExec("{$ddCmd} if=/dev/zero of={$swapFile} bs={$bs} count={$countBlock}");
1691
1692
        $mkSwapCmd = Util::which('mkswap');
1693
1694
        // Set up swap space on the file
1695
        Processes::mwExec("{$mkSwapCmd} {$swapFile}");
1696
    }
1697
1698
    /**
1699
     * Retrieves the amount of free storage space in megabytes.
1700
     *
1701
     * @return int The amount of free storage space in megabytes.
1702
     */
1703
    public function getStorageFreeSpaceMb(): int
1704
    {
1705
        $size = 0;
1706
        $mntDir = '';
1707
        $mounted = self::isStorageDiskMounted('', $mntDir);
1708
        if (!$mounted) {
1709
            return 0;
1710
        }
1711
        $hd = $this->getAllHdd(true);
1712
        foreach ($hd as $disk) {
1713
            if ($disk['mounted'] === $mntDir) {
1714
                $size = $disk['free_space'];
1715
                break;
1716
            }
1717
        }
1718
        return $size;
1719
    }
1720
1721
    /**
1722
     * Saves the disk settings to the database.
1723
     *
1724
     * @param array $data The disk settings data to be saved.
1725
     * @param string $id The ID of the disk settings to be updated (default: '1').
1726
     * @return void
1727
     */
1728
    public function saveDiskSettings(array $data, string $id = '1'): void
1729
    {
1730
        $disk_data = $this->getDiskSettings($id);
1731
        if (count($disk_data) === 0) {
1732
            $storage_settings = new StorageModel();
1733
        } else {
1734
            $storage_settings = StorageModel::findFirst("id = '$id'");
1735
        }
1736
        foreach ($data as $key => $value) {
1737
            $storage_settings->writeAttribute($key, $value);
1738
        }
1739
        $storage_settings->save();
1740
    }
1741
1742
    /**
1743
     * Retrieves the name of the disk used for recovery. (conf.recover.)
1744
     *
1745
     * @return string The name of the recovery disk (e.g., '/dev/sda').
1746
     */
1747
    public function getRecoverDiskName(): string
1748
    {
1749
        $disks = $this->diskGetDevices(true);
1750
        foreach ($disks as $disk => $diskInfo) {
1751
            // Check if the disk is a RAID or virtual device
1752
            if (isset($diskInfo['children'][0]['children'])) {
1753
                $diskInfo = $diskInfo['children'][0];
1754
                // Adjust the disk name for RAID or other virtual devices
1755
                $disk = $diskInfo['name'];
1756
            }
1757
            foreach ($diskInfo['children'] as $child) {
1758
                $mountpoint = $child['mountpoint'] ?? '';
1759
                $diskPath = "/dev/{$disk}";
1760
                if ($mountpoint === '/conf.recover' && file_exists($diskPath)) {
1761
                    return "/dev/{$disk}";
1762
                }
1763
            }
1764
        }
1765
        return '';
1766
    }
1767
1768
1769
}