uploader.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. var utils = require('./utils')
  2. var event = require('./event')
  3. var File = require('./file')
  4. var Chunk = require('./chunk')
  5. var version = '__VERSION__'
  6. var isServer = typeof window === 'undefined'
  7. // ie10+
  8. var ie10plus = isServer ? false : window.navigator.msPointerEnabled
  9. var support = (function () {
  10. if (isServer) {
  11. return false
  12. }
  13. var sliceName = 'slice'
  14. var _support = utils.isDefined(window.File) && utils.isDefined(window.Blob) &&
  15. utils.isDefined(window.FileList)
  16. var bproto = null
  17. if (_support) {
  18. bproto = window.Blob.prototype
  19. utils.each(['slice', 'webkitSlice', 'mozSlice'], function (n) {
  20. if (bproto[n]) {
  21. sliceName = n
  22. return false
  23. }
  24. })
  25. _support = !!bproto[sliceName]
  26. }
  27. if (_support) Uploader.sliceName = sliceName
  28. bproto = null
  29. return _support
  30. })()
  31. var supportDirectory = (function () {
  32. if (isServer) {
  33. return false
  34. }
  35. var input = window.document.createElement('input')
  36. input.type = 'file'
  37. var sd = 'webkitdirectory' in input || 'directory' in input
  38. input = null
  39. return sd
  40. })()
  41. function Uploader (opts) {
  42. this.support = support
  43. /* istanbul ignore if */
  44. if (!this.support) {
  45. return
  46. }
  47. this.supportDirectory = supportDirectory
  48. utils.defineNonEnumerable(this, 'filePaths', {})
  49. this.opts = utils.extend({}, Uploader.defaults, opts || {})
  50. this.preventEvent = utils.bind(this._preventEvent, this)
  51. File.call(this, this)
  52. }
  53. /**
  54. * Default read function using the webAPI
  55. *
  56. * @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk)
  57. *
  58. */
  59. var webAPIFileRead = function (fileObj, fileType, startByte, endByte, chunk) {
  60. chunk.readFinished(fileObj.file[Uploader.sliceName](startByte, endByte, fileType))
  61. }
  62. Uploader.version = version
  63. Uploader.defaults = {
  64. chunkSize: 1024 * 1024,
  65. forceChunkSize: false,
  66. simultaneousUploads: 3,
  67. singleFile: false,
  68. fileParameterName: 'file',
  69. progressCallbacksInterval: 500,
  70. speedSmoothingFactor: 0.1,
  71. query: {},
  72. headers: {},
  73. withCredentials: false,
  74. preprocess: null,
  75. method: 'multipart',
  76. testMethod: 'GET',
  77. uploadMethod: 'POST',
  78. prioritizeFirstAndLastChunk: false,
  79. allowDuplicateUploads: false,
  80. target: '/',
  81. testChunks: true,
  82. generateUniqueIdentifier: null,
  83. maxChunkRetries: 0,
  84. chunkRetryInterval: null,
  85. permanentErrors: [404, 415, 500, 501],
  86. successStatuses: [200, 201, 202],
  87. onDropStopPropagation: false,
  88. initFileFn: null,
  89. readFileFn: webAPIFileRead,
  90. checkChunkUploadedByResponse: null,
  91. initialPaused: false,
  92. processResponse: function (response, cb) {
  93. cb(null, response)
  94. },
  95. processParams: function (params) {
  96. return params
  97. }
  98. }
  99. Uploader.utils = utils
  100. Uploader.event = event
  101. Uploader.File = File
  102. Uploader.Chunk = Chunk
  103. // inherit file
  104. Uploader.prototype = utils.extend({}, File.prototype)
  105. // inherit event
  106. utils.extend(Uploader.prototype, event)
  107. utils.extend(Uploader.prototype, {
  108. constructor: Uploader,
  109. _trigger: function (name) {
  110. var args = utils.toArray(arguments)
  111. var preventDefault = !this.trigger.apply(this, arguments)
  112. if (name !== 'catchAll') {
  113. args.unshift('catchAll')
  114. preventDefault = !this.trigger.apply(this, args) || preventDefault
  115. }
  116. return !preventDefault
  117. },
  118. _triggerAsync: function () {
  119. var args = arguments
  120. utils.nextTick(function () {
  121. this._trigger.apply(this, args)
  122. }, this)
  123. },
  124. addFiles: function (files, evt) {
  125. var _files = []
  126. var oldFileListLen = this.fileList.length
  127. utils.each(files, function (file) {
  128. // Uploading empty file IE10/IE11 hangs indefinitely
  129. // Directories have size `0` and name `.`
  130. // Ignore already added files if opts.allowDuplicateUploads is set to false
  131. if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
  132. var uniqueIdentifier = this.generateUniqueIdentifier(file)
  133. if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) {
  134. var _file = new File(this, file, this)
  135. _file.uniqueIdentifier = uniqueIdentifier
  136. if (this._trigger('fileAdded', _file, evt)) {
  137. _files.push(_file)
  138. } else {
  139. File.prototype.removeFile.call(this, _file)
  140. }
  141. }
  142. }
  143. }, this)
  144. // get new fileList
  145. var newFileList = this.fileList.slice(oldFileListLen)
  146. if (this._trigger('filesAdded', _files, newFileList, evt)) {
  147. utils.each(_files, function (file) {
  148. if (this.opts.singleFile && this.files.length > 0) {
  149. this.removeFile(this.files[0])
  150. }
  151. this.files.push(file)
  152. }, this)
  153. this._trigger('filesSubmitted', _files, newFileList, evt)
  154. } else {
  155. utils.each(newFileList, function (file) {
  156. File.prototype.removeFile.call(this, file)
  157. }, this)
  158. }
  159. },
  160. addFile: function (file, evt) {
  161. this.addFiles([file], evt)
  162. },
  163. cancel: function () {
  164. for (var i = this.fileList.length - 1; i >= 0; i--) {
  165. this.fileList[i].cancel()
  166. }
  167. },
  168. removeFile: function (file) {
  169. File.prototype.removeFile.call(this, file)
  170. this._trigger('fileRemoved', file)
  171. },
  172. generateUniqueIdentifier: function (file) {
  173. var custom = this.opts.generateUniqueIdentifier
  174. if (utils.isFunction(custom)) {
  175. return custom(file)
  176. }
  177. /* istanbul ignore next */
  178. // Some confusion in different versions of Firefox
  179. var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name
  180. /* istanbul ignore next */
  181. return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')
  182. },
  183. getFromUniqueIdentifier: function (uniqueIdentifier) {
  184. var ret = false
  185. utils.each(this.files, function (file) {
  186. if (file.uniqueIdentifier === uniqueIdentifier) {
  187. ret = file
  188. return false
  189. }
  190. })
  191. return ret
  192. },
  193. uploadNextChunk: function (preventEvents) {
  194. var found = false
  195. var pendingStatus = Chunk.STATUS.PENDING
  196. var checkChunkUploaded = this.uploader.opts.checkChunkUploadedByResponse
  197. if (this.opts.prioritizeFirstAndLastChunk) {
  198. utils.each(this.files, function (file) {
  199. if (file.paused) {
  200. return
  201. }
  202. if (checkChunkUploaded && !file._firstResponse && file.isUploading()) {
  203. // waiting for current file's first chunk response
  204. return
  205. }
  206. if (file.chunks.length && file.chunks[0].status() === pendingStatus) {
  207. file.chunks[0].send()
  208. found = true
  209. return false
  210. }
  211. if (file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === pendingStatus) {
  212. file.chunks[file.chunks.length - 1].send()
  213. found = true
  214. return false
  215. }
  216. })
  217. if (found) {
  218. return found
  219. }
  220. }
  221. // Now, simply look for the next, best thing to upload
  222. utils.each(this.files, function (file) {
  223. if (!file.paused) {
  224. if (checkChunkUploaded && !file._firstResponse && file.isUploading()) {
  225. // waiting for current file's first chunk response
  226. return
  227. }
  228. utils.each(file.chunks, function (chunk) {
  229. if (chunk.status() === pendingStatus) {
  230. chunk.send()
  231. found = true
  232. return false
  233. }
  234. })
  235. }
  236. if (found) {
  237. return false
  238. }
  239. })
  240. if (found) {
  241. return true
  242. }
  243. // The are no more outstanding chunks to upload, check is everything is done
  244. var outstanding = false
  245. utils.each(this.files, function (file) {
  246. if (!file.isComplete()) {
  247. outstanding = true
  248. return false
  249. }
  250. })
  251. // should check files now
  252. // if now files in list
  253. // should not trigger complete event
  254. if (!outstanding && !preventEvents && this.files.length) {
  255. // All chunks have been uploaded, complete
  256. this._triggerAsync('complete')
  257. }
  258. return outstanding
  259. },
  260. upload: function (preventEvents) {
  261. // Make sure we don't start too many uploads at once
  262. var ret = this._shouldUploadNext()
  263. if (ret === false) {
  264. return
  265. }
  266. !preventEvents && this._trigger('uploadStart')
  267. var started = false
  268. for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
  269. started = this.uploadNextChunk(!preventEvents) || started
  270. if (!started && preventEvents) {
  271. // completed
  272. break
  273. }
  274. }
  275. if (!started && !preventEvents) {
  276. this._triggerAsync('complete')
  277. }
  278. },
  279. /**
  280. * should upload next chunk
  281. * @function
  282. * @returns {Boolean|Number}
  283. */
  284. _shouldUploadNext: function () {
  285. var num = 0
  286. var should = true
  287. var simultaneousUploads = this.opts.simultaneousUploads
  288. var uploadingStatus = Chunk.STATUS.UPLOADING
  289. utils.each(this.files, function (file) {
  290. utils.each(file.chunks, function (chunk) {
  291. if (chunk.status() === uploadingStatus) {
  292. num++
  293. if (num >= simultaneousUploads) {
  294. should = false
  295. return false
  296. }
  297. }
  298. })
  299. return should
  300. })
  301. // if should is true then return uploading chunks's length
  302. return should && num
  303. },
  304. /**
  305. * Assign a browse action to one or more DOM nodes.
  306. * @function
  307. * @param {Element|Array.<Element>} domNodes
  308. * @param {boolean} isDirectory Pass in true to allow directories to
  309. * @param {boolean} singleFile prevent multi file upload
  310. * @param {Object} attributes set custom attributes:
  311. * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
  312. * eg: accept: 'image/*'
  313. * be selected (Chrome only).
  314. */
  315. assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
  316. if (typeof domNodes.length === 'undefined') {
  317. domNodes = [domNodes]
  318. }
  319. utils.each(domNodes, function (domNode) {
  320. var input
  321. if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
  322. input = domNode
  323. } else {
  324. input = document.createElement('input')
  325. input.setAttribute('type', 'file')
  326. // display:none - not working in opera 12
  327. utils.extend(input.style, {
  328. visibility: 'hidden',
  329. position: 'absolute',
  330. width: '1px',
  331. height: '1px'
  332. })
  333. // for opera 12 browser, input must be assigned to a document
  334. domNode.appendChild(input)
  335. // https://developer.mozilla.org/en/using_files_from_web_applications)
  336. // event listener is executed two times
  337. // first one - original mouse click event
  338. // second - input.click(), input is inside domNode
  339. domNode.addEventListener('click', function (e) {
  340. if (domNode.tagName.toLowerCase() === 'label') {
  341. return
  342. }
  343. input.click()
  344. }, false)
  345. }
  346. if (!this.opts.singleFile && !singleFile) {
  347. input.setAttribute('multiple', 'multiple')
  348. }
  349. if (isDirectory) {
  350. input.setAttribute('webkitdirectory', 'webkitdirectory')
  351. }
  352. attributes && utils.each(attributes, function (value, key) {
  353. input.setAttribute(key, value)
  354. })
  355. // When new files are added, simply append them to the overall list
  356. var that = this
  357. input.addEventListener('change', function (e) {
  358. that._trigger(e.type, e)
  359. if (e.target.value) {
  360. that.addFiles(e.target.files, e)
  361. e.target.value = ''
  362. }
  363. }, false)
  364. }, this)
  365. },
  366. onDrop: function (evt) {
  367. this._trigger(evt.type, evt)
  368. if (this.opts.onDropStopPropagation) {
  369. evt.stopPropagation()
  370. }
  371. evt.preventDefault()
  372. this._parseDataTransfer(evt.dataTransfer, evt)
  373. },
  374. _parseDataTransfer: function (dataTransfer, evt) {
  375. if (dataTransfer.items && dataTransfer.items[0] &&
  376. dataTransfer.items[0].webkitGetAsEntry) {
  377. this.webkitReadDataTransfer(dataTransfer, evt)
  378. } else {
  379. this.addFiles(dataTransfer.files, evt)
  380. }
  381. },
  382. webkitReadDataTransfer: function (dataTransfer, evt) {
  383. var self = this
  384. var queue = dataTransfer.items.length
  385. var files = []
  386. utils.each(dataTransfer.items, function (item) {
  387. var entry = item.webkitGetAsEntry()
  388. if (!entry) {
  389. decrement()
  390. return
  391. }
  392. if (entry.isFile) {
  393. // due to a bug in Chrome's File System API impl - #149735
  394. fileReadSuccess(item.getAsFile(), entry.fullPath)
  395. } else {
  396. readDirectory(entry.createReader())
  397. }
  398. })
  399. function readDirectory (reader) {
  400. reader.readEntries(function (entries) {
  401. if (entries.length) {
  402. queue += entries.length
  403. utils.each(entries, function (entry) {
  404. if (entry.isFile) {
  405. var fullPath = entry.fullPath
  406. entry.file(function (file) {
  407. fileReadSuccess(file, fullPath)
  408. }, readError)
  409. } else if (entry.isDirectory) {
  410. readDirectory(entry.createReader())
  411. }
  412. })
  413. readDirectory(reader)
  414. } else {
  415. decrement()
  416. }
  417. }, readError)
  418. }
  419. function fileReadSuccess (file, fullPath) {
  420. // relative path should not start with "/"
  421. file.relativePath = fullPath.substring(1)
  422. files.push(file)
  423. decrement()
  424. }
  425. function readError (fileError) {
  426. throw fileError
  427. }
  428. function decrement () {
  429. if (--queue === 0) {
  430. self.addFiles(files, evt)
  431. }
  432. }
  433. },
  434. _assignHelper: function (domNodes, handles, remove) {
  435. if (typeof domNodes.length === 'undefined') {
  436. domNodes = [domNodes]
  437. }
  438. var evtMethod = remove ? 'removeEventListener' : 'addEventListener'
  439. utils.each(domNodes, function (domNode) {
  440. utils.each(handles, function (handler, name) {
  441. domNode[evtMethod](name, handler, false)
  442. }, this)
  443. }, this)
  444. },
  445. _preventEvent: function (e) {
  446. utils.preventEvent(e)
  447. this._trigger(e.type, e)
  448. },
  449. /**
  450. * Assign one or more DOM nodes as a drop target.
  451. * @function
  452. * @param {Element|Array.<Element>} domNodes
  453. */
  454. assignDrop: function (domNodes) {
  455. this._onDrop = utils.bind(this.onDrop, this)
  456. this._assignHelper(domNodes, {
  457. dragover: this.preventEvent,
  458. dragenter: this.preventEvent,
  459. dragleave: this.preventEvent,
  460. drop: this._onDrop
  461. })
  462. },
  463. /**
  464. * Un-assign drop event from DOM nodes
  465. * @function
  466. * @param domNodes
  467. */
  468. unAssignDrop: function (domNodes) {
  469. this._assignHelper(domNodes, {
  470. dragover: this.preventEvent,
  471. dragenter: this.preventEvent,
  472. dragleave: this.preventEvent,
  473. drop: this._onDrop
  474. }, true)
  475. this._onDrop = null
  476. }
  477. })
  478. module.exports = Uploader