Last active
June 20, 2024 15:14
-
-
Save aleksandr-smechov/73159e93a0f5c73af56312e4d4e341ba to your computer and use it in GitHub Desktop.
Django Direct-to-S3 Uploads Vanilla JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const BASE_BACKEND_URL = window.location.protocol + '//' + window.location.host; | |
const fileInput = document.getElementById('fileInput'); | |
const uploadButton = document.getElementById('uploadButton') | |
const closeUploadModalButton = document.getElementById('closeUploadModalButton'); // the upload box is in a modal | |
const progressBar = document.getElementById('uploadProgressBar'); | |
uploadButton.disabled = false; | |
let xhr; | |
const getBaseConfig = (method) => ({ | |
method, | |
credentials: 'include', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Session ${sessionKey}`, // define this in a view and pass it to a template, see comment below for details | |
"X-CSRFToken": csrfToken // define this in a template | |
} | |
}); | |
const directUploadStart = async ({ fileName, fileType }) => { | |
const response = await fetch(`${BASE_BACKEND_URL}/api/files/upload/direct/start/`, { | |
...getBaseConfig('POST'), | |
body: JSON.stringify({ file_name: fileName, file_type: fileType }), | |
}); | |
return response.json(); | |
}; | |
const directUploadDo = ({ data, file, progressCallback }) => { | |
return new Promise((resolve, reject) => { | |
xhr = new XMLHttpRequest(); | |
const postData = new FormData(); | |
for (const key in data.fields) { | |
if (data.fields.hasOwnProperty(key)) { | |
postData.append(key, data.fields[key]); | |
} | |
} | |
postData.append('file', file); | |
xhr.onload = () => { | |
if (xhr.status >= 200 && xhr.status < 300) { | |
console.log('File uploaded successfully:', { fileId: data.id }) | |
notyf.success('File uploaded successfully'); // notyf is an external library, works great: https://github.com/caroso1222/notyf | |
resolve({ fileId: data.id }); | |
} else { | |
reject(new Error(`HTTP error, status: ${xhr.status}`)); | |
notyf.error('File upload failed'); | |
} | |
} | |
xhr.onerror = () => { | |
reject(new Error(`Network error: ${xhr.statusText}`)); | |
notyf.error('File upload failed'); | |
}; | |
if (progressCallback) { | |
xhr.upload.onprogress = (event) => { | |
if (event.lengthComputable) { | |
let percentComplete = (event.loaded / event.total) * 100; // progress bar updates, you can use a Bootstrap 5 progress bar | |
progressCallback(percentComplete); | |
} | |
}; | |
} | |
xhr.open('POST', data.url, true); | |
xhr.send(postData); | |
}); | |
}; | |
const directUploadFinish = async ({ data }) => { | |
const response = await fetch(`${BASE_BACKEND_URL}/api/files/upload/direct/finish/`, { | |
...getBaseConfig('POST'), | |
body: JSON.stringify({ file_id: data.id }) | |
}); | |
try { | |
const startTranscriptionResponse = await fetch(`${BASE_BACKEND_URL}/start-job/`, { | |
...getBaseConfig('POST'), | |
body: JSON.stringify({ | |
file_id: data.id, // send to Django for post-processing | |
}) | |
}); | |
if (startTranscriptionResponse.ok) { | |
return response.json(); | |
} else { | |
throw new Error('Request failed with status ' + response.status); | |
} | |
} catch (error) { | |
notyf.error('Failed to start transcription'); | |
throw new Error('Request failed with status ' + response.status); | |
} | |
}; | |
function updateProgressBar(percent) { | |
progressBar.style.width = `${percent}%`; | |
progressBar.setAttribute('aria-valuenow', percent); | |
if (percent === 100) { | |
progressBar.classList.add('bg-success'); // turn the bar green when it's done! | |
} | |
} | |
const onInputChange = async (event) => { | |
uploadButton.disabled = true; | |
const file = fileInput.files[0]; | |
if (file) { | |
try { | |
fileInput.disabled = true; | |
const startResponse = await directUploadStart({ | |
fileName: file.name, | |
fileType: file.type, | |
}); | |
await directUploadDo({ | |
data: startResponse, | |
file, | |
progressCallback: updateProgressBar, | |
}); | |
await directUploadFinish({ data: startResponse }); | |
fileInput.disabled = false; | |
setTimeout(() => window.location.reload(), 500); | |
} catch (error) { | |
notyf.error('File upload failed'); | |
console.error('File upload failed:', error); | |
fileInput.disabled = false; | |
progressBar.style.width = '0%'; | |
progressBar.setAttribute('aria-valuenow', '0'); | |
} | |
} | |
}; | |
uploadButton.addEventListener('click', onInputChange); | |
closeUploadModalButton.addEventListener('click', () => { // optional, cancels the upload and resets things if you close the modal | |
fileInput.value = ''; | |
if (xhr) { | |
xhr.abort(); | |
notyf.open({ | |
type: 'warning', | |
message: 'File upload canceled', | |
}); | |
} | |
progressBar.style.width = '0%'; | |
progressBar.setAttribute('aria-valuenow', '0'); | |
uploadButton.disabled = false; | |
fileInput.disabled = false; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For the
sessionKey
, I used an encrypted value that contained user details. A custom DRF authentication class then decrypted this and updated a File model accordingly.