Compare commits
2 commits
b33bb7aa9d
...
511bd741dd
Author | SHA1 | Date | |
---|---|---|---|
xenofem | 511bd741dd | ||
xenofem | 99b93b4e7d |
|
@ -22,8 +22,6 @@ enum Error {
|
||||||
Parse(#[from] serde_json::Error),
|
Parse(#[from] serde_json::Error),
|
||||||
#[error("Error writing to stored file")]
|
#[error("Error writing to stored file")]
|
||||||
Storage(#[from] std::io::Error),
|
Storage(#[from] std::io::Error),
|
||||||
#[error("Time formatting error")]
|
|
||||||
TimeFormat(#[from] time::error::Format),
|
|
||||||
#[error("Duplicate filename could not be deduplicated")]
|
#[error("Duplicate filename could not be deduplicated")]
|
||||||
DuplicateFilename,
|
DuplicateFilename,
|
||||||
#[error("This message type was not expected at this stage")]
|
#[error("This message type was not expected at this stage")]
|
||||||
|
@ -45,8 +43,7 @@ enum Error {
|
||||||
impl Error {
|
impl Error {
|
||||||
fn close_code(&self) -> CloseCode {
|
fn close_code(&self) -> CloseCode {
|
||||||
match self {
|
match self {
|
||||||
Self::Storage(_)
|
Self::Storage(_) => CloseCode::Error,
|
||||||
| Self::TimeFormat(_) => CloseCode::Error,
|
|
||||||
Self::Parse(_)
|
Self::Parse(_)
|
||||||
| Self::UnexpectedMessageType
|
| Self::UnexpectedMessageType
|
||||||
| Self::ClosedEarly(_)
|
| Self::ClosedEarly(_)
|
||||||
|
@ -245,7 +242,7 @@ impl Uploader {
|
||||||
let zip_writer = super::zip::ZipGenerator::new(files, writer);
|
let zip_writer = super::zip::ZipGenerator::new(files, writer);
|
||||||
let size = zip_writer.total_size();
|
let size = zip_writer.total_size();
|
||||||
let download_filename =
|
let download_filename =
|
||||||
super::APP_NAME.to_owned() + &now.format(FILENAME_DATE_FORMAT)? + ".zip";
|
super::APP_NAME.to_owned() + &now.format(FILENAME_DATE_FORMAT).unwrap() + ".zip";
|
||||||
(
|
(
|
||||||
Box::new(zip_writer),
|
Box::new(zip_writer),
|
||||||
download_filename,
|
download_filename,
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<link rel="stylesheet" type="text/css" href="transbeam.css"/>
|
<link rel="stylesheet" type="text/css" href="transbeam.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="states.css"/>
|
||||||
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
|
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
|
||||||
<link rel="manifest" href="manifest.json"/>
|
<link rel="manifest" href="manifest.json"/>
|
||||||
<script src="util.js"></script>
|
<script src="util.js"></script>
|
||||||
|
<script src="transbeam.js"></script>
|
||||||
<title>transbeam</title>
|
<title>transbeam</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="no_files selecting">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<img src="images/site-icons/transbeam.svg" height="128">
|
<img src="images/site-icons/transbeam.svg" height="128">
|
||||||
<h1>transbeam</h1>
|
<h1>transbeam</h1>
|
||||||
|
@ -17,8 +19,12 @@
|
||||||
|
|
||||||
<noscript>This page requires Javascript :(</noscript>
|
<noscript>This page requires Javascript :(</noscript>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
<div id="upload_controls">
|
||||||
|
<div>
|
||||||
<button id="upload">Upload</button>
|
<button id="upload">Upload</button>
|
||||||
<div id="lifetime_container" style="display: none;">
|
</div>
|
||||||
|
<div id="lifetime_container">
|
||||||
<label>
|
<label>
|
||||||
Keep files for:
|
Keep files for:
|
||||||
<select id="lifetime">
|
<select id="lifetime">
|
||||||
|
@ -29,30 +35,26 @@
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="download_link_container" style="display: none;">
|
</div>
|
||||||
|
<div id="download_link_container">
|
||||||
<div id="download_link_main">
|
<div id="download_link_main">
|
||||||
<div>Download link: <span id="download_link"></span></div><div class="copy_button"></div>
|
<div>Download link: <span id="download_link"></span></div><div class="copy_button"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="copied_message" style="display: none;">Copied!</div>
|
<div id="copied_message">Copied!</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="progress_container" style="display: none;">
|
<div id="progress_container">
|
||||||
<div id="progress"></div>
|
<div id="progress"></div>
|
||||||
<div id="progress_bar"></div>
|
<div id="progress_bar"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="file_list_container" style="display: none;">
|
|
||||||
<table id="file_list">
|
<table id="file_list">
|
||||||
</table>
|
</table>
|
||||||
</div>
|
<label id="file_input_container">
|
||||||
<div id="file_input_container">
|
|
||||||
<label>
|
|
||||||
<input type="file" multiple id="file_input"/>
|
<input type="file" multiple id="file_input"/>
|
||||||
<span class="fake_button" id="file_input_message">Select files to upload...</span>
|
<span class="fake_button" id="file_input_message">Select files to upload...</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<h5>(c) 2022 xenofem, MIT licensed</h5>
|
<h5>(c) 2022 xenofem, MIT licensed</h5>
|
||||||
<h5><a target="_blank" href="https://git.xeno.science/xenofem/transbeam">source</a></h5>
|
<h5><a target="_blank" href="https://git.xeno.science/xenofem/transbeam">source</a></h5>
|
||||||
</div>
|
</div>
|
||||||
<script src="transbeam.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
39
static/states.css
Normal file
39
static/states.css
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* List of classes the body can have:
|
||||||
|
*
|
||||||
|
* no_files: no files are selected
|
||||||
|
* selecting: upload hasn't started yet
|
||||||
|
* uploading: upload is in progress
|
||||||
|
* completed: upload is done
|
||||||
|
* error: an error has occurred
|
||||||
|
*/
|
||||||
|
|
||||||
|
#message { display: none; }
|
||||||
|
body.completed #message {
|
||||||
|
display: revert;
|
||||||
|
color: #060;
|
||||||
|
border-color: #0a0;
|
||||||
|
}
|
||||||
|
body.error #message {
|
||||||
|
display: revert;
|
||||||
|
color: #d00;
|
||||||
|
border-color: #f24;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload_controls { display: none; }
|
||||||
|
body.selecting #upload_controls { display: revert; }
|
||||||
|
body.no_files #upload_controls { display: none; }
|
||||||
|
|
||||||
|
body.selecting #download_link_container { display: none; }
|
||||||
|
|
||||||
|
#progress_container { display: none; }
|
||||||
|
body.uploading #progress_container { display: revert; }
|
||||||
|
|
||||||
|
body.no_files #file_list { display: none; }
|
||||||
|
body.completed #file_list { background-color: #7af; }
|
||||||
|
|
||||||
|
.delete_button { display: none; }
|
||||||
|
body.selecting .delete_button { display: revert; }
|
||||||
|
|
||||||
|
#file_input_container { display: none; }
|
||||||
|
body.selecting #file_input_container { display: revert; }
|
|
@ -10,6 +10,14 @@ body {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#message {
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
#progress_container {
|
#progress_container {
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
}
|
}
|
||||||
|
@ -64,7 +72,15 @@ body {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
#download_link_container.copied #copied_message {
|
||||||
|
display: revert;
|
||||||
|
}
|
||||||
|
#download_link_container.copied #download_link_main {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
@ -85,7 +101,7 @@ td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.file_delete {
|
.delete_button {
|
||||||
background-color: #888;
|
background-color: #888;
|
||||||
mask-image: url("images/feather-icons/x.svg");
|
mask-image: url("images/feather-icons/x.svg");
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
@ -96,7 +112,7 @@ td.file_delete {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.file_delete:hover {
|
.delete_button:hover {
|
||||||
background-color: #f00;
|
background-color: #f00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +143,7 @@ button:hover, .fake_button:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled, input:disabled + .fake_button {
|
button:disabled, input:disabled + .fake_button {
|
||||||
color: #aaa;
|
color: #666;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
border-color: #ddd;
|
border-color: #ddd;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
|
@ -1,36 +1,135 @@
|
||||||
const FILE_CHUNK_SIZE = 16384;
|
const FILE_CHUNK_SIZE = 16384;
|
||||||
const MAX_FILES = 256;
|
const MAX_FILES = 256;
|
||||||
|
|
||||||
const fileInputContainer = document.getElementById('file_input_container');
|
|
||||||
const fileInput = document.getElementById('file_input');
|
|
||||||
const fileInputMessage = document.getElementById('file_input_message');
|
|
||||||
|
|
||||||
const fileListContainer = document.getElementById('file_list_container');
|
|
||||||
const fileList = document.getElementById('file_list');
|
|
||||||
|
|
||||||
const lifetimeContainer = document.getElementById('lifetime_container');
|
|
||||||
const lifetimeInput = document.getElementById('lifetime');
|
|
||||||
|
|
||||||
const uploadButton = document.getElementById('upload');
|
|
||||||
|
|
||||||
const downloadLinkContainer = document.getElementById('download_link_container');
|
|
||||||
const downloadLinkMain = document.getElementById('download_link_main');
|
|
||||||
const downloadLink = document.getElementById('download_link');
|
|
||||||
const copiedMessage = document.getElementById('copied_message');
|
|
||||||
|
|
||||||
const progressContainer = document.getElementById('progress_container');
|
|
||||||
const progress = document.getElementById('progress');
|
|
||||||
const progressBar = document.getElementById('progress_bar');
|
|
||||||
|
|
||||||
let files = [];
|
let files = [];
|
||||||
|
|
||||||
let socket = null;
|
let socket;
|
||||||
let fileIndex = 0;
|
let fileIndex = 0;
|
||||||
let byteIndex = 0;
|
let byteIndex = 0;
|
||||||
let bytesSent = 0;
|
let bytesSent = 0;
|
||||||
let totalBytes = 0;
|
let totalBytes = 0;
|
||||||
|
|
||||||
function sendManifest(lifetime) {
|
let maxSize = null;
|
||||||
|
|
||||||
|
let messageBox;
|
||||||
|
let fileInput;
|
||||||
|
let fileList;
|
||||||
|
let uploadButton;
|
||||||
|
let lifetimeInput;
|
||||||
|
let downloadLink;
|
||||||
|
let progress;
|
||||||
|
let progressBar;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
messageBox = document.getElementById('message');
|
||||||
|
fileInput = document.getElementById('file_input');
|
||||||
|
fileList = document.getElementById('file_list');
|
||||||
|
uploadButton = document.getElementById('upload');
|
||||||
|
lifetimeInput = document.getElementById('lifetime');
|
||||||
|
downloadLink = document.getElementById('download_link');
|
||||||
|
progress = document.getElementById('progress');
|
||||||
|
progressBar = document.getElementById('progress_bar');
|
||||||
|
|
||||||
|
fileInput.addEventListener('input', () => {
|
||||||
|
for (const file of fileInput.files) { addFile(file); }
|
||||||
|
updateFiles();
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadButton.addEventListener('click', beginUpload);
|
||||||
|
|
||||||
|
const downloadLinkContainer = document.getElementById('download_link_container');
|
||||||
|
downloadLinkContainer.addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(downloadLink.textContent);
|
||||||
|
downloadLinkContainer.className = 'copied';
|
||||||
|
});
|
||||||
|
downloadLinkContainer.addEventListener('mouseleave', () => {
|
||||||
|
downloadLinkContainer.className = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
updateFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
socket = new WebSocket(`${window.location.protocol === 'http:' ? 'ws' : 'wss'}://${window.location.host}/upload`);
|
||||||
|
socket.addEventListener('open', sendManifest);
|
||||||
|
socket.addEventListener('message', handleMessage);
|
||||||
|
socket.addEventListener('close', handleClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendManifest() {
|
||||||
|
const lifetime = parseInt(lifetimeInput.value);
|
||||||
const fileMetadata = files.map((file) => ({
|
const fileMetadata = files.map((file) => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
@ -39,14 +138,55 @@ function sendManifest(lifetime) {
|
||||||
socket.send(JSON.stringify({ lifetime, files: fileMetadata }));
|
socket.send(JSON.stringify({ lifetime, files: fileMetadata }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishSending() {
|
function handleMessage(msg) {
|
||||||
if (socket.bufferedAmount > 0) {
|
if (bytesSent === 0) {
|
||||||
window.setTimeout(finishSending, 1000);
|
let reply;
|
||||||
|
try {
|
||||||
|
reply = JSON.parse(msg.data);
|
||||||
|
} catch (error) {
|
||||||
|
socket.close();
|
||||||
|
displayError('Received an invalid response from the server');
|
||||||
|
console.error(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (reply.type === 'ready') {
|
||||||
|
downloadLink.textContent = `${window.location.origin}/download/${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();
|
socket.close();
|
||||||
progressContainer.textContent = "Upload complete!";
|
if (reply.type === 'too_big') {
|
||||||
fileList.style.backgroundColor = "#7af";
|
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 === '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() {
|
function sendData() {
|
||||||
|
@ -91,108 +231,49 @@ function updateProgress() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFiles() {
|
function handleClose(e) {
|
||||||
totalBytes = files.reduce((acc, file) => acc + file.size, 0);
|
console.log('Websocket closed', e);
|
||||||
|
if (fileIndex >= files.length) {
|
||||||
if (files.length === 0) {
|
displayCompletion();
|
||||||
fileInputMessage.textContent = 'Select files to upload...';
|
|
||||||
fileListContainer.style.display = 'none';
|
|
||||||
uploadButton.style.display = 'none';
|
|
||||||
lifetimeContainer.style.display = 'none';
|
|
||||||
} else {
|
} else {
|
||||||
fileInputMessage.textContent = 'Select more files to upload...';
|
let error;
|
||||||
fileListContainer.style.display = '';
|
if (e.code === 1011) {
|
||||||
uploadButton.textContent = `Upload ${files.length} file${files.length > 1 ? 's' : ''} (${displaySize(totalBytes)})`;
|
if (e.reason) {
|
||||||
uploadButton.style.display = '';
|
error = `Server error: ${e.reason}`;
|
||||||
lifetimeContainer.style.display = '';
|
} else {
|
||||||
|
error = "A server error has occurred."
|
||||||
|
}
|
||||||
|
} else if (e.reason) {
|
||||||
|
error = e.reason;
|
||||||
|
}
|
||||||
|
displayError(error);
|
||||||
}
|
}
|
||||||
fileInput.disabled = (files.length >= MAX_FILES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFiles();
|
function finishSending() {
|
||||||
downloadLinkContainer.style.display = 'none';
|
if (socket.bufferedAmount > 0) {
|
||||||
progressContainer.style.display = 'none';
|
window.setTimeout(finishSending, 1000);
|
||||||
|
return;
|
||||||
function addFile(newFile) {
|
}
|
||||||
if (files.length >= MAX_FILES) { return; }
|
socket.close();
|
||||||
if (files.some((oldFile) => newFile.name === oldFile.name)) { return; }
|
displayCompletion();
|
||||||
|
|
||||||
files.push(newFile);
|
|
||||||
|
|
||||||
addListEntry(newFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addListEntry(file) {
|
function displayCompletion() {
|
||||||
const listEntry = document.createElement('tr');
|
messageBox.textContent = 'Upload complete!';
|
||||||
|
document.body.className = 'completed';
|
||||||
const deleteButtonCell = document.createElement('td');
|
removePerFileProgressBars(); // completed file backgrounds are handled in CSS
|
||||||
deleteButtonCell.className = 'file_delete';
|
|
||||||
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) {
|
function displayError(error) {
|
||||||
files = files.filter((file) => file.name !== name);
|
messageBox.textContent = error || 'An error has occurred.';
|
||||||
|
document.body.className = 'selecting error';
|
||||||
|
removePerFileProgressBars();
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInput.addEventListener('input', (e) => {
|
function removePerFileProgressBars() {
|
||||||
for (const file of e.target.files) { addFile(file); }
|
const fileEntries = Array.from(fileList.children);
|
||||||
updateFiles();
|
for (entry of fileEntries) {
|
||||||
e.target.value = '';
|
entry.style.backgroundSize = "0%";
|
||||||
});
|
|
||||||
|
|
||||||
uploadButton.addEventListener('click', (e) => {
|
|
||||||
if (files.length === 0) { return; }
|
|
||||||
|
|
||||||
const lifetime = parseInt(lifetimeInput.value);
|
|
||||||
lifetimeContainer.remove();
|
|
||||||
fileInputContainer.remove();
|
|
||||||
for (const button of Array.from(document.getElementsByTagName('button')).concat(...document.getElementsByClassName('file_delete'))) {
|
|
||||||
button.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = new WebSocket(`${window.location.protocol === 'http:' ? 'ws' : 'wss'}://${window.location.host}/upload`);
|
|
||||||
socket.addEventListener('open', () => sendManifest(lifetime));
|
|
||||||
socket.addEventListener('message', (msg) => {
|
|
||||||
if (bytesSent === 0) {
|
|
||||||
const reply = JSON.parse(msg.data);
|
|
||||||
if (reply.type === 'ready' && reply.code.match(/^[A-Za-z0-9]+$/)) {
|
|
||||||
downloadLink.textContent = `${window.location.origin}/download/${reply.code}`;
|
|
||||||
downloadLinkContainer.style.display = '';
|
|
||||||
updateProgress();
|
|
||||||
progressContainer.style.display = '';
|
|
||||||
sendData();
|
|
||||||
}
|
}
|
||||||
} else if (msg.data === 'ack') {
|
|
||||||
sendData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
downloadLinkContainer.addEventListener('click', (e) => {
|
|
||||||
navigator.clipboard.writeText(downloadLink.textContent);
|
|
||||||
downloadLinkMain.style.visibility = 'hidden';
|
|
||||||
copiedMessage.style.display = '';
|
|
||||||
})
|
|
||||||
|
|
||||||
downloadLinkContainer.addEventListener('mouseleave', (e) => {
|
|
||||||
copiedMessage.style.display = 'none';
|
|
||||||
downloadLinkMain.style.visibility = 'visible';
|
|
||||||
});
|
|
||||||
|
|
Loading…
Reference in a new issue