Skip to content

Instantly share code, notes, and snippets.

@aleksandr-smechov
Last active June 20, 2024 15:14
Show Gist options
  • Save aleksandr-smechov/73159e93a0f5c73af56312e4d4e341ba to your computer and use it in GitHub Desktop.
Save aleksandr-smechov/73159e93a0f5c73af56312e4d4e341ba to your computer and use it in GitHub Desktop.
Django Direct-to-S3 Uploads Vanilla JS
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;
});
@aleksandr-smechov
Copy link
Author

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.

@aleksandr-smechov
Copy link
Author

I used notyf for notifications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment