@@ -22,9 +22,14 @@ final class ResumeUploader
22
22
private $ params ;
23
23
private $ mime ;
24
24
private $ contexts ;
25
+ private $ finishedEtags ;
25
26
private $ host ;
27
+ private $ bucket ;
26
28
private $ currentUrl ;
27
29
private $ config ;
30
+ private $ resumeRecordFile ;
31
+ private $ version ;
32
+ private $ partSize ;
28
33
29
34
/**
30
35
* 上传二进制流到七牛
@@ -36,6 +41,9 @@ final class ResumeUploader
36
41
* @param string $params 自定义变量
37
42
* @param string $mime 上传数据的mimeType
38
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
39
47
*
40
48
* @link http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
41
49
*/
@@ -46,7 +54,10 @@ public function __construct(
46
54
$ size ,
47
55
$ params ,
48
56
$ mime ,
49
- $ config
57
+ $ config ,
58
+ $ resumeRecordFile = null ,
59
+ $ version = 'v1 ' ,
60
+ $ partSize = config::BLOCK_SIZE
50
61
) {
51
62
52
63
$ this ->upToken = $ upToken ;
@@ -56,9 +67,14 @@ public function __construct(
56
67
$ this ->params = $ params ;
57
68
$ this ->mime = $ mime ;
58
69
$ this ->contexts = array ();
70
+ $ this ->finishedEtags = array ("etags " =>array (), "uploadId " =>"" , "expiredAt " =>0 , "uploaded " =>0 );
59
71
$ this ->config = $ config ;
72
+ $ this ->resumeRecordFile = $ resumeRecordFile ? $ resumeRecordFile : null ;
73
+ $ this ->version = $ version ? $ version : 'v1 ' ;
74
+ $ this ->partSize = $ partSize ? $ partSize : config::BLOCK_SIZE ;
60
75
61
76
list ($ accessKey , $ bucket , $ err ) = \Qiniu \explodeUpToken ($ upToken );
77
+ $ this ->bucket = $ bucket ;
62
78
if ($ err != null ) {
63
79
return array (null , $ err );
64
80
}
@@ -76,14 +92,88 @@ public function __construct(
76
92
public function upload ($ fname )
77
93
{
78
94
$ uploaded = 0 ;
95
+ if ($ this ->version == 'v2 ' ) {
96
+ $ partNumber = 1 ;
97
+ $ encodedObjectName = $ this ->key ? \Qiniu \base64_urlSafeEncode ($ this ->key ) : '~ ' ;
98
+ };
99
+ // get upload record from resumeRecordFile
100
+ if ($ this ->resumeRecordFile != null ) {
101
+ $ blkputRets = null ;
102
+ if (file_exists ($ this ->resumeRecordFile )) {
103
+ $ stream = fopen ($ this ->resumeRecordFile , 'r ' );
104
+ if ($ stream ) {
105
+ $ streamLen = filesize ($ this ->resumeRecordFile );
106
+ if ($ streamLen > 0 ) {
107
+ $ contents = fread ($ stream , $ streamLen );
108
+ fclose ($ stream );
109
+ if ($ contents ) {
110
+ $ blkputRets = json_decode ($ contents , true );
111
+ if ($ blkputRets === null ) {
112
+ error_log ("resumeFile contents decode error " );
113
+ }
114
+ } else {
115
+ error_log ("read resumeFile failed " );
116
+ }
117
+ } else {
118
+ error_log ("resumeFile is empty " );
119
+ }
120
+ } else {
121
+ error_log ("resumeFile open failed " );
122
+ }
123
+ } else {
124
+ error_log ("resumeFile not exists " );
125
+ }
126
+
127
+ if ($ blkputRets ) {
128
+ if ($ this ->version == 'v1 ' ) {
129
+ 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
+ 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
+ $ this ->finishedEtags ["expiredAt " ] = $ blkputRets ["expiredAt " ];
142
+ $ this ->finishedEtags ["uploaded " ] = $ blkputRets ["uploaded " ];
143
+ $ uploaded = $ blkputRets ["uploaded " ];
144
+ $ partNumber = count ($ this ->finishedEtags ["etags " ]) + 1 ;
145
+ } else {
146
+ $ this ->makeInitReq ($ encodedObjectName );
147
+ }
148
+ } else {
149
+ throw new \Exception ("only support v1/v2 now! " );
150
+ }
151
+ } else {
152
+ if ($ this ->version == 'v2 ' ) {
153
+ $ this ->makeInitReq ($ encodedObjectName );
154
+ }
155
+ }
156
+ } else {
157
+ // init a Multipart Upload task if choose v2
158
+ if ($ this ->version == 'v2 ' ) {
159
+ $ this ->makeInitReq ($ encodedObjectName );
160
+ }
161
+ }
162
+
79
163
while ($ uploaded < $ this ->size ) {
80
164
$ blockSize = $ this ->blockSize ($ uploaded );
81
165
$ data = fread ($ this ->inputStream , $ blockSize );
82
166
if ($ data === false ) {
83
167
throw new \Exception ("file read failed " , 1 );
84
168
}
85
- $ crc = \Qiniu \crc32_data ($ data );
86
- $ response = $ this ->makeBlock ($ data , $ blockSize );
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 );
175
+ }
176
+
87
177
$ ret = null ;
88
178
if ($ response ->ok () && $ response ->json () != null ) {
89
179
$ ret = $ response ->json ();
@@ -93,22 +183,69 @@ public function upload($fname)
93
183
if ($ err != null ) {
94
184
return array (null , $ err );
95
185
}
96
-
97
186
$ upHostBackup = $ this ->config ->getUpBackupHost ($ accessKey , $ bucket );
98
187
$ this ->host = $ upHostBackup ;
99
188
}
100
- if ($ response ->needRetry () || !isset ($ ret ['crc32 ' ]) || $ crc != $ ret ['crc32 ' ]) {
101
- $ response = $ this ->makeBlock ($ data , $ blockSize );
102
- $ ret = $ response ->json ();
103
- }
104
189
105
- if (!$ response ->ok () || !isset ($ ret ['crc32 ' ]) || $ crc != $ ret ['crc32 ' ]) {
106
- return array (null , new Error ($ this ->currentUrl , $ response ));
190
+ if ($ this ->version == 'v1 ' ) {
191
+ if ($ response ->needRetry () || !isset ($ ret ['crc32 ' ]) || $ crc != $ ret ['crc32 ' ]) {
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 ' ]) {
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 ;
107
217
}
108
- array_push ( $ this -> contexts , $ ret [ ' ctx ' ]);
218
+
109
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 );
110
248
}
111
- return $ this ->makeFile ($ fname );
112
249
}
113
250
114
251
/**
@@ -163,9 +300,84 @@ private function post($url, $data)
163
300
164
301
private function blockSize ($ uploaded )
165
302
{
166
- if ($ this ->size < $ uploaded + Config:: BLOCK_SIZE ) {
303
+ if ($ this ->size < $ uploaded + $ this -> partSize ) {
167
304
return $ this ->size - $ uploaded ;
168
305
}
169
- return Config::BLOCK_SIZE ;
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 );
170
382
}
171
383
}
0 commit comments