Passed
Pull Request — master (#362)
by
unknown
20:33
created

ResumeUploader   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 391
Duplicated Lines 0 %

Test Coverage

Coverage 84.15%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 233
dl 0
loc 391
ccs 69
cts 82
cp 0.8415
rs 2
c 1
b 0
f 0
wmc 83

13 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 37 6
A makeBlock() 0 4 1
A makeFile() 0 12 3
A makeInitReq() 0 5 1
A fileUrl() 0 15 4
A post() 0 5 1
F upload() 0 163 52
A blockSize() 0 6 2
A uploadPart() 0 11 1
A initReq() 0 9 1
B completeParts() 0 42 9
A put() 0 4 1
A postWithHeaders() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ResumeUploader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResumeUploader, and based on these observations, apply Extract Interface, too.

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