You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vue-upload-component/src/chunk/ChunkUploadHandler.js

353 lines
7.3 KiB

import {
default as request,
createRequest,
sendFormRequest
} from '../utils/request'
export default class ChunkUploadHandler {
/**
* Constructor
*
* @param {File} file
* @param {Object} options
*/
constructor (file, options) {
this.file = file
this.options = options
}
/**
* Gets the max retries from options
*/
get maxRetries () {
return parseInt(this.options.maxRetries)
}
/**
* Gets the max number of active chunks being uploaded at once from options
*/
get maxActiveChunks () {
return parseInt(this.options.maxActive)
}
/**
* Gets the file type
*/
get fileType () {
return this.file.type
}
/**
* Gets the file size
*/
get fileSize () {
return this.file.size
}
/**
* Gets action (url) to upload the file
*/
get action () {
return this.options.action || null
}
/**
* Gets the body to be merged when sending the request in start phase
*/
get startBody () {
return this.options.startBody || {}
}
/**
* Gets the body to be merged when sending the request in upload phase
*/
get uploadBody () {
return this.options.uploadBody || {}
}
/**
* Gets the body to be merged when sending the request in finish phase
*/
get finishBody () {
return this.options.finishBody || {}
}
/**
* Gets the headers of the requests from options
*/
get headers () {
return this.options.headers || {}
}
/**
* Whether it's ready to upload files or not
*/
get readyToUpload () {
return !!this.chunks
}
/**
* Gets the progress of the chunk upload
* - Gets all the completed chunks
* - Gets the progress of all the chunks that are being uploaded
*/
get progress () {
const completedProgress = (this.chunksUploaded.length / this.chunks.length) * 100
const uploadingProgress = this.chunksUploading.reduce((progress, chunk) => {
return progress + ((chunk.progress | 0) / this.chunks.length)
}, 0)
return Math.min(completedProgress + uploadingProgress, 100)
}
/**
* Gets all the chunks that are pending to be uploaded
*/
get chunksToUpload () {
return this.chunks.filter(chunk => {
return !chunk.active && !chunk.uploaded
})
}
/**
* Whether there are chunks to upload or not
*/
get hasChunksToUpload () {
return this.chunksToUpload.length > 0
}
/**
* Gets all the chunks that are uploading
*/
get chunksUploading () {
return this.chunks.filter(chunk => {
return !!chunk.xhr && !!chunk.active
})
}
/**
* Gets all the chunks that have finished uploading
*/
get chunksUploaded () {
return this.chunks.filter(chunk => {
return !!chunk.uploaded
})
}
/**
* Creates all the chunks in the initial state
*/
createChunks () {
this.chunks = []
let start = 0
let end = this.chunkSize
while (start < this.fileSize) {
this.chunks.push({
blob: this.file.file.slice(start, end),
startOffset: start,
active: false,
retries: this.maxRetries
})
start = end
end = start + this.chunkSize
}
}
/**
* Updates the progress of the file with the handler's progress
*/
updateFileProgress () {
this.file.progress = this.progress
}
/**
* Paues the upload process
* - Stops all active requests
* - Sets the file not active
*/
pause () {
this.file.active = false
this.stopChunks()
}
/**
* Stops all the current chunks
*/
stopChunks () {
this.chunksUploading.forEach(chunk => {
chunk.xhr.abort()
chunk.active = false
})
}
/**
* Resumes the file upload
* - Sets the file active
* - Starts the following chunks
*/
resume () {
this.file.active = true
this.startChunking()
}
/**
* Starts the file upload
*
* @returns Promise
* - resolve The file was uploaded
* - reject The file upload failed
*/
upload () {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
this.start()
return this.promise
}
/**
* Start phase
* Sends a request to the backend to initialise the chunks
*/
start () {
request({
method: 'POST',
headers: Object.assign({}, this.headers, {
'Content-Type': 'application/json'
}),
url: this.action,
body: Object.assign(this.startBody, {
phase: 'start',
mime_type: this.fileType,
size: this.fileSize
})
}).then(res => {
if (res.status !== 'success') {
this.file.response = res
return this.reject('server')
}
this.sessionId = res.data.session_id
this.chunkSize = res.data.end_offset
this.createChunks()
this.startChunking()
}).catch(res => {
this.file.response = res
this.reject('server')
})
}
/**
* Starts to upload chunks
*/
startChunking () {
for (let i = 0; i < this.maxActiveChunks; i++) {
this.uploadNextChunk()
}
}
/**
* Uploads the next chunk
* - Won't do anything if the process is paused
* - Will start finish phase if there are no more chunks to upload
*/
uploadNextChunk () {
if (this.file.active) {
if (this.hasChunksToUpload) {
return this.uploadChunk(this.chunksToUpload[0])
}
if (this.chunksUploading.length === 0) {
return this.finish()
}
}
}
/**
* Uploads a chunk
* - Sends the chunk to the backend
* - Sets the chunk as uploaded if everything went well
* - Decreases the number of retries if anything went wrong
* - Fails if there are no more retries
*
* @param {Object} chunk
*/
uploadChunk (chunk) {
chunk.progress = 0
chunk.active = true
this.updateFileProgress()
chunk.xhr = createRequest({
method: 'POST',
headers: this.headers,
url: this.action
})
chunk.xhr.upload.addEventListener('progress', function (evt) {
if (evt.lengthComputable) {
chunk.progress = Math.round(evt.loaded / evt.total * 100)
}
}, false)
sendFormRequest(chunk.xhr, Object.assign(this.uploadBody, {
phase: 'upload',
session_id: this.sessionId,
start_offset: chunk.startOffset,
chunk: chunk.blob
})).then(res => {
chunk.active = false
if (res.status === 'success') {
chunk.uploaded = true
} else {
if (chunk.retries-- <= 0) {
this.stopChunks()
return this.reject('upload')
}
}
this.uploadNextChunk()
}).catch(() => {
chunk.active = false
if (chunk.retries-- <= 0) {
this.stopChunks()
return this.reject('upload')
}
this.uploadNextChunk()
})
}
/**
* Finish phase
* Sends a request to the backend to finish the process
*/
finish () {
this.updateFileProgress()
request({
method: 'POST',
headers: Object.assign({}, this.headers, {
'Content-Type': 'application/json'
}),
url: this.action,
body: Object.assign(this.finishBody, {
phase: 'finish',
session_id: this.sessionId
})
}).then(res => {
this.file.response = res
if (res.status !== 'success') {
return this.reject('server')
}
this.resolve(res)
}).catch(res => {
this.file.response = res
this.reject('server')
})
}
}