Passed
Push — develop ( 3bb550...c283a9 )
by Nikolay
06:01 queued 11s
created

FilesManagementProcessor::callBack()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 42
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 42
rs 7.7724
c 0
b 0
f 0
cc 9
nc 9
nop 1
1
<?php
2
/**
3
 * Copyright © MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Alexey Portnov, 8 2020
7
 */
8
9
namespace MikoPBX\PBXCoreREST\Lib;
10
11
use MikoPBX\Common\Models\CustomFiles;
12
use MikoPBX\Core\System\Util;
13
use MikoPBX\PBXCoreREST\Workers\WorkerDownloader;
14
use MikoPBX\PBXCoreREST\Workers\WorkerMergeUploadedFile;
15
use Phalcon\Di;
16
use Phalcon\Di\Injectable;
17
use Phalcon\Http\Message\StreamFactory;
18
use Phalcon\Http\Message\UploadedFile;
19
20
class FilesManagementProcessor extends Injectable
21
{
22
    /**
23
     * Processes file upload requests
24
     *
25
     * @param array $request
26
     *
27
     * @return \MikoPBX\PBXCoreREST\Lib\PBXApiResult
28
     */
29
    public static function callBack(array $request): PBXApiResult
30
    {
31
        $action   = $request['action'];
32
        $postData = $request['data'];
33
        switch ($action) {
34
            case 'uploadResumable':
35
                $res = FilesManagementProcessor::uploadResumable($postData);
36
                break;
37
            case 'statusUploadFile':
38
                $res = FilesManagementProcessor::statusUploadFile($request['data']);
39
                break;
40
            case 'removeAudioFile':
41
                $res = FilesManagementProcessor::removeAudioFile($postData['filename']);
42
                break;
43
            case 'fileReadContent':
44
                $res = FilesManagementProcessor::fileReadContent($postData['filename'], $postData['needOriginal']);
45
                break;
46
            case 'downloadNewFirmware':
47
                $res = FilesManagementProcessor::downloadNewFirmware($request['data']);
48
                break;
49
            case 'firmwareDownloadStatus':
50
                $res = FilesManagementProcessor::firmwareDownloadStatus();
51
                break;
52
            case 'downloadNewModule':
53
                $module = $request['data']['uniqid'];
54
                $url    = $request['data']['url'];
55
                $md5    = $request['data']['md5'];
56
                $res    = FilesManagementProcessor::moduleStartDownload($module, $url, $md5);
57
                break;
58
            case 'moduleDownloadStatus':
59
                $module = $request['data']['uniqid'];
60
                $res    = FilesManagementProcessor::moduleDownloadStatus($module);
61
                break;
62
            default:
63
                $res             = new PBXApiResult();
64
                $res->processor  = __METHOD__;
65
                $res->messages[] = "Unknown action - {$action} in uploadCallBack";
66
        }
67
68
        $res->function = $action;
69
70
        return $res;
71
    }
72
73
    /**
74
     * Process resumable upload files
75
     *
76
     * @param array $parameters
77
     *
78
     * @return \MikoPBX\PBXCoreREST\Lib\PBXApiResult
79
     */
80
    public static function uploadResumable(array $parameters): PBXApiResult
81
    {
82
        $res            = new PBXApiResult();
83
        $res->processor = __METHOD__;
84
        $di             = Di::getDefault();
85
        if ($di === null) {
86
            $res->success    = false;
87
            $res->messages[] = 'Dependency injector does not initialized';
88
            return $res;
89
        }
90
        $parameters['uploadDir'] = $di->getShared('config')->path('www.uploadDir');
91
        $parameters['tempDir'] = "{$parameters['uploadDir']}/{$parameters['resumableIdentifier']}";
92
        if ( ! Util::mwMkdir($parameters['tempDir'])) {
93
            $res->messages[] = 'Temp dir does not exist ' . $parameters['tempDir'];
94
            return $res;
95
        }
96
97
        $fileName                           = pathinfo($parameters['resumableFilename'], PATHINFO_BASENAME);
98
        $parameters['fullUploadedFileName'] = "{$parameters['tempDir']}/{$fileName}";
99
100
        // Delete old progress and result file
101
        $oldMergeProgressFile = "{$parameters['tempDir']}/merging_progress";
102
        if (file_exists($oldMergeProgressFile)){
103
            unlink($oldMergeProgressFile);
104
        }
105
        if (file_exists($parameters['fullUploadedFileName'])){
106
            unlink($parameters['fullUploadedFileName']);
107
        }
108
109
        foreach ($parameters['files'] as $file_data) {
110
            if (!self::moveUploadedPartToSeparateDir($parameters, $file_data)){
111
                $res->messages[] = 'Does not found any uploaded chunks on with path '.$file_data['file_path'];
112
                return $res;
113
            }
114
            $res->success           = true;
115
            $res->data['upload_id'] = $parameters['resumableIdentifier'];
116
            $res->data['filename']  = $parameters['fullUploadedFileName'];
117
118
            if (self::tryToMergeChunksIfAllPartsUploaded($parameters)) {
119
                $res->data['d_status'] = 'MERGING';
120
            } else {
121
                $res->data['d_status'] = 'WAITING_FOR_NEXT_PART';
122
            }
123
        }
124
125
        return $res;
126
    }
127
128
    /**
129
     * Moves uploaded file part to separate directory with "upload_id" name on the system uploadDir folder.
130
     *
131
     * @param array $parameters data from of resumable request
132
     * @param array $file_data  data from uploaded file part
133
     *
134
     * @return bool
135
     */
136
    private static function moveUploadedPartToSeparateDir(array $parameters, array $file_data):bool
137
    {
138
        if ( ! file_exists($file_data['file_path'])) {
139
            return false;
140
        }
141
        $factory          = new StreamFactory();
142
        $stream           = $factory->createStreamFromFile($file_data['file_path'], 'r');
143
        $file             = new UploadedFile(
144
            $stream,
145
            $file_data['file_size'],
146
            $file_data['file_error'],
147
            $file_data['file_name'],
148
            $file_data['file_type']
149
        );
150
        $chunks_dest_file = "{$parameters['tempDir']}/{$parameters['resumableFilename']}.part{$parameters['resumableChunkNumber']}";
151
        if (file_exists($chunks_dest_file)){
152
            $rm = Util::which('rm');
153
            Util::mwExec("{$rm} -f {$chunks_dest_file}");
154
        }
155
        $file->moveTo($chunks_dest_file);
156
        return true;
157
    }
158
159
    /**
160
     * If the size of all the chunks on the server is equal to the size of the file uploaded starts a merge process.
161
     *
162
     * @param array $parameters
163
     *
164
     * @return bool
165
     */
166
    private static function tryToMergeChunksIfAllPartsUploaded(array $parameters): bool
167
    {
168
        $totalFilesOnServerSize = 0;
169
        foreach (scandir($parameters['tempDir']) as $file) {
170
            $totalFilesOnServerSize += filesize($parameters['tempDir'] . '/' . $file);
171
        }
172
173
        if ($totalFilesOnServerSize >= $parameters['resumableTotalSize']) {
174
            // Parts upload complete
175
            $merge_settings = [
176
                    'fullUploadedFileName' => $parameters['fullUploadedFileName'],
177
                    'tempDir'              => $parameters['tempDir'],
178
                    'resumableFilename'    => $parameters['resumableFilename'],
179
                    'resumableTotalSize'   => $parameters['resumableTotalSize'],
180
                    'resumableTotalChunks' => $parameters['resumableTotalChunks'],
181
            ];
182
            $settings_file  = "{$parameters['tempDir']}/merge_settings";
183
            file_put_contents(
184
                $settings_file,
185
                json_encode($merge_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
186
            );
187
188
            // We will start the background process to merge parts into one file
189
            $phpPath               = Util::which('php');
190
            $workerFilesMergerPath = Util::getFilePathByClassName(WorkerMergeUploadedFile::class);
191
            Util::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} '{$settings_file}'");
192
            return true;
193
        }
194
        return false;
195
    }
196
197
    /**
198
     * Returns Status of uploading process
199
     *
200
     * @param array $postData
201
     *
202
     * @return \MikoPBX\PBXCoreREST\Lib\PBXApiResult
203
     */
204
    public static function statusUploadFile(array $postData): PBXApiResult
205
    {
206
        $res            = new PBXApiResult();
207
        $res->processor = __METHOD__;
208
        $di             = Di::getDefault();
209
        if ($di === null) {
210
            $res->messages[] = 'Dependency injector does not initialized';
211
212
            return $res;
213
        }
214
        $uploadDir = $di->getShared('config')->path('www.uploadDir');
215
216
        $upload_id     = $postData['id'] ?? null;
217
        $progress_dir  = $uploadDir . '/' . $upload_id;
218
        $progress_file = $progress_dir . '/merging_progress';
219
        if (empty($upload_id)) {
220
            $res->success                   = false;
221
            $res->data['d_status_progress'] = '0';
222
            $res->data['d_status']          = 'ID_NOT_SET';
223
            $res->messages[]                = 'Upload ID does not set';
224
        } elseif ( ! file_exists($progress_file) && file_exists($progress_dir)) {
225
            $res->success                   = true;
226
            $res->data['d_status_progress'] = '0';
227
            $res->data['d_status']          = 'INPROGRESS';
228
        } elseif ( ! file_exists($progress_dir)) {
229
            $res->success                   = false;
230
            $res->data['d_status_progress'] = '0';
231
            $res->data['d_status']          = 'NOT_FOUND';
232
            $res->messages[]                = 'Does not found anything with path: ' . $progress_dir;
233
        } elseif ('100' === file_get_contents($progress_file)) {
234
            $res->success                   = true;
235
            $res->data['d_status_progress'] = '100';
236
            $res->data['d_status']          = 'UPLOAD_COMPLETE';
237
        } else {
238
            $res->success                   = true;
239
            $res->data['d_status_progress'] = file_get_contents($progress_file);
240
        }
241
242
243
        return $res;
244
    }
245
246
    /**
247
     * Returns file content
248
     *
249
     * @param $filename
250
     * @param $needOriginal
251
     *
252
     * @return PBXApiResult
253
     */
254
    public static function fileReadContent($filename, $needOriginal = true): PBXApiResult
255
    {
256
        $res            = new PBXApiResult();
257
        $res->processor = __METHOD__;
258
        $customFile     = CustomFiles::findFirst("filepath = '{$filename}'");
259
        if ($customFile !== null) {
260
            $filename_orgn = "{$filename}.orgn";
261
            if ($needOriginal && file_exists($filename_orgn)) {
262
                $filename = $filename_orgn;
263
            }
264
            $res->success = true;
265
            $cat          = Util::which('cat');
266
            $di           = Di::getDefault();
267
            $dirsConfig   = $di->getShared('config');
268
            $filenameTmp  = $dirsConfig->path('www.downloadCacheDir') . '/' . __FUNCTION__ . '_' . time() . '.conf';
269
            $cmd          = "{$cat} {$filename} > {$filenameTmp}";
270
            Util::mwExec("{$cmd}; chown www:www {$filenameTmp}");
271
            $res->data['filename'] = $filenameTmp;
272
        } else {
273
            $res->success    = false;
274
            $res->messages[] = 'No access to the file ' . $filename;
275
        }
276
277
        return $res;
278
    }
279
280
    /**
281
     * Downloads IMG from MikoPBX repository
282
     *
283
     * @param $data
284
     *
285
     * @return PBXApiResult
286
     */
287
    public static function downloadNewFirmware($data): PBXApiResult
288
    {
289
        $di = Di::getDefault();
290
        if ($di !== null) {
291
            $tempDir = $di->getConfig()->path('www.uploadDir');
292
        } else {
293
            $tempDir = '/tmp';
294
        }
295
        $rmPath = Util::which('rm');
296
        $module = 'NewFirmware';
297
        if ( ! file_exists($tempDir . "/{$module}")) {
298
            Util::mwMkdir($tempDir . "/{$module}");
299
        } else {
300
            // Чистим файлы, загруженные онлайн.
301
            Util::mwExec("{$rmPath} -rf {$tempDir}/{$module}/* ");
302
        }
303
        if (file_exists("{$tempDir}/update.img")) {
304
            // Чистим вручную загруженный файл.
305
            Util::mwExec("{$rmPath} -rf {$tempDir}/update.img");
306
        }
307
308
        $download_settings = [
309
            'res_file' => "{$tempDir}/{$module}/update.img",
310
            'url'      => $data['url'],
311
            'module'   => $module,
312
            'md5'      => $data['md5'],
313
            'action'   => $module,
314
        ];
315
316
        $workerDownloaderPath = Util::getFilePathByClassName(WorkerDownloader::class);
317
318
        file_put_contents($tempDir . "/{$module}/progress", '0');
319
        file_put_contents(
320
            $tempDir . "/{$module}/download_settings.json",
321
            json_encode($download_settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
322
        );
323
        $phpPath = Util::which('php');
324
        Util::mwExecBg("{$phpPath} -f {$workerDownloaderPath} " . $tempDir . "/{$module}/download_settings.json");
325
326
        $res                   = new PBXApiResult();
327
        $res->processor        = __METHOD__;
328
        $res->success          = true;
329
        $res->data['filename'] = $download_settings['res_file'];
330
        $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
331
332
        return $res;
333
    }
334
335
    /**
336
     * Returns download Firmware from remote repository progress
337
     *
338
     * @return PBXApiResult
339
     */
340
    public static function firmwareDownloadStatus(): PBXApiResult
341
    {
342
        clearstatcache();
343
        $res            = new PBXApiResult();
344
        $res->processor = __METHOD__;
345
        $res->success   = true;
346
        $di             = Di::getDefault();
347
        if ($di !== null) {
348
            $tempDir = $di->getConfig()->path('www.uploadDir');
349
        } else {
350
            $tempDir = '/tmp';
351
        }
352
        $modulesDir    = $tempDir . '/NewFirmware';
353
        $progress_file = $modulesDir . '/progress';
354
355
        $error = '';
356
        if (file_exists($modulesDir . '/error')) {
357
            $error = trim(file_get_contents($modulesDir . '/error'));
358
        }
359
360
        if ( ! file_exists($progress_file)) {
361
            $res->data['d_status_progress'] = '0';
362
            $res->data['d_status']          = 'NOT_FOUND';
363
        } elseif ('' !== $error) {
364
            $res->data['d_status']          = 'DOWNLOAD_ERROR';
365
            $res->data['d_status_progress'] = file_get_contents($progress_file);
366
            $res->data['d_error']           = $error;
367
        } elseif ('100' === file_get_contents($progress_file)) {
368
            $res->data['d_status_progress'] = '100';
369
            $res->data['d_status']          = 'DOWNLOAD_COMPLETE';
370
            $res->data['filePath']          = "{$tempDir}/NewFirmware/update.img";
371
        } else {
372
            $res->data['d_status_progress'] = file_get_contents($progress_file);
373
            $d_pid                          = Util::getPidOfProcess($tempDir . '/NewFirmware/download_settings.json');
374
            if (empty($d_pid)) {
375
                $res->data['d_status'] = 'DOWNLOAD_ERROR';
376
                $error                 = '';
377
                if (file_exists($modulesDir . '/error')) {
378
                    $error = file_get_contents($modulesDir . '/error');
379
                }
380
                $res->data['d_error'] = $error;
381
            } else {
382
                $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
383
            }
384
        }
385
386
        return $res;
387
    }
388
389
    /**
390
     * Starts module download in background separate process
391
     *
392
     * @param $module
393
     * @param $url
394
     * @param $md5
395
     *
396
     * @return PBXApiResult
397
     */
398
    public static function moduleStartDownload($module, $url, $md5): PBXApiResult
399
    {
400
        $res            = new PBXApiResult();
401
        $res->processor = __METHOD__;
402
        $di             = Di::getDefault();
403
        if ($di !== null) {
404
            $tempDir = $di->getConfig()->path('www.uploadDir');
405
        } else {
406
            $tempDir = '/tmp';
407
        }
408
409
        $moduleDirTmp = "{$tempDir}/{$module}";
410
        Util::mwMkdir($moduleDirTmp);
411
412
        $download_settings = [
413
            'res_file' => "$moduleDirTmp/modulefile.zip",
414
            'url'      => $url,
415
            'module'   => $module,
416
            'md5'      => $md5,
417
            'action'   => 'moduleInstall',
418
        ];
419
        if (file_exists("$moduleDirTmp/error")) {
420
            unlink("$moduleDirTmp/error");
421
        }
422
        if (file_exists("$moduleDirTmp/installed")) {
423
            unlink("$moduleDirTmp/installed");
424
        }
425
        file_put_contents("$moduleDirTmp/progress", '0');
426
        file_put_contents(
427
            "$moduleDirTmp/download_settings.json",
428
            json_encode($download_settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
429
        );
430
        $workerDownloaderPath = Util::getFilePathByClassName(WorkerDownloader::class);
431
        $phpPath              = Util::which('php');
432
        Util::mwExecBg("{$phpPath} -f {$workerDownloaderPath} $moduleDirTmp/download_settings.json");
433
434
        $res->data['uniqid']   = $module;
435
        $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
436
        $res->success          = true;
437
438
        return $res;
439
    }
440
441
    /**
442
     * Returns module download status
443
     *
444
     * @param $moduleUniqueID
445
     *
446
     * @return PBXApiResult
447
     */
448
    public static function moduleDownloadStatus(string $moduleUniqueID): PBXApiResult
449
    {
450
        clearstatcache();
451
        $res            = new PBXApiResult();
452
        $res->processor = __METHOD__;
453
        $di             = Di::getDefault();
454
        if ($di !== null) {
455
            $tempDir = $di->getConfig()->path('www.uploadDir');
456
        } else {
457
            $tempDir = '/tmp';
458
        }
459
        $moduleDirTmp  = $tempDir . '/' . $moduleUniqueID;
460
        $progress_file = $moduleDirTmp . '/progress';
461
        $error         = '';
462
        if (file_exists($moduleDirTmp . '/error')) {
463
            $error = trim(file_get_contents($moduleDirTmp . '/error'));
464
        }
465
466
        // Ожидание запуска процесса загрузки.
467
        $d_pid = Util::getPidOfProcess("{$moduleDirTmp}/download_settings.json");
468
        if (empty($d_pid)) {
469
            usleep(500000);
470
        }
471
472
        if ( ! file_exists($progress_file)) {
473
            $res->data['d_status_progress'] = '0';
474
            $res->data['d_status']          = 'NOT_FOUND';
475
            $res->success                   = false;
476
        } elseif ('' !== $error) {
477
            $res->data['d_status']          = 'DOWNLOAD_ERROR';
478
            $res->data['d_status_progress'] = file_get_contents($progress_file);
479
            $res->data['d_error']           = $error;
480
            $res->success                   = false;
481
        } elseif ('100' === file_get_contents($progress_file)) {
482
            $res->data['d_status_progress'] = '100';
483
            $res->data['d_status']          = 'DOWNLOAD_COMPLETE';
484
            $res->data['filePath']          = "$moduleDirTmp/modulefile.zip";
485
            $res->success                   = true;
486
        } else {
487
            $res->data['d_status_progress'] = file_get_contents($progress_file);
488
            $d_pid                          = Util::getPidOfProcess($moduleDirTmp . '/download_settings.json');
489
            if (empty($d_pid)) {
490
                $res->data['d_status'] = 'DOWNLOAD_ERROR';
491
                if (file_exists($moduleDirTmp . '/error')) {
492
                    $res->messages[] = file_get_contents($moduleDirTmp . '/error');
493
                } else {
494
                    $res->messages[]                 = "Download process interrupted at {$res->data['d_status_progress']}%";
495
                }
496
                $res->success    = false;
497
            } else {
498
                $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
499
                $res->success          = true;
500
            }
501
        }
502
503
        return $res;
504
    }
505
506
    /**
507
     * Delete file from disk by filepath
508
     *
509
     * @param $filePath
510
     *
511
     * @return PBXApiResult
512
     */
513
    public static function removeAudioFile($filePath): PBXApiResult
514
    {
515
        $res            = new PBXApiResult();
516
        $res->processor = __METHOD__;
517
        $extension      = Util::getExtensionOfFile($filePath);
518
        if ( ! in_array($extension, ['mp3', 'wav', 'alaw'])) {
519
            $res->success    = false;
520
            $res->messages[] = "It is forbidden to remove the file type $extension.";
521
522
            return $res;
523
        }
524
525
        if ( ! file_exists($filePath)) {
526
            $res->success         = true;
527
            $res->data['message'] = "File '{$filePath}' already deleted";
528
529
            return $res;
530
        }
531
532
        $out = [];
533
534
        $arrDeletedFiles = [
535
            escapeshellarg(Util::trimExtensionForFile($filePath) . ".wav"),
536
            escapeshellarg(Util::trimExtensionForFile($filePath) . ".mp3"),
537
            escapeshellarg(Util::trimExtensionForFile($filePath) . ".alaw"),
538
        ];
539
540
        $rmPath = Util::which('rm');
541
        Util::mwExec("{$rmPath} -rf " . implode(' ', $arrDeletedFiles), $out);
542
        if (file_exists($filePath)) {
543
            $res->success  = false;
544
            $res->messages = $out;
545
        } else {
546
            $res->success = true;
547
        }
548
549
        return $res;
550
    }
551
552
553
    /**
554
     * Unpack ModuleFile and get metadata information
555
     *
556
     * @param $filePath
557
     *
558
     * @return \MikoPBX\PBXCoreREST\Lib\PBXApiResult
559
     */
560
    public static function getMetadataFromModuleFile(string $filePath): PBXApiResult
561
    {
562
        $res            = new PBXApiResult();
563
        $res->processor = __METHOD__;
564
565
        if (file_exists($filePath)) {
566
            $sevenZaPath = Util::which('7za');
567
            $grepPath    = Util::which('grep');
568
            $echoPath    = Util::which('echo');
569
            $awkPath     = Util::which('awk');
570
            $cmd         = 'f="' . $filePath . '"; p=`' . $sevenZaPath . ' l $f | ' . $grepPath . ' module.json`;if [ "$?" == "0" ]; then ' . $sevenZaPath . ' -so e -y -r $f `' . $echoPath . ' $p |  ' . $awkPath . ' -F" " \'{print $6}\'`; fi';
571
572
            Util::mwExec($cmd, $out);
573
            $settings = json_decode(implode("\n", $out), true);
574
575
            $moduleUniqueID = $settings['moduleUniqueID'] ?? null;
576
            if ( ! $moduleUniqueID) {
577
                $res->messages[] = 'The" moduleUniqueID " in the module file is not described.the json or file does not exist.';
578
579
                return $res;
580
            }
581
            $res->success = true;
582
            $res->data    = [
583
                'filePath' => $filePath,
584
                'uniqid'   => $moduleUniqueID,
585
            ];
586
        }
587
588
        return $res;
589
    }
590
591
592
}