diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a6db9d3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "web/vendor/MPLUS_FONTS"] + path = web/vendor/MPLUS_FONTS + url = git@github.com:coz-m/MPLUS_FONTS.git +[submodule "web/vendor/highlight.js"] + path = web/vendor/highlight.js + url = git@github.com:highlightjs/highlight.js.git +[submodule "web/vendor/text-fragments-polyfill"] + path = web/vendor/text-fragments-polyfill + url = git@github.com:GoogleChromeLabs/text-fragments-polyfill.git +[submodule "web/vendor/highlightjs-line-numbers.js"] + path = web/vendor/highlightjs-line-numbers.js + url = git@github.com:wcoder/highlightjs-line-numbers.js.git diff --git a/Cargo.lock b/Cargo.lock index 1ad9d32..cd7fc60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,8 +912,13 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "bytes", "chacha20poly1305", + "chrono", + "headers", + "lazy_static", "rand", + "serde", "sha2", "thiserror", "url", @@ -929,7 +934,7 @@ dependencies = [ "bytes", "chrono", "headers", - "lazy_static", + "omegaupload-common", "rand", "rocksdb", "serde", diff --git a/Cargo.toml b/Cargo.toml index 0b2820c..cef3701 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,8 @@ members = [ "common", "server", "web", -] \ No newline at end of file +] + +[profile.release] +lto = true +codegen-units = 1 \ No newline at end of file diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 38e4c00..0dd3cfa 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "omegaupload-cli" version = "0.1.0" -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/cli/src/main.rs b/cli/src/main.rs index 1507c99..101a6ca 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,8 +7,9 @@ use anyhow::{anyhow, bail, Context, Result}; use atty::Stream; use clap::Clap; use omegaupload_common::crypto::{gen_key_nonce, open, seal, Key}; -use omegaupload_common::{base64, hash, ParsedUrl, Url}; +use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url}; use reqwest::blocking::Client; +use reqwest::header::EXPIRES; use reqwest::StatusCode; use secrecy::{ExposeSecret, SecretString}; @@ -108,6 +109,13 @@ fn handle_download(url: ParsedUrl) -> Result<()> { bail!("Got bad response from server: {}", res.status()); } + let expiration_text = dbg!(res.headers()) + .get(EXPIRES) + .and_then(|v| Expiration::try_from(v).ok()) + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| "This paste will not expire.".to_string()); + let mut data = res.bytes()?.as_ref().to_vec(); if url.needs_password { @@ -140,5 +148,7 @@ fn handle_download(url: ParsedUrl) -> Result<()> { std::io::stdout().write_all(&data)?; } + eprintln!("{}", expiration_text); + Ok(()) } diff --git a/common/Cargo.toml b/common/Cargo.toml index c0cf8f6..046fff4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,15 +1,20 @@ [package] name = "omegaupload-common" version = "0.1.0" -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1" base64 = "0.13" +bytes = { version = "*", features = ["serde"] } chacha20poly1305 = "0.9" +chrono = { version = "0.4", features = ["serde"] } +headers = "*" +lazy_static = "1" rand = "0.8" +serde = { version = "1", features = ["derive"] } sha2 = "0.9" thiserror = "1" url = "2" diff --git a/common/src/lib.rs b/common/src/lib.rs index 5f86d10..c3c3bba 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,10 +1,15 @@ #![warn(clippy::nursery, clippy::pedantic)] -#![deny(unsafe_code)] //! Contains common functions and structures used by multiple projects +use std::fmt::Display; use std::str::FromStr; +use bytes::Bytes; +use chrono::{DateTime, Duration, Utc}; +use headers::{Header, HeaderName, HeaderValue}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; pub use url::Url; @@ -94,7 +99,9 @@ pub mod crypto { impl Nonce { #[must_use] pub fn increment(&self) -> Self { - todo!() + let mut inner = self.0; + inner.as_mut_slice()[0] += 1; + Self(inner) } #[must_use] @@ -136,9 +143,7 @@ impl From<&str> for PartialParsedUrl { for (key, value) in args { match (key, value) { ("key", Some(value)) => { - decryption_key = base64::decode(value) - .map(|k| Key::from_slice(&k).clone()) - .ok(); + decryption_key = base64::decode(value).map(|k| *Key::from_slice(&k)).ok(); } ("pw", _) => { needs_password = true; @@ -203,3 +208,96 @@ impl FromStr for ParsedUrl { }) } } + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub enum Expiration { + BurnAfterReading, + UnixTime(DateTime), +} + +impl Display for Expiration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expiration::BurnAfterReading => { + write!(f, "This paste has been burned. You now have the only copy.") + } + Expiration::UnixTime(time) => write!( + f, + "{}", + time.format("This paste will expire on %A, %B %-d, %Y at %T %Z.") + ), + } + } +} + +lazy_static! { + pub static ref EXPIRATION_HEADER_NAME: HeaderName = HeaderName::from_static("burn-after"); +} + +impl Header for Expiration { + fn name() -> &'static HeaderName { + &*EXPIRATION_HEADER_NAME + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + match values + .next() + .ok_or_else(headers::Error::invalid)? + .as_bytes() + { + b"read" => Ok(Self::BurnAfterReading), + b"5m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(5))), + b"10m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(10))), + b"1h" => Ok(Self::UnixTime(Utc::now() + Duration::hours(1))), + b"1d" => Ok(Self::UnixTime(Utc::now() + Duration::days(1))), + // We disallow permanent pastes + _ => Err(headers::Error::invalid()), + } + } + + fn encode>(&self, container: &mut E) { + container.extend(std::iter::once(self.into())); + } +} + +impl From<&Expiration> for HeaderValue { + fn from(expiration: &Expiration) -> Self { + unsafe { + Self::from_maybe_shared_unchecked(match expiration { + Expiration::BurnAfterReading => Bytes::from_static(b"0"), + Expiration::UnixTime(duration) => Bytes::from(duration.to_rfc3339()), + }) + } + } +} + +impl From for HeaderValue { + fn from(expiration: Expiration) -> Self { + (&expiration).into() + } +} + +pub struct ParseHeaderValueError; + +impl TryFrom<&HeaderValue> for Expiration { + type Error = ParseHeaderValueError; + + fn try_from(value: &HeaderValue) -> Result { + value + .to_str() + .map_err(|_| ParseHeaderValueError)? + .parse::>() + .map_err(|_| ParseHeaderValueError) + .map(Self::UnixTime) + } +} + +impl Default for Expiration { + fn default() -> Self { + Self::UnixTime(Utc::now() + Duration::days(1)) + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 359dd85..283dbee 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "omegaupload-server" version = "0.1.0" -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +omegaupload-common = { path = "../common" } anyhow = "1" axum = { version = "0.2", features = ["http2", "headers"] } bincode = "1" @@ -15,7 +16,6 @@ bytes = { version = "*", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] } # We just need to pull in whatever axum is pulling in headers = "*" -lazy_static = "1" rand = "0.8" rocksdb = { version = "0.17", default_features = false, features = ["zstd"] } serde = { version = "1", features = ["derive"] } diff --git a/server/src/main.rs b/server/src/main.rs index 074e3e5..b83c66b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,5 @@ #![warn(clippy::nursery, clippy::pedantic)] -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use anyhow::Result; @@ -10,18 +9,18 @@ use axum::handler::{get, post}; use axum::http::header::EXPIRES; use axum::http::StatusCode; use axum::{AddExtensionLayer, Router}; -use chrono::Duration; +use chrono::Utc; use headers::HeaderMap; +use omegaupload_common::Expiration; use rand::thread_rng; use rand::Rng; use rocksdb::IteratorMode; -use rocksdb::WriteBatch; use rocksdb::{Options, DB}; use tokio::task; -use tracing::warn; use tracing::{error, instrument}; +use tracing::{info, warn}; -use crate::paste::{Expiration, Paste}; +use crate::paste::Paste; use crate::short_code::ShortCode; mod paste; @@ -36,8 +35,7 @@ async fn main() -> Result<()> { let db = Arc::new(DB::open_default(DB_PATH)?); - let stop_signal = Arc::new(AtomicBool::new(false)); - task::spawn(cleanup(Arc::clone(&stop_signal), Arc::clone(&db))); + set_up_expirations(Arc::clone(&db)); axum::Server::bind(&"0.0.0.0:8081".parse()?) .serve( @@ -52,12 +50,65 @@ async fn main() -> Result<()> { ) .await?; - stop_signal.store(true, Ordering::Release); // Must be called for correct shutdown DB::destroy(&Options::default(), DB_PATH)?; Ok(()) } +fn set_up_expirations(db: Arc) { + let mut corrupted = 0; + let mut expired = 0; + let mut pending = 0; + let mut permanent = 0; + + info!("Setting up cleanup timers, please wait..."); + + for (key, value) in db.iterator(IteratorMode::Start) { + let paste = if let Ok(value) = bincode::deserialize::(&value) { + value + } else { + corrupted += 1; + if let Err(e) = db.delete(key) { + warn!("{}", e); + } + continue; + }; + if let Some(Expiration::UnixTime(time)) = paste.expiration { + let now = Utc::now(); + + if time < now { + expired += 1; + if let Err(e) = db.delete(key) { + warn!("{}", e); + } + } else { + let sleep_duration = (time - now).to_std().unwrap(); + pending += 1; + + let db_ref = Arc::clone(&db); + task::spawn_blocking(move || async move { + tokio::time::sleep(sleep_duration).await; + if let Err(e) = db_ref.delete(key) { + warn!("{}", e); + } + }); + } + } else { + permanent += 1; + } + } + + if corrupted == 0 { + info!("No corrupted pastes found."); + } else { + warn!("Found {} corrupted pastes.", corrupted); + } + info!("Found {} expired pastes.", expired); + info!("Found {} active pastes.", pending); + info!("Found {} permanent pastes.", permanent); + info!("Cleanup timers have been initialized."); +} + #[instrument(skip(db), err)] async fn upload( Extension(db): Extension>, @@ -102,8 +153,30 @@ async fn upload( return Err(StatusCode::INTERNAL_SERVER_ERROR); }; - match task::spawn_blocking(move || db.put(key, value)).await { - Ok(Ok(_)) => (), + let db_ref = Arc::clone(&db); + match task::spawn_blocking(move || db_ref.put(key, value)).await { + Ok(Ok(_)) => { + if let Some(expires) = maybe_expires { + if let Expiration::UnixTime(time) = expires.0 { + let now = Utc::now(); + + if time < now { + if let Err(e) = db.delete(key) { + warn!("{}", e); + } + } else { + let sleep_duration = (time - now).to_std().unwrap(); + + task::spawn_blocking(move || async move { + tokio::time::sleep(sleep_duration).await; + if let Err(e) = db.delete(key) { + warn!("{}", e); + } + }); + } + } + } + } e => { error!("Failed to insert paste into db: {:?}", e); return Err(StatusCode::INTERNAL_SERVER_ERROR); @@ -185,47 +258,3 @@ async fn delete( _ => StatusCode::INTERNAL_SERVER_ERROR, } } - -/// Periodic clean-up task that deletes expired entries. -async fn cleanup(stop_signal: Arc, db: Arc) { - while !stop_signal.load(Ordering::Acquire) { - tokio::time::sleep(Duration::minutes(5).to_std().expect("infallible")).await; - let mut batch = WriteBatch::default(); - for (key, value) in db.snapshot().iterator(IteratorMode::Start) { - // TODO: only partially decode struct for max perf - let join_handle = task::spawn_blocking(move || { - bincode::deserialize::(&value) - .as_ref() - .map(Paste::expired) - .unwrap_or_default() - }) - .await; - - let should_delete = match join_handle { - Ok(should_delete) => should_delete, - Err(e) => { - error!("Failed to join thread?! {}", e); - false - } - }; - - if should_delete { - batch.delete(key); - } - } - - let db = Arc::clone(&db); - let join_handle = task::spawn_blocking(move || db.write(batch)).await; - let db_op_res = match join_handle { - Ok(res) => res, - Err(e) => { - error!("Failed to join handle?! {}", e); - continue; - } - }; - - if let Err(e) = db_op_res { - warn!("Failed to cleanup db: {}", e); - } - } -} diff --git a/server/src/paste.rs b/server/src/paste.rs index c5e6741..70c30ed 100644 --- a/server/src/paste.rs +++ b/server/src/paste.rs @@ -1,7 +1,6 @@ use axum::body::Bytes; -use chrono::{DateTime, Duration, Utc}; -use headers::{Header, HeaderName, HeaderValue}; -use lazy_static::lazy_static; +use chrono::Utc; +use omegaupload_common::Expiration; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -31,65 +30,3 @@ impl Paste { matches!(self.expiration, Some(Expiration::BurnAfterReading)) } } - -#[derive(Serialize, Deserialize, Clone, Copy, Debug)] -pub enum Expiration { - BurnAfterReading, - UnixTime(DateTime), -} - -lazy_static! { - pub static ref EXPIRATION_HEADER_NAME: HeaderName = HeaderName::from_static("burn-after"); -} - -impl Header for Expiration { - fn name() -> &'static HeaderName { - &*EXPIRATION_HEADER_NAME - } - - fn decode<'i, I>(values: &mut I) -> Result - where - Self: Sized, - I: Iterator, - { - match values - .next() - .ok_or_else(headers::Error::invalid)? - .as_bytes() - { - b"read" => Ok(Self::BurnAfterReading), - b"5m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(5))), - b"10m" => Ok(Self::UnixTime(Utc::now() + Duration::minutes(10))), - b"1h" => Ok(Self::UnixTime(Utc::now() + Duration::hours(1))), - b"1d" => Ok(Self::UnixTime(Utc::now() + Duration::days(1))), - _ => Err(headers::Error::invalid()), - } - } - - fn encode>(&self, container: &mut E) { - container.extend(std::iter::once(self.into())); - } -} - -impl From<&Expiration> for HeaderValue { - fn from(expiration: &Expiration) -> Self { - unsafe { - HeaderValue::from_maybe_shared_unchecked(match expiration { - Expiration::BurnAfterReading => Bytes::from_static(b"0"), - Expiration::UnixTime(duration) => Bytes::from(duration.to_rfc3339()), - }) - } - } -} - -impl From for HeaderValue { - fn from(expiration: Expiration) -> Self { - (&expiration).into() - } -} - -impl Default for Expiration { - fn default() -> Self { - Self::UnixTime(Utc::now() + Duration::days(1)) - } -} diff --git a/server/src/short_code.rs b/server/src/short_code.rs index 63fb86b..e9c4edc 100644 --- a/server/src/short_code.rs +++ b/server/src/short_code.rs @@ -1,6 +1,4 @@ -use std::convert::{TryFrom, TryInto}; use std::fmt::Debug; -use std::iter::FromIterator; use rand::prelude::Distribution; use serde::de::{Unexpected, Visitor}; @@ -125,6 +123,7 @@ impl Distribution> for Generator { for c in arr.iter_mut() { *c = self.sample(rng); } + ShortCode(arr) } } diff --git a/web/Cargo.toml b/web/Cargo.toml index 7355786..34de8bc 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "omegaupload-web" version = "0.1.0" -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,7 +16,7 @@ downcast-rs = "1" gloo-console = "0.1" http = "0.2" reqwest = { version = "0.11", default_features = false, features = ["tokio-rustls"] } -web-sys = { version = "0.3", features = ["Request", "Window"] } +web-sys = { version = "0.3" } yew = { version = "0.18", features = ["wasm-bindgen-futures"] } yew-router = "0.15" yewtil = "0.4" \ No newline at end of file diff --git a/web/index.html b/web/index.html index 407ae88..7e956e4 100644 --- a/web/index.html +++ b/web/index.html @@ -5,33 +5,28 @@ Omegaupload - - - - + + + + - - - + + + + + - diff --git a/web/src/Mplus2-Regular.ttf b/web/src/Mplus2-Regular.ttf deleted file mode 100644 index f010dab..0000000 Binary files a/web/src/Mplus2-Regular.ttf and /dev/null differ diff --git a/web/src/MplusCodeLatin-varwidthweight.ttf b/web/src/MplusCodeLatin-varwidthweight.ttf deleted file mode 100644 index 6790637..0000000 Binary files a/web/src/MplusCodeLatin-varwidthweight.ttf and /dev/null differ diff --git a/web/src/github-dark.min.css b/web/src/github-dark.min.css deleted file mode 100644 index 03b6da8..0000000 --- a/web/src/github-dark.min.css +++ /dev/null @@ -1,10 +0,0 @@ -pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! - Theme: GitHub Dark - Description: Dark theme as seen on github.com - Author: github.com - Maintainer: @Hirse - Updated: 2021-05-15 - - Outdated base version: https://github.com/primer/github-syntax-dark - Current colors taken from GitHub's CSS -*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c} \ No newline at end of file diff --git a/web/src/main.rs b/web/src/main.rs index a3d1d28..bf1133c 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -1,4 +1,5 @@ -use std::convert::TryFrom; +#![warn(clippy::nursery, clippy::pedantic)] + use std::fmt::Debug; use std::str::FromStr; @@ -54,6 +55,7 @@ enum Route { Path(String), } +#[allow(clippy::needless_pass_by_value)] fn render_route(route: Route) -> Html { match route { Route::Index => html! { @@ -82,9 +84,9 @@ impl Component for Paste { let url = String::from(window().location().to_string()); let request_uri = { let mut uri_parts = url.parse::().unwrap().into_parts(); - uri_parts.path_and_query.as_mut().map(|parts| { - *parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap() - }); + if let Some(parts) = uri_parts.path_and_query.as_mut() { + *parts = PathAndQuery::from_str(&format!("/api{}", parts.path())).unwrap(); + } Uri::from_parts(uri_parts).unwrap() }; @@ -100,13 +102,13 @@ impl Component for Paste { Ok(bytes) => PastePartial::new( bytes, expires, - url.split_once('#') + &url.split_once('#') .map(|(_, fragment)| PartialParsedUrl::from(fragment)) .unwrap_or_default(), link_clone, ), Err(e) => { - return Box::new(PasteError(anyhow!("Got resp error: {}", e))) + return Box::new(PasteError(anyhow!("Got {}.", e))) as Box } }; @@ -117,11 +119,16 @@ impl Component for Paste { Box::new(partial) as Box } } - Ok(err) => Box::new(PasteError(anyhow!("Got resp error: {}", err.status()))) - as Box, - Err(err) => { - Box::new(PasteError(anyhow!("Got resp error: {}", err))) as Box + Ok(resp) if resp.status() == StatusCode::NOT_FOUND => { + Box::new(PasteNotFound) as Box } + Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => { + Box::new(PasteBadRequest) as Box + } + Ok(err) => { + Box::new(PasteError(anyhow!("Got {}.", err.status()))) as Box + } + Err(err) => Box::new(PasteError(anyhow!("Got {}.", err))) as Box, } }); Self { @@ -147,13 +154,23 @@ impl Component for Paste { if self.state.is::() { return html! { -

{ "Either the paste has been burned or one never existed." }

+
+

{ "Either the paste has been burned or one never existed." }

+
+ }; + } + + if self.state.is::() { + return html! { +
+

{ "Bad Request. Is this a valid paste URL?" }

+
}; } if let Some(error) = self.state.downcast_ref::() { return html! { -

{ error.0.to_string() }

+

{ error.0.to_string() }

}; } @@ -171,9 +188,6 @@ impl Component for Paste { } } -struct PasteLoading; -struct PasteNotFound; - struct PasteError(anyhow::Error); #[derive(Properties, Clone, Debug)] @@ -198,17 +212,29 @@ struct PasteComplete { trait PasteState: Downcast {} impl_downcast!(PasteState); -impl PasteState for PasteLoading {} -impl PasteState for PasteNotFound {} + impl PasteState for PasteError {} impl PasteState for PastePartial {} impl PasteState for PasteComplete {} +macro_rules! impl_paste_type_state { + ( + $($state:ident),* $(,)? + ) => { + $( + struct $state; + impl PasteState for $state {} + )* + }; +} + +impl_paste_type_state!(PasteLoading, PasteNotFound, PasteBadRequest); + impl PastePartial { fn new( data: Bytes, expires: Option, - partial_parsed_url: PartialParsedUrl, + partial_parsed_url: &PartialParsedUrl, parent: ComponentLink, ) -> Self { Self { @@ -251,7 +277,7 @@ impl Component for PastePartial { || (!self.needs_pw && maybe_password.is_none()) => { let data = self.data.clone(); - let expires = self.expires.clone(); + let expires = self.expires; self.parent.callback_once(move |Nothing| { Box::new(PasteComplete::new( data, @@ -265,7 +291,8 @@ impl Component for PastePartial { _ => (), } - // parent should re-render so this element should be dropped. + // parent should re-render so this element should be dropped; no point + // in saying this needs to be re-rendered. false } @@ -293,7 +320,7 @@ impl TryFrom for PasteComplete { password: Some(password), needs_pw: true, .. - } => Ok(PasteComplete { + } => Ok(Self { data, expires, key, @@ -307,7 +334,7 @@ impl TryFrom for PasteComplete { nonce: Some(nonce), needs_pw: false, .. - } => Ok(PasteComplete { + } => Ok(Self { data, key, expires, @@ -337,30 +364,31 @@ impl PasteComplete { } fn view(&self) -> Html { - let stage_one = if let Some(password) = self.password { - open(&self.data, &self.nonce.increment(), &password).unwrap() - } else { - self.data.to_vec() - }; - + let stage_one = self.password.map_or_else( + || self.data.to_vec(), + |password| open(&self.data, &self.nonce.increment(), &password).unwrap(), + ); let decrypted = open(&stage_one, &self.nonce, &self.key).unwrap(); if let Ok(str) = String::from_utf8(decrypted) { html! { <> -
{ - if let Some(expires) = &self.expires { - match expires { - Expiration::BurnAfterReading => "This paste has been burned. You now have the only copy.".to_string(), - Expiration::UnixTime(time) => time.format("This paste will expire on %A, %B %-d, %Y at %T %Z.").to_string(), - } - } else { - "This paste will not expire.".to_string() +
+                    
+ { + self.expires.as_ref().map(ToString::to_string).unwrap_or_else(|| + "This paste will not expire.".to_string() + ) } - }
-
{str}
+
+
+ {str} + - + } } else { diff --git a/web/src/reload_on_hash_change.js b/web/src/reload_on_hash_change.js new file mode 100644 index 0000000..489dc5d --- /dev/null +++ b/web/src/reload_on_hash_change.js @@ -0,0 +1 @@ +window.addEventListener("hashchange", () => location.reload()); \ No newline at end of file diff --git a/web/vendor/MPLUS_FONTS b/web/vendor/MPLUS_FONTS new file mode 160000 index 0000000..6ee9e7c --- /dev/null +++ b/web/vendor/MPLUS_FONTS @@ -0,0 +1 @@ +Subproject commit 6ee9e7ca06f40f2303d839ccac8bfb8b56d2b3cd diff --git a/web/vendor/highlight.js b/web/vendor/highlight.js new file mode 160000 index 0000000..257cfee --- /dev/null +++ b/web/vendor/highlight.js @@ -0,0 +1 @@ +Subproject commit 257cfee803426333af25b68da17601aec2663172 diff --git a/web/src/highlight.min.js b/web/vendor/highlight.min.js similarity index 100% rename from web/src/highlight.min.js rename to web/vendor/highlight.min.js diff --git a/web/vendor/highlightjs-line-numbers.js b/web/vendor/highlightjs-line-numbers.js new file mode 160000 index 0000000..8480334 --- /dev/null +++ b/web/vendor/highlightjs-line-numbers.js @@ -0,0 +1 @@ +Subproject commit 8480334a29f01ad8b7fb0497c65285872781ee96