Passed
Pull Request — master (#394)
by
unknown
22:34
created

ResumeUploader::__construct()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 8.2037

Importance

Changes 0
Metric Value
cc 8
eloc 26
nc 32
nop 11
dl 0
loc 48
ccs 29
cts 34
cp 0.8529
crap 8.2037
rs 8.4444
c 0
b 0
f 0

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
use Qiniu\Enum\SplitUploadVersion;
9
use Qiniu\Http\RequestOptions;
10
11
/**
12
 * 断点续上传类, 该类主要实现了断点续上传中的分块上传,
13
 * 以及相应地创建块和创建文件过程.
14
 *
15
 * @link http://developer.qiniu.com/docs/v6/api/reference/up/mkblk.html
16
 * @link http://developer.qiniu.com/docs/v6/api/reference/up/mkfile.html
17
 */
18
final class ResumeUploader
19
{
20
    private $upToken;
21
    private $key;
22
    private $inputStream;
23
    private $size;
24
    private $params;
25
    private $mime;
26
    private $contexts;
27
    private $finishedEtags;
28
    private $host;
29
    private $bucket;
30
    private $currentUrl;
31
    private $config;
32
    private $resumeRecordFile;
33
    private $version;
34
    private $partSize;
35
    /**
36
     * @var RequestOptions
37
     */
38
    private $reqOpt;
39
40 6
    /**
41
     * 上传二进制流到七牛
42
     *
43
     * @param string $upToken 上传凭证
44
     * @param string $key 上传文件名
45
     * @param resource $inputStream 上传二进制流
46
     * @param int $size 上传流的大小
47
     * @param array<string, string> $params 自定义变量
48
     * @param string $mime 上传数据的mimeType
49
     * @param Config $config
50 6
     * @param string $resumeRecordFile 断点续传的已上传的部分信息记录文件
51 6
     * @param string $version 分片上传版本 目前支持v1/v2版本 默认v1
52 6
     * @param int $partSize 分片上传v2字段 默认大小为4MB 分片大小范围为1 MB - 1 GB
53 6
     * @param RequestOptions $reqOpt 分片上传v2字段 默认大小为4MB 分片大小范围为1 MB - 1 GB
54 6
     * @throws \Exception
55 6
     *
56 6
     * @link http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
57 6
     */
58
    public function __construct(
59 6
        $upToken,
60 6
        $key,
61
        $inputStream,
62
        $size,
63
        $params,
64 6
        $mime,
65 6
        $config,
66
        $resumeRecordFile = null,
67
        $version = 'v1',
68 6
        $partSize = config::BLOCK_SIZE,
69 6
        $reqOpt = null
70
    ) {
71
72
        $this->upToken = $upToken;
73
        $this->key = $key;
74 6
        $this->inputStream = $inputStream;
75
        $this->size = $size;
76 6
        $this->params = $params;
77 6
        $this->mime = $mime;
78 6
        $this->contexts = array();
79 6
        $this->finishedEtags = array("etags" => array(), "uploadId" => "", "expiredAt" => 0, "uploaded" => 0);
80 6
        $this->config = $config;
81
        $this->resumeRecordFile = $resumeRecordFile ? $resumeRecordFile : null;
82
        $this->partSize = $partSize ? $partSize : config::BLOCK_SIZE;
83 6
84 6
        if ($reqOpt === null) {
85 6
            $reqOpt = new RequestOptions();
86 6
        }
87 6
        $this->reqOpt = $reqOpt;
88 6
89 6
        try {
90 3
            $this->version = SplitUploadVersion::from($version ? $version : 'v1');
91 3
        } catch (\Exception $e) {
92
            throw new \Exception("only support v1/v2 now!", 0, $e);
93
        }
94
95 3
        list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($upToken);
96 3
        $this->bucket = $bucket;
97 3
        if ($err != null) {
98 6
            return array(null, $err);
99 3
        }
100 3
101 3
        list($upHost, $err) = $config->getUpHostV2($accessKey, $bucket);
102
        if ($err != null) {
103 6
            throw new \Exception($err->message(), 1);
104
        }
105
        $this->host = $upHost;
106 6
    }
107 6
108 6
    /**
109 6
     * 上传操作
110
     * @param $fname string 文件名
111
     *
112
     * @throws \Exception
113
     */
114
    public function upload($fname)
115 6
    {
116
        $blkputRets = null;
117 6
        // get upload record from resumeRecordFile
118 6
        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...
119
            if (file_exists($this->resumeRecordFile)) {
120
                $stream = fopen($this->resumeRecordFile, 'r');
121 6
                if ($stream) {
0 ignored issues
show
introduced by
$stream is of type resource, thus it always evaluated to false.
Loading history...
122
                    $streamLen = filesize($this->resumeRecordFile);
123 6
                    if ($streamLen > 0) {
124 6
                        $contents = fread($stream, $streamLen);
125 6
                        fclose($stream);
126 6
                        if ($contents) {
127 6
                            $blkputRets = json_decode($contents, true);
128 6
                            if ($blkputRets === null) {
129 6
                                error_log("resumeFile contents decode error");
130
                            }
131
                        } else {
132
                            error_log("read resumeFile failed");
133
                        }
134
                    } else {
135 6
                        error_log("resumeFile is empty");
136
                    }
137
                } else {
138
                    error_log("resumeFile open failed");
139
                }
140
            } else {
141 6
                error_log("resumeFile not exists");
142
            }
143 6
        }
144 6
145 6
        if ($this->version == SplitUploadVersion::V1) {
0 ignored issues
show
introduced by
The condition $this->version == Qiniu\...\SplitUploadVersion::V1 is always false.
Loading history...
146 6
            return $this->uploadV1($fname, $blkputRets);
147
        } elseif ($this->version == SplitUploadVersion::V2) {
0 ignored issues
show
introduced by
The condition $this->version == Qiniu\...\SplitUploadVersion::V2 is always false.
Loading history...
148
            return $this->uploadV2($fname, $blkputRets);
149 6
        } else {
150
            throw new \Exception("only support v1/v2 now!");
151
        }
152 6
    }
153
154
    /**
155 6
     * @param string $fname 文件名
156
     * @param null|array $blkputRets
157 6
     *
158 6
     * @throws \Exception
159 6
     */
160
    private function uploadV1($fname, $blkputRets = null)
161
    {
162 6
        // 尝试恢复恢复已上传的数据
163
        $isResumeUpload = $blkputRets !== null;
164 6
        $this->contexts = array();
165 6
166
        if ($blkputRets) {
167 6
            if (isset($blkputRets['contexts']) && isset($blkputRets['uploaded']) &&
168
                is_array($blkputRets['contexts']) && is_int($blkputRets['uploaded'])
169
            ) {
170
                $this->contexts = array_map(function ($ctx) {
171
                    if (is_array($ctx)) {
172
                        return $ctx;
173
                    } else {
174
                        // 兼容旧版本(旧版本没有存储 expireAt)
175
                        return array(
176
                            "ctx" => $ctx,
177
                            "expiredAt" => 0,
178
                        );
179
                    }
180
                }, $blkputRets['contexts']);
181
            }
182
        }
183
184
        // 上传分片
185
        $uploaded = 0;
186
        while ($uploaded < $this->size) {
187
            $blockSize = $this->blockSize($uploaded);
188
            $blockIndex = $uploaded / $this->partSize;
189
            if (!is_int($blockIndex)) {
190
                throw new \Exception("v1 part size changed");
191
            }
192
            // 如果已上传该分片且没有过期
193
            if (isset($this->contexts[$blockIndex]) && $this->contexts[$blockIndex]["expiredAt"] >= time()) {
194
                $uploaded += $blockSize;
195
                fseek($this->inputStream, $blockSize, SEEK_CUR);
196
                continue;
197
            }
198
            $data = fread($this->inputStream, $blockSize);
199
            if ($data === false) {
200
                throw new \Exception("file read failed", 1);
201
            }
202
            $crc = \Qiniu\crc32_data($data);
203
            $response = $this->makeBlock($data, $blockSize);
204
205
206
            $ret = null;
207
            if ($response->ok() && $response->json() != null) {
208
                $ret = $response->json();
209
            }
210
            if ($response->statusCode < 0) {
211
                list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($this->upToken);
212
                if ($err != null) {
213
                    return array(null, $err);
214
                }
215
                list($upHostBackup, $err) = $this->config->getUpBackupHostV2($accessKey, $bucket);
216
                if ($err != null) {
217
                    return array(null, $err);
218
                }
219
                $this->host = $upHostBackup;
220
            }
221
222
            if ($response->needRetry() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
223
                $response = $this->makeBlock($data, $blockSize);
224
                $ret = $response->json();
225
            }
226
            if (!$response->ok() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
227
                return array(null, new Error($this->currentUrl, $response));
228
            }
229
230
            // 如果可以在已上传取到说明是过期分片直接修改已上传信息,否则是新的片添加到已上传分片尾部
231
            if (isset($this->contexts[$blockIndex])) {
232
                $this->contexts[$blockIndex] = array(
233
                    'ctx' => $ret['ctx'],
234
                    'expiredAt' => $ret['expired_at'],
235
                );
236
            } else {
237
                array_push($this->contexts, array(
238
                    'ctx' => $ret['ctx'],
239
                    'expiredAt' => $ret['expired_at'],
240
                ));
241
            }
242
            $uploaded += $blockSize;
243
244
            // 记录断点
245
            if ($this->resumeRecordFile !== null) {
246
                $recordData = array(
247
                    'contexts' => $this->contexts,
248
                    'uploaded' => $uploaded
249
                );
250
                $recordData = json_encode($recordData);
251
252
                if ($recordData) {
253
                    $isWritten = file_put_contents($this->resumeRecordFile, $recordData);
254
                    if ($isWritten === false) {
255
                        error_log("write resumeRecordFile failed");
256
                    }
257
                } else {
258
                    error_log('resumeRecordData encode failed');
259
                }
260
            }
261
        }
262
263
        // 完成上传
264
        list($ret, $err) = $this->makeFile($fname);
265
        if ($err !== null) {
266
            $response = $err->getResponse();
267
            if ($isResumeUpload && $response->statusCode === 701) {
268
                fseek($this->inputStream, 0);
269
                return $this->uploadV1($fname);
270
            }
271
        }
272
        return array($ret, $err);
273
    }
274
275
    /**
276
     * @param string $fname 文件名
277
     * @param null|array $blkputRets
278
     *
279
     * @throws \Exception
280
     */
281
    private function uploadV2($fname, $blkputRets = null)
282
    {
283
        $uploaded = 0;
284
        $partNumber = 1;
285
        $encodedObjectName = $this->key ? \Qiniu\base64_urlSafeEncode($this->key) : '~';
286
287
        $isResumeUpload = $blkputRets !== null;
288
        if ($blkputRets) {
289
            if (isset($blkputRets["etags"]) && isset($blkputRets["uploadId"]) &&
290
                isset($blkputRets["expiredAt"]) && $blkputRets["expiredAt"] > time() &&
291
                $blkputRets["uploaded"] > 0 && is_array($blkputRets["etags"]) &&
292
                is_string($blkputRets["uploadId"]) && is_int($blkputRets["expiredAt"])
293
            ) {
294
                $this->finishedEtags['etags'] = $blkputRets["etags"];
295
                $this->finishedEtags["uploadId"] = $blkputRets["uploadId"];
296
                $this->finishedEtags["expiredAt"] = $blkputRets["expiredAt"];
297
                $this->finishedEtags["uploaded"] = $blkputRets["uploaded"];
298
                $uploaded = $blkputRets["uploaded"];
299
                $partNumber = count($this->finishedEtags["etags"]) + 1;
300
            } else {
301
                $this->makeInitReq($encodedObjectName);
302
            }
303
        } else {
304
            $this->makeInitReq($encodedObjectName);
305
        }
306
307
        fseek($this->inputStream, $uploaded);
308
        while ($uploaded < $this->size) {
309
            $blockSize = $this->blockSize($uploaded);
310
            $data = fread($this->inputStream, $blockSize);
311
            if ($data === false) {
312
                throw new \Exception("file read failed", 1);
313
            }
314
            $md5 = md5($data);
315
            $response = $this->uploadPart(
316
                $data,
317
                $partNumber,
318
                $this->finishedEtags["uploadId"],
319
                $encodedObjectName,
320
                $md5
321
            );
322
323
            $ret = null;
324
            if ($response->ok() && $response->json() != null) {
325
                $ret = $response->json();
326
            }
327
            if ($response->statusCode < 0) {
328
                list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($this->upToken);
329
                if ($err != null) {
330
                    return array(null, $err);
331
                }
332
                list($upHostBackup, $err) = $this->config->getUpBackupHostV2($accessKey, $bucket);
333
                if ($err != null) {
334
                    return array(null, $err);
335
                }
336
                $this->host = $upHostBackup;
337
            }
338
339
            if ($response->needRetry() || !isset($ret['md5']) || $md5 != $ret['md5']) {
340
                $response = $this->uploadPart(
341
                    $data,
342
                    $partNumber,
343
                    $this->finishedEtags["uploadId"],
344
                    $encodedObjectName,
345
                    $md5
346
                );
347
                $ret = $response->json();
348
            }
349
350
            if (!$response->ok() || !isset($ret['md5']) || $md5 != $ret['md5']) {
351
                return array(null, new Error($this->currentUrl, $response));
352
            }
353
            $blockStatus = array('etag' => $ret['etag'], 'partNumber' => $partNumber);
354
            array_push($this->finishedEtags['etags'], $blockStatus);
355
            $partNumber += 1;
356
357
            $uploaded += $blockSize;
358
            $this->finishedEtags['uploaded'] = $uploaded;
359
360
            if ($this->resumeRecordFile !== null) {
361
                $recordData = json_encode($this->finishedEtags);
362
                if ($recordData) {
363
                    $isWritten = file_put_contents($this->resumeRecordFile, $recordData);
364
                    if ($isWritten === false) {
365
                        error_log("write resumeRecordFile failed");
366
                    }
367
                } else {
368
                    error_log('resumeRecordData encode failed');
369
                }
370
            }
371
        }
372
373
        list($ret, $err) = $this->completeParts($fname, $this->finishedEtags['uploadId'], $encodedObjectName);
374
        if ($err !== null) {
375
            $response = $err->getResponse();
376
            if ($isResumeUpload && $response->statusCode === 612) {
377
                return $this->uploadV2($fname);
378
            }
379
        }
380
        return array($ret, $err);
381
    }
382
383
    /**
384
     * 创建块
385
     */
386
    private function makeBlock($block, $blockSize)
387
    {
388
        $url = $this->host . '/mkblk/' . $blockSize;
389
        return $this->post($url, $block);
390
    }
391
392
    private function fileUrl($fname)
393
    {
394
        $url = $this->host . '/mkfile/' . $this->size;
395
        $url .= '/mimeType/' . \Qiniu\base64_urlSafeEncode($this->mime);
396
        if ($this->key != null) {
397
            $url .= '/key/' . \Qiniu\base64_urlSafeEncode($this->key);
398
        }
399
        $url .= '/fname/' . \Qiniu\base64_urlSafeEncode($fname);
400
        if (!empty($this->params)) {
401
            foreach ($this->params as $key => $value) {
402
                $val = \Qiniu\base64_urlSafeEncode($value);
403
                $url .= "/$key/$val";
404
            }
405
        }
406
        return $url;
407
    }
408
409
    /**
410
     * 创建文件
411
     *
412
     * @param string $fname 文件名
413
     * @return array{array | null, Error | null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{array | null, Error | null} at position 2 could not be parsed: Expected ':' at position 2, but found 'array'.
Loading history...
414
     */
415
    private function makeFile($fname)
416
    {
417
        $url = $this->fileUrl($fname);
418
        $body = implode(',', array_map(function ($ctx) {
419
            return $ctx['ctx'];
420
        }, $this->contexts));
421
        $response = $this->post($url, $body);
422
        if ($response->needRetry()) {
423
            $response = $this->post($url, $body);
424
        }
425
        if ($response->statusCode === 200 || $response->statusCode === 701) {
426
            if ($this->resumeRecordFile !== null) {
427
                @unlink($this->resumeRecordFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

427
                /** @scrutinizer ignore-unhandled */ @unlink($this->resumeRecordFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
428
            }
429
        }
430
        if (!$response->ok()) {
431
            return array(null, new Error($this->currentUrl, $response));
432
        }
433
        return array($response->json(), null);
434
    }
435
436
    private function post($url, $data)
437
    {
438
        $this->currentUrl = $url;
439
        $headers = array('Authorization' => 'UpToken ' . $this->upToken);
440
        return Client::post($url, $data, $headers, $this->reqOpt);
441
    }
442
443
    private function blockSize($uploaded)
444
    {
445
        if ($this->size < $uploaded + $this->partSize) {
446
            return $this->size - $uploaded;
447
        }
448
        return $this->partSize;
449
    }
450
451
    private function makeInitReq($encodedObjectName)
452
    {
453
        $res = $this->initReq($encodedObjectName);
454
        $this->finishedEtags["uploadId"] = $res['uploadId'];
455
        $this->finishedEtags["expiredAt"] = $res['expireAt'];
456
    }
457
458
    /**
459
     * 初始化上传任务
460
     */
461
    private function initReq($encodedObjectName)
462
    {
463
        $url = $this->host . '/buckets/' . $this->bucket . '/objects/' . $encodedObjectName . '/uploads';
464
        $headers = array(
465
            'Authorization' => 'UpToken ' . $this->upToken,
466
            'Content-Type' => 'application/json'
467
        );
468
        $response = $this->postWithHeaders($url, null, $headers);
469
        return $response->json();
470
    }
471
472
    /**
473
     * 分块上传v2
474
     */
475
    private function uploadPart($block, $partNumber, $uploadId, $encodedObjectName, $md5)
476
    {
477
        $headers = array(
478
            'Authorization' => 'UpToken ' . $this->upToken,
479
            'Content-Type' => 'application/octet-stream',
480
            'Content-MD5' => $md5
481
        );
482
        $url = $this->host . '/buckets/' . $this->bucket . '/objects/' . $encodedObjectName .
483
            '/uploads/' . $uploadId . '/' . $partNumber;
484
        $response = $this->put($url, $block, $headers);
485
        return $response;
486
    }
487
488
    /**
489
     * 完成分片上传V2
490
     *
491
     * @param string $fname 文件名
492
     * @param int $uploadId 由 {@see initReq} 获取
493
     * @param string $encodedObjectName 经过编码的存储路径
494
     * @return array{array | null, Error | null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{array | null, Error | null} at position 2 could not be parsed: Expected ':' at position 2, but found 'array'.
Loading history...
495
     */
496
    private function completeParts($fname, $uploadId, $encodedObjectName)
497
    {
498
        $headers = array(
499
            'Authorization' => 'UpToken ' . $this->upToken,
500
            'Content-Type' => 'application/json'
501
        );
502
        $etags = $this->finishedEtags['etags'];
503
        $sortedEtags = \Qiniu\arraySort($etags, 'partNumber');
504
        $metadata = array();
505
        $customVars = array();
506
        if ($this->params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->params of type array<string,string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
507
            foreach ($this->params as $k => $v) {
508
                if (strpos($k, 'x:') === 0) {
509
                    $customVars[$k] = $v;
510
                } elseif (strpos($k, 'x-qn-meta-') === 0) {
511
                    $metadata[$k] = $v;
512
                }
513
            }
514
        }
515
        if (empty($metadata)) {
516
            $metadata = null;
517
        }
518
        if (empty($customVars)) {
519
            $customVars = null;
520
        }
521
        $body = array(
522
            'fname' => $fname,
523
            'mimeType' => $this->mime,
524
            'metadata' => $metadata,
525
            'customVars' => $customVars,
526
            'parts' => $sortedEtags
527
        );
528
        $jsonBody = json_encode($body);
529
        $url = $this->host . '/buckets/' . $this->bucket . '/objects/' . $encodedObjectName . '/uploads/' . $uploadId;
530
        $response = $this->postWithHeaders($url, $jsonBody, $headers);
531
        if ($response->needRetry()) {
532
            $response = $this->postWithHeaders($url, $jsonBody, $headers);
533
        }
534
        if ($response->statusCode === 200 || $response->statusCode === 612) {
535
            if ($this->resumeRecordFile !== null) {
536
                @unlink($this->resumeRecordFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

536
                /** @scrutinizer ignore-unhandled */ @unlink($this->resumeRecordFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
537
            }
538
        }
539
        if (!$response->ok()) {
540
            return array(null, new Error($this->currentUrl, $response));
541
        }
542
        return array($response->json(), null);
543
    }
544
545
    private function put($url, $data, $headers)
546
    {
547
        $this->currentUrl = $url;
548
        return Client::put($url, $data, $headers, $this->reqOpt);
549
    }
550
551
    private function postWithHeaders($url, $data, $headers)
552
    {
553
        $this->currentUrl = $url;
554
        return Client::post($url, $data, $headers, $this->reqOpt);
555
    }
556
}
557