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

ResumeUploader::postWithHeaders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

152
            $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

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