Passed
Push — develop ( d7480a...c500ef )
by Портнов
24:00 queued 11:40
created

Storage::statusMkfs()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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