Storage::selectAndConfigureStorageDisk()   F
last analyzed

Complexity

Conditions 34
Paths > 20000

Size

Total Lines 178
Code Lines 111

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 111
c 8
b 0
f 0
dl 0
loc 178
rs 0
cc 34
nc 79800
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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