PageRenderTime 29ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/resumable.js

http://github.com/23/resumable.js
JavaScript | 1170 lines | 970 code | 63 blank | 137 comment | 201 complexity | 50713196a264191510c4c3f3cbfb4a54 MD5 | raw file
  1. /*
  2. * MIT Licensed
  3. * https://www.twentythree.com
  4. * https://github.com/23/resumable.js
  5. * Steffen Fagerström Christensen, steffen@twentythree.com
  6. */
  7. (function(){
  8. "use strict";
  9. var Resumable = function(opts){
  10. if ( !(this instanceof Resumable) ) {
  11. return new Resumable(opts);
  12. }
  13. this.version = 1.0;
  14. // SUPPORTED BY BROWSER?
  15. // Check if these features are support by the browser:
  16. // - File object type
  17. // - Blob object type
  18. // - FileList object type
  19. // - slicing files
  20. this.support = (
  21. (typeof(File)!=='undefined')
  22. &&
  23. (typeof(Blob)!=='undefined')
  24. &&
  25. (typeof(FileList)!=='undefined')
  26. &&
  27. (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false)
  28. );
  29. if(!this.support) return(false);
  30. // PROPERTIES
  31. var $ = this;
  32. $.files = [];
  33. $.defaults = {
  34. chunkSize:1*1024*1024,
  35. forceChunkSize:false,
  36. simultaneousUploads:3,
  37. fileParameterName:'file',
  38. chunkNumberParameterName: 'resumableChunkNumber',
  39. chunkSizeParameterName: 'resumableChunkSize',
  40. currentChunkSizeParameterName: 'resumableCurrentChunkSize',
  41. totalSizeParameterName: 'resumableTotalSize',
  42. typeParameterName: 'resumableType',
  43. identifierParameterName: 'resumableIdentifier',
  44. fileNameParameterName: 'resumableFilename',
  45. relativePathParameterName: 'resumableRelativePath',
  46. totalChunksParameterName: 'resumableTotalChunks',
  47. dragOverClass: 'dragover',
  48. throttleProgressCallbacks: 0.5,
  49. query:{},
  50. headers:{},
  51. preprocess:null,
  52. preprocessFile:null,
  53. method:'multipart',
  54. uploadMethod: 'POST',
  55. testMethod: 'GET',
  56. prioritizeFirstAndLastChunk:false,
  57. target:'/',
  58. testTarget: null,
  59. parameterNamespace:'',
  60. testChunks:true,
  61. generateUniqueIdentifier:null,
  62. getTarget:null,
  63. maxChunkRetries:100,
  64. chunkRetryInterval:undefined,
  65. permanentErrors:[400, 401, 403, 404, 409, 415, 500, 501],
  66. maxFiles:undefined,
  67. withCredentials:false,
  68. xhrTimeout:0,
  69. clearInput:true,
  70. chunkFormat:'blob',
  71. setChunkTypeFromFile:false,
  72. maxFilesErrorCallback:function (files, errorCount) {
  73. var maxFiles = $.getOpt('maxFiles');
  74. alert('Please upload no more than ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.');
  75. },
  76. minFileSize:1,
  77. minFileSizeErrorCallback:function(file, errorCount) {
  78. alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.');
  79. },
  80. maxFileSize:undefined,
  81. maxFileSizeErrorCallback:function(file, errorCount) {
  82. alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.');
  83. },
  84. fileType: [],
  85. fileTypeErrorCallback: function(file, errorCount) {
  86. alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.');
  87. }
  88. };
  89. $.opts = opts||{};
  90. $.getOpt = function(o) {
  91. var $opt = this;
  92. // Get multiple option if passed an array
  93. if(o instanceof Array) {
  94. var options = {};
  95. $h.each(o, function(option){
  96. options[option] = $opt.getOpt(option);
  97. });
  98. return options;
  99. }
  100. // Otherwise, just return a simple option
  101. if ($opt instanceof ResumableChunk) {
  102. if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
  103. else { $opt = $opt.fileObj; }
  104. }
  105. if ($opt instanceof ResumableFile) {
  106. if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
  107. else { $opt = $opt.resumableObj; }
  108. }
  109. if ($opt instanceof Resumable) {
  110. if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
  111. else { return $opt.defaults[o]; }
  112. }
  113. };
  114. $.indexOf = function(array, obj) {
  115. if (array.indexOf) { return array.indexOf(obj); }
  116. for (var i = 0; i < array.length; i++) {
  117. if (array[i] === obj) { return i; }
  118. }
  119. return -1;
  120. }
  121. // EVENTS
  122. // catchAll(event, ...)
  123. // fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file),
  124. // fileError(file, message), complete(), progress(), error(message, file), pause()
  125. $.events = [];
  126. $.on = function(event,callback){
  127. $.events.push(event.toLowerCase(), callback);
  128. };
  129. $.fire = function(){
  130. // `arguments` is an object, not array, in FF, so:
  131. var args = [];
  132. for (var i=0; i<arguments.length; i++) args.push(arguments[i]);
  133. // Find event listeners, and support pseudo-event `catchAll`
  134. var event = args[0].toLowerCase();
  135. for (var i=0; i<=$.events.length; i+=2) {
  136. if($.events[i]==event) $.events[i+1].apply($,args.slice(1));
  137. if($.events[i]=='catchall') $.events[i+1].apply(null,args);
  138. }
  139. if(event=='fileerror') $.fire('error', args[2], args[1]);
  140. if(event=='fileprogress') $.fire('progress');
  141. };
  142. // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
  143. var $h = {
  144. stopEvent: function(e){
  145. e.stopPropagation();
  146. e.preventDefault();
  147. },
  148. each: function(o,callback){
  149. if(typeof(o.length)!=='undefined') {
  150. for (var i=0; i<o.length; i++) {
  151. // Array or FileList
  152. if(callback(o[i])===false) return;
  153. }
  154. } else {
  155. for (i in o) {
  156. // Object
  157. if(callback(i,o[i])===false) return;
  158. }
  159. }
  160. },
  161. generateUniqueIdentifier:function(file, event){
  162. var custom = $.getOpt('generateUniqueIdentifier');
  163. if(typeof custom === 'function') {
  164. return custom(file, event);
  165. }
  166. var relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name; // Some confusion in different versions of Firefox
  167. var size = file.size;
  168. return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
  169. },
  170. contains:function(array,test) {
  171. var result = false;
  172. $h.each(array, function(value) {
  173. if (value == test) {
  174. result = true;
  175. return false;
  176. }
  177. return true;
  178. });
  179. return result;
  180. },
  181. formatSize:function(size){
  182. if(size<1024) {
  183. return size + ' bytes';
  184. } else if(size<1024*1024) {
  185. return (size/1024.0).toFixed(0) + ' KB';
  186. } else if(size<1024*1024*1024) {
  187. return (size/1024.0/1024.0).toFixed(1) + ' MB';
  188. } else {
  189. return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB';
  190. }
  191. },
  192. getTarget:function(request, params){
  193. var target = $.getOpt('target');
  194. if (request === 'test' && $.getOpt('testTarget')) {
  195. target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget');
  196. }
  197. if (typeof target === 'function') {
  198. return target(params);
  199. }
  200. var separator = target.indexOf('?') < 0 ? '?' : '&';
  201. var joinedParams = params.join('&');
  202. if (joinedParams) target = target + separator + joinedParams;
  203. return target;
  204. }
  205. };
  206. var onDrop = function(e){
  207. e.currentTarget.classList.remove($.getOpt('dragOverClass'));
  208. $h.stopEvent(e);
  209. //handle dropped things as items if we can (this lets us deal with folders nicer in some cases)
  210. if (e.dataTransfer && e.dataTransfer.items) {
  211. loadFiles(e.dataTransfer.items, e);
  212. }
  213. //else handle them as files
  214. else if (e.dataTransfer && e.dataTransfer.files) {
  215. loadFiles(e.dataTransfer.files, e);
  216. }
  217. };
  218. var onDragLeave = function(e){
  219. e.currentTarget.classList.remove($.getOpt('dragOverClass'));
  220. };
  221. var onDragOverEnter = function(e) {
  222. e.preventDefault();
  223. var dt = e.dataTransfer;
  224. if ($.indexOf(dt.types, "Files") >= 0) { // only for file drop
  225. e.stopPropagation();
  226. dt.dropEffect = "copy";
  227. dt.effectAllowed = "copy";
  228. e.currentTarget.classList.add($.getOpt('dragOverClass'));
  229. } else { // not work on IE/Edge....
  230. dt.dropEffect = "none";
  231. dt.effectAllowed = "none";
  232. }
  233. };
  234. /**
  235. * processes a single upload item (file or directory)
  236. * @param {Object} item item to upload, may be file or directory entry
  237. * @param {string} path current file path
  238. * @param {File[]} items list of files to append new items to
  239. * @param {Function} cb callback invoked when item is processed
  240. */
  241. function processItem(item, path, items, cb) {
  242. var entry;
  243. if(item.isFile){
  244. // file provided
  245. return item.file(function(file){
  246. file.relativePath = path + file.name;
  247. items.push(file);
  248. cb();
  249. });
  250. }else if(item.isDirectory){
  251. // item is already a directory entry, just assign
  252. entry = item;
  253. }else if(item instanceof File) {
  254. items.push(item);
  255. }
  256. if('function' === typeof item.webkitGetAsEntry){
  257. // get entry from file object
  258. entry = item.webkitGetAsEntry();
  259. }
  260. if(entry && entry.isDirectory){
  261. // directory provided, process it
  262. return processDirectory(entry, path + entry.name + '/', items, cb);
  263. }
  264. if('function' === typeof item.getAsFile){
  265. // item represents a File object, convert it
  266. item = item.getAsFile();
  267. if(item instanceof File) {
  268. item.relativePath = path + item.name;
  269. items.push(item);
  270. }
  271. }
  272. cb(); // indicate processing is done
  273. }
  274. /**
  275. * cps-style list iteration.
  276. * invokes all functions in list and waits for their callback to be
  277. * triggered.
  278. * @param {Function[]} items list of functions expecting callback parameter
  279. * @param {Function} cb callback to trigger after the last callback has been invoked
  280. */
  281. function processCallbacks(items, cb){
  282. if(!items || items.length === 0){
  283. // empty or no list, invoke callback
  284. return cb();
  285. }
  286. // invoke current function, pass the next part as continuation
  287. items[0](function(){
  288. processCallbacks(items.slice(1), cb);
  289. });
  290. }
  291. /**
  292. * recursively traverse directory and collect files to upload
  293. * @param {Object} directory directory to process
  294. * @param {string} path current path
  295. * @param {File[]} items target list of items
  296. * @param {Function} cb callback invoked after traversing directory
  297. */
  298. function processDirectory (directory, path, items, cb) {
  299. var dirReader = directory.createReader();
  300. var allEntries = [];
  301. function readEntries () {
  302. dirReader.readEntries(function(entries){
  303. if (entries.length) {
  304. allEntries = allEntries.concat(entries);
  305. return readEntries();
  306. }
  307. // process all conversion callbacks, finally invoke own one
  308. processCallbacks(
  309. allEntries.map(function(entry){
  310. // bind all properties except for callback
  311. return processItem.bind(null, entry, path, items);
  312. }),
  313. cb
  314. );
  315. });
  316. }
  317. readEntries();
  318. }
  319. /**
  320. * process items to extract files to be uploaded
  321. * @param {File[]} items items to process
  322. * @param {Event} event event that led to upload
  323. */
  324. function loadFiles(items, event) {
  325. if(!items.length){
  326. return; // nothing to do
  327. }
  328. $.fire('beforeAdd');
  329. var files = [];
  330. processCallbacks(
  331. Array.prototype.map.call(items, function(item){
  332. // bind all properties except for callback
  333. var entry = item;
  334. if('function' === typeof item.webkitGetAsEntry){
  335. entry = item.webkitGetAsEntry();
  336. }
  337. return processItem.bind(null, entry, "", files);
  338. }),
  339. function(){
  340. if(files.length){
  341. // at least one file found
  342. appendFilesFromFileList(files, event);
  343. }
  344. }
  345. );
  346. };
  347. var appendFilesFromFileList = function(fileList, event){
  348. // check for uploading too many files
  349. var errorCount = 0;
  350. var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']);
  351. if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) {
  352. // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
  353. if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) {
  354. $.removeFile($.files[0]);
  355. } else {
  356. o.maxFilesErrorCallback(fileList, errorCount++);
  357. return false;
  358. }
  359. }
  360. var files = [], filesSkipped = [], remaining = fileList.length;
  361. var decreaseReamining = function(){
  362. if(!--remaining){
  363. // all files processed, trigger event
  364. if(!files.length && !filesSkipped.length){
  365. // no succeeded files, just skip
  366. return;
  367. }
  368. window.setTimeout(function(){
  369. $.fire('filesAdded', files, filesSkipped);
  370. },0);
  371. }
  372. };
  373. $h.each(fileList, function(file){
  374. var fileName = file.name;
  375. var fileType = file.type; // e.g video/mp4
  376. if(o.fileType.length > 0){
  377. var fileTypeFound = false;
  378. for(var index in o.fileType){
  379. // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all
  380. o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase();
  381. // Allowing for both [extension, .extension, mime/type, mime/*]
  382. var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index];
  383. if ((fileName.substr(-1 * extension.length).toLowerCase() === extension) ||
  384. //If MIME type, check for wildcard or if extension matches the files tiletype
  385. (extension.indexOf('/') !== -1 && (
  386. (extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) ||
  387. fileType === extension
  388. ))
  389. ){
  390. fileTypeFound = true;
  391. break;
  392. }
  393. }
  394. if (!fileTypeFound) {
  395. o.fileTypeErrorCallback(file, errorCount++);
  396. return true;
  397. }
  398. }
  399. if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) {
  400. o.minFileSizeErrorCallback(file, errorCount++);
  401. return true;
  402. }
  403. if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) {
  404. o.maxFileSizeErrorCallback(file, errorCount++);
  405. return true;
  406. }
  407. function addFile(uniqueIdentifier){
  408. if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){
  409. file.uniqueIdentifier = uniqueIdentifier;
  410. var f = new ResumableFile($, file, uniqueIdentifier);
  411. $.files.push(f);
  412. files.push(f);
  413. f.container = (typeof event != 'undefined' ? event.srcElement : null);
  414. window.setTimeout(function(){
  415. $.fire('fileAdded', f, event)
  416. },0);
  417. })()} else {
  418. filesSkipped.push(file);
  419. };
  420. decreaseReamining();
  421. }
  422. // directories have size == 0
  423. var uniqueIdentifier = $h.generateUniqueIdentifier(file, event);
  424. if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){
  425. // Promise or Promise-like object provided as unique identifier
  426. uniqueIdentifier
  427. .then(
  428. function(uniqueIdentifier){
  429. // unique identifier generation succeeded
  430. addFile(uniqueIdentifier);
  431. },
  432. function(){
  433. // unique identifier generation failed
  434. // skip further processing, only decrease file count
  435. decreaseReamining();
  436. }
  437. );
  438. }else{
  439. // non-Promise provided as unique identifier, process synchronously
  440. addFile(uniqueIdentifier);
  441. }
  442. });
  443. };
  444. // INTERNAL OBJECT TYPES
  445. function ResumableFile(resumableObj, file, uniqueIdentifier){
  446. var $ = this;
  447. $.opts = {};
  448. $.getOpt = resumableObj.getOpt;
  449. $._prevProgress = 0;
  450. $.resumableObj = resumableObj;
  451. $.file = file;
  452. $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox
  453. $.size = file.size;
  454. $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName;
  455. $.uniqueIdentifier = uniqueIdentifier;
  456. $._pause = false;
  457. $.container = '';
  458. $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
  459. var _error = uniqueIdentifier !== undefined;
  460. // Callback when something happens within the chunk
  461. var chunkEvent = function(event, message){
  462. // event can be 'progress', 'success', 'error' or 'retry'
  463. switch(event){
  464. case 'progress':
  465. $.resumableObj.fire('fileProgress', $, message);
  466. break;
  467. case 'error':
  468. $.abort();
  469. _error = true;
  470. $.chunks = [];
  471. $.resumableObj.fire('fileError', $, message);
  472. break;
  473. case 'success':
  474. if(_error) return;
  475. $.resumableObj.fire('fileProgress', $, message); // it's at least progress
  476. if($.isComplete()) {
  477. $.resumableObj.fire('fileSuccess', $, message);
  478. }
  479. break;
  480. case 'retry':
  481. $.resumableObj.fire('fileRetry', $);
  482. break;
  483. }
  484. };
  485. // Main code to set up a file object with chunks,
  486. // packaged to be able to handle retries if needed.
  487. $.chunks = [];
  488. $.abort = function(){
  489. // Stop current uploads
  490. var abortCount = 0;
  491. $h.each($.chunks, function(c){
  492. if(c.status()=='uploading') {
  493. c.abort();
  494. abortCount++;
  495. }
  496. });
  497. if(abortCount>0) $.resumableObj.fire('fileProgress', $);
  498. };
  499. $.cancel = function(){
  500. // Reset this file to be void
  501. var _chunks = $.chunks;
  502. $.chunks = [];
  503. // Stop current uploads
  504. $h.each(_chunks, function(c){
  505. if(c.status()=='uploading') {
  506. c.abort();
  507. $.resumableObj.uploadNextChunk();
  508. }
  509. });
  510. $.resumableObj.removeFile($);
  511. $.resumableObj.fire('fileProgress', $);
  512. };
  513. $.retry = function(){
  514. $.bootstrap();
  515. var firedRetry = false;
  516. $.resumableObj.on('chunkingComplete', function(){
  517. if(!firedRetry) $.resumableObj.upload();
  518. firedRetry = true;
  519. });
  520. };
  521. $.bootstrap = function(){
  522. $.abort();
  523. _error = false;
  524. // Rebuild stack of chunks from file
  525. $.chunks = [];
  526. $._prevProgress = 0;
  527. var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor;
  528. var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1);
  529. for (var offset=0; offset<maxOffset; offset++) {(function(offset){
  530. $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
  531. $.resumableObj.fire('chunkingProgress',$,offset/maxOffset);
  532. })(offset)}
  533. window.setTimeout(function(){
  534. $.resumableObj.fire('chunkingComplete',$);
  535. },0);
  536. };
  537. $.progress = function(){
  538. if(_error) return(1);
  539. // Sum up progress across everything
  540. var ret = 0;
  541. var error = false;
  542. $h.each($.chunks, function(c){
  543. if(c.status()=='error') error = true;
  544. ret += c.progress(true); // get chunk progress relative to entire file
  545. });
  546. ret = (error ? 1 : (ret>0.99999 ? 1 : ret));
  547. ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused
  548. $._prevProgress = ret;
  549. return(ret);
  550. };
  551. $.isUploading = function(){
  552. var uploading = false;
  553. $h.each($.chunks, function(chunk){
  554. if(chunk.status()=='uploading') {
  555. uploading = true;
  556. return(false);
  557. }
  558. });
  559. return(uploading);
  560. };
  561. $.isComplete = function(){
  562. var outstanding = false;
  563. if ($.preprocessState === 1) {
  564. return(false);
  565. }
  566. $h.each($.chunks, function(chunk){
  567. var status = chunk.status();
  568. if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) {
  569. outstanding = true;
  570. return(false);
  571. }
  572. });
  573. return(!outstanding);
  574. };
  575. $.pause = function(pause){
  576. if(typeof(pause)==='undefined'){
  577. $._pause = ($._pause ? false : true);
  578. }else{
  579. $._pause = pause;
  580. }
  581. };
  582. $.isPaused = function() {
  583. return $._pause;
  584. };
  585. $.preprocessFinished = function(){
  586. $.preprocessState = 2;
  587. $.upload();
  588. };
  589. $.upload = function () {
  590. var found = false;
  591. if ($.isPaused() === false) {
  592. var preprocess = $.getOpt('preprocessFile');
  593. if(typeof preprocess === 'function') {
  594. switch($.preprocessState) {
  595. case 0: $.preprocessState = 1; preprocess($); return(true);
  596. case 1: return(true);
  597. case 2: break;
  598. }
  599. }
  600. $h.each($.chunks, function (chunk) {
  601. if (chunk.status() == 'pending' && chunk.preprocessState !== 1) {
  602. chunk.send();
  603. found = true;
  604. return(false);
  605. }
  606. });
  607. }
  608. return(found);
  609. }
  610. $.markChunksCompleted = function (chunkNumber) {
  611. if (!$.chunks || $.chunks.length <= chunkNumber) {
  612. return;
  613. }
  614. for (var num = 0; num < chunkNumber; num++) {
  615. $.chunks[num].markComplete = true;
  616. }
  617. };
  618. // Bootstrap and return
  619. $.resumableObj.fire('chunkingStart', $);
  620. $.bootstrap();
  621. return(this);
  622. }
  623. function ResumableChunk(resumableObj, fileObj, offset, callback){
  624. var $ = this;
  625. $.opts = {};
  626. $.getOpt = resumableObj.getOpt;
  627. $.resumableObj = resumableObj;
  628. $.fileObj = fileObj;
  629. $.fileObjSize = fileObj.size;
  630. $.fileObjType = fileObj.file.type;
  631. $.offset = offset;
  632. $.callback = callback;
  633. $.lastProgressCallback = (new Date);
  634. $.tested = false;
  635. $.retries = 0;
  636. $.pendingRetry = false;
  637. $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
  638. $.markComplete = false;
  639. // Computed properties
  640. var chunkSize = $.getOpt('chunkSize');
  641. $.loaded = 0;
  642. $.startByte = $.offset*chunkSize;
  643. $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize);
  644. if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
  645. // The last chunk will be bigger than the chunk size, but less than 2*chunkSize
  646. $.endByte = $.fileObjSize;
  647. }
  648. $.xhr = null;
  649. // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
  650. $.test = function(){
  651. // Set up request and listen for event
  652. $.xhr = new XMLHttpRequest();
  653. var testHandler = function(e){
  654. $.tested = true;
  655. var status = $.status();
  656. if(status=='success') {
  657. $.callback(status, $.message());
  658. $.resumableObj.uploadNextChunk();
  659. } else {
  660. $.send();
  661. }
  662. };
  663. $.xhr.addEventListener('load', testHandler, false);
  664. $.xhr.addEventListener('error', testHandler, false);
  665. $.xhr.addEventListener('timeout', testHandler, false);
  666. // Add data from the query options
  667. var params = [];
  668. var parameterNamespace = $.getOpt('parameterNamespace');
  669. var customQuery = $.getOpt('query');
  670. if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
  671. $h.each(customQuery, function(k,v){
  672. params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('='));
  673. });
  674. // Add extra data to identify chunk
  675. params = params.concat(
  676. [
  677. // define key/value pairs for additional parameters
  678. ['chunkNumberParameterName', $.offset + 1],
  679. ['chunkSizeParameterName', $.getOpt('chunkSize')],
  680. ['currentChunkSizeParameterName', $.endByte - $.startByte],
  681. ['totalSizeParameterName', $.fileObjSize],
  682. ['typeParameterName', $.fileObjType],
  683. ['identifierParameterName', $.fileObj.uniqueIdentifier],
  684. ['fileNameParameterName', $.fileObj.fileName],
  685. ['relativePathParameterName', $.fileObj.relativePath],
  686. ['totalChunksParameterName', $.fileObj.chunks.length]
  687. ].filter(function(pair){
  688. // include items that resolve to truthy values
  689. // i.e. exclude false, null, undefined and empty strings
  690. return $.getOpt(pair[0]);
  691. })
  692. .map(function(pair){
  693. // map each key/value pair to its final form
  694. return [
  695. parameterNamespace + $.getOpt(pair[0]),
  696. encodeURIComponent(pair[1])
  697. ].join('=');
  698. })
  699. );
  700. // Append the relevant chunk and send it
  701. $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params));
  702. $.xhr.timeout = $.getOpt('xhrTimeout');
  703. $.xhr.withCredentials = $.getOpt('withCredentials');
  704. // Add data from header options
  705. var customHeaders = $.getOpt('headers');
  706. if(typeof customHeaders === 'function') {
  707. customHeaders = customHeaders($.fileObj, $);
  708. }
  709. $h.each(customHeaders, function(k,v) {
  710. $.xhr.setRequestHeader(k, v);
  711. });
  712. $.xhr.send(null);
  713. };
  714. $.preprocessFinished = function(){
  715. $.preprocessState = 2;
  716. $.send();
  717. };
  718. // send() uploads the actual data in a POST call
  719. $.send = function(){
  720. var preprocess = $.getOpt('preprocess');
  721. if(typeof preprocess === 'function') {
  722. switch($.preprocessState) {
  723. case 0: $.preprocessState = 1; preprocess($); return;
  724. case 1: return;
  725. case 2: break;
  726. }
  727. }
  728. if($.getOpt('testChunks') && !$.tested) {
  729. $.test();
  730. return;
  731. }
  732. // Set up request and listen for event
  733. $.xhr = new XMLHttpRequest();
  734. // Progress
  735. $.xhr.upload.addEventListener('progress', function(e){
  736. if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) {
  737. $.callback('progress');
  738. $.lastProgressCallback = (new Date);
  739. }
  740. $.loaded=e.loaded||0;
  741. }, false);
  742. $.loaded = 0;
  743. $.pendingRetry = false;
  744. $.callback('progress');
  745. // Done (either done, failed or retry)
  746. var doneHandler = function(e){
  747. var status = $.status();
  748. if(status=='success'||status=='error') {
  749. $.callback(status, $.message());
  750. $.resumableObj.uploadNextChunk();
  751. } else {
  752. $.callback('retry', $.message());
  753. $.abort();
  754. $.retries++;
  755. var retryInterval = $.getOpt('chunkRetryInterval');
  756. if(retryInterval !== undefined) {
  757. $.pendingRetry = true;
  758. setTimeout($.send, retryInterval);
  759. } else {
  760. $.send();
  761. }
  762. }
  763. };
  764. $.xhr.addEventListener('load', doneHandler, false);
  765. $.xhr.addEventListener('error', doneHandler, false);
  766. $.xhr.addEventListener('timeout', doneHandler, false);
  767. // Set up the basic query data from Resumable
  768. var query = [
  769. ['chunkNumberParameterName', $.offset + 1],
  770. ['chunkSizeParameterName', $.getOpt('chunkSize')],
  771. ['currentChunkSizeParameterName', $.endByte - $.startByte],
  772. ['totalSizeParameterName', $.fileObjSize],
  773. ['typeParameterName', $.fileObjType],
  774. ['identifierParameterName', $.fileObj.uniqueIdentifier],
  775. ['fileNameParameterName', $.fileObj.fileName],
  776. ['relativePathParameterName', $.fileObj.relativePath],
  777. ['totalChunksParameterName', $.fileObj.chunks.length],
  778. ].filter(function(pair){
  779. // include items that resolve to truthy values
  780. // i.e. exclude false, null, undefined and empty strings
  781. return $.getOpt(pair[0]);
  782. })
  783. .reduce(function(query, pair){
  784. // assign query key/value
  785. query[$.getOpt(pair[0])] = pair[1];
  786. return query;
  787. }, {});
  788. // Mix in custom data
  789. var customQuery = $.getOpt('query');
  790. if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
  791. $h.each(customQuery, function(k,v){
  792. query[k] = v;
  793. });
  794. var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice')));
  795. var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : "");
  796. var data = null;
  797. var params = [];
  798. var parameterNamespace = $.getOpt('parameterNamespace');
  799. if ($.getOpt('method') === 'octet') {
  800. // Add data from the query options
  801. data = bytes;
  802. $h.each(query, function (k, v) {
  803. params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
  804. });
  805. } else {
  806. // Add data from the query options
  807. data = new FormData();
  808. $h.each(query, function (k, v) {
  809. data.append(parameterNamespace + k, v);
  810. params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
  811. });
  812. if ($.getOpt('chunkFormat') == 'blob') {
  813. data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName);
  814. }
  815. else if ($.getOpt('chunkFormat') == 'base64') {
  816. var fr = new FileReader();
  817. fr.onload = function (e) {
  818. data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result);
  819. $.xhr.send(data);
  820. }
  821. fr.readAsDataURL(bytes);
  822. }
  823. }
  824. var target = $h.getTarget('upload', params);
  825. var method = $.getOpt('uploadMethod');
  826. $.xhr.open(method, target);
  827. if ($.getOpt('method') === 'octet') {
  828. $.xhr.setRequestHeader('Content-Type', 'application/octet-stream');
  829. }
  830. $.xhr.timeout = $.getOpt('xhrTimeout');
  831. $.xhr.withCredentials = $.getOpt('withCredentials');
  832. // Add data from header options
  833. var customHeaders = $.getOpt('headers');
  834. if(typeof customHeaders === 'function') {
  835. customHeaders = customHeaders($.fileObj, $);
  836. }
  837. $h.each(customHeaders, function(k,v) {
  838. $.xhr.setRequestHeader(k, v);
  839. });
  840. if ($.getOpt('chunkFormat') == 'blob') {
  841. $.xhr.send(data);
  842. }
  843. };
  844. $.abort = function(){
  845. // Abort and reset
  846. if($.xhr) $.xhr.abort();
  847. $.xhr = null;
  848. };
  849. $.status = function(){
  850. // Returns: 'pending', 'uploading', 'success', 'error'
  851. if($.pendingRetry) {
  852. // if pending retry then that's effectively the same as actively uploading,
  853. // there might just be a slight delay before the retry starts
  854. return('uploading');
  855. } else if($.markComplete) {
  856. return 'success';
  857. } else if(!$.xhr) {
  858. return('pending');
  859. } else if($.xhr.readyState<4) {
  860. // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
  861. return('uploading');
  862. } else {
  863. if($.xhr.status == 200 || $.xhr.status == 201) {
  864. // HTTP 200, 201 (created)
  865. return('success');
  866. } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) {
  867. // HTTP 400, 404, 409, 415, 500, 501 (permanent error)
  868. return('error');
  869. } else {
  870. // this should never happen, but we'll reset and queue a retry
  871. // a likely case for this would be 503 service unavailable
  872. $.abort();
  873. return('pending');
  874. }
  875. }
  876. };
  877. $.message = function(){
  878. return($.xhr ? $.xhr.responseText : '');
  879. };
  880. $.progress = function(relative){
  881. if(typeof(relative)==='undefined') relative = false;
  882. var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1);
  883. if($.pendingRetry) return(0);
  884. if((!$.xhr || !$.xhr.status) && !$.markComplete) factor*=.95;
  885. var s = $.status();
  886. switch(s){
  887. case 'success':
  888. case 'error':
  889. return(1*factor);
  890. case 'pending':
  891. return(0*factor);
  892. default:
  893. return($.loaded/($.endByte-$.startByte)*factor);
  894. }
  895. };
  896. return(this);
  897. }
  898. // QUEUE
  899. $.uploadNextChunk = function(){
  900. var found = false;
  901. // In some cases (such as videos) it's really handy to upload the first
  902. // and last chunk of a file quickly; this let's the server check the file's
  903. // metadata and determine if there's even a point in continuing.
  904. if ($.getOpt('prioritizeFirstAndLastChunk')) {
  905. $h.each($.files, function(file){
  906. if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) {
  907. file.chunks[0].send();
  908. found = true;
  909. return(false);
  910. }
  911. if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) {
  912. file.chunks[file.chunks.length-1].send();
  913. found = true;
  914. return(false);
  915. }
  916. });
  917. if(found) return(true);
  918. }
  919. // Now, simply look for the next, best thing to upload
  920. $h.each($.files, function(file){
  921. found = file.upload();
  922. if(found) return(false);
  923. });
  924. if(found) return(true);
  925. // The are no more outstanding chunks to upload, check is everything is done
  926. var outstanding = false;
  927. $h.each($.files, function(file){
  928. if(!file.isComplete()) {
  929. outstanding = true;
  930. return(false);
  931. }
  932. });
  933. if(!outstanding) {
  934. // All chunks have been uploaded, complete
  935. $.fire('complete');
  936. }
  937. return(false);
  938. };
  939. // PUBLIC METHODS FOR RESUMABLE.JS
  940. $.assignBrowse = function(domNodes, isDirectory){
  941. if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
  942. $h.each(domNodes, function(domNode) {
  943. var input;
  944. if(domNode.tagName==='INPUT' && domNode.type==='file'){
  945. input = domNode;
  946. } else {
  947. input = document.createElement('input');
  948. input.setAttribute('type', 'file');
  949. input.style.display = 'none';
  950. domNode.addEventListener('click', function(){
  951. input.style.opacity = 0;
  952. input.style.display='block';
  953. input.focus();
  954. input.click();
  955. input.style.display='none';
  956. }, false);
  957. domNode.appendChild(input);
  958. }
  959. var maxFiles = $.getOpt('maxFiles');
  960. if (typeof(maxFiles)==='undefined'||maxFiles!=1){
  961. input.setAttribute('multiple', 'multiple');
  962. } else {
  963. input.removeAttribute('multiple');
  964. }
  965. if(isDirectory){
  966. input.setAttribute('webkitdirectory', 'webkitdirectory');
  967. } else {
  968. input.removeAttribute('webkitdirectory');
  969. }
  970. var fileTypes = $.getOpt('fileType');
  971. if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) {
  972. input.setAttribute('accept', fileTypes.map(function (e) {
  973. e = e.replace(/\s/g, '').toLowerCase();
  974. if(e.match(/^[^.][^/]+$/)){
  975. e = '.' + e;
  976. }
  977. return e;
  978. }).join(','));
  979. }
  980. else {
  981. input.removeAttribute('accept');
  982. }
  983. // When new files are added, simply append them to the overall list
  984. input.addEventListener('change', function(e){
  985. appendFilesFromFileList(e.target.files,e);
  986. var clearInput = $.getOpt('clearInput');
  987. if (clearInput) {
  988. e.target.value = '';
  989. }
  990. }, false);
  991. });
  992. };
  993. $.assignDrop = function(domNodes){
  994. if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
  995. $h.each(domNodes, function(domNode) {
  996. domNode.addEventListener('dragover', onDragOverEnter, false);
  997. domNode.addEventListener('dragenter', onDragOverEnter, false);
  998. domNode.addEventListener('dragleave', onDragLeave, false);
  999. domNode.addEventListener('drop', onDrop, false);
  1000. });
  1001. };
  1002. $.unAssignDrop = function(domNodes) {
  1003. if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes];
  1004. $h.each(domNodes, function(domNode) {
  1005. domNode.removeEventListener('dragover', onDragOverEnter);
  1006. domNode.removeEventListener('dragenter', onDragOverEnter);
  1007. domNode.removeEventListener('dragleave', onDragLeave);
  1008. domNode.removeEventListener('drop', onDrop);
  1009. });
  1010. };
  1011. $.isUploading = function(){
  1012. var uploading = false;
  1013. $h.each($.files, function(file){
  1014. if (file.isUploading()) {
  1015. uploading = true;
  1016. return(false);
  1017. }
  1018. });
  1019. return(uploading);
  1020. };
  1021. $.upload = function(){
  1022. // Make sure we don't start too many uploads at once
  1023. if($.isUploading()) return;
  1024. // Kick off the queue
  1025. $.fire('uploadStart');
  1026. for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) {
  1027. $.uploadNextChunk();
  1028. }
  1029. };
  1030. $.pause = function(){
  1031. // Resume all chunks currently being uploaded
  1032. $h.each($.files, function(file){
  1033. file.abort();
  1034. });
  1035. $.fire('pause');
  1036. };
  1037. $.cancel = function(){
  1038. $.fire('beforeCancel');
  1039. for(var i = $.files.length - 1; i >= 0; i--) {
  1040. $.files[i].cancel();
  1041. }
  1042. $.fire('cancel');
  1043. };
  1044. $.progress = function(){
  1045. var totalDone = 0;
  1046. var totalSize = 0;
  1047. // Resume all chunks currently being uploaded
  1048. $h.each($.files, function(file){
  1049. totalDone += file.progress()*file.size;
  1050. totalSize += file.size;
  1051. });
  1052. return(totalSize>0 ? totalDone/totalSize : 0);
  1053. };
  1054. $.addFile = function(file, event){
  1055. appendFilesFromFileList([file], event);
  1056. };
  1057. $.addFiles = function(files, event){
  1058. appendFilesFromFileList(files, event);
  1059. };
  1060. $.removeFile = function(file){
  1061. for(var i = $.files.length - 1; i >= 0; i--) {
  1062. if($.files[i] === file) {
  1063. $.files.splice(i, 1);
  1064. }
  1065. }
  1066. };
  1067. $.getFromUniqueIdentifier = function(uniqueIdentifier){
  1068. var ret = false;
  1069. $h.each($.files, function(f){
  1070. if(f.uniqueIdentifier==uniqueIdentifier) ret = f;
  1071. });
  1072. return(ret);
  1073. };
  1074. $.getSize = function(){
  1075. var totalSize = 0;
  1076. $h.each($.files, function(file){
  1077. totalSize += file.size;
  1078. });
  1079. return(totalSize);
  1080. };
  1081. $.handleDropEvent = function (e) {
  1082. onDrop(e);
  1083. };
  1084. $.handleChangeEvent = function (e) {
  1085. appendFilesFromFileList(e.target.files, e);
  1086. e.target.value = '';
  1087. };
  1088. $.updateQuery = function(query){
  1089. $.opts.query = query;
  1090. };
  1091. return(this);
  1092. };
  1093. // Node.js-style export for Node and Component
  1094. if (typeof module != 'undefined') {
  1095. // left here for backwards compatibility
  1096. module.exports = Resumable;
  1097. module.exports.Resumable = Resumable;
  1098. } else if (typeof define === "function" && define.amd) {
  1099. // AMD/requirejs: Define the module
  1100. define(function(){
  1101. return Resumable;
  1102. });
  1103. } else {
  1104. // Browser: Expose to window
  1105. window.Resumable = Resumable;
  1106. }
  1107. })();