UNPKG

oss-uploader.js

Version:
706 lines (662 loc) 19.8 kB
const utils = require('./utils') // import utils from './utils' const event = require('./event') const File = require('./file') const Chunk = require('./chunk') // const LocalFile = require('./localFile') const version = require('../package.json') // const recorder = require('./recoder') // import event from './event' // import File from './file' // import Chunk from './chunk' // import LocalFile from './localFile' // import version from '../package.json' // import * as recorder from './recoder' let oss = ['qiniu', 'aliyun', 'tencent'] let isServer = typeof window === 'undefined' // ie10+ let ie10plus = isServer ? false : window.navigator.msPointerEnabled let support = (function () { if (isServer) { return false } let sliceName = 'slice' let _support = utils.isDefined(window.File) && utils.isDefined(window.Blob) && utils.isDefined(window.FileList) let bproto = null if (_support) { bproto = window.Blob.prototype utils.each(['slice', 'webkitSlice', 'mozSlice'], function (n) { if (bproto[n]) { sliceName = n return false } }) _support = !!bproto[sliceName] } if (_support) Uploader.sliceName = sliceName bproto = null return _support })() let supportDirectory = (function () { if (isServer) { return false } let input = window.document.createElement('input') input.type = 'file' let sd = 'webkitdirectory' in input || 'directory' in input input = null return sd })() /** * Default read function using the webAPI * * @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk) * */ let webAPIFileRead = function (fileObj, fileType, startByte, endByte, chunk) { chunk.readFinished( fileObj.file[Uploader.sliceName](startByte, endByte, fileType) ) } function Uploader (opts) { this.support = support /* istanbul ignore if */ if (!this.support) { return } this.supportDirectory = supportDirectory utils.defineNonEnumerable(this, 'filePaths', {}) this.opts = utils.extend({}, Uploader.defaults, opts || {}) this.opts.chunkSize = oss.includes(this.opts.oss) ? Number.MAX_SAFE_INTEGER : this.opts.chunkSize this.preventEvent = utils.bind(this._preventEvent, this) File.call(this, this) this.opts.autoRestoreLocalRecord && this.restoreLocalRecord(0) } Uploader.defaults = { chunkSize: 1024 * 1024, forceChunkSize: false, simultaneousUploads: 3, singleFile: false, fileParameterName: 'file', progressCallbacksInterval: 500, speedSmoothingFactor: 0.1, query: {}, headers: {}, withCredentials: false, preprocess: null, method: 'multipart', testMethod: 'GET', uploadMethod: 'POST', prioritizeFirstAndLastChunk: false, allowDuplicateUploads: false, target: '/', testChunks: true, generateUniqueIdentifier: null, maxChunkRetries: 0, chunkRetryInterval: null, permanentErrors: [404, 415, 500, 501], successStatuses: [200, 201, 202], onDropStopPropagation: false, initFileFn: null, readFileFn: webAPIFileRead, checkChunkUploadedByResponse: null, initialPaused: false, processResponse: function (response, cb) { cb(null, response) }, processParams: function (params) { return params }, beforeFileUplod: null, beforeLastChunkUplod: null, ossParams: null, oss: null } Uploader.utils = utils Uploader.event = event Uploader.File = File Uploader.Chunk = Chunk Uploader.version = version // inherit file Uploader.prototype = utils.extend({}, File.prototype) // inherit event utils.extend(Uploader.prototype, event) utils.extend(Uploader.prototype, { constructor: Uploader, _trigger: function (name) { let args = utils.toArray(arguments) let preventDefault = !this.trigger.apply(this, arguments) if (name !== 'catchAll') { args.unshift('catchAll') preventDefault = !this.trigger.apply(this, args) || preventDefault } return !preventDefault }, _triggerAsync: function () { let args = arguments utils.nextTick(function () { this._trigger.apply(this, args) }, this) }, addFiles: function (files, evt) { let _files = [] let oldFileListLen = this.fileList.length utils.each( files, function (file) { // Uploading empty file IE10/IE11 hangs indefinitely // Directories have size `0` and name `.` // Ignore already added files if opts.allowDuplicateUploads is set to false if ( (!ie10plus || (ie10plus && file.size > 0)) && !( file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.') ) ) { let uniqueIdentifier = file.uniqueIdentifier || this.generateUniqueIdentifier(file) if ( this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier) ) { let _file = new File(this, file, this) _file.uniqueIdentifier = uniqueIdentifier if (this._trigger('fileAdded', _file, evt)) { _files.push(_file) // TODO this._createResumeLog(_file) } else { File.prototype.removeFile.call(this, _file) } } } }, this ) // get new fileList let newFileList = this.fileList.slice(oldFileListLen) if (this._trigger('filesAdded', _files, newFileList, evt)) { utils.each( _files, function (file) { if (this.opts.singleFile && this.files.length > 0) { this.removeFile(this.files[0]) } this.files.push(file) }, this ) this._trigger('filesSubmitted', _files, newFileList, evt) // TODO this._addLocalRecord() } else { utils.each( newFileList, function (file) { File.prototype.removeFile.call(this, file) }, this ) } }, addFile: function (file, evt) { this.addFiles([file], evt) }, addFilesByPath (paths) { let files = this.createFilesByPath(paths) this.addFiles(files, new Event('fileAdded')) }, createFilesByPath (paths) { // if (!Array.isArray(paths)) { // return // } // let files = [] // paths.forEach(p => { // let file = new LocalFile(p) // if (file.isDirectory()) { // files.push(...file.listFiles()) // } else { // files.push(file) // } // }) // return files }, cancel: function () { for (let i = this.fileList.length - 1; i >= 0; i--) { this.fileList[i].cancel() } }, removeFile: function (file) { File.prototype.removeFile.call(this, file) this._trigger('fileRemoved', file) // TODO this._addLocalRecord() }, generateUniqueIdentifier: function (file) { let custom = this.opts.generateUniqueIdentifier if (utils.isFunction(custom)) { return custom(file) } /* istanbul ignore next */ // Some confusion in different versions of Firefox let relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name /* istanbul ignore next */ return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/gim, '') }, getFromUniqueIdentifier: function (uniqueIdentifier) { let ret = false utils.each(this.files, function (file) { if (file.uniqueIdentifier === uniqueIdentifier) { ret = file return false } }) return ret }, async dealOssParams (file) { try { let optsOss = 'oss' in file.opts ? file.opts.oss : this.opts.oss if (oss.includes(optsOss)) { let { ossParams } = file if (!utils.isEmptyObject(ossParams)) { return true } if (!this.opts.ossParams) { return false } if (typeof this.opts.ossParams === 'function') { ossParams = await this.opts.ossParams(file) } else if (typeof this.opts.ossParams === 'object') { ossParams = utils.extend({}, this.opts.ossParams) } file.ossParams = utils.extend({}, ossParams) return !!(ossParams && ossParams.key) } } catch (e) { console.error(e) throw e } return true }, uploadNextChunk: async function (preventEvents, uploadFile) { let $ = this let found = false let pendingStatus = Chunk.STATUS.PENDING let checkChunkUploaded = this.uploader.opts.checkChunkUploadedByResponse let thisFiles = uploadFile ? Array.isArray(uploadFile) ? uploadFile : [uploadFile] : this.files if (this.opts.prioritizeFirstAndLastChunk) { for (let file of thisFiles) { if (file.isComplete() || file.error) { continue } if (file.paused) { return } let ossParamsFlag try { ossParamsFlag = await this.dealOssParams(file) } catch (error) { file._error() this._triggerAsync('fileError', file.getRoot(), file, error.message || '获取参数错误!') continue } if (!ossParamsFlag) { file._error() this._triggerAsync('fileError', file.getRoot(), file, '获取参数错误!') continue } if (typeof $.opts.beforeChunkUplod === 'function') { this._trigger('beforeChunkUplod', file) await $.opts.beforeChunkUplod(file) } if (checkChunkUploaded && !file._firstResponse && file.isUploading()) { // waiting for current file's first chunk response return } if (file.chunks.length && file.chunks[0].status() === pendingStatus) { file.chunks[0].send() found = true return false } if ( file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === pendingStatus ) { file.chunks[file.chunks.length - 1].send() found = true return false } } if (found) { return found } } for (let file of thisFiles) { if (file.isComplete() || file.error) { continue } if (!file.paused) { if (checkChunkUploaded && !file._firstResponse && file.isUploading()) { // waiting for current file's first chunk response return } let ossParamsFlag try { ossParamsFlag = await this.dealOssParams(file) } catch (error) { file._error() this._triggerAsync('fileError', file.getRoot(), file, error.message || '获取参数错误!') continue } if (!ossParamsFlag) { file._error() this._triggerAsync('fileError', file.getRoot(), file, '获取参数错误!') continue } this._trigger('fileStartUpload', file) if (typeof $.opts.beforeChunkUplod === 'function') { this._trigger('beforeChunkUplod', file) await $.opts.beforeChunkUplod(file) } utils.each(file.chunks, function (chunk) { if (chunk.status() === pendingStatus) { let chunkParams = chunk.getParams() let isLastChunk = chunkParams.chunkNumber === chunkParams.totalChunks if (isLastChunk) { typeof $.opts.beforeLastChunkUplod === 'function' && $.opts.beforeLastChunkUplod(file, chunk) } chunk.send() found = true return false } }) } if (found) { break } } if (found) { return true } // The are no more outstanding chunks to upload, check is everything is done let outstanding = false utils.each(this.files, function (file) { if (!file.isComplete()) { outstanding = true return false } }) // should check files now // if now files in list // should not trigger complete event if (!outstanding && !preventEvents && this.files.length) { // All chunks have been uploaded, complete this._triggerAsync('complete') } return outstanding }, upload: async function (preventEvents, uploadFile) { // Make sure we don't start too many uploads at once let ret = this._shouldUploadNext() if (ret === false) { return } !preventEvents && this._trigger('uploadStart') let started = false for (let num = 1; num <= this.opts.simultaneousUploads - ret; num++) { started = (await this.uploadNextChunk(!preventEvents, uploadFile)) || started if (!started && preventEvents) { // completed break } } if (!started && !preventEvents) { this._triggerAsync('complete') } }, /** * should upload next chunk * @function * @returns {Boolean|Number} */ _shouldUploadNext: function () { let num = 0 let should = true let simultaneousUploads = this.opts.simultaneousUploads let uploadingStatus = Chunk.STATUS.UPLOADING utils.each(this.files, function (file) { utils.each(file.chunks, function (chunk) { if (chunk.status() === uploadingStatus) { num++ if (num >= simultaneousUploads) { should = false return false } } }) return should }) // if should is true then return uploading chunks's length return should && num }, /** * Assign a browse action to one or more DOM nodes. * @function * @param {Element|Array.<Element>} domNodes * @param {boolean} isDirectory Pass in true to allow directories to * @param {boolean} singleFile prevent multi file upload * @param {Object} attributes set custom attributes: * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes * eg: accept: 'image/*' * be selected (Chrome only). */ assignBrowse: function (domNodes, isDirectory, singleFile, attributes) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes] } utils.each( domNodes, function (domNode) { let input if (domNode.tagName === 'INPUT' && domNode.type === 'file') { input = domNode } else { input = document.createElement('input') input.setAttribute('type', 'file') // display:none - not working in opera 12 utils.extend(input.style, { visibility: 'hidden', position: 'absolute', width: '1px', height: '1px' }) // for opera 12 browser, input must be assigned to a document domNode.appendChild(input) // https://developer.mozilla.org/en/using_files_from_web_applications) // event listener is executed two times // first one - original mouse click event // second - input.click(), input is inside domNode domNode.addEventListener( 'click', function (e) { if (domNode.tagName.toLowerCase() === 'label') { return } input.click() }, false ) } if (!this.opts.singleFile && !singleFile) { input.setAttribute('multiple', 'multiple') } if (isDirectory) { input.setAttribute('webkitdirectory', 'webkitdirectory') } attributes && utils.each(attributes, function (value, key) { input.setAttribute(key, value) }) // When new files are added, simply append them to the overall list let that = this input.addEventListener( 'change', function (e) { that._trigger(e.type, e) if (e.target.value) { that.addFiles(e.target.files, e) e.target.value = '' } }, false ) }, this ) }, onDrop: function (evt) { this._trigger(evt.type, evt) if (this.opts.onDropStopPropagation) { evt.stopPropagation() } evt.preventDefault() this._parseDataTransfer(evt.dataTransfer, evt) }, _parseDataTransfer: function (dataTransfer, evt) { if ( dataTransfer.items && dataTransfer.items[0] && dataTransfer.items[0].webkitGetAsEntry ) { this.webkitReadDataTransfer(dataTransfer, evt) } else { this.addFiles(dataTransfer.files, evt) } }, webkitReadDataTransfer: function (dataTransfer, evt) { let self = this let queue = dataTransfer.items.length let files = [] utils.each(dataTransfer.items, function (item) { let entry = item.webkitGetAsEntry() if (!entry) { decrement() return } if (entry.isFile) { // due to a bug in Chrome's File System API impl - #149735 fileReadSuccess(item.getAsFile(), entry.fullPath) } else { readDirectory(entry.createReader()) } }) function readDirectory (reader) { reader.readEntries(function (entries) { if (entries.length) { queue += entries.length utils.each(entries, function (entry) { if (entry.isFile) { let fullPath = entry.fullPath entry.file(function (file) { fileReadSuccess(file, fullPath) }, readError) } else if (entry.isDirectory) { readDirectory(entry.createReader()) } }) readDirectory(reader) } else { decrement() } }, readError) } function fileReadSuccess (file, fullPath) { // relative path should not start with "/" file.relativePath = fullPath.substring(1) files.push(file) decrement() } function readError (fileError) { throw fileError } function decrement () { if (--queue === 0) { self.addFiles(files, evt) } } }, _assignHelper: function (domNodes, handles, remove) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes] } let evtMethod = remove ? 'removeEventListener' : 'addEventListener' utils.each( domNodes, function (domNode) { utils.each( handles, function (handler, name) { domNode[evtMethod](name, handler, false) }, this ) }, this ) }, _preventEvent: function (e) { utils.preventEvent(e) this._trigger(e.type, e) }, /** * Assign one or more DOM nodes as a drop target. * @function * @param {Element|Array.<Element>} domNodes */ assignDrop: function (domNodes) { this._onDrop = utils.bind(this.onDrop, this) this._assignHelper(domNodes, { dragover: this.preventEvent, dragenter: this.preventEvent, dragleave: this.preventEvent, drop: this._onDrop }) }, /** * Un-assign drop event from DOM nodes * @function * @param domNodes */ unAssignDrop: function (domNodes) { this._assignHelper( domNodes, { dragover: this.preventEvent, dragenter: this.preventEvent, dragleave: this.preventEvent, drop: this._onDrop }, true ) this._onDrop = null }, // 恢复本地上传记录 restoreLocalRecord (timeout = 1000) { // setTimeout(_ => { recorder.restoreLocalRecord.call(this) }, timeout) }, // 创建单个文件的分块上传记录 _createResumeLog (file) { // recorder.createResumeLog.call(this, file) }, // 添加本地上传记录 _addLocalRecord: function (files = this.uploader.files, fileList = this.uploader.fileList) { // recorder.addLocalRecord.call(this, files, fileList) } }) module.exports = Uploader