Passed
Pull Request — master (#355)
by
unknown
23:05
created

ResumeUploader::__construct()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 6.0493

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 20
c 1
b 0
f 0
nc 24
nop 10
dl 0
loc 37
ccs 24
cts 27
cp 0.8889
crap 6.0493
rs 8.9777

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Qiniu\Storage;
4
5
use Qiniu\Config;
6
use Qiniu\Http\Client;
7
use Qiniu\Http\Error;
8
9
/**
10
 * 断点续上传类, 该类主要实现了断点续上传中的分块上传,
11
 * 以及相应地创建块和创建文件过程.
12
 *
13
 * @link http://developer.qiniu.com/docs/v6/api/reference/up/mkblk.html
14
 * @link http://developer.qiniu.com/docs/v6/api/reference/up/mkfile.html
15
 */
16
final class ResumeUploader
17
{
18
    private $upToken;
19
    private $key;
20
    private $inputStream;
21
    private $size;
22
    private $params;
23
    private $mime;
24
    private $contexts;
25
    private $finishedEtags;
26
    private $host;
27
    private $bucket;
28
    private $currentUrl;
29
    private $config;
30
    private $resumeRecordFile;
31
    private $version;
32
    private $partSize;
33
34
    /**
35
     * 上传二进制流到七牛
36
     *
37
     * @param string $upToken 上传凭证
38
     * @param string $key 上传文件名
39
     * @param string $inputStream 上传二进制流
40 6
     * @param string $size 上传流的大小
41
     * @param string $params 自定义变量
42
     * @param string $mime 上传数据的mimeType
43
     * @param string $config
44
     * @param string $resumeRecordFile 断点续传的已上传的部分信息记录文件
45
     * @param string $version 分片上传版本 目前支持v1/v2版本 默认v1
46
     * @param string $partSize 分片上传v2必传字段 默认大小为4MB 分片大小范围为1 MB - 1 GB
47
     *
48
     * @link http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
49
     */
50 6
    public function __construct(
51 6
        $upToken,
52 6
        $key,
53 6
        $inputStream,
54 6
        $size,
55 6
        $params,
56 6
        $mime,
57 6
        $config,
58
        $resumeRecordFile = null,
59 6
        $version = 'v1',
60 6
        $partSize = config::BLOCK_SIZE
61
    ) {
62
63
        $this->upToken = $upToken;
64 6
        $this->key = $key;
65 6
        $this->inputStream = $inputStream;
66
        $this->size = $size;
67
        $this->params = $params;
68 6
        $this->mime = $mime;
69 6
        $this->contexts = array();
70
        $this->finishedEtags = array("etags"=>array(), "uploadId"=>"", "expiredAt"=>0, "uploaded"=>0);
71
        $this->config = $config;
72
        $this->resumeRecordFile = $resumeRecordFile ? $resumeRecordFile : null;
73
        $this->version = $version ? $version : 'v1';
74 6
        $this->partSize = $partSize ? $partSize : config::BLOCK_SIZE;
75
76 6
        list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($upToken);
77 6
        $this->bucket = $bucket;
78 6
        if ($err != null) {
79 6
            return array(null, $err);
80 6
        }
81
82
        $upHost = $config->getUpHost($accessKey, $bucket);
83 6
        if ($err != null) {
84 6
            throw new \Exception($err->message(), 1);
85 6
        }
86 6
        $this->host = $upHost;
87 6
    }
88 6
89 6
    /**
90 3
     * 上传操作
91 3
     */
92
    public function upload($fname)
93
    {
94
        $uploaded = 0;
95 3
        if ($this->version == 'v2') {
96 3
            $partNumber = 1;
97 3
            $encodedObjectName = $this->key? \Qiniu\base64_urlSafeEncode($this->key) : '~';
98 6
        };
99 3
        // get upload record from resumeRecordFile
100 3
        if ($this->resumeRecordFile != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $this->resumeRecordFile of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
101 3
            $blkputRets = null;
102
            if (file_exists($this->resumeRecordFile)) {
103 6
                $stream = fopen($this->resumeRecordFile, 'r');
104
                if ($stream) {
0 ignored issues
show
introduced by
$stream is of type resource, thus it always evaluated to false.
Loading history...
105
                    $streamLen = filesize($this->resumeRecordFile);
106 6
                    if ($streamLen > 0) {
107 6
                        $contents = fread($stream, $streamLen);
108 6
                        fclose($stream);
109 6
                        if ($contents) {
110
                            $blkputRets = json_decode($contents, true);
111
                            if ($blkputRets === null) {
112
                                error_log("resumeFile contents decode error");
113
                            }
114
                        } else {
115 6
                            error_log("read resumeFile failed");
116
                        }
117 6
                    } else {
118 6
                        error_log("resumeFile is empty");
119
                    }
120
                } else {
121 6
                    error_log("resumeFile open failed");
122
                }
123 6
            } else {
124 6
                error_log("resumeFile not exists");
125 6
            }
126 6
127 6
            if ($blkputRets) {
0 ignored issues
show
introduced by
$blkputRets is of type null, thus it always evaluated to false.
Loading history...
128 6
                if ($this->version == 'v1') {
129 6
                    if (isset($blkputRets['contexts']) && isset($blkputRets['uploaded']) &&
130
                        is_array($blkputRets['contexts']) && is_int($blkputRets['uploaded'])) {
131
                        $this->contexts = $blkputRets['contexts'];
132
                        $uploaded = $blkputRets['uploaded'];
133
                    }
134
                } elseif ($this->version == 'v2') {
135 6
                    if (isset($blkputRets["etags"]) && isset($blkputRets["uploadId"]) &&
136
                        isset($blkputRets["expiredAt"]) && $blkputRets["expiredAt"] > time()
137
                        && $blkputRets["uploaded"] > 0 && is_array($blkputRets["etags"]) &&
138
                        is_string($blkputRets["uploadId"]) && is_int($blkputRets["expiredAt"])) {
139
                        $this->finishedEtags['etags'] = $blkputRets["etags"];
140
                        $this->finishedEtags["uploadId"] = $blkputRets["uploadId"];
141 6
                        $this->finishedEtags["expiredAt"] = $blkputRets["expiredAt"];
142
                        $this->finishedEtags["uploaded"] = $blkputRets["uploaded"];
143 6
                        $uploaded = $blkputRets["uploaded"];
144 6
                        $partNumber = count($this->finishedEtags["etags"]) + 1;
145 6
                    } else {
146 6
                        $this->makeInitReq($encodedObjectName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $encodedObjectName does not seem to be defined for all execution paths leading up to this point.
Loading history...
147
                    }
148
                } else {
149 6
                    throw new \Exception("only support v1/v2 now!");
150
                }
151
            } else {
152 6
                if ($this->version == 'v2') {
153
                    $this->makeInitReq($encodedObjectName);
154
                }
155 6
            }
156
        } else {
157 6
            // init a Multipart Upload task if choose v2
158 6
            if ($this->version == 'v2') {
159 6
                $this->makeInitReq($encodedObjectName);
160
            }
161
        }
162 6
163
        while ($uploaded < $this->size) {
164 6
            $blockSize = $this->blockSize($uploaded);
165 6
            $data = fread($this->inputStream, $blockSize);
0 ignored issues
show
Bug introduced by
$this->inputStream of type string is incompatible with the type resource expected by parameter $stream of fread(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
            $data = fread(/** @scrutinizer ignore-type */ $this->inputStream, $blockSize);
Loading history...
Bug introduced by
It seems like $blockSize can also be of type string; however, parameter $length of fread() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
            $data = fread($this->inputStream, /** @scrutinizer ignore-type */ $blockSize);
Loading history...
166
            if ($data === false) {
167 6
                throw new \Exception("file read failed", 1);
168
            }
169
            if ($this->version == 'v1') {
170
                $crc = \Qiniu\crc32_data($data);
171
                $response = $this->makeBlock($data, $blockSize);
172
            } else {
173
                $md5 = md5($data);
174
                $response = $this->uploadPart($data, $partNumber, $this->finishedEtags["uploadId"], $encodedObjectName);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $partNumber does not seem to be defined for all execution paths leading up to this point.
Loading history...
175
            }
176
177
            $ret = null;
178
            if ($response->ok() && $response->json() != null) {
179
                $ret = $response->json();
180
            }
181
            if ($response->statusCode < 0) {
182
                list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($this->upToken);
183
                if ($err != null) {
184
                    return array(null, $err);
185
                }
186
                $upHostBackup = $this->config->getUpBackupHost($accessKey, $bucket);
187
                $this->host = $upHostBackup;
188
            }
189
190
            if ($this->version == 'v1') {
191
                if ($response->needRetry() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $crc does not seem to be defined for all execution paths leading up to this point.
Loading history...
192
                    $response = $this->makeBlock($data, $blockSize);
193
                    $ret = $response->json();
194
                }
195
196
                if (!$response->ok() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
197
                    return array(null, new Error($this->currentUrl, $response));
198
                }
199
                array_push($this->contexts, $ret['ctx']);
200
            } else {
201
                if ($response->needRetry() || !isset($ret['md5']) || $md5 != $ret['md5']) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $md5 does not seem to be defined for all execution paths leading up to this point.
Loading history...
202
                    $response = $this->uploadPart(
203
                        $data,
204
                        $partNumber,
205
                        $this->finishedEtags["uploadId"],
206
                        $encodedObjectName
207
                    );
208
                    $ret = $response->json();
209
                }
210
211
                if (!$response->ok() || !isset($ret['md5']) || $md5 != $ret['md5']) {
212
                    return array(null, new Error($this->currentUrl, $response));
213
                }
214
                $blockStatus = array('etag' => $ret['etag'], 'partNumber' => $partNumber);
215
                array_push($this->finishedEtags['etags'], $blockStatus);
216
                $partNumber += 1;
217
            }
218
219
            $uploaded += $blockSize;
220
            if ($this->version == 'v2') {
221
                $this->finishedEtags['uploaded'] = $uploaded;
222
            }
223
224
            if ($this->resumeRecordFile !== null) {
225
                if ($this->version == 'v1') {
226
                    $recordData = array(
227
                        'contexts' => $this->contexts,
228
                        'uploaded' => $uploaded
229
                    );
230
                    $recordData = json_encode($recordData);
231
                } else {
232
                    $recordData = json_encode($this->finishedEtags);
233
                }
234
                if ($recordData) {
235
                    $isWritten = file_put_contents($this->resumeRecordFile, $recordData);
236
                    if ($isWritten === false) {
237
                        error_log("write resumeRecordFile failed");
238
                    }
239
                } else {
240
                    error_log('resumeRecordData encode failed');
241
                }
242
            }
243
        }
244
        if ($this->version == 'v1') {
245
            return $this->makeFile($fname);
246
        } else {
247
            return $this->completeParts($fname, $this->finishedEtags['uploadId'], $encodedObjectName);
248
        }
249
    }
250
251
    /**
252
     * 创建块
253
     */
254
    private function makeBlock($block, $blockSize)
255
    {
256
        $url = $this->host . '/mkblk/' . $blockSize;
257
        return $this->post($url, $block);
258
    }
259
260
    private function fileUrl($fname)
261
    {
262
        $url = $this->host . '/mkfile/' . $this->size;
263
        $url .= '/mimeType/' . \Qiniu\base64_urlSafeEncode($this->mime);
264
        if ($this->key != null) {
265
            $url .= '/key/' . \Qiniu\base64_urlSafeEncode($this->key);
266
        }
267
        $url .= '/fname/' . \Qiniu\base64_urlSafeEncode($fname);
268
        if (!empty($this->params)) {
269
            foreach ($this->params as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $this->params of type string is not traversable.
Loading history...
270
                $val = \Qiniu\base64_urlSafeEncode($value);
271
                $url .= "/$key/$val";
272
            }
273
        }
274
        return $url;
275
    }
276
277
    /**
278
     * 创建文件
279
     */
280
    private function makeFile($fname)
281
    {
282
        $url = $this->fileUrl($fname);
283
        $body = implode(',', $this->contexts);
284
        $response = $this->post($url, $body);
285
        if ($response->needRetry()) {
286
            $response = $this->post($url, $body);
287
        }
288
        if (!$response->ok()) {
289
            return array(null, new Error($this->currentUrl, $response));
290
        }
291
        return array($response->json(), null);
292
    }
293
294
    private function post($url, $data)
295
    {
296
        $this->currentUrl = $url;
297
        $headers = array('Authorization' => 'UpToken ' . $this->upToken);
298
        return Client::post($url, $data, $headers);
299
    }
300
301
    private function blockSize($uploaded)
302
    {
303
        if ($this->size < $uploaded + $this->partSize) {
304
            return $this->size - $uploaded;
305
        }
306
        return $this->partSize;
307
    }
308
309
    private function makeInitReq($encodedObjectName)
310
    {
311
        $res = $this->initReq($encodedObjectName);
312
        $this->finishedEtags["uploadId"] = $res['uploadId'];
313
        $this->finishedEtags["expiredAt"] = $res['expireAt'];
314
    }
315
316
    /**
317
     * 初始化上传任务
318
     */
319
    private function initReq($encodedObjectName)
320
    {
321
        $url = $this->host.'/buckets/'.$this->bucket.'/objects/'.$encodedObjectName.'/uploads';
322
        $headers = array(
323
            'Authorization' => 'UpToken ' . $this->upToken,
324
            'Content-Type' => 'application/json'
325
        );
326
        $response = $this->postWithHeaders($url, null, $headers);
327
        return $response->json();
328
    }
329
330
    /**
331
     * 分块上传v2
332
     */
333
    private function uploadPart($block, $partNumber, $uploadId, $encodedObjectName)
334
    {
335
        $headers = array(
336
            'Authorization' => 'UpToken ' . $this->upToken,
337
            'Content-Type' => 'application/octet-stream',
338
            'Content-MD5' => $block
339
        );
340
        $url = $this->host.'/buckets/'.$this->bucket.'/objects/'.$encodedObjectName.
341
            '/uploads/'.$uploadId.'/'.$partNumber;
342
        $response = $this->put($url, $block, $headers);
343
        return $response;
344
    }
345
346
    private function completeParts($fname, $uploadId, $encodedObjectName)
347
    {
348
        $headers = array(
349
            'Authorization' => 'UpToken '.$this->upToken,
350
            'Content-Type' => 'application/json'
351
        );
352
        $etags = $this->finishedEtags['etags'];
353
        $sortedEtags = \Qiniu\arraySort($etags, 'partNumber');
354
        $body = array(
355
            'fname' => $fname,
356
            '$mimeType' => $this->mime,
357
            'customVars' => $this->params,
358
            'parts' => $sortedEtags
359
        );
360
        $jsonBody = json_encode($body);
361
        $url = $this->host.'/buckets/'.$this->bucket.'/objects/'.$encodedObjectName.'/uploads/'.$uploadId;
362
        $response = $this->postWithHeaders($url, $jsonBody, $headers);
363
        if ($response->needRetry()) {
364
            $response = $this->postWithHeaders($url, $jsonBody, $headers);
365
        }
366
        if (!$response->ok()) {
367
            return array(null, new Error($this->currentUrl, $response));
368
        }
369
        return array($response->json(), null);
370
    }
371
372
    private function put($url, $data, $headers)
373
    {
374
        $this->currentUrl = $url;
375
        return Client::put($url, $data, $headers);
376
    }
377
378
    private function postWithHeaders($url, $data, $headers)
379
    {
380
        $this->currentUrl = $url;
381
        return Client::post($url, $data, $headers);
382
    }
383
}