├── .gitignore
├── .jshintrc
├── README.md
├── bower.json
├── demo.html
└── lib
└── upyun-mu.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | # Logs
3 | logs
4 | *.log
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # Compiled binary addons (http://nodejs.org/api/addons.html)
21 | build/Release
22 |
23 | # Dependency directory
24 | # Commenting this out is preferred by some people, see
25 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
26 | node_modules
27 |
28 | # Users Environment Variables
29 | .lock-wscript
30 | # workspace files are user-specific
31 | *.sublime-workspace
32 |
33 | # project files should be checked into the repository, unless a significant
34 | # proportion of contributors will probably not be using SublimeText
35 | # *.sublime-project
36 |
37 | #sftp configuration file
38 | sftp-config.json
39 |
40 | bower_components/*
41 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "browser": true,
3 | "esnext": true,
4 | "bitwise": true,
5 | "camelcase": false,
6 | "curly": true,
7 | "immed": true,
8 | "newcap": true,
9 | "noarg": true,
10 | "undef": true,
11 | "unused": "vars",
12 | "strict": true,
13 | "mocha": true
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # upyun-multipart-upload
2 | 使用 HTML 5 相关 API 开发的分块上传 DEMO
3 |
4 |
5 | > 在您决定将本项目用于生产环境之前,请确保知晓如何保护表单密钥的安全性,以及本项目在部分低配设备上可能会有的兼容性和性能问题!
6 |
7 |
8 | ## 安装
9 | 你可以通过如下两种方式中任意一种引入本项目:
10 |
11 | ### 1.bower
12 | ```sh
13 | $ bower install --save upyun-multipart-upload
14 | ```
15 |
16 | ### 2.直接下载
17 | 1. 下载本项目最新的 [Release](https://github.com/upyun/js-multipart-upload/releases/latest)
18 | 2. 下载依赖 [async](https://github.com/caolan/async/releases/latest)
19 | 3. 下载依赖 [SparkMD5](https://github.com/satazor/SparkMD5/releases/latest)
20 | 4. 通过 `` 标签以此引入文件,注意将依赖放在前面
21 |
22 | ```html
23 |
24 |
25 |
26 | ```
27 |
28 | ## Usage
29 |
30 | eg:
31 |
32 | ```js
33 | document.getElementById('submit').onclick = function() {
34 | var ext = '.' + document.getElementById('file').files[0].name.split('.').pop();
35 |
36 | var config = {
37 | bucket: 'demonstration',
38 | expiration: parseInt((new Date().getTime() + 3600000) / 1000),
39 | signature: 'something'
40 | };
41 |
42 | var instance = new Sand(config);
43 | var options = {
44 | 'notify_url': 'http://upyun.com'
45 | };
46 |
47 | instance.setOptions(options);
48 | instance.upload('/upload/test' + parseInt((new Date().getTime() + 3600000) / 1000) + ext);
49 | };
50 | ```
51 |
52 |
53 | ## API
54 |
55 | ### 构建实例
56 | ```js
57 | new Sand(config);
58 | ```
59 |
60 | __参数说明__
61 |
62 | * `config` 必要参数
63 | * `bucket`: 空间名称
64 | * `expiration`: 上传请求过期时间(单位为:`秒`)
65 | * `signature`: 初始化上传所需的签名
66 | * `form_api_secret`: 表单 API (慎用)
67 |
68 | __注意__
69 |
70 | 其中 `signature` 和 `form_api_secret` 为互斥项,为了避免表单 API 泄露造成安全隐患,请尽可能根据[所需参数](https://github.com/upyun/js-multipart-upload/wiki/%E5%88%86%E5%9D%97%E4%B8%8A%E4%BC%A0%E8%AF%B4%E6%98%8E#%E5%85%83%E4%BF%A1%E6%81%AF)自行传入初始化上传所需的 `signature` 参数
71 |
72 | 计算签名算法,请参考[文档](https://github.com/upyun/js-multipart-upload/wiki/%E5%88%86%E5%9D%97%E4%B8%8A%E4%BC%A0%E8%AF%B4%E6%98%8E#signature-%E5%92%8C-policy-%E7%AE%97%E6%B3%95)
73 |
74 |
75 | ### 设置额外上传参数
76 |
77 | ```js
78 | instance.setOptions(options)
79 | ```
80 | __参数说明__
81 |
82 | * `options`: Object 类型,包含额外的上传参数(详情见 [表单 API Policy](http://docs.upyun.com/api/form_api/#api_1))
83 |
84 | ## 上传
85 | ```js
86 | instance.upload(path)
87 | ```
88 |
89 | __参数说明__
90 |
91 | * `path`: 文件在空间中的存放路径
92 |
93 |
94 | ## 事件
95 |
96 | ### `uploaded`
97 | 上传完成后,会触发自定义事件 `uploaded`, 在事件对象中,会包含一些基本的信息,以供使用
98 |
99 |
100 | ### `error`
101 | 上传出错,会触发自定义事件 `error`, 在事件对象中,会包含错误的详情
102 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "upyun-multipart-upload",
3 | "version": "0.3.0",
4 | "authors": [
5 | "Leigh Zhu "
6 | ],
7 | "description": "a js sdk for upyun multipart upload",
8 | "main": "lib/upyun-mu.js",
9 | "keywords": [
10 | "upyun",
11 | "upload",
12 | "cdn"
13 | ],
14 | "license": "MIT",
15 | "homepage": "https://github.com/upyun/js-multipart-upload",
16 | "ignore": [
17 | "**/.*",
18 | "node_modules",
19 | "bower_components",
20 | "test",
21 | "tests"
22 | ],
23 | "dependencies": {
24 | "spark-md5": "~0.0.5",
25 | "async": "~0.9.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demo
6 |
7 |
8 |
9 |
10 |
52 |
53 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
107 |
108 |
--------------------------------------------------------------------------------
/lib/upyun-mu.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 | /**
4 | *
5 | * Base64 encode / decode
6 | * http://www.webtoolkit.info/
7 | *
8 | */
9 | var Base64 = {
10 | // private property
11 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
12 | // public method for encoding
13 | encode : function (input) {
14 | var output = "";
15 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
16 | var i = 0;
17 | input = Base64._utf8_encode(input);
18 | while (i < input.length) {
19 | chr1 = input.charCodeAt(i++);
20 | chr2 = input.charCodeAt(i++);
21 | chr3 = input.charCodeAt(i++);
22 | enc1 = chr1 >> 2;
23 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
24 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
25 | enc4 = chr3 & 63;
26 | if (isNaN(chr2)) {
27 | enc3 = enc4 = 64;
28 | } else if (isNaN(chr3)) {
29 | enc4 = 64;
30 | }
31 | output = output +
32 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
33 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
34 | }
35 | return output;
36 | },
37 | // public method for decoding
38 | decode : function (input) {
39 | var output = "";
40 | var chr1, chr2, chr3;
41 | var enc1, enc2, enc3, enc4;
42 | var i = 0;
43 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
44 | while (i < input.length) {
45 | enc1 = this._keyStr.indexOf(input.charAt(i++));
46 | enc2 = this._keyStr.indexOf(input.charAt(i++));
47 | enc3 = this._keyStr.indexOf(input.charAt(i++));
48 | enc4 = this._keyStr.indexOf(input.charAt(i++));
49 | chr1 = (enc1 << 2) | (enc2 >> 4);
50 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
51 | chr3 = ((enc3 & 3) << 6) | enc4;
52 | output = output + String.fromCharCode(chr1);
53 | if (enc3 != 64) {
54 | output = output + String.fromCharCode(chr2);
55 | }
56 | if (enc4 != 64) {
57 | output = output + String.fromCharCode(chr3);
58 | }
59 | }
60 | output = Base64._utf8_decode(output);
61 | return output;
62 | },
63 | // private method for UTF-8 encoding
64 | _utf8_encode : function (string) {
65 | string = string.replace(/\r\n/g,"\n");
66 | var utftext = "";
67 | for (var n = 0; n < string.length; n++) {
68 | var c = string.charCodeAt(n);
69 | if (c < 128) {
70 | utftext += String.fromCharCode(c);
71 | }
72 | else if((c > 127) && (c < 2048)) {
73 | utftext += String.fromCharCode((c >> 6) | 192);
74 | utftext += String.fromCharCode((c & 63) | 128);
75 | }
76 | else {
77 | utftext += String.fromCharCode((c >> 12) | 224);
78 | utftext += String.fromCharCode(((c >> 6) & 63) | 128);
79 | utftext += String.fromCharCode((c & 63) | 128);
80 | }
81 | }
82 | return utftext;
83 | },
84 | // private method for UTF-8 decoding
85 | _utf8_decode : function (utftext) {
86 | var string = "";
87 | var i = 0;
88 | var c = c1 = c2 = 0;
89 | while ( i < utftext.length ) {
90 | c = utftext.charCodeAt(i);
91 | if (c < 128) {
92 | string += String.fromCharCode(c);
93 | i++;
94 | }
95 | else if((c > 191) && (c < 224)) {
96 | c2 = utftext.charCodeAt(i+1);
97 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
98 | i += 2;
99 | }
100 | else {
101 | c2 = utftext.charCodeAt(i+1);
102 | c3 = utftext.charCodeAt(i+2);
103 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
104 | i += 3;
105 | }
106 | }
107 | return string;
108 | }
109 | };
110 | var _config = {
111 | api: '//m0.api.upyun.com/',
112 | chunkSize: 1048576
113 | };
114 |
115 | function _extend(dst, src) {
116 | for (var i in src) {
117 | dst[i] = src[i];
118 | }
119 | }
120 |
121 | function sortPropertiesByKey(obj) {
122 | var keys = [];
123 | var sorted_obj = {};
124 | for (var key in obj) {
125 | if (obj.hasOwnProperty(key)) {
126 | keys.push(key);
127 | }
128 | }
129 | keys.sort();
130 | for (var i = 0; i < keys.length; i++) {
131 | var k = keys[i];
132 | sorted_obj[k] = obj[k];
133 | }
134 | return sorted_obj;
135 | }
136 |
137 | function calcSign(data, secret) {
138 | if (typeof data !== 'object') {
139 | return;
140 | }
141 | var sortedData = sortPropertiesByKey(data);
142 | var md5Str = '';
143 | for (var key in sortedData) {
144 | if (key !== 'signature') {
145 | md5Str = md5Str + key + sortedData[key];
146 | }
147 | }
148 | var sign = SparkMD5.hash(md5Str + secret);
149 | return sign;
150 | }
151 |
152 | function formatParams(data) {
153 | var arr = [];
154 | for (var name in data) {
155 | arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
156 | }
157 | return arr.join("&");
158 | }
159 |
160 | function checkBlocks(arr) {
161 | var indices = [];
162 | var idx = arr.indexOf(0);
163 | while (idx != -1) {
164 | indices.push(idx);
165 | idx = arr.indexOf(0, idx + 1);
166 | }
167 | return indices;
168 | }
169 |
170 |
171 | function upload(path, fileSelector) {
172 | var self = this;
173 | var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
174 | var chunkSize = _config.chunkSize;
175 | var chunks;
176 |
177 | async.waterfall([
178 |
179 | function(callback) {
180 | var chunkInfo = {
181 | chunksHash: {}
182 | };
183 | var files;
184 | if(fileSelector === void 0) {
185 | files = document.getElementById('file').files;
186 | } else {
187 | files = document.querySelector(fileSelector).files;
188 | }
189 | if (!files.length) {
190 | console.log('no file is selected');
191 | return;
192 | }
193 | var file = files[0];
194 | chunks = Math.ceil(file.size / chunkSize);
195 | if(!file.slice) {
196 | chunkSize = file.size;
197 | chunks = 1;
198 | }
199 | var currentChunk = 0;
200 | var spark = new SparkMD5.ArrayBuffer();
201 | var frOnload = function(e) {
202 | chunkInfo.chunksHash[currentChunk] = SparkMD5.ArrayBuffer.hash(e.target.result);
203 | spark.append(e.target.result);
204 | currentChunk++;
205 | if (currentChunk < chunks) {
206 | loadNext();
207 | } else {
208 | chunkInfo.entire = spark.end();
209 | chunkInfo.chunksNum = chunks;
210 | chunkInfo.file_size = file.size;
211 | callback(null, chunkInfo);
212 | return;
213 | }
214 | };
215 | var frOnerror = function() {
216 | console.warn("oops, something went wrong.");
217 | };
218 |
219 | function loadNext() {
220 | var fileReader = new FileReader();
221 | fileReader.onload = frOnload;
222 | fileReader.onerror = frOnerror;
223 | var start = currentChunk * chunkSize,
224 | end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
225 | var blobPacket = blobSlice.call(file, start, end);
226 | fileReader.readAsArrayBuffer(blobPacket);
227 | }
228 | loadNext();
229 | },
230 | function(chunkInfo, callback) {
231 | var options = {
232 | 'path': path,
233 | 'expiration': _config.expiration,
234 | 'file_blocks': chunkInfo.chunksNum,
235 | 'file_size': chunkInfo.file_size,
236 | 'file_hash': chunkInfo.entire
237 | };
238 | var signature;
239 |
240 | _extend(options, self.options);
241 |
242 | if (self._signature) {
243 | signature = self._signature;
244 | } else {
245 | signature = calcSign(options, _config.form_api_secret);
246 | }
247 | var policy = Base64.encode(JSON.stringify(options));
248 | var paramsData = {
249 | policy: policy,
250 | signature: signature
251 | };
252 | var urlencParams = formatParams(paramsData);
253 | var request = new XMLHttpRequest();
254 | request.open('POST', _config.api + _config.bucket + '/');
255 | request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
256 | request.onload = function(e) {
257 | if (request.status == 200) {
258 | if (JSON.parse(request.response).status.indexOf(0) < 0) {
259 | return callback(new Error('file already exists'));
260 | }
261 |
262 | callback(null, chunkInfo, request.response);
263 | } else {
264 | request.send(urlencParams);
265 | }
266 | };
267 | request.send(urlencParams);
268 | },
269 | function(chunkInfo, res, callback) {
270 | res = JSON.parse(res);
271 |
272 | var file;
273 | if(fileSelector === void 0) {
274 | file = document.getElementById('file').files[0];
275 | } else {
276 | file = document.querySelector(fileSelector).files[0];
277 | }
278 | var _status = res.status;
279 | var result;
280 | async.until(function() {
281 | return checkBlocks(_status).length <= 0;
282 | }, function(callback) {
283 | var idx = checkBlocks(_status)[0];
284 | var start = idx * chunkSize,
285 | end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
286 | var packet = blobSlice.call(file, start, end);
287 |
288 | var options = {
289 | 'save_token': res.save_token,
290 | 'expiration': _config.expiration,
291 | 'block_index': idx,
292 | 'block_hash': chunkInfo.chunksHash[idx]
293 | };
294 |
295 | var signature = calcSign(options, res.token_secret);
296 | var policy = Base64.encode(JSON.stringify(options));
297 |
298 | var formDataPart = new FormData();
299 | formDataPart.append('policy', policy);
300 | formDataPart.append('signature', signature);
301 | formDataPart.append('file', chunks === 1 ? file : packet);
302 |
303 | var request = new XMLHttpRequest();
304 | request.onreadystatechange = function(e) {
305 | if (e.currentTarget.readyState === 4 && e.currentTarget.status == 200) {
306 | _status = JSON.parse(e.currentTarget.response).status;
307 | result = request.response;
308 | setTimeout(function() {
309 | return callback(null);
310 | }, 0);
311 | }
312 | };
313 | request.open('POST', _config.api + _config.bucket + '/', false);
314 | request.send(formDataPart);
315 |
316 | }, function(err) {
317 | if (err) {
318 | callback(err);
319 | }
320 | callback(null, chunkInfo, result);
321 | });
322 | },
323 | function(chunkInfo, res, callback) {
324 | res = JSON.parse(res);
325 |
326 | var options = {
327 | 'save_token': res.save_token,
328 | 'expiration': _config.expiration
329 | };
330 |
331 | var signature = calcSign(options, res.token_secret);
332 | var policy = Base64.encode(JSON.stringify(options));
333 | var formParams = {
334 | policy: policy,
335 | signature: signature
336 | };
337 | var formParamsUrlenc = formatParams(formParams);
338 | var request = new XMLHttpRequest();
339 | request.open('POST', _config.api + _config.bucket + '/');
340 | request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
341 | request.onload = function(e) {
342 | if (request.status == 200) {
343 | callback(null, request.response);
344 | } else {
345 | callback(null, request.response);
346 | }
347 | };
348 | request.send(formParamsUrlenc);
349 | }
350 | ], function(err, res) {
351 | var event;
352 | if (err) {
353 | if (typeof CustomEvent === 'function') {
354 | event = new CustomEvent('error', {
355 | 'detail': err
356 | });
357 | document.dispatchEvent(event);
358 | return;
359 | } else {
360 | //IE compatibility
361 | event = document.createEvent("CustomEvent");
362 | event.initCustomEvent("error", false, false, err);
363 | document.dispatchEvent(event);
364 | return;
365 | }
366 | }
367 | if(typeof CustomEvent === 'function') {
368 | event = new CustomEvent('uploaded', {
369 | 'detail': JSON.parse(res)
370 | });
371 | document.dispatchEvent(event);
372 | } else {
373 | //IE compatibility
374 | event = document.createEvent("CustomEvent");
375 | event.initCustomEvent("uploaded", false, false, JSON.parse(res));
376 | document.dispatchEvent(event);
377 | }
378 | });
379 | }
380 |
381 | function Sand(config) {
382 | _extend(_config, config);
383 |
384 | if(config.signature) {
385 | this._signature = config.signature;
386 | }
387 |
388 | this.setOptions = function(options) {
389 | this.options = options;
390 | };
391 |
392 | this.upload = upload;
393 | }
394 |
395 | // bind the construct fn. to global
396 | this.Sand = Sand;
397 |
398 | }).call(this);
399 |
--------------------------------------------------------------------------------