|
@@ -0,0 +1,291 @@
|
|
|
+/**
|
|
|
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
|
|
|
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * @module cloud-services-core/uploadgateway/fileuploader
|
|
|
+ */
|
|
|
+
|
|
|
+/* globals XMLHttpRequest, FormData, Blob, atob */
|
|
|
+
|
|
|
+import mix from '@ckeditor/ckeditor5-utils/src/mix'
|
|
|
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'
|
|
|
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'
|
|
|
+
|
|
|
+const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/
|
|
|
+
|
|
|
+/**
|
|
|
+ * FileUploader class used to upload single file.
|
|
|
+ */
|
|
|
+export default class FileUploader {
|
|
|
+ /**
|
|
|
+ * Creates `FileUploader` instance.
|
|
|
+ *
|
|
|
+ * @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
|
|
|
+ * @param {module:cloud-services-core/token~Token} token Token used for authentication.
|
|
|
+ * @param {String} apiAddress API address.
|
|
|
+ */
|
|
|
+ constructor(fileOrData, token, apiAddress) {
|
|
|
+ if (!fileOrData) {
|
|
|
+ /**
|
|
|
+ * File must be provided as the first argument.
|
|
|
+ *
|
|
|
+ * @error fileuploader-missing-file
|
|
|
+ */
|
|
|
+ throw new CKEditorError('fileuploader-missing-file: File must be provided as the first argument', null)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!token) {
|
|
|
+ /**
|
|
|
+ * Token must be provided as the second argument.
|
|
|
+ *
|
|
|
+ * @error fileuploader-missing-token
|
|
|
+ */
|
|
|
+ throw new CKEditorError('fileuploader-missing-token: Token must be provided as the second argument.', null)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!apiAddress) {
|
|
|
+ /**
|
|
|
+ * Api address must be provided as the third argument.
|
|
|
+ *
|
|
|
+ * @error fileuploader-missing-api-address
|
|
|
+ */
|
|
|
+ throw new CKEditorError('fileuploader-missing-api-address: Api address must be provided as the third argument.', null)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A file that is being uploaded.
|
|
|
+ *
|
|
|
+ * @type {Blob}
|
|
|
+ */
|
|
|
+ this.file = _isBase64(fileOrData) ? _base64ToBlob(fileOrData) : fileOrData
|
|
|
+
|
|
|
+ /**
|
|
|
+ * CKEditor Cloud Services access token.
|
|
|
+ *
|
|
|
+ * @type {module:cloud-services-core/token~Token}
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ this._token = token
|
|
|
+
|
|
|
+ /**
|
|
|
+ * CKEditor Cloud Services API address.
|
|
|
+ *
|
|
|
+ * @type {String}
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ this._apiAddress = apiAddress
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Registers callback on `progress` event.
|
|
|
+ *
|
|
|
+ * @chainable
|
|
|
+ * @param {Function} callback
|
|
|
+ * @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader}
|
|
|
+ */
|
|
|
+ onProgress(callback) {
|
|
|
+ this.on('progress', (event, data) => callback(data))
|
|
|
+
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Registers callback on `error` event. Event is called once when error occurs.
|
|
|
+ *
|
|
|
+ * @chainable
|
|
|
+ * @param {Function} callback
|
|
|
+ * @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader}
|
|
|
+ */
|
|
|
+ onError(callback) {
|
|
|
+ this.once('error', (event, data) => callback(data))
|
|
|
+
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Aborts upload process.
|
|
|
+ */
|
|
|
+ abort() {
|
|
|
+ this.xhr.abort()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sends XHR request to API.
|
|
|
+ *
|
|
|
+ * @chainable
|
|
|
+ * @returns {Promise.<Object>}
|
|
|
+ */
|
|
|
+ send() {
|
|
|
+ this._prepareRequest()
|
|
|
+ this._attachXHRListeners()
|
|
|
+
|
|
|
+ return this._sendRequest()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Prepares XHR request.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ _prepareRequest() {
|
|
|
+ const xhr = new XMLHttpRequest()
|
|
|
+
|
|
|
+ xhr.open('POST', this._apiAddress)
|
|
|
+ xhr.setRequestHeader('Authorization', this._token.value)
|
|
|
+ xhr.responseType = 'json'
|
|
|
+
|
|
|
+ this.xhr = xhr
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Attaches listeners to the XHR.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ _attachXHRListeners() {
|
|
|
+ const that = this
|
|
|
+ const xhr = this.xhr
|
|
|
+
|
|
|
+ xhr.addEventListener('error', onError('Network Error'))
|
|
|
+ xhr.addEventListener('abort', onError('Abort'))
|
|
|
+
|
|
|
+ /* istanbul ignore else */
|
|
|
+ if (xhr.upload) {
|
|
|
+ xhr.upload.addEventListener('progress', event => {
|
|
|
+ if (event.lengthComputable) {
|
|
|
+ this.fire('progress', {
|
|
|
+ total: event.total,
|
|
|
+ uploaded: event.loaded
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ xhr.addEventListener('load', () => {
|
|
|
+ const statusCode = xhr.status
|
|
|
+ const xhrResponse = xhr.response
|
|
|
+
|
|
|
+ if (statusCode < 200 || statusCode > 299) {
|
|
|
+ return this.fire('error', xhrResponse.message || xhrResponse.error)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ function onError(message) {
|
|
|
+ return () => that.fire('error', message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sends XHR request.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ _sendRequest() {
|
|
|
+ const formData = new FormData()
|
|
|
+ const xhr = this.xhr
|
|
|
+
|
|
|
+ formData.append('file', this.file)
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ xhr.addEventListener('load', () => {
|
|
|
+ const statusCode = xhr.status
|
|
|
+ const xhrResponse = xhr.response
|
|
|
+
|
|
|
+ if (statusCode < 200 || statusCode > 299) {
|
|
|
+ if (xhrResponse.message) {
|
|
|
+ /**
|
|
|
+ * Uploading file failed.
|
|
|
+ *
|
|
|
+ * @error fileuploader-uploading-data-failed
|
|
|
+ */
|
|
|
+ return reject(new CKEditorError(
|
|
|
+ 'fileuploader-uploading-data-failed: Uploading file failed.',
|
|
|
+ this,
|
|
|
+ { message: xhrResponse.message }
|
|
|
+ ))
|
|
|
+ }
|
|
|
+
|
|
|
+ return reject(xhrResponse.error)
|
|
|
+ }
|
|
|
+
|
|
|
+ return resolve(xhrResponse)
|
|
|
+ })
|
|
|
+
|
|
|
+ xhr.addEventListener('error', () => reject(new Error('Network Error')))
|
|
|
+ xhr.addEventListener('abort', () => reject(new Error('Abort')))
|
|
|
+
|
|
|
+ xhr.send(formData)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fired when error occurs.
|
|
|
+ *
|
|
|
+ * @event error
|
|
|
+ * @param {String} error Error message
|
|
|
+ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fired on upload progress.
|
|
|
+ *
|
|
|
+ * @event progress
|
|
|
+ * @param {Object} status Total and uploaded status
|
|
|
+ */
|
|
|
+}
|
|
|
+
|
|
|
+mix(FileUploader, EmitterMixin)
|
|
|
+
|
|
|
+/**
|
|
|
+ * Transforms Base64 string data into file.
|
|
|
+ *
|
|
|
+ * @param {String} base64 String data.
|
|
|
+ * @param {Number} [sliceSize=512]
|
|
|
+ * @returns {Blob}
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+function _base64ToBlob(base64, sliceSize = 512) {
|
|
|
+ try {
|
|
|
+ const contentType = base64.match(BASE64_HEADER_REG_EXP)[ 1 ]
|
|
|
+ const base64Data = atob(base64.replace(BASE64_HEADER_REG_EXP, ''))
|
|
|
+
|
|
|
+ const byteArrays = []
|
|
|
+
|
|
|
+ for (let offset = 0; offset < base64Data.length; offset += sliceSize) {
|
|
|
+ const slice = base64Data.slice(offset, offset + sliceSize)
|
|
|
+ const byteNumbers = new Array(slice.length)
|
|
|
+
|
|
|
+ for (let i = 0; i < slice.length; i++) {
|
|
|
+ byteNumbers[ i ] = slice.charCodeAt(i)
|
|
|
+ }
|
|
|
+
|
|
|
+ byteArrays.push(new Uint8Array(byteNumbers))
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Blob(byteArrays, { type: contentType })
|
|
|
+ } catch (error) {
|
|
|
+ /**
|
|
|
+ * Problem with decoding Base64 image data.
|
|
|
+ *
|
|
|
+ * @error fileuploader-decoding-image-data-error
|
|
|
+ */
|
|
|
+ throw new CKEditorError('fileuploader-decoding-image-data-error: Problem with decoding Base64 image data.', null)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Checks that string is Base64.
|
|
|
+ *
|
|
|
+ * @param {String} string
|
|
|
+ * @returns {Boolean}
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+function _isBase64(string) {
|
|
|
+ if (typeof string !== 'string') {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ const match = string.match(BASE64_HEADER_REG_EXP)
|
|
|
+ return !!(match && match.length)
|
|
|
+}
|