Changeset View
Changeset View
Standalone View
Standalone View
src/resources/js/files.js
- This file was added.
function FileAPI(params = {}) | |||||
{ | |||||
// Note that chunk size here is only body, Swoole's package_max_length is body + headers, | |||||
// so don't forget to subtract some margin (e.g. 8KB) | |||||
// FIXME: From my preliminary tests it looks like on the PHP side you need | |||||
// about 3-4 times as much memory as the request size when using Swoole | |||||
// (only 1 time without Swoole). And I didn't find a way to lower the memory usage, | |||||
// it looks like it happens before we even start to process the request in FilesController. | |||||
const maxChunkSize = params.maxChunkSize || 10 * 1024 * 1024 - 1024 * 8 | |||||
const area = $(params.dropArea) | |||||
// Add hidden input to the drop area, so we can handle upload by click | |||||
const input = $('<input>') | |||||
.attr({type: 'file', multiple: true, style: 'visibility: hidden'}) | |||||
.on('change', event => { fileDropHandler(event) }) | |||||
.appendTo(area) | |||||
.get(0) | |||||
// Register events on the upload area element | |||||
area.on('click', () => input.click()) | |||||
.on('drop', event => fileDropHandler(event)) | |||||
.on('dragenter dragleave drop', event => fileDragHandler(event)) | |||||
.on('dragover', event => event.preventDefault()) // prevents file from being opened on drop) | |||||
// Handle dragging on the whole page, so we can style the area in a different way | |||||
$(document.documentElement).off('.fileapi') | |||||
.on('dragenter.fileapi dragleave.fileapi', event => area.toggleClass('dragactive')) | |||||
// Handle dragging file(s) - style the upload area element | |||||
const fileDragHandler = (event) => { | |||||
if (event.type == 'drop') { | |||||
area.removeClass('dragover dragactive') | |||||
} else { | |||||
area[event.type == 'dragenter' ? 'addClass' : 'removeClass']('dragover') | |||||
} | |||||
} | |||||
// Handler for both a ondrop event and file input onchange event | |||||
const fileDropHandler = (event) => { | |||||
let files = event.target.files || event.dataTransfer.files | |||||
if (!files || !files.length) { | |||||
return | |||||
} | |||||
// Prevent default behavior (prevent file from being opened on drop) | |||||
event.preventDefault(); | |||||
// TODO: Check file size limit, limit number of files to upload at once? | |||||
// For every file... | |||||
for (const file of files) { | |||||
const progress = { | |||||
id: Date.now(), | |||||
name: file.name, | |||||
total: file.size, | |||||
completed: 0 | |||||
} | |||||
file.uploaded = 0 | |||||
// Upload request configuration | |||||
const config = { | |||||
onUploadProgress: progressEvent => { | |||||
progress.completed = Math.round(((file.uploaded + progressEvent.loaded) * 100) / file.size) | |||||
// Don't trigger the event when 100% of the file has been sent | |||||
// We need to wait until the request response is available, then | |||||
// we'll trigger it (see below where the axios request is created) | |||||
if (progress.completed < 100) { | |||||
params.eventHandler('upload-progress', progress) | |||||
} | |||||
}, | |||||
headers: { | |||||
'Content-Type': file.type | |||||
}, | |||||
params: { name: file.name }, | |||||
maxBodyLength: -1, // no limit | |||||
timeout: 0, // no limit | |||||
transformRequest: [] // disable body transformation | |||||
} | |||||
// FIXME: It might be better to handle multiple-files upload as a one | |||||
// "progress source", i.e. we might want to refresh the files list once | |||||
// all files finished uploading, not multiple times in the middle | |||||
// of the upload process. | |||||
params.eventHandler('upload-progress', progress) | |||||
// A "recursive" function to upload the file in chunks (if needed) | |||||
const uploadFn = (start = 0, uploadId) => { | |||||
let body = file | |||||
if (file.size > maxChunkSize) { | |||||
body = file.slice(start, start + maxChunkSize, file.type) | |||||
if (uploadId) { | |||||
config.params.upload = uploadId | |||||
config.params.from = start | |||||
} else { | |||||
config.params.upload = 'resumable' | |||||
config.params.size = file.size | |||||
} | |||||
} | |||||
start += maxChunkSize | |||||
axios.post('api/v4/files', body, config) | |||||
.then(response => { | |||||
if (start < file.size) { | |||||
file.uploaded = start | |||||
uploadFn(start, response.data.uploadId) | |||||
} else { | |||||
progress.completed = 100 | |||||
params.eventHandler('upload-progress', progress) | |||||
} | |||||
}) | |||||
.catch(error => { | |||||
// TODO: The process might get stopped if the authentication token expires | |||||
// in the middle of the upload process, we have to detect 401 response, | |||||
// refresh the token and continue with the last chunk that failed. | |||||
// Related setting: OAUTH_TOKEN_EXPIRY | |||||
// console.error(error) | |||||
progress.error = error | |||||
progress.completed = 100 | |||||
params.eventHandler('upload-progress', progress) | |||||
}) | |||||
} | |||||
// Start uploading | |||||
uploadFn() | |||||
} | |||||
} | |||||
/** | |||||
* Convert file size as a number of bytes to a human-readable format | |||||
*/ | |||||
this.sizeText = (bytes) => { | |||||
if (bytes >= 1073741824) | |||||
return parseFloat(bytes/1073741824).toFixed(2) + ' GB'; | |||||
if (bytes >= 1048576) | |||||
return parseFloat(bytes/1048576).toFixed(2) + ' MB'; | |||||
if (bytes >= 1024) | |||||
return parseInt(bytes/1024) + ' kB'; | |||||
return parseInt(bytes || 0) + ' B'; | |||||
} | |||||
} | |||||
export default FileAPI |