UNPKG

oss-uploader.js

Version:
549 lines (504 loc) 15 kB
const utils = require('./utils') const AliOSS = require('ali-oss') const qiniu = require('qiniu-js') function Chunk (uploader, file, offset) { utils.defineNonEnumerable(this, 'uploader', uploader) utils.defineNonEnumerable(this, 'file', file) utils.defineNonEnumerable(this, 'bytes', null) this.offset = offset this.tested = false this.retries = 0 this.pendingRetry = false this.preprocessState = 0 this.readState = 0 this.loaded = 0 this.total = 0 this.chunkSize = this.uploader.opts.chunkSize this.startByte = this.offset * this.chunkSize this.endByte = this.computeEndByte() this.xhr = null this.id = [this.offset, this.startByte, this.endByte].join('-') } const STATUS = { PENDING: 'pending', UPLOADING: 'uploading', READING: 'reading', SUCCESS: 'success', ERROR: 'error', COMPLETE: 'complete', PROGRESS: 'progress', RETRY: 'retry' } Chunk.STATUS = STATUS utils.extend(Chunk.prototype, { _event: function (evt, args) { args = utils.toArray(arguments) args.unshift(this) this.file._chunkEvent.apply(this.file, args) }, computeEndByte: function () { let endByte = Math.min(this.file.size, (this.offset + 1) * this.chunkSize) if ( this.file.size - endByte < this.chunkSize && !this.uploader.opts.forceChunkSize ) { // The last chunk will be bigger than the chunk size, // but less than 2 * this.chunkSize endByte = this.file.size } return endByte }, getParams: function () { return { chunkNumber: this.offset + 1, chunkSize: this.uploader.opts.chunkSize, currentChunkSize: this.endByte - this.startByte, totalSize: this.file.size, identifier: this.file.uniqueIdentifier, filename: this.file.name, relativePath: this.file.relativePath, totalChunks: this.file.chunks.length } }, getTarget: function (target, params) { if (!params.length) { return target } if (target.indexOf('?') < 0) { target += '?' } else { target += '&' } return target + params.join('&') }, test: function () { this.xhr = new XMLHttpRequest() this.xhr.addEventListener('load', testHandler, false) this.xhr.addEventListener('error', testHandler, false) let testMethod = utils.evalOpts( this.uploader.opts.testMethod, this.file, this ) let data = this.prepareXhrRequest(testMethod, true) this.xhr.send(data) let $ = this function testHandler (event) { let status = $.status(true) if (status === STATUS.ERROR) { $._event(status, $.message()) $.uploader.uploadNextChunk() } else if (status === STATUS.SUCCESS) { $._event(status, $.message()) $.tested = true } else if (!$.file.paused) { // Error might be caused by file pause method // Chunks does not exist on the server side $.tested = true $.send() } } }, preprocessFinished: function () { // Compute the endByte after the preprocess function to allow an // implementer of preprocess to set the fileObj size this.endByte = this.computeEndByte() this.preprocessState = 2 this.send() }, readFinished: function (bytes) { this.readState = 2 this.bytes = bytes this.send() }, _aliyunUploadHandler: async function () { // console.log('aliyun send...', this) const $ = this const progressHandler = function (event) { if (event) { $.loaded = event.loaded $.total = event.size } $._event(STATUS.PROGRESS, event) } const doneHandler = function (event) { let msg = $.message() $.processingResponse = true $.uploader.opts.processResponse(msg, function (err, res) { $.processingResponse = false if (!$.xhr) { return } $.processedState = { err: err, res: res } let status = $.status() if (status === STATUS.SUCCESS || status === STATUS.ERROR) { // delete this.data $._event(status, res) status === STATUS.ERROR && $.uploader.uploadNextChunk() } }) } ;(_ => { try { let ossParams = $.file.ossParams let { key, name, options, ossConfig } = ossParams let { progress } = options || {} options = utils.isObject(options) ? options : {} options = utils.extend(options, { progress: function (percent, checkpoint) { // console.log(checkpoint) // console.log('progress', arguments) $.xhr.readyState = 3 progressHandler({ loaded: $.file.size * percent, size: $.file.size }) utils.isFunction(progress) && progress(percent, checkpoint) } }) let file = $.bytes let ossName = key || name || file.name || 'untitiled_' + Date.now() const client = new AliOSS(ossConfig) client.multipartUpload(ossName, file, options).then(result => { // console.log('result', result) if (result && result.etag) { $.xhr.readyState = 4 $.xhr.status = 200 $.xhr.responseText = result doneHandler() } }) $.xhr = { readyState: 1, abort: client.cancel.bind(client) } } catch (e) { // console.error(e) if (e.name !== 'cancel') { console.error(e) if ($.xhr) { $.xhr.responseText = e.message || 'error' $.xhr.readyState = 4 $.xhr.status = 500 } doneHandler() } } })() }, _qiniuUploadHandler: async function () { const $ = this const progressHandler = function (event) { if (event) { $.loaded = event.loaded $.total = event.size } $._event(STATUS.PROGRESS, event) } const doneHandler = function (event) { // console.log('doneHandler', event) let msg = $.message() $.processingResponse = true $.uploader.opts.processResponse(msg, function (err, res) { $.processingResponse = false if (!$.xhr) { return } $.processedState = { err: err, res: res } let status = $.status() if (status === STATUS.SUCCESS || status === STATUS.ERROR) { // delete this.data $._event(status, res) status === STATUS.ERROR && $.uploader.uploadNextChunk() } }) } const observer = { next (res) { $.xhr.readyState = 3 progressHandler(res.total) }, error (err) { console.error(err) $.xhr.readyState = 4 if (err.isRequestError) { $.xhr.status = err.code } else { $.xhr.status = 500 } $.xhr.responseText = err doneHandler() }, complete (res) { // console.log('complete', res) $.xhr.readyState = 4 $.xhr.status = 200 $.xhr.responseText = res doneHandler() } } let ossParams = $.file.ossParams let file = $.bytes let { key, token, putExtra, config } = ossParams const observable = qiniu.upload(file, key, token, putExtra, config) $.xhr = { readyState: 1, abort: _ => 'abort' } const subscription = observable.subscribe(observer) $.xhr.abort = subscription.unsubscribe.bind(subscription) }, _defaultUploadHandler: function () { this.xhr = new XMLHttpRequest() this.xhr.upload.addEventListener('progress', progressHandler, false) this.xhr.addEventListener('load', doneHandler, false) this.xhr.addEventListener('error', doneHandler, false) let uploadMethod = utils.evalOpts( this.uploader.opts.uploadMethod, this.file, this ) let data = this.prepareXhrRequest( uploadMethod, false, this.uploader.opts.method, this.bytes ) this.xhr.send(data) let $ = this function progressHandler (event) { if (event.lengthComputable) { $.loaded = event.loaded $.total = event.total } $._event(STATUS.PROGRESS, event) } function doneHandler (event) { let msg = $.message() $.processingResponse = true $.uploader.opts.processResponse(msg, function (err, res) { $.processingResponse = false if (!$.xhr) { return } $.processedState = { err: err, res: res } let status = $.status() if (status === STATUS.SUCCESS || status === STATUS.ERROR) { // delete this.data $._event(status, res) status === STATUS.ERROR && $.uploader.uploadNextChunk() // TODO status === STATUS.SUCCESS && $._resumeRecord() } else { $._event(STATUS.RETRY, res) $.pendingRetry = true $.abort() $.retries++ let retryInterval = $.uploader.opts.chunkRetryInterval if (retryInterval !== null) { setTimeout(function () { $.send() }, retryInterval) } else { $.send() } } }) } }, _tencentUploadHandler: function () { // TODO console.log('TODO') }, send: function () { this.loaded = 0 this.total = 0 this.pendingRetry = false this.xhr = {} // preprocessState let preprocess = this.uploader.opts.preprocess let read = this.uploader.opts.readFileFn if (utils.isFunction(preprocess)) { switch (this.preprocessState) { case 0: this.preprocessState = 1 preprocess(this) return case 1: return } } let oss = 'oss' in this.file.opts ? this.file.opts.oss : this.uploader.opts.oss let uploaderFnName switch (oss) { case 'qiniu': // uploaderFnName = '_qiniuResumeUploadHandler' uploaderFnName = '_qiniuUploadHandler' break case 'aliyun': uploaderFnName = '_aliyunUploadHandler' break case 'tencent': uploaderFnName = '_tencentUploadHandler' break default: uploaderFnName = '_defaultUploadHandler' break } if (this.readState === 2 && !this.bytes) { this.readState = 0 } switch (this.readState) { case 0: this.readState = 1 if (uploaderFnName === '_qiniuResumeUploadHandler') { this.readFinished(this.file.file) } else { read(this.file, this.file.fileType, this.startByte, this.endByte, this) } return case 1: return } if (this.uploader.opts.testChunks && !this.tested) { this.test() return } // console.log('uploaderFnName', uploaderFnName) utils.isFunction(this[uploaderFnName]) && this[uploaderFnName]() }, abort: function () { let xhr = this.xhr xhr && xhr.abort() this.xhr = xhr = null this.processingResponse = false this.processedState = null this.xhr && this.xhr.abort() }, status: function (isTest) { if (this.readState === 1) { return STATUS.READING } else if (this.pendingRetry || this.preprocessState === 1) { // if pending retry then that's effectively the same as actively uploading, // there might just be a slight delay before the retry starts return STATUS.UPLOADING } else if (!this.xhr) { return STATUS.PENDING } else if (this.xhr.readyState < 4 || this.processingResponse) { // Status is really 'OPENED', 'HEADERS_RECEIVED' // or 'LOADING' - meaning that stuff is happening return STATUS.UPLOADING } else { let _status if (this.uploader.opts.successStatuses.indexOf(this.xhr.status) > -1) { // HTTP 200, perfect // HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed. _status = STATUS.SUCCESS } else if ( this.uploader.opts.permanentErrors.indexOf(this.xhr.status) > -1 || (!isTest && this.retries >= this.uploader.opts.maxChunkRetries) ) { // HTTP 415/500/501, permanent error _status = STATUS.ERROR } else { // this should never happen, but we'll reset and queue a retry // a likely case for this would be 503 service unavailable this.abort() _status = STATUS.PENDING } let processedState = this.processedState if (processedState && processedState.err) { _status = STATUS.ERROR } return _status } }, message: function () { return this.xhr ? this.xhr.responseText : '' }, progress: function () { if (this.pendingRetry) { return 0 } let s = this.status() if (s === STATUS.SUCCESS || s === STATUS.ERROR) { return 1 } else { return this.total > 0 ? this.loaded / this.total : 0 } // if (s === STATUS.SUCCESS || s === STATUS.ERROR) { // return 1 // } else if (s === STATUS.PENDING) { // return 0 // } else { // return this.total > 0 ? this.loaded / this.total : 0 // } }, sizeUploaded: function () { let size = this.endByte - this.startByte // can't return only chunk.loaded value, because it is bigger than chunk size if (this.status() !== STATUS.SUCCESS) { size = this.progress() * size } return size }, prepareXhrRequest: function (method, isTest, paramsMethod, blob) { // Add data from the query options let query = utils.evalOpts( this.uploader.opts.query, this.file, this, isTest ) query = utils.extend(this.getParams(), query) // processParams query = this.uploader.opts.processParams(query) let target = utils.evalOpts( this.uploader.opts.target, this.file, this, isTest ) let data = null if (method === 'GET' || paramsMethod === 'octet') { // Add data from the query options let params = [] utils.each(query, function (v, k) { params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')) }) target = this.getTarget(target, params) data = blob || null } else { // Add data from the query options data = new FormData() utils.each(query, function (v, k) { data.append(k, v) }) blob && data.append(this.uploader.opts.fileParameterName, blob, this.file.name) } this.xhr.open(method, target, true) this.xhr.withCredentials = this.uploader.opts.withCredentials // Add data from header options utils.each( utils.evalOpts(this.uploader.opts.headers, this.file, this, isTest), function (v, k) { this.xhr.setRequestHeader(k, v) }, this ) return data }, // 记录分块上传记录 _resumeRecord () { } }) module.exports = Chunk