Compare commits
2 commits
b33bb7aa9d
...
511bd741dd
Author | SHA1 | Date | |
---|---|---|---|
xenofem | 511bd741dd | ||
xenofem | 99b93b4e7d |
|
@ -22,8 +22,6 @@ enum Error {
|
|||
Parse(#[from] serde_json::Error),
|
||||
#[error("Error writing to stored file")]
|
||||
Storage(#[from] std::io::Error),
|
||||
#[error("Time formatting error")]
|
||||
TimeFormat(#[from] time::error::Format),
|
||||
#[error("Duplicate filename could not be deduplicated")]
|
||||
DuplicateFilename,
|
||||
#[error("This message type was not expected at this stage")]
|
||||
|
@ -45,8 +43,7 @@ enum Error {
|
|||
impl Error {
|
||||
fn close_code(&self) -> CloseCode {
|
||||
match self {
|
||||
Self::Storage(_)
|
||||
| Self::TimeFormat(_) => CloseCode::Error,
|
||||
Self::Storage(_) => CloseCode::Error,
|
||||
Self::Parse(_)
|
||||
| Self::UnexpectedMessageType
|
||||
| Self::ClosedEarly(_)
|
||||
|
@ -245,7 +242,7 @@ impl Uploader {
|
|||
let zip_writer = super::zip::ZipGenerator::new(files, writer);
|
||||
let size = zip_writer.total_size();
|
||||
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),
|
||||
download_filename,
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
<meta charset="utf-8"/>
|
||||
<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="states.css"/>
|
||||
<link rel="apple-touch-icon" href="images/site-icons/transbeam-apple.png"/>
|
||||
<link rel="manifest" href="manifest.json"/>
|
||||
<script src="util.js"></script>
|
||||
<script src="transbeam.js"></script>
|
||||
<title>transbeam</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="no_files selecting">
|
||||
<div id="header">
|
||||
<img src="images/site-icons/transbeam.svg" height="128">
|
||||
<h1>transbeam</h1>
|
||||
|
@ -17,8 +19,12 @@
|
|||
|
||||
<noscript>This page requires Javascript :(</noscript>
|
||||
|
||||
<div id="message"></div>
|
||||
<div id="upload_controls">
|
||||
<div>
|
||||
<button id="upload">Upload</button>
|
||||
<div id="lifetime_container" style="display: none;">
|
||||
</div>
|
||||
<div id="lifetime_container">
|
||||
<label>
|
||||
Keep files for:
|
||||
<select id="lifetime">
|
||||
|
@ -29,30 +35,26 @@
|
|||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="download_link_container" style="display: none;">
|
||||
</div>
|
||||
<div id="download_link_container">
|
||||
<div id="download_link_main">
|
||||
<div>Download link: <span id="download_link"></span></div><div class="copy_button"></div>
|
||||
</div>
|
||||
<div id="copied_message" style="display: none;">Copied!</div>
|
||||
<div id="copied_message">Copied!</div>
|
||||
</div>
|
||||
<div id="progress_container" style="display: none;">
|
||||
<div id="progress_container">
|
||||
<div id="progress"></div>
|
||||
<div id="progress_bar"></div>
|
||||
</div>
|
||||
<div id="file_list_container" style="display: none;">
|
||||
<table id="file_list">
|
||||
</table>
|
||||
</div>
|
||||
<div id="file_input_container">
|
||||
<label>
|
||||
<label id="file_input_container">
|
||||
<input type="file" multiple id="file_input"/>
|
||||
<span class="fake_button" id="file_input_message">Select files to upload...</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<h5>(c) 2022 xenofem, MIT licensed</h5>
|
||||
<h5><a target="_blank" href="https://git.xeno.science/xenofem/transbeam">source</a></h5>
|
||||
</div>
|
||||
<script src="transbeam.js"></script>
|
||||
</body>
|
||||
</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;
|
||||
}
|
||||
|
||||
#message {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
width: fit-content;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
#progress_container {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
@ -64,7 +72,15 @@ body {
|
|||
bottom: 0;
|
||||
margin: auto;
|
||||
height: fit-content;
|
||||
display: none;
|
||||
}
|
||||
#download_link_container.copied #copied_message {
|
||||
display: revert;
|
||||
}
|
||||
#download_link_container.copied #download_link_main {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
|
@ -85,7 +101,7 @@ td {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
td.file_delete {
|
||||
.delete_button {
|
||||
background-color: #888;
|
||||
mask-image: url("images/feather-icons/x.svg");
|
||||
mask-size: contain;
|
||||
|
@ -96,7 +112,7 @@ td.file_delete {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
td.file_delete:hover {
|
||||
.delete_button:hover {
|
||||
background-color: #f00;
|
||||
}
|
||||
|
||||
|
@ -127,7 +143,7 @@ button:hover, .fake_button:hover {
|
|||
}
|
||||
|
||||
button:disabled, input:disabled + .fake_button {
|
||||
color: #aaa;
|
||||
color: #666;
|
||||
background-color: #eee;
|
||||
border-color: #ddd;
|
||||
cursor: not-allowed;
|
||||
|
|
|
@ -1,36 +1,135 @@
|
|||
const FILE_CHUNK_SIZE = 16384;
|
||||
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 socket = null;
|
||||
let socket;
|
||||
let fileIndex = 0;
|
||||
let byteIndex = 0;
|
||||
let bytesSent = 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) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
|
@ -39,14 +138,55 @@ function sendManifest(lifetime) {
|
|||
socket.send(JSON.stringify({ lifetime, files: fileMetadata }));
|
||||
}
|
||||
|
||||
function finishSending() {
|
||||
if (socket.bufferedAmount > 0) {
|
||||
window.setTimeout(finishSending, 1000);
|
||||
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') {
|
||||
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();
|
||||
progressContainer.textContent = "Upload complete!";
|
||||
fileList.style.backgroundColor = "#7af";
|
||||
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 === '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() {
|
||||
|
@ -91,108 +231,49 @@ function updateProgress() {
|
|||
}
|
||||
}
|
||||
|
||||
function updateFiles() {
|
||||
totalBytes = files.reduce((acc, file) => acc + file.size, 0);
|
||||
|
||||
if (files.length === 0) {
|
||||
fileInputMessage.textContent = 'Select files to upload...';
|
||||
fileListContainer.style.display = 'none';
|
||||
uploadButton.style.display = 'none';
|
||||
lifetimeContainer.style.display = 'none';
|
||||
function handleClose(e) {
|
||||
console.log('Websocket closed', e);
|
||||
if (fileIndex >= files.length) {
|
||||
displayCompletion();
|
||||
} else {
|
||||
fileInputMessage.textContent = 'Select more files to upload...';
|
||||
fileListContainer.style.display = '';
|
||||
uploadButton.textContent = `Upload ${files.length} file${files.length > 1 ? 's' : ''} (${displaySize(totalBytes)})`;
|
||||
uploadButton.style.display = '';
|
||||
lifetimeContainer.style.display = '';
|
||||
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);
|
||||
}
|
||||
fileInput.disabled = (files.length >= MAX_FILES);
|
||||
}
|
||||
|
||||
updateFiles();
|
||||
downloadLinkContainer.style.display = 'none';
|
||||
progressContainer.style.display = 'none';
|
||||
|
||||
function addFile(newFile) {
|
||||
if (files.length >= MAX_FILES) { return; }
|
||||
if (files.some((oldFile) => newFile.name === oldFile.name)) { return; }
|
||||
|
||||
files.push(newFile);
|
||||
|
||||
addListEntry(newFile);
|
||||
function finishSending() {
|
||||
if (socket.bufferedAmount > 0) {
|
||||
window.setTimeout(finishSending, 1000);
|
||||
return;
|
||||
}
|
||||
socket.close();
|
||||
displayCompletion();
|
||||
}
|
||||
|
||||
function addListEntry(file) {
|
||||
const listEntry = document.createElement('tr');
|
||||
|
||||
const deleteButtonCell = document.createElement('td');
|
||||
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 displayCompletion() {
|
||||
messageBox.textContent = 'Upload complete!';
|
||||
document.body.className = 'completed';
|
||||
removePerFileProgressBars(); // completed file backgrounds are handled in CSS
|
||||
}
|
||||
|
||||
function removeFile(name) {
|
||||
files = files.filter((file) => file.name !== name);
|
||||
function displayError(error) {
|
||||
messageBox.textContent = error || 'An error has occurred.';
|
||||
document.body.className = 'selecting error';
|
||||
removePerFileProgressBars();
|
||||
}
|
||||
|
||||
fileInput.addEventListener('input', (e) => {
|
||||
for (const file of e.target.files) { addFile(file); }
|
||||
updateFiles();
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
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();
|
||||
function removePerFileProgressBars() {
|
||||
const fileEntries = Array.from(fileList.children);
|
||||
for (entry of fileEntries) {
|
||||
entry.style.backgroundSize = "0%";
|
||||
}
|
||||
|
||||
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