Compare commits
2 commits
86bdac20af
...
bfe7fcde99
Author | SHA1 | Date | |
---|---|---|---|
xenofem | bfe7fcde99 | ||
xenofem | 8275b940ac |
23
src/main.rs
23
src/main.rs
|
@ -10,6 +10,7 @@ use actix_web::{
|
||||||
};
|
};
|
||||||
use actix_web_actors::ws;
|
use actix_web_actors::ws;
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use serde::Deserialize;
|
||||||
use store::FileStore;
|
use store::FileStore;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
@ -17,20 +18,25 @@ const APP_NAME: &str = "transbeam";
|
||||||
|
|
||||||
type AppData = web::Data<RwLock<FileStore>>;
|
type AppData = web::Data<RwLock<FileStore>>;
|
||||||
|
|
||||||
#[get("/download/{file_code}")]
|
#[derive(Deserialize)]
|
||||||
|
struct DownloadRequest {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/download")]
|
||||||
async fn handle_download(
|
async fn handle_download(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
path: web::Path<String>,
|
download: web::Query<DownloadRequest>,
|
||||||
data: AppData,
|
data: AppData,
|
||||||
) -> actix_web::Result<HttpResponse> {
|
) -> actix_web::Result<HttpResponse> {
|
||||||
let file_code = path.into_inner();
|
let code = &download.code;
|
||||||
if !store::is_valid_storage_code(&file_code) {
|
if !store::is_valid_storage_code(code) {
|
||||||
return Ok(HttpResponse::NotFound().finish());
|
return Ok(HttpResponse::NotFound().finish());
|
||||||
}
|
}
|
||||||
let data = data.read().await;
|
let data = data.read().await;
|
||||||
let info = data.lookup_file(&file_code);
|
let info = data.lookup_file(code);
|
||||||
if let Some(info) = info {
|
if let Some(info) = info {
|
||||||
let storage_path = store::storage_dir().join(file_code);
|
let storage_path = store::storage_dir().join(code);
|
||||||
let file = File::open(&storage_path)?;
|
let file = File::open(&storage_path)?;
|
||||||
Ok(download::DownloadingFile {
|
Ok(download::DownloadingFile {
|
||||||
file,
|
file,
|
||||||
|
@ -55,8 +61,9 @@ async fn main() -> std::io::Result<()> {
|
||||||
let data: AppData = web::Data::new(RwLock::new(FileStore::load().await?));
|
let data: AppData = web::Data::new(RwLock::new(FileStore::load().await?));
|
||||||
start_reaper(data.clone());
|
start_reaper(data.clone());
|
||||||
|
|
||||||
let static_dir =
|
let static_dir = PathBuf::from(
|
||||||
PathBuf::from(std::env::var("TRANSBEAM_STATIC_DIR").unwrap_or_else(|_| String::from("static")));
|
std::env::var("TRANSBEAM_STATIC_DIR").unwrap_or_else(|_| String::from("static")),
|
||||||
|
);
|
||||||
let port = std::env::var("TRANSBEAM_PORT")
|
let port = std::env::var("TRANSBEAM_PORT")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|p| p.parse::<u16>().ok())
|
.and_then(|p| p.parse::<u16>().ok())
|
||||||
|
|
31
src/store.rs
31
src/store.rs
|
@ -1,7 +1,10 @@
|
||||||
use std::{collections::HashMap, io::ErrorKind, path::PathBuf, str::FromStr};
|
use std::{collections::HashMap, io::ErrorKind, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use rand::{distributions::{Alphanumeric, DistString}, thread_rng, Rng};
|
use rand::{
|
||||||
|
distributions::{Alphanumeric, DistString},
|
||||||
|
thread_rng, Rng,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
@ -12,9 +15,9 @@ use tokio::{
|
||||||
const STATE_FILE_NAME: &str = "files.json";
|
const STATE_FILE_NAME: &str = "files.json";
|
||||||
const DEFAULT_STORAGE_DIR: &str = "storage";
|
const DEFAULT_STORAGE_DIR: &str = "storage";
|
||||||
const DEFAULT_MAX_LIFETIME: u32 = 30;
|
const DEFAULT_MAX_LIFETIME: u32 = 30;
|
||||||
const GIGA: u64 = 1024*1024*1024;
|
const GIGA: u64 = 1024 * 1024 * 1024;
|
||||||
const DEFAULT_MAX_UPLOAD_SIZE: u64 = 16*GIGA;
|
const DEFAULT_MAX_UPLOAD_SIZE: u64 = 16 * GIGA;
|
||||||
const DEFAULT_MAX_STORAGE_SIZE: u64 = 64*GIGA;
|
const DEFAULT_MAX_STORAGE_SIZE: u64 = 64 * GIGA;
|
||||||
|
|
||||||
pub fn gen_storage_code() -> String {
|
pub fn gen_storage_code() -> String {
|
||||||
if std::env::var("TRANSBEAM_MNEMONIC_CODES").as_deref() == Ok("false") {
|
if std::env::var("TRANSBEAM_MNEMONIC_CODES").as_deref() == Ok("false") {
|
||||||
|
@ -25,15 +28,23 @@ pub fn gen_storage_code() -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_valid_storage_code(s: &str) -> bool {
|
pub fn is_valid_storage_code(s: &str) -> bool {
|
||||||
s.as_bytes().iter().all(|c| c.is_ascii_alphanumeric() || c == &b'-')
|
s.as_bytes()
|
||||||
|
.iter()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == &b'-')
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn storage_dir() -> PathBuf {
|
pub(crate) fn storage_dir() -> PathBuf {
|
||||||
PathBuf::from(std::env::var("TRANSBEAM_STORAGE_DIR").unwrap_or_else(|_| String::from(DEFAULT_STORAGE_DIR)))
|
PathBuf::from(
|
||||||
|
std::env::var("TRANSBEAM_STORAGE_DIR")
|
||||||
|
.unwrap_or_else(|_| String::from(DEFAULT_STORAGE_DIR)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_env_var<T: FromStr>(var: &str, default: T) -> T {
|
fn parse_env_var<T: FromStr>(var: &str, default: T) -> T {
|
||||||
std::env::var(var).ok().and_then(|val| val.parse::<T>().ok()).unwrap_or(default)
|
std::env::var(var)
|
||||||
|
.ok()
|
||||||
|
.and_then(|val| val.parse::<T>().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn max_lifetime() -> u32 {
|
pub(crate) fn max_lifetime() -> u32 {
|
||||||
|
@ -194,7 +205,9 @@ impl FileStore {
|
||||||
) -> std::io::Result<Result<(), u64>> {
|
) -> std::io::Result<Result<(), u64>> {
|
||||||
let remaining_size = max_total_size().saturating_sub(self.total_size());
|
let remaining_size = max_total_size().saturating_sub(self.total_size());
|
||||||
let allowed_size = std::cmp::min(remaining_size, max_single_size());
|
let allowed_size = std::cmp::min(remaining_size, max_single_size());
|
||||||
if file.size > allowed_size { return Ok(Err(allowed_size)); }
|
if file.size > allowed_size {
|
||||||
|
return Ok(Err(allowed_size));
|
||||||
|
}
|
||||||
self.0.insert(key, file);
|
self.0.insert(key, file);
|
||||||
self.save().await.map(Ok)
|
self.save().await.map(Ok)
|
||||||
}
|
}
|
||||||
|
@ -212,7 +225,7 @@ impl FileStore {
|
||||||
pub(crate) async fn remove_expired_files(&mut self) -> std::io::Result<()> {
|
pub(crate) async fn remove_expired_files(&mut self) -> std::io::Result<()> {
|
||||||
info!("Checking for expired files");
|
info!("Checking for expired files");
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
for (key, file) in std::mem::replace(&mut self.0, HashMap::new()).into_iter() {
|
for (key, file) in std::mem::take(&mut self.0).into_iter() {
|
||||||
if file.expiry > now {
|
if file.expiry > now {
|
||||||
self.0.insert(key, file);
|
self.0.insert(key, file);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use unicode_normalization::UnicodeNormalization;
|
use unicode_normalization::UnicodeNormalization;
|
||||||
|
|
||||||
use crate::store::{storage_dir, StoredFile, self};
|
use crate::store::{self, storage_dir, StoredFile};
|
||||||
|
|
||||||
const MAX_FILES: usize = 256;
|
const MAX_FILES: usize = 256;
|
||||||
const FILENAME_DATE_FORMAT: &[time::format_description::FormatItem] =
|
const FILENAME_DATE_FORMAT: &[time::format_description::FormatItem] =
|
||||||
|
@ -44,14 +44,14 @@ impl Error {
|
||||||
match self {
|
match self {
|
||||||
Self::Storage(_) => CloseCode::Error,
|
Self::Storage(_) => CloseCode::Error,
|
||||||
Self::Parse(_)
|
Self::Parse(_)
|
||||||
| Self::UnexpectedMessageType
|
| Self::UnexpectedMessageType
|
||||||
| Self::ClosedEarly(_)
|
| Self::ClosedEarly(_)
|
||||||
| Self::UnexpectedExtraData => CloseCode::Invalid,
|
| Self::UnexpectedExtraData => CloseCode::Invalid,
|
||||||
Self::DuplicateFilename
|
Self::DuplicateFilename
|
||||||
| Self::NoFiles
|
| Self::NoFiles
|
||||||
| Self::TooManyFiles
|
| Self::TooManyFiles
|
||||||
| Self::TooLong
|
| Self::TooLong
|
||||||
| Self::TooBig(_) => CloseCode::Policy,
|
| Self::TooBig(_) => CloseCode::Policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,9 +132,15 @@ enum ServerMessage {
|
||||||
impl From<&Error> for ServerMessage {
|
impl From<&Error> for ServerMessage {
|
||||||
fn from(e: &Error) -> Self {
|
fn from(e: &Error) -> Self {
|
||||||
match e {
|
match e {
|
||||||
Error::TooBig(max_size) => ServerMessage::TooBig { max_size: *max_size },
|
Error::TooBig(max_size) => ServerMessage::TooBig {
|
||||||
Error::TooLong => ServerMessage::TooLong { max_days: store::max_lifetime() },
|
max_size: *max_size,
|
||||||
_ => ServerMessage::Error { details: e.to_string() },
|
},
|
||||||
|
Error::TooLong => ServerMessage::TooLong {
|
||||||
|
max_days: store::max_lifetime(),
|
||||||
|
},
|
||||||
|
_ => ServerMessage::Error {
|
||||||
|
details: e.to_string(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,18 +197,17 @@ impl Uploader {
|
||||||
self.cleanup_after_error(ctx);
|
self.cleanup_after_error(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_message(
|
fn handle_message(&mut self, msg: ws::Message, ctx: &mut Context) -> Result<bool, Error> {
|
||||||
&mut self,
|
|
||||||
msg: ws::Message,
|
|
||||||
ctx: &mut Context,
|
|
||||||
) -> Result<bool, Error> {
|
|
||||||
trace!("Websocket message: {:?}", msg);
|
trace!("Websocket message: {:?}", msg);
|
||||||
match msg {
|
match msg {
|
||||||
ws::Message::Text(text) => {
|
ws::Message::Text(text) => {
|
||||||
if self.writer.is_some() {
|
if self.writer.is_some() {
|
||||||
return Err(Error::UnexpectedMessageType);
|
return Err(Error::UnexpectedMessageType);
|
||||||
}
|
}
|
||||||
let UploadManifest { files: raw_files, lifetime, } = serde_json::from_slice(text.as_bytes())?;
|
let UploadManifest {
|
||||||
|
files: raw_files,
|
||||||
|
lifetime,
|
||||||
|
} = serde_json::from_slice(text.as_bytes())?;
|
||||||
if lifetime > store::max_lifetime() {
|
if lifetime > store::max_lifetime() {
|
||||||
return Err(Error::TooLong);
|
return Err(Error::TooLong);
|
||||||
}
|
}
|
||||||
|
@ -235,19 +240,15 @@ impl Uploader {
|
||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(&storage_path)?;
|
.open(&storage_path)?;
|
||||||
let (writer, name, size, modtime): (Box<dyn Write>,_,_,_) = if files.len() > 1 {
|
let (writer, name, size, modtime): (Box<dyn Write>, _, _, _) = if files.len() > 1 {
|
||||||
info!("Wrapping in zipfile generator");
|
info!("Wrapping in zipfile generator");
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
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()
|
||||||
super::APP_NAME.to_owned() + &now.format(FILENAME_DATE_FORMAT).unwrap() + ".zip";
|
+ &now.format(FILENAME_DATE_FORMAT).unwrap()
|
||||||
(
|
+ ".zip";
|
||||||
Box::new(zip_writer),
|
(Box::new(zip_writer), download_filename, size, now)
|
||||||
download_filename,
|
|
||||||
size,
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
Box::new(writer),
|
Box::new(writer),
|
||||||
|
@ -261,23 +262,29 @@ impl Uploader {
|
||||||
name,
|
name,
|
||||||
size,
|
size,
|
||||||
modtime,
|
modtime,
|
||||||
expiry: OffsetDateTime::now_utc() + lifetime*time::Duration::DAY,
|
expiry: OffsetDateTime::now_utc() + lifetime * time::Duration::DAY,
|
||||||
};
|
};
|
||||||
let data = self.app_data.clone();
|
let data = self.app_data.clone();
|
||||||
let storage_filename = self.storage_filename.clone();
|
let storage_filename = self.storage_filename.clone();
|
||||||
ctx.spawn(actix::fut::wrap_future(async move {
|
ctx.spawn(
|
||||||
debug!("Spawned future to add entry {} to state", storage_filename);
|
actix::fut::wrap_future(async move {
|
||||||
data.write()
|
debug!("Spawned future to add entry {} to state", storage_filename);
|
||||||
.await
|
data.write()
|
||||||
.add_file(storage_filename, stored_file)
|
.await
|
||||||
.await
|
.add_file(storage_filename, stored_file)
|
||||||
}).map(|res, u: &mut Self, ctx: &mut Context| {
|
.await
|
||||||
match res {
|
})
|
||||||
Ok(Ok(())) => ctx.text(serde_json::to_string(&ServerMessage::Ready { code: u.storage_filename.clone() }).unwrap()),
|
.map(|res, u: &mut Self, ctx: &mut Context| match res {
|
||||||
|
Ok(Ok(())) => ctx.text(
|
||||||
|
serde_json::to_string(&ServerMessage::Ready {
|
||||||
|
code: u.storage_filename.clone(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
Ok(Err(size)) => u.notify_error_and_cleanup(Error::TooBig(size), ctx),
|
Ok(Err(size)) => u.notify_error_and_cleanup(Error::TooBig(size), ctx),
|
||||||
Err(e) => u.notify_error_and_cleanup(Error::from(e), ctx)
|
Err(e) => u.notify_error_and_cleanup(Error::from(e), ctx),
|
||||||
}
|
}),
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
ws::Message::Binary(data) | ws::Message::Continuation(Item::Last(data)) => {
|
ws::Message::Binary(data) | ws::Message::Continuation(Item::Last(data)) => {
|
||||||
let result = self.handle_data(data)?;
|
let result = self.handle_data(data)?;
|
||||||
|
|
|
@ -16,9 +16,6 @@
|
||||||
<img src="images/site-icons/transbeam.svg" height="128">
|
<img src="images/site-icons/transbeam.svg" height="128">
|
||||||
<h1>transbeam</h1>
|
<h1>transbeam</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<noscript>This page requires Javascript :(</noscript>
|
|
||||||
|
|
||||||
<div id="message"></div>
|
<div id="message"></div>
|
||||||
<div id="upload_controls">
|
<div id="upload_controls">
|
||||||
<div>
|
<div>
|
||||||
|
@ -36,11 +33,11 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="download_link_container">
|
<div id="download_code_container">
|
||||||
<div id="download_link_main">
|
<div id="download_code_main">
|
||||||
<div>Download link: <span id="download_link"></span></div><div class="copy_button"></div>
|
<div>Download code: <span id="download_code"></span></div><div class="copy_button"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="copied_message">Copied!</div>
|
<div id="copied_message">Link copied!</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="progress_container">
|
<div id="progress_container">
|
||||||
<div id="progress"></div>
|
<div id="progress"></div>
|
||||||
|
@ -52,6 +49,17 @@
|
||||||
<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 id="download">
|
||||||
|
<form id="download_form" action="download" method="get">
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<h4>Or enter a code to download files:</h4>
|
||||||
|
<input type="text" id="download_code_input" name="code" placeholder="Download code"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input id="download_button" type="submit" value="Download"/>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
|
|
@ -24,7 +24,7 @@ body.error #message {
|
||||||
body.selecting #upload_controls { display: revert; }
|
body.selecting #upload_controls { display: revert; }
|
||||||
body.no_files #upload_controls { display: none; }
|
body.no_files #upload_controls { display: none; }
|
||||||
|
|
||||||
body.selecting #download_link_container { display: none; }
|
body.selecting #download_code_container { display: none; }
|
||||||
|
|
||||||
#progress_container { display: none; }
|
#progress_container { display: none; }
|
||||||
body.uploading #progress_container { display: revert; }
|
body.uploading #progress_container { display: revert; }
|
||||||
|
@ -37,3 +37,6 @@ body.selecting .delete_button { display: revert; }
|
||||||
|
|
||||||
#file_input_container { display: none; }
|
#file_input_container { display: none; }
|
||||||
body.selecting #file_input_container { display: revert; }
|
body.selecting #file_input_container { display: revert; }
|
||||||
|
|
||||||
|
#download { display: none; }
|
||||||
|
body.no_files #download { display: revert; }
|
||||||
|
|
|
@ -31,7 +31,7 @@ body {
|
||||||
background-size: 0;
|
background-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download_link_container {
|
#download_code_container {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -41,11 +41,11 @@ body {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download_link_container:hover {
|
#download_code_container:hover {
|
||||||
border-color: #777;
|
border-color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download_link_main {
|
#download_code_main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
@ -60,7 +60,7 @@ body {
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download_link_container:hover .copy_button {
|
#download_code_container:hover .copy_button {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,10 +74,10 @@ body {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#download_link_container.copied #copied_message {
|
#download_code_container.copied #copied_message {
|
||||||
display: revert;
|
display: revert;
|
||||||
}
|
}
|
||||||
#download_link_container.copied #download_link_main {
|
#download_code_container.copied #download_code_main {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ input[type="file"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, .fake_button {
|
button, .fake_button, input[type="submit"] {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #000;
|
color: #000;
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
|
@ -138,11 +138,11 @@ button, .fake_button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover, .fake_button:hover {
|
button:hover, .fake_button:hover, input[type="submit"]:hover {
|
||||||
background-color: #aaa;
|
background-color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled, input:disabled + .fake_button {
|
button:disabled, input:disabled + .fake_button, input[type="submit"]:disabled {
|
||||||
color: #666;
|
color: #666;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
border-color: #ddd;
|
border-color: #ddd;
|
||||||
|
@ -153,6 +153,15 @@ button:disabled, input:disabled + .fake_button {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#download {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#download_code_input {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ let fileInput;
|
||||||
let fileList;
|
let fileList;
|
||||||
let uploadButton;
|
let uploadButton;
|
||||||
let lifetimeInput;
|
let lifetimeInput;
|
||||||
let downloadLink;
|
let downloadCode;
|
||||||
let progress;
|
let progress;
|
||||||
let progressBar;
|
let progressBar;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
fileList = document.getElementById('file_list');
|
fileList = document.getElementById('file_list');
|
||||||
uploadButton = document.getElementById('upload');
|
uploadButton = document.getElementById('upload');
|
||||||
lifetimeInput = document.getElementById('lifetime');
|
lifetimeInput = document.getElementById('lifetime');
|
||||||
downloadLink = document.getElementById('download_link');
|
downloadCode = document.getElementById('download_code');
|
||||||
progress = document.getElementById('progress');
|
progress = document.getElementById('progress');
|
||||||
progressBar = document.getElementById('progress_bar');
|
progressBar = document.getElementById('progress_bar');
|
||||||
|
|
||||||
|
@ -38,13 +38,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
uploadButton.addEventListener('click', beginUpload);
|
uploadButton.addEventListener('click', beginUpload);
|
||||||
|
|
||||||
const downloadLinkContainer = document.getElementById('download_link_container');
|
const downloadCodeContainer = document.getElementById('download_code_container');
|
||||||
downloadLinkContainer.addEventListener('click', () => {
|
downloadCodeContainer.addEventListener('click', () => {
|
||||||
navigator.clipboard.writeText(downloadLink.textContent);
|
const downloadUrl = new URL(`download?code=${downloadCode.textContent}`, window.location);
|
||||||
downloadLinkContainer.className = 'copied';
|
navigator.clipboard.writeText(downloadUrl.href);
|
||||||
|
downloadCodeContainer.className = 'copied';
|
||||||
});
|
});
|
||||||
downloadLinkContainer.addEventListener('mouseleave', () => {
|
downloadCodeContainer.addEventListener('mouseleave', () => {
|
||||||
downloadLinkContainer.className = '';
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateFiles();
|
updateFiles();
|
||||||
|
@ -122,7 +147,9 @@ function beginUpload() {
|
||||||
byteIndex = 0;
|
byteIndex = 0;
|
||||||
bytesSent = 0;
|
bytesSent = 0;
|
||||||
|
|
||||||
socket = new WebSocket(`${window.location.protocol === 'http:' ? 'ws' : 'wss'}://${window.location.host}/upload`);
|
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('open', sendManifest);
|
||||||
socket.addEventListener('message', handleMessage);
|
socket.addEventListener('message', handleMessage);
|
||||||
socket.addEventListener('close', handleClose);
|
socket.addEventListener('close', handleClose);
|
||||||
|
@ -150,7 +177,7 @@ function handleMessage(msg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (reply.type === 'ready') {
|
if (reply.type === 'ready') {
|
||||||
downloadLink.textContent = `${window.location.origin}/download/${reply.code}`;
|
downloadCode.textContent = reply.code;
|
||||||
updateProgress();
|
updateProgress();
|
||||||
document.body.className = 'uploading';
|
document.body.className = 'uploading';
|
||||||
sendData();
|
sendData();
|
||||||
|
@ -252,7 +279,7 @@ function handleClose(e) {
|
||||||
|
|
||||||
function finishSending() {
|
function finishSending() {
|
||||||
if (socket.bufferedAmount > 0) {
|
if (socket.bufferedAmount > 0) {
|
||||||
window.setTimeout(finishSending, 1000);
|
setTimeout(finishSending, 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket.close();
|
socket.close();
|
||||||
|
|
Loading…
Reference in a new issue