Passed
Push — develop ( a56058...ddfce4 )
by Nikolay
04:39
created

UploadFileAction   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 136
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 15
eloc 73
c 0
b 0
f 0
dl 0
loc 136
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
B main() 0 55 9
A tryToMergeChunksIfAllPartsUploaded() 0 31 3
A moveUploadedPartToSeparateDir() 0 22 3
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2024 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\Files;
21
22
use MikoPBX\Core\System\Processes;
23
use MikoPBX\Core\System\Util;
24
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
25
use MikoPBX\PBXCoreREST\Workers\WorkerMergeUploadedFile;
26
use Phalcon\Di;
27
use Phalcon\Http\Message\StreamFactory;
28
use Phalcon\Http\Message\UploadedFile;
29
30
/**
31
 *  Class UploadFile
32
 *  Process upload files by chunks.
33
 *
34
 * @package MikoPBX\PBXCoreREST\Lib\Files
35
 */
36
class UploadFileAction extends \Phalcon\Di\Injectable
37
{
38
    /**
39
     * Process upload files by chunks.
40
     *
41
     * @param array $parameters The upload parameters.
42
     *
43
     * @return PBXApiResult An object containing the result of the API call.
44
     */
45
    public static function main(array $parameters): PBXApiResult
46
    {
47
        $res = new PBXApiResult();
48
        $res->processor = __METHOD__;
49
        $di = Di::getDefault();
50
        if ($di === null) {
51
            $res->success = false;
52
            $res->messages[] = 'Dependency injector does not initialized';
53
54
            return $res;
55
        }
56
        $parameters['uploadDir'] = $di->getShared('config')->path('www.uploadDir');
57
        $parameters['tempDir'] = "{$parameters['uploadDir']}/{$parameters['resumableIdentifier']}";
58
        if (!Util::mwMkdir($parameters['tempDir'])) {
59
            $res->messages[] = 'Temp dir does not exist ' . $parameters['tempDir'];
60
61
            return $res;
62
        }
63
64
        $fileName = (string)pathinfo($parameters['resumableFilename'], PATHINFO_FILENAME);
65
        $fileName = preg_replace('/[\W]/', '', $fileName);
66
        if (strlen($fileName) < 10) {
67
            $fileName = '' . md5(microtime()) . '-' . $fileName;
68
        }
69
        $extension = (string)pathinfo($parameters['resumableFilename'], PATHINFO_EXTENSION);
70
        $fileName .= '.' . $extension;
71
        $parameters['resumableFilename'] = $fileName;
72
        $parameters['fullUploadedFileName'] = "{$parameters['tempDir']}/{$fileName}";
73
74
        // Delete old progress and result file
75
        $oldMergeProgressFile = "{$parameters['tempDir']}/merging_progress";
76
        if (file_exists($oldMergeProgressFile)) {
77
            unlink($oldMergeProgressFile);
78
        }
79
        if (file_exists($parameters['fullUploadedFileName'])) {
80
            unlink($parameters['fullUploadedFileName']);
81
        }
82
83
        foreach ($parameters['files'] as $file_data) {
84
            if (!self::moveUploadedPartToSeparateDir($parameters, $file_data)) {
85
                $res->messages[] = 'Does not found any uploaded chunks on with path ' . $file_data['file_path'];
86
                break;
87
            }
88
            $res->success = true;
89
            $res->data['upload_id'] = $parameters['resumableIdentifier'];
90
            $res->data['filename'] = $parameters['fullUploadedFileName'];
91
92
            if (self::tryToMergeChunksIfAllPartsUploaded($parameters)) {
93
                $res->data[FilesConstants::D_STATUS] = FilesConstants::UPLOAD_MERGING;
94
            } else {
95
                $res->data[FilesConstants::D_STATUS] = FilesConstants::UPLOAD_WAITING_FOR_NEXT_PART;
96
            }
97
        }
98
99
        return $res;
100
    }
101
102
    /**
103
     * Moves uploaded file part to separate directory with "upload_id" name on the system uploadDir folder.
104
     *
105
     * @param array $parameters data from of resumable request
106
     * @param array $file_data  data from uploaded file part
107
     *
108
     * @return bool
109
     */
110
    private static function moveUploadedPartToSeparateDir(array $parameters, array $file_data): bool
111
    {
112
        if ( ! file_exists($file_data['file_path'])) {
113
            return false;
114
        }
115
        $factory          = new StreamFactory();
116
        $stream           = $factory->createStreamFromFile($file_data['file_path'], 'r');
117
        $file             = new UploadedFile(
118
            $stream,
119
            $file_data['file_size'],
120
            $file_data['file_error'],
121
            $file_data['file_name'],
122
            $file_data['file_type']
123
        );
124
        $chunks_dest_file = "{$parameters['tempDir']}/{$parameters['resumableFilename']}.part{$parameters['resumableChunkNumber']}";
125
        if (file_exists($chunks_dest_file)) {
126
            $rm = Util::which('rm');
127
            Processes::mwExec("{$rm} -f {$chunks_dest_file}");
128
        }
129
        $file->moveTo($chunks_dest_file);
130
131
        return true;
132
    }
133
134
    /**
135
     * If the size of all the chunks on the server is equal to the size of the file uploaded starts a merge process.
136
     *
137
     * @param array $parameters
138
     *
139
     * @return bool
140
     */
141
    private static function tryToMergeChunksIfAllPartsUploaded(array $parameters): bool
142
    {
143
        $totalFilesOnServerSize = 0;
144
        foreach (scandir($parameters['tempDir']) as $file) {
145
            $totalFilesOnServerSize += filesize($parameters['tempDir'] . '/' . $file);
146
        }
147
148
        if ($totalFilesOnServerSize >= $parameters['resumableTotalSize']) {
149
            // Parts upload complete
150
            $merge_settings = [
151
                'fullUploadedFileName' => $parameters['fullUploadedFileName'],
152
                'tempDir'              => $parameters['tempDir'],
153
                'resumableFilename'    => $parameters['resumableFilename'],
154
                'resumableTotalSize'   => $parameters['resumableTotalSize'],
155
                'resumableTotalChunks' => $parameters['resumableTotalChunks'],
156
            ];
157
            $settings_file  = "{$parameters['tempDir']}/merge_settings";
158
            file_put_contents(
159
                $settings_file,
160
                json_encode($merge_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
161
            );
162
163
            // We will start the background process to merge parts into one file
164
            $phpPath               = Util::which('php');
165
            $workerFilesMergerPath = Util::getFilePathByClassName(WorkerMergeUploadedFile::class);
166
            Processes::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} start '{$settings_file}'");
167
168
            return true;
169
        }
170
171
        return false;
172
    }
173
}