Passed
Push — develop ( 20b00c...f7632d )
by Nikolay
14:31
created

Storage::isStorageDisk()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 41
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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