Passed
Push — develop ( c1253e...f72558 )
by Nikolay
12:51
created

FilesManagementProcessor::moduleStartDownload()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 41
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 41
rs 9.44
c 0
b 0
f 0
cc 4
nc 8
nop 3
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\PBXCoreREST\Lib;
21
22
use MikoPBX\Common\Models\CustomFiles;
23
use MikoPBX\Core\System\Processes;
24
use MikoPBX\Core\System\Util;
25
use MikoPBX\PBXCoreREST\Workers\WorkerDownloader;
26
use MikoPBX\PBXCoreREST\Workers\WorkerMergeUploadedFile;
27
use Phalcon\Di;
28
use Phalcon\Di\Injectable;
29
use Phalcon\Http\Message\StreamFactory;
30
use Phalcon\Http\Message\UploadedFile;
31
32
33
/**
34
 * Class FilesManagementProcessor
35
 *
36
 * @package MikoPBX\PBXCoreREST\Lib
37
 *
38
 */
39
class FilesManagementProcessor extends Injectable
40
{
41
42
    public const PROCESSOR_NAME='files';
43
44
    /**
45
     * Processes file upload requests
46
     *
47
     * @param array $request
48
     *
49
     * @return PBXApiResult An object containing the result of the API call.
50
     */
51
    public static function callBack(array $request): PBXApiResult
52
    {
53
        $res = new PBXApiResult();
54
        $res->processor = __METHOD__;
55
56
        $action   = $request['action'];
57
        $postData = $request['data'];
58
        switch ($action) {
59
            case 'uploadFile':
60
                $res = self::uploadFile($postData);
61
                break;
62
            case 'statusUploadFile':
63
                $res = self::statusUploadFile($request['data']);
64
                break;
65
            case 'removeAudioFile':
66
                $res = self::removeAudioFile($postData['filename']);
67
                break;
68
            case 'getFileContent':
69
                $res = self::getFileContent($postData['filename'], $postData['needOriginal']);
70
                break;
71
            case 'downloadNewFirmware':
72
                $res = self::downloadNewFirmware($request['data']);
73
                break;
74
            case 'firmwareDownloadStatus':
75
                $res = self::firmwareDownloadStatus($postData['filename']);
76
                break;
77
            default:
78
                $res->messages[] = "Unknown action - {$action} in uploadCallBack";
79
        }
80
81
        $res->function = $action;
82
83
        return $res;
84
    }
85
86
    /**
87
     * Process upload files by chunks.
88
     *
89
     * @param array $parameters The upload parameters.
90
     *
91
     * @return PBXApiResult An object containing the result of the API call.
92
     */
93
    public static function uploadFile(array $parameters): PBXApiResult
94
    {
95
        $res            = new PBXApiResult();
96
        $res->processor = __METHOD__;
97
        $di             = Di::getDefault();
98
        if ($di === null) {
99
            $res->success    = false;
100
            $res->messages[] = 'Dependency injector does not initialized';
101
102
            return $res;
103
        }
104
        $parameters['uploadDir'] = $di->getShared('config')->path('www.uploadDir');
105
        $parameters['tempDir']   = "{$parameters['uploadDir']}/{$parameters['resumableIdentifier']}";
106
        if ( ! Util::mwMkdir($parameters['tempDir'])) {
107
            $res->messages[] = 'Temp dir does not exist ' . $parameters['tempDir'];
108
109
            return $res;
110
        }
111
112
        $fileName = (string)pathinfo($parameters['resumableFilename'], PATHINFO_FILENAME);
113
        $fileName = preg_replace('/[\W]/', '', $fileName);
114
        if (strlen($fileName) < 10) {
115
            $fileName = '' . md5(time()) . '-' . $fileName;
116
        }
117
        $extension                          = (string)pathinfo($parameters['resumableFilename'], PATHINFO_EXTENSION);
118
        $fileName                           .= '.' . $extension;
119
        $parameters['resumableFilename']    = $fileName;
120
        $parameters['fullUploadedFileName'] = "{$parameters['tempDir']}/{$fileName}";
121
122
        // Delete old progress and result file
123
        $oldMergeProgressFile = "{$parameters['tempDir']}/merging_progress";
124
        if (file_exists($oldMergeProgressFile)) {
125
            unlink($oldMergeProgressFile);
126
        }
127
        if (file_exists($parameters['fullUploadedFileName'])) {
128
            unlink($parameters['fullUploadedFileName']);
129
        }
130
131
        foreach ($parameters['files'] as $file_data) {
132
            if ( ! self::moveUploadedPartToSeparateDir($parameters, $file_data)) {
133
                $res->messages[] = 'Does not found any uploaded chunks on with path ' . $file_data['file_path'];
134
                break;
135
            }
136
            $res->success           = true;
137
            $res->data['upload_id'] = $parameters['resumableIdentifier'];
138
            $res->data['filename']  = $parameters['fullUploadedFileName'];
139
140
            if (self::tryToMergeChunksIfAllPartsUploaded($parameters)) {
141
                $res->data['d_status'] = 'MERGING';
142
            } else {
143
                $res->data['d_status'] = 'WAITING_FOR_NEXT_PART';
144
            }
145
        }
146
147
        return $res;
148
    }
149
150
    /**
151
     * Moves uploaded file part to separate directory with "upload_id" name on the system uploadDir folder.
152
     *
153
     * @param array $parameters data from of resumable request
154
     * @param array $file_data  data from uploaded file part
155
     *
156
     * @return bool
157
     */
158
    private static function moveUploadedPartToSeparateDir(array $parameters, array $file_data): bool
159
    {
160
        if ( ! file_exists($file_data['file_path'])) {
161
            return false;
162
        }
163
        $factory          = new StreamFactory();
164
        $stream           = $factory->createStreamFromFile($file_data['file_path'], 'r');
165
        $file             = new UploadedFile(
166
            $stream,
167
            $file_data['file_size'],
168
            $file_data['file_error'],
169
            $file_data['file_name'],
170
            $file_data['file_type']
171
        );
172
        $chunks_dest_file = "{$parameters['tempDir']}/{$parameters['resumableFilename']}.part{$parameters['resumableChunkNumber']}";
173
        if (file_exists($chunks_dest_file)) {
174
            $rm = Util::which('rm');
175
            Processes::mwExec("{$rm} -f {$chunks_dest_file}");
176
        }
177
        $file->moveTo($chunks_dest_file);
178
179
        return true;
180
    }
181
182
    /**
183
     * If the size of all the chunks on the server is equal to the size of the file uploaded starts a merge process.
184
     *
185
     * @param array $parameters
186
     *
187
     * @return bool
188
     */
189
    private static function tryToMergeChunksIfAllPartsUploaded(array $parameters): bool
190
    {
191
        $totalFilesOnServerSize = 0;
192
        foreach (scandir($parameters['tempDir']) as $file) {
193
            $totalFilesOnServerSize += filesize($parameters['tempDir'] . '/' . $file);
194
        }
195
196
        if ($totalFilesOnServerSize >= $parameters['resumableTotalSize']) {
197
            // Parts upload complete
198
            $merge_settings = [
199
                'fullUploadedFileName' => $parameters['fullUploadedFileName'],
200
                'tempDir'              => $parameters['tempDir'],
201
                'resumableFilename'    => $parameters['resumableFilename'],
202
                'resumableTotalSize'   => $parameters['resumableTotalSize'],
203
                'resumableTotalChunks' => $parameters['resumableTotalChunks'],
204
            ];
205
            $settings_file  = "{$parameters['tempDir']}/merge_settings";
206
            file_put_contents(
207
                $settings_file,
208
                json_encode($merge_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
209
            );
210
211
            // We will start the background process to merge parts into one file
212
            $phpPath               = Util::which('php');
213
            $workerFilesMergerPath = Util::getFilePathByClassName(WorkerMergeUploadedFile::class);
214
            Processes::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} start '{$settings_file}'");
215
216
            return true;
217
        }
218
219
        return false;
220
    }
221
222
    /**
223
     * Returns Status of uploading and merging process
224
     *
225
     * @param array $postData
226
     *
227
     * @return PBXApiResult An object containing the result of the API call.
228
     */
229
    public static function statusUploadFile(array $postData): PBXApiResult
230
    {
231
        $res            = new PBXApiResult();
232
        $res->processor = __METHOD__;
233
        $di             = Di::getDefault();
234
        if ($di === null) {
235
            $res->messages[] = 'Dependency injector does not initialized';
236
237
            return $res;
238
        }
239
        $uploadDir = $di->getShared('config')->path('www.uploadDir');
240
241
        $upload_id     = $postData['id'] ?? null;
242
        $progress_dir  = $uploadDir . '/' . $upload_id;
243
        $progress_file = $progress_dir . '/merging_progress';
244
        if (empty($upload_id)) {
245
            $res->success                   = false;
246
            $res->data['d_status_progress'] = '0';
247
            $res->data['d_status']          = 'ID_NOT_SET';
248
            $res->messages[]                = 'Upload ID does not set';
249
        } elseif ( ! file_exists($progress_file) && file_exists($progress_dir)) {
250
            $res->success                   = true;
251
            $res->data['d_status_progress'] = '0';
252
            $res->data['d_status']          = 'INPROGRESS';
253
        } elseif ( ! file_exists($progress_dir)) {
254
            $res->success                   = false;
255
            $res->data['d_status_progress'] = '0';
256
            $res->data['d_status']          = 'NOT_FOUND';
257
            $res->messages[]                = 'Does not found anything with path: ' . $progress_dir;
258
        } elseif ('100' === file_get_contents($progress_file)) {
259
            $res->success                   = true;
260
            $res->data['d_status_progress'] = '100';
261
            $res->data['d_status']          = 'UPLOAD_COMPLETE';
262
        } else {
263
            $res->success                   = true;
264
            $res->data['d_status_progress'] = file_get_contents($progress_file);
265
        }
266
267
268
        return $res;
269
    }
270
271
    /**
272
     * Delete file from disk by filepath
273
     *
274
     * @param string $filePath
275
     *
276
     * @return PBXApiResult An object containing the result of the API call.
277
     */
278
    public static function removeAudioFile(string $filePath): PBXApiResult
279
    {
280
        $res            = new PBXApiResult();
281
        $res->processor = __METHOD__;
282
        $extension      = Util::getExtensionOfFile($filePath);
283
        if ( ! in_array($extension, ['mp3', 'wav', 'alaw'])) {
284
            $res->success    = false;
285
            $res->messages[] = "It is forbidden to remove the file type $extension.";
286
287
            return $res;
288
        }
289
290
        if ( ! file_exists($filePath)) {
291
            $res->success         = true;
292
            $res->data['message'] = "File '{$filePath}' already deleted";
293
294
            return $res;
295
        }
296
297
        $out = [];
298
299
        $arrDeletedFiles = [
300
            escapeshellarg(Util::trimExtensionForFile($filePath) . ".wav"),
301
            escapeshellarg(Util::trimExtensionForFile($filePath) . ".mp3"),
302
            escapeshellarg(Util::trimExtensionForFile($filePath) . ".alaw"),
303
        ];
304
305
        $rmPath = Util::which('rm');
306
        Processes::mwExec("{$rmPath} -rf " . implode(' ', $arrDeletedFiles), $out);
307
        if (file_exists($filePath)) {
308
            $res->success  = false;
309
            $res->messages = $out;
310
        } else {
311
            $res->success = true;
312
        }
313
314
        return $res;
315
    }
316
317
    /**
318
     * Returns file content
319
     *
320
     * @param string $filename
321
     * @param bool   $needOriginal
322
     *
323
     * @return PBXApiResult An object containing the result of the API call.
324
     */
325
    public static function getFileContent(string $filename, bool $needOriginal = true): PBXApiResult
326
    {
327
        $res            = new PBXApiResult();
328
        $res->processor = __METHOD__;
329
        $customFile     = CustomFiles::findFirst("filepath = '{$filename}'");
330
        if ($customFile !== null) {
331
            $filename_orgn = "{$filename}.orgn";
332
            if ($needOriginal && file_exists($filename_orgn)) {
333
                $filename = $filename_orgn;
334
            }
335
            $res->success = true;
336
            $cat          = Util::which('cat');
337
            $di           = Di::getDefault();
338
            $dirsConfig   = $di->getShared('config');
339
            $filenameTmp  = $dirsConfig->path('www.downloadCacheDir') . '/' . __FUNCTION__ . '_' . time() . '.conf';
340
            $cmd          = "{$cat} {$filename} > {$filenameTmp}";
341
            Processes::mwExec("{$cmd}; chown www:www {$filenameTmp}");
342
            $res->data['filename'] = $filenameTmp;
343
        } else {
344
            $res->success    = false;
345
            $res->messages[] = 'No access to the file ' . $filename;
346
        }
347
348
        return $res;
349
    }
350
351
    /**
352
     * Downloads the firmware file from the provided URL.
353
     *
354
     * @param array $data The data array containing the following parameters:
355
     *   - md5: The MD5 hash of the file.
356
     *   - size: The size of the file.
357
     *   - version: The version of the file.
358
     *   - url: The download URL of the file.
359
     *
360
     * @return PBXApiResult An object containing the result of the API call.
361
     */
362
    public static function downloadNewFirmware(array $data): PBXApiResult
363
    {
364
        $di = Di::getDefault();
365
        if ($di !== null) {
366
            $uploadDir = $di->getConfig()->path('www.uploadDir');
367
        } else {
368
            $uploadDir = '/tmp';
369
        }
370
        $firmwareDirTmp = "{$uploadDir}/{$data['version']}";
371
372
        if (file_exists($firmwareDirTmp)) {
373
            $rmPath = Util::which('rm');
374
            Processes::mwExec("{$rmPath} -rf {$firmwareDirTmp}/* ");
375
        } else {
376
            Util::mwMkdir($firmwareDirTmp);
377
        }
378
379
        $download_settings = [
380
            'res_file' => "{$firmwareDirTmp}/update.img",
381
            'url'      => $data['url'],
382
            'size'     => $data['size'],
383
            'md5'      => $data['md5'],
384
        ];
385
386
        $workerDownloaderPath = Util::getFilePathByClassName(WorkerDownloader::class);
387
        file_put_contents(
388
            "{$firmwareDirTmp}/download_settings.json",
389
            json_encode($download_settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
390
        );
391
        $phpPath = Util::which('php');
392
        Processes::mwExecBg("{$phpPath} -f {$workerDownloaderPath} start {$firmwareDirTmp}/download_settings.json");
393
394
        $res                   = new PBXApiResult();
395
        $res->processor        = __METHOD__;
396
        $res->success          = true;
397
        $res->data['filename'] = $download_settings['res_file'];
398
        $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
399
400
        return $res;
401
    }
402
403
    /**
404
     * Get the progress status of the firmware file download.
405
     *
406
     * @param string $imgFileName The filename of the firmware file.
407
     *
408
     * @return PBXApiResult An object containing the result of the API call.
409
     */
410
    public static function firmwareDownloadStatus(string $imgFileName): PBXApiResult
411
    {
412
        clearstatcache();
413
        $res            = new PBXApiResult();
414
        $res->processor = __METHOD__;
415
        $res->success   = true;
416
417
        $firmwareDirTmp = dirname($imgFileName);
418
        $progress_file  = $firmwareDirTmp . '/progress';
419
420
        // Wait until download process started
421
        $d_pid = Processes::getPidOfProcess("{$firmwareDirTmp}/download_settings.json");
422
        if (empty($d_pid)) {
423
            usleep(500000);
424
        }
425
        $error = '';
426
        if (file_exists("{$firmwareDirTmp}/error")) {
427
            $error = trim(file_get_contents("{$firmwareDirTmp}/error"));
428
        }
429
430
        if ( ! file_exists($progress_file)) {
431
            $res->data['d_status_progress'] = '0';
432
            $res->messages[]                = 'NOT_FOUND';
433
            $res->success                   = false;
434
        } elseif ('' !== $error) {
435
            $res->data['d_status']          = 'DOWNLOAD_ERROR';
436
            $res->data['d_status_progress'] = file_get_contents($progress_file);
437
            $res->messages[]                = file_get_contents("{$firmwareDirTmp}/error");
438
            $res->success                   = false;
439
        } elseif ('100' === file_get_contents($progress_file)) {
440
            $res->data['d_status_progress'] = '100';
441
            $res->data['d_status']          = 'DOWNLOAD_COMPLETE';
442
            $res->data['filePath']          = $imgFileName;
443
            $res->success                   = true;
444
        } else {
445
            $res->data['d_status_progress'] = file_get_contents($progress_file);
446
            $d_pid                          = Processes::getPidOfProcess("{$firmwareDirTmp}/download_settings.json");
447
            if (empty($d_pid)) {
448
                $res->data['d_status'] = 'DOWNLOAD_ERROR';
449
                if (file_exists("{$firmwareDirTmp}/error")) {
450
                    $res->messages[] = file_get_contents("{$firmwareDirTmp}/error");
451
                } else {
452
                    $res->messages[] = "Download process interrupted at {$res->data['d_status_progress']}%";
453
                }
454
                $res->success = false;
455
            } else {
456
                $res->data['d_status'] = 'DOWNLOAD_IN_PROGRESS';
457
                $res->success          = true;
458
            }
459
        }
460
461
        return $res;
462
    }
463
464
}