More work
This commit is contained in:
parent
2c21698841
commit
5d4adc91ed
22 changed files with 340 additions and 202 deletions
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
|
@ -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
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -4,4 +4,8 @@ members = [
|
|||
"common",
|
||||
"server",
|
||||
"web",
|
||||
]
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<Utc>),
|
||||
}
|
||||
|
||||
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<Self, headers::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Iterator<Item = &'i HeaderValue>,
|
||||
{
|
||||
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<E: Extend<HeaderValue>>(&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<Expiration> 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<Self, Self::Error> {
|
||||
value
|
||||
.to_str()
|
||||
.map_err(|_| ParseHeaderValueError)?
|
||||
.parse::<DateTime<Utc>>()
|
||||
.map_err(|_| ParseHeaderValueError)
|
||||
.map(Self::UnixTime)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Expiration {
|
||||
fn default() -> Self {
|
||||
Self::UnixTime(Utc::now() + Duration::days(1))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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<DB>) {
|
||||
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::<Paste>(&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<const N: usize>(
|
||||
Extension(db): Extension<Arc<DB>>,
|
||||
|
@ -102,8 +153,30 @@ async fn upload<const N: usize>(
|
|||
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<const N: usize>(
|
|||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodic clean-up task that deletes expired entries.
|
||||
async fn cleanup(stop_signal: Arc<AtomicBool>, db: Arc<DB>) {
|
||||
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::<Paste>(&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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Utc>),
|
||||
}
|
||||
|
||||
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<Self, headers::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Iterator<Item = &'i HeaderValue>,
|
||||
{
|
||||
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<E: Extend<HeaderValue>>(&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<Expiration> for HeaderValue {
|
||||
fn from(expiration: Expiration) -> Self {
|
||||
(&expiration).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Expiration {
|
||||
fn default() -> Self {
|
||||
Self::UnixTime(Utc::now() + Duration::days(1))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<const N: usize> Distribution<ShortCode<N>> for Generator {
|
|||
for c in arr.iter_mut() {
|
||||
*c = self.sample(rng);
|
||||
}
|
||||
|
||||
ShortCode(arr)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -5,33 +5,28 @@
|
|||
<meta charset="utf-8" />
|
||||
<title>Omegaupload</title>
|
||||
|
||||
<link data-trunk rel="copy-file" href="src/Mplus2-Regular.ttf" dest="/" />
|
||||
<link data-trunk rel="copy-file" href="src/MplusCodeLatin-varwidthweight.ttf" dest="/" />
|
||||
<link data-trunk rel="copy-file" href="src/highlight.min.js" dest="/" />
|
||||
<link data-trunk rel="css" href="src/github-dark.min.css" />
|
||||
<link data-trunk rel="copy-file" href="vendor/MPLUS_FONTS/fonts/ttf/MplusCodeLatin[wdth,wght].ttf" dest="/" />
|
||||
<link data-trunk rel="copy-file" href="vendor/highlight.min.js" dest="/" />
|
||||
<link data-trunk rel="copy-file" href="vendor/highlightjs-line-numbers.js/dist/highlightjs-line-numbers.min.js"
|
||||
dest="/" />
|
||||
<link data-trunk rel="copy-file" href="src/reload_on_hash_change.js" dest="/" />
|
||||
|
||||
<link rel="preload" href="highlight.min.js" as="script" type="application/javascript">
|
||||
<link rel="preload" href="Mplus2-Regular.ttf" as="font" type="font/ttf" crossorigin>
|
||||
<link rel="preload" href="MplusCodeLatin-varwidthweight.ttf" as="font" type="font/ttf" crossorigin>
|
||||
<link data-trunk rel="css" href="vendor/highlight.js/src/styles/github-dark.css" />
|
||||
|
||||
<script src="reload_on_hash_change.js" async></script>
|
||||
<script src="highlight.min.js" defer></script>
|
||||
<script src="highlightjs-line-numbers.min.js" defer></script>
|
||||
|
||||
<script src="highlight.min.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Mplus2 Regular";
|
||||
src: url("./Mplus2-Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Mplus Code";
|
||||
src: url("./MplusCodeLatin-varwidthweight.ttf") format("truetype");
|
||||
src: url("./MplusCodeLatin[wdth,wght].ttf") format("truetype");
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
header.banner {
|
||||
font-family: 'Mplus2 Regular', sans-serif;
|
||||
font-family: 'Mplus Code', sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.paste {
|
||||
|
@ -46,6 +41,28 @@
|
|||
.hljs {
|
||||
font-family: 'Mplus Code', sans-serif;
|
||||
}
|
||||
|
||||
.hljs-ln td.hljs-ln-numbers {
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
pre header {
|
||||
user-select: none;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.error {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
10
web/src/github-dark.min.css
vendored
10
web/src/github-dark.min.css
vendored
|
@ -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}
|
106
web/src/main.rs
106
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::<Uri>().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<dyn PasteState>
|
||||
}
|
||||
};
|
||||
|
@ -117,11 +119,16 @@ impl Component for Paste {
|
|||
Box::new(partial) as Box<dyn PasteState>
|
||||
}
|
||||
}
|
||||
Ok(err) => Box::new(PasteError(anyhow!("Got resp error: {}", err.status())))
|
||||
as Box<dyn PasteState>,
|
||||
Err(err) => {
|
||||
Box::new(PasteError(anyhow!("Got resp error: {}", err))) as Box<dyn PasteState>
|
||||
Ok(resp) if resp.status() == StatusCode::NOT_FOUND => {
|
||||
Box::new(PasteNotFound) as Box<dyn PasteState>
|
||||
}
|
||||
Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => {
|
||||
Box::new(PasteBadRequest) as Box<dyn PasteState>
|
||||
}
|
||||
Ok(err) => {
|
||||
Box::new(PasteError(anyhow!("Got {}.", err.status()))) as Box<dyn PasteState>
|
||||
}
|
||||
Err(err) => Box::new(PasteError(anyhow!("Got {}.", err))) as Box<dyn PasteState>,
|
||||
}
|
||||
});
|
||||
Self {
|
||||
|
@ -147,13 +154,23 @@ impl Component for Paste {
|
|||
|
||||
if self.state.is::<PasteNotFound>() {
|
||||
return html! {
|
||||
<p>{ "Either the paste has been burned or one never existed." }</p>
|
||||
<section class={"hljs error"}>
|
||||
<p>{ "Either the paste has been burned or one never existed." }</p>
|
||||
</section>
|
||||
};
|
||||
}
|
||||
|
||||
if self.state.is::<PasteBadRequest>() {
|
||||
return html! {
|
||||
<section class={"hljs error"}>
|
||||
<p>{ "Bad Request. Is this a valid paste URL?" }</p>
|
||||
</section>
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(error) = self.state.downcast_ref::<PasteError>() {
|
||||
return html! {
|
||||
<p>{ error.0.to_string() }</p>
|
||||
<section class={"hljs error"}><p>{ error.0.to_string() }</p></section>
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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<Expiration>,
|
||||
partial_parsed_url: PartialParsedUrl,
|
||||
partial_parsed_url: &PartialParsedUrl,
|
||||
parent: ComponentLink<Paste>,
|
||||
) -> 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<PastePartial> for PasteComplete {
|
|||
password: Some(password),
|
||||
needs_pw: true,
|
||||
..
|
||||
} => Ok(PasteComplete {
|
||||
} => Ok(Self {
|
||||
data,
|
||||
expires,
|
||||
key,
|
||||
|
@ -307,7 +334,7 @@ impl TryFrom<PastePartial> 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! {
|
||||
<>
|
||||
<header class={"hljs paste banner"}>{
|
||||
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()
|
||||
<pre class={"paste"}>
|
||||
<header class={"hljs"}>
|
||||
{
|
||||
self.expires.as_ref().map(ToString::to_string).unwrap_or_else(||
|
||||
"This paste will not expire.".to_string()
|
||||
)
|
||||
}
|
||||
}</header>
|
||||
<pre class={"paste"}><code>{str}</code></pre>
|
||||
</header>
|
||||
<hr class={"hljs"} />
|
||||
<code>{str}</code>
|
||||
</pre>
|
||||
|
||||
<script>{"hljs.highlightAll();"}</script>
|
||||
<script>{"
|
||||
hljs.highlightAll();
|
||||
hljs.initLineNumbersOnLoad();
|
||||
"}</script>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
|
|
1
web/src/reload_on_hash_change.js
Normal file
1
web/src/reload_on_hash_change.js
Normal file
|
@ -0,0 +1 @@
|
|||
window.addEventListener("hashchange", () => location.reload());
|
1
web/vendor/MPLUS_FONTS
vendored
Submodule
1
web/vendor/MPLUS_FONTS
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 6ee9e7ca06f40f2303d839ccac8bfb8b56d2b3cd
|
1
web/vendor/highlight.js
vendored
Submodule
1
web/vendor/highlight.js
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 257cfee803426333af25b68da17601aec2663172
|
1
web/vendor/highlightjs-line-numbers.js
vendored
Submodule
1
web/vendor/highlightjs-line-numbers.js
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 8480334a29f01ad8b7fb0497c65285872781ee96
|
Loading…
Reference in a new issue