transbeam/static/transbeam.js

336 lines
10 KiB
JavaScript

const FILE_CHUNK_SIZE = 16384;
const MAX_FILES = 256;
let files = [];
let socket;
let fileIndex = 0;
let byteIndex = 0;
let bytesSent = 0;
let totalBytes = 0;
let maxSize = null;
let uploadPassword;
let messageBox;
let fileInput;
let fileList;
let uploadButton;
let lifetimeInput;
let downloadCode;
let progress;
let progressBar;
document.addEventListener('DOMContentLoaded', () => {
document.body.className = "landing";
messageBox = document.getElementById('message');
fileInput = document.getElementById('file_input');
fileList = document.getElementById('file_list');
uploadButton = document.getElementById('upload_button');
lifetimeInput = document.getElementById('lifetime');
downloadCode = document.getElementById('download_code');
progress = document.getElementById('progress');
progressBar = document.getElementById('progress_bar');
const uploadPasswordInput = document.getElementById('upload_password');
const uploadPasswordForm = document.getElementById('upload_password_form');
uploadPasswordForm.addEventListener('submit', (e) => {
e.preventDefault();
uploadPassword = uploadPasswordInput.value;
fetch('/upload/check_password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: uploadPassword }),
}).then((res) => {
if (res.ok) {
updateFiles();
} else {
messageBox.textContent = (res.status === 403) ? 'Incorrect password' : 'An error occurred';
uploadPasswordInput.value = '';
document.body.className = 'error landing';
}
});
});
fileInput.addEventListener('input', () => {
for (const file of fileInput.files) { addFile(file); }
updateFiles();
fileInput.value = '';
});
uploadButton.addEventListener('click', beginUpload);
const downloadCodeContainer = document.getElementById('download_code_container');
downloadCodeContainer.addEventListener('click', () => {
const downloadUrl = new URL(`download?code=${downloadCode.textContent}`, window.location);
navigator.clipboard.writeText(downloadUrl.href);
downloadCodeContainer.className = 'copied';
});
downloadCodeContainer.addEventListener('mouseleave', () => {
downloadCodeContainer.className = '';
});
const downloadCodeInput = document.getElementById('download_code_input');
const downloadButton = document.getElementById('download_button');
const downloadForm = document.getElementById('download_form');
downloadCodeInput.addEventListener('beforeinput', (e) => {
if (/^[a-zA-Z0-9-]+$/.test(e.data)) { return; }
e.preventDefault();
if (e.data === ' ') {
downloadCodeInput.value += '-';
}
});
const disableEnableDownload = () => { downloadButton.disabled = (downloadCodeInput.value === ''); };
disableEnableDownload();
downloadCodeInput.addEventListener('input', disableEnableDownload);
downloadForm.addEventListener('submit', (e) => {
if (downloadCodeInput.value === '') {
e.preventDefault();
} else {
setTimeout(() => {
downloadCodeInput.value = '';
downloadButton.disabled = true;
}, 0);
}
});
});
function updateFiles() {
const fileInputMessage = document.getElementById('file_input_message');
totalBytes = files.reduce((acc, file) => acc + file.size, 0);
fileInput.disabled = (files.length >= MAX_FILES || (maxSize !== null && totalBytes >= maxSize));
let extraClasses = '';
if (maxSize !== null && totalBytes > maxSize) {
uploadButton.disabled = true;
displayError(`The maximum size for uploads is ${displaySize(maxSize)}`);
extraClasses = ' error';
} else {
uploadButton.disabled = false;
}
if (files.length === 0) {
fileInputMessage.textContent = 'Select files to upload...';
document.body.className = 'no_files selecting' + extraClasses;
} else {
fileInputMessage.textContent = 'Select more files to upload...';
uploadButton.textContent = `Upload ${files.length} file${files.length > 1 ? 's' : ''} (${displaySize(totalBytes)})`;
document.body.className = 'selecting' + extraClasses;
}
}
function addFile(newFile) {
if (files.length >= MAX_FILES) { return; }
if (files.some((oldFile) => newFile.name === oldFile.name)) { return; }
files.push(newFile);
addListEntry(newFile);
}
function addListEntry(file) {
const listEntry = document.createElement('tr');
const deleteButtonCell = document.createElement('td');
deleteButtonCell.className = 'delete_button';
deleteButtonCell.addEventListener('click', () => {
removeFile(file.name);
listEntry.remove();
updateFiles();
});
const sizeCell = document.createElement('td');
sizeCell.className = 'file_size';
sizeCell.textContent = displaySize(file.size);
const nameCell = document.createElement('td');
nameCell.className = 'file_name';
nameCell.textContent = file.name;
listEntry.appendChild(deleteButtonCell);
listEntry.appendChild(sizeCell);
listEntry.appendChild(nameCell);
fileList.appendChild(listEntry);
}
function removeFile(name) {
files = files.filter((file) => file.name !== name);
}
function beginUpload() {
if (files.length === 0 || files.length > MAX_FILES) { return; }
fileIndex = 0;
byteIndex = 0;
bytesSent = 0;
let websocketUrl = new URL('upload', window.location);
websocketUrl.protocol = (window.location.protocol === 'http:') ? 'ws:' : 'wss:';
socket = new WebSocket(websocketUrl);
socket.addEventListener('open', sendManifest);
socket.addEventListener('message', handleMessage);
socket.addEventListener('close', handleClose);
}
function sendManifest() {
const lifetime = parseInt(lifetimeInput.value);
const fileMetadata = files.map((file) => ({
name: file.name,
size: file.size,
modtime: file.lastModified,
}));
socket.send(JSON.stringify({
files: fileMetadata,
lifetime,
password: uploadPassword,
}));
}
function handleMessage(msg) {
if (bytesSent === 0) {
let reply;
try {
reply = JSON.parse(msg.data);
} catch (error) {
socket.close();
displayError('Received an invalid response from the server');
console.error(error);
return;
}
if (reply.type === 'ready') {
downloadCode.textContent = reply.code;
updateProgress();
document.body.className = 'uploading';
sendData();
return;
}
// we're going to display a more useful error message
socket.removeEventListener('close', handleClose);
socket.close();
if (reply.type === 'too_big') {
maxSize = reply.max_size;
updateFiles();
} else if (reply.type === 'too_long') {
let options = Array.from(lifetimeInput.options);
options.reverse();
for (const option of options) {
if (option.value > reply.max_days) {
option.disabled = true;
} else {
option.selected = true;
break;
}
}
displayError(`The maximum retention time for uploads is ${reply.max_days} days`);
} else if (reply.type === 'incorrect_password') {
messageBox.textContent = ('Incorrect password');
document.body.className = 'error landing';
} else if (reply.type === 'error') {
displayError(reply.details);
}
} else {
if (msg.data === 'ack') {
sendData();
} else {
console.error('Received unexpected message from server instead of ack', msg.data);
displayError();
socket.close();
}
}
}
function sendData() {
if (fileIndex >= files.length) {
finishSending();
return;
}
const currentFile = files[fileIndex];
if (byteIndex < currentFile.size) {
const endpoint = Math.min(byteIndex+FILE_CHUNK_SIZE, currentFile.size);
const data = currentFile.slice(byteIndex, endpoint);
socket.send(data);
byteIndex = endpoint;
bytesSent += data.size;
updateProgress();
} else {
fileIndex += 1;
byteIndex = 0;
sendData();
}
}
function updateProgress() {
let percentage;
if (totalBytes === 0) {
percentage = "0%";
} else {
percentage = `${(bytesSent*100/totalBytes).toFixed(1)}%`;
}
progress.textContent = `${percentage} (${displaySize(bytesSent)}/${displaySize(totalBytes)})`;
progressBar.style.backgroundSize = percentage;
const fileEntries = Array.from(fileList.children);
for (entry of fileEntries.slice(0, fileIndex)) {
entry.style.backgroundSize = "100%";
}
if (fileIndex < files.length) {
const currentFile = files[fileIndex];
if (currentFile.size > 0) {
fileEntries[fileIndex].style.backgroundSize = `${(byteIndex*100/currentFile.size)}%`;
}
}
}
function handleClose(e) {
console.log('Websocket closed', e);
if (fileIndex >= files.length) {
displayCompletion();
} else {
let error;
if (e.code === 1011) {
if (e.reason) {
error = `Server error: ${e.reason}`;
} else {
error = "A server error has occurred."
}
} else if (e.reason) {
error = e.reason;
}
displayError(error);
}
}
function finishSending() {
if (socket.bufferedAmount > 0) {
setTimeout(finishSending, 1000);
return;
}
socket.close();
displayCompletion();
}
function displayCompletion() {
messageBox.textContent = 'Upload complete!';
document.body.className = 'completed';
removePerFileProgressBars(); // completed file backgrounds are handled in CSS
}
function displayError(error) {
messageBox.textContent = error || 'An error has occurred.';
document.body.className = 'selecting error';
removePerFileProgressBars();
}
function removePerFileProgressBars() {
const fileEntries = Array.from(fileList.children);
for (entry of fileEntries) {
entry.style.backgroundSize = "0%";
}
}