Passed
Push — develop ( 79cd63...703881 )
by Nikolay
33:58 queued 26:40
created

Storage::mkfsDisk()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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