diff --git a/Cargo.lock b/Cargo.lock index 3541649..26b7c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,6 +1241,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "leptos_meta" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab2fcfe2ae8f14a58798007dd90126c60f529f265e5fa0bf816e047b60b753c" +dependencies = [ + "cfg-if", + "indexmap 2.0.0", + "leptos", + "tracing", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "leptos_reactive" version = "0.4.6" @@ -1524,6 +1538,7 @@ name = "nyazoom" version = "0.1.0" dependencies = [ "async-bincode", + "async-trait", "async_zip", "axum", "bincode", @@ -1531,6 +1546,7 @@ dependencies = [ "futures", "headers", "leptos", + "leptos_meta", "leptos_router", "rand", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 2d8b7ff..e43e8c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] async-bincode = { version = "0.7.0", features = ["tokio"] } +async-trait = "0.1.72" async_zip = { version = "0.0.13", features = ["deflate", "tokio", "tokio-fs", "async-compression"] } axum = { version = "0.6.12", features = ["multipart", "http2", "headers", "macros", "original-uri"] } bincode = "1.3.3" @@ -15,6 +16,7 @@ chrono = { version = "0.4.24", features = ["serde"] } futures = "0.3.28" headers = "0.3.8" leptos = { version = "0.4.6", features = ["ssr", "nightly", "tracing", "default-tls"] } +leptos_meta = { version = "0.4.6", features = ["ssr"] } leptos_router = { version = "0.4.6", features = ["ssr"] } rand = { version = "0.8.5", features = ["small_rng"] } reqwest = { version = "0.11.18", features = ["json", "native-tls", "blocking"] } diff --git a/dist/css/link.css b/dist/css/link.css index a8bdfef..9b7a56a 100644 --- a/dist/css/link.css +++ b/dist/css/link.css @@ -16,6 +16,7 @@ body { padding: 1.5em; border-radius: 1em; border: 1px solid #25283d; + list-style: none; } .return-button { diff --git a/dist/css/main.css b/dist/css/main.css index 49d748c..b370086 100644 --- a/dist/css/main.css +++ b/dist/css/main.css @@ -25,6 +25,7 @@ body { justify-content: center; } + .cat-img { width: 250px; height: 250px; diff --git a/src/main.rs b/src/main.rs index ced316e..8690bd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,13 @@ use axum::{ http::{Request, Response, StatusCode}, middleware::{self, Next}, response::{Html, IntoResponse, Redirect}, - routing::{get, post}, + routing::{delete, get, post}, Json, Router, TypedHeader, }; use futures::TryStreamExt; +use headers::HeaderMap; use leptos::IntoView; use nyazoom_headers::ForwardedFor; @@ -36,7 +37,8 @@ mod views; use state::{AppState, UploadRecord}; -use crate::views::{DownloadLinkPage, LinkView, Welcome}; +use crate::state::AsyncRemoveRecord; +use crate::views::{DownloadLinkPage, HtmxPage, LinkView, Welcome}; pub mod error { use std::io::{Error, ErrorKind}; @@ -74,10 +76,8 @@ async fn main() -> io::Result<()> { for (key, record) in records.clone().into_iter() { if !record.can_be_downloaded() { - tracing::info!("{:?} should be culled", record); - let _ = tokio::fs::remove_file(&record.file).await; - records.remove(key.as_str()); - cache::write_to_cache(&records).await.unwrap(); + tracing::info!("culling: {:?}", record); + records.remove_record(&key).await.unwrap(); } } } @@ -92,6 +92,8 @@ async fn main() -> io::Result<()> { .route("/records/links", get(records_links)) .route("/download/:id", get(download)) .route("/link/:id", get(link)) + .route("/link/:id", delete(link_delete)) + .route("/link/:id/remaining", get(remaining)) .layer(DefaultBodyLimit::disable()) .layer(RequestBodyLimitLayer::new( 10 * 1024 * 1024 * 1024, // 10GiB @@ -112,6 +114,24 @@ async fn main() -> io::Result<()> { Ok(()) } +async fn remaining( + State(state): State, + axum::extract::Path(id): axum::extract::Path, +) -> impl IntoResponse { + let records = state.records.lock().await; + if let Some(record) = records.get(&id) { + let downloads_remaining = record.downloads_remaining(); + let plural = if downloads_remaining > 1 { "s" } else { "" }; + let out = format!( + "You have {} download{} remaining!", + downloads_remaining, plural + ); + Html(out) + } else { + Html("?".to_string()) + } +} + async fn welcome() -> impl IntoResponse { let cat_fact = views::get_cat_fact().await; Html(leptos::ssr::render_to_string(move |cx| { @@ -123,45 +143,71 @@ async fn records(State(state): State) -> impl IntoResponse { Json(state.records.lock().await.clone()) } +// This function is to remain ugly until that time in which I properly hide +// this behind some kind of authentication async fn records_links(State(state): State) -> impl IntoResponse { let records = state.records.lock().await.clone(); Html(leptos::ssr::render_to_string(move |cx| { leptos::view! { cx, -
    - {records - .iter() - .map(|(key, _)| - leptos::view! { cx,
  • {key}
  • }) - .collect::>()} -
+ +
+
+
    + {records.keys().map(|key| leptos::view! { cx, + + }) + .collect::>()} +
+
+
+
} })) } async fn link( axum::extract::Path(id): axum::extract::Path, - State(state): State, + State(mut state): State, ) -> Result, Redirect> { - let mut records = state.records.lock().await; + { + let mut records = state.records.lock().await; - if let Some(record) = records.get_mut(&id) { - if record.can_be_downloaded() { - return Ok(Html(leptos::ssr::render_to_string({ - let record = record.clone(); - |cx| { - leptos::view! { cx, } - } - }))); - } else { - let _ = tokio::fs::remove_file(&record.file).await; - records.remove(&id); - cache::write_to_cache(&records).await.unwrap(); + if let Some(record) = records.get_mut(&id) { + if record.can_be_downloaded() { + return Ok(Html(leptos::ssr::render_to_string({ + let record = record.clone(); + |cx| { + leptos::view! { cx, } + } + }))); + } } } + // TODO: This.... + state.remove_record(&id).await.unwrap(); + Err(Redirect::to(&format!("/404.html"))) } +async fn link_delete( + axum::extract::Path(id): axum::extract::Path, + State(mut state): State, +) -> Result, (StatusCode, String)> { + state + .remove_record(&id) + .await + .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?; + + Ok(Html("".to_string())) +} + async fn log_source( ConnectInfo(addr): ConnectInfo, forwarded_for: Option>, @@ -245,27 +291,38 @@ async fn upload_to_zip( async fn download( axum::extract::Path(id): axum::extract::Path, - State(state): State, + headers: HeaderMap, + State(mut state): State, ) -> Result { - let mut records = state.records.lock().await; - - if let Some(record) = records.get_mut(&id) { - if record.can_be_downloaded() { - record.downloads += 1; - - let file = tokio::fs::File::open(&record.file).await.unwrap(); - + { + let mut records = state.records.lock().await; + tracing::info!("{headers:?}"); + if headers.get("hx-request").is_some() { return Ok(axum::http::Response::builder() - .header("Content-Type", "application/zip") - .body(StreamBody::new(ReaderStream::new(file))) + .header("HX-Redirect", format!("/download/{id}")) + .status(204) + .body("".to_owned()) .unwrap() .into_response()); - } else { - let _ = tokio::fs::remove_file(&record.file).await; - records.remove(&id); - cache::write_to_cache(&records).await.unwrap(); + } + + if let Some(record) = records.get_mut(&id) { + if record.can_be_downloaded() { + record.downloads += 1; + + let file = tokio::fs::File::open(&record.file).await.unwrap(); + + return Ok(axum::response::Response::builder() + .header("Content-Type", "application/zip") + .body(StreamBody::new(ReaderStream::new(file))) + .unwrap() + .into_response()); + } } } + // TODO: This.... + state.remove_record(&id).await.unwrap(); + Ok(Redirect::to("/404.html").into_response()) } diff --git a/src/state.rs b/src/state.rs index 96724da..4908ba4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,10 +4,13 @@ use std::{ sync::Arc, }; +use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; +use crate::cache; + #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UploadRecord { @@ -30,6 +33,10 @@ impl UploadRecord { dur_since_upload < Duration::days(3) && self.downloads < self.max_downloads } + + pub fn downloads_remaining(&self) -> u8 { + self.max_downloads - self.downloads + } } impl Default for UploadRecord { @@ -55,3 +62,36 @@ impl AppState { } } } + +#[async_trait] +pub trait AsyncRemoveRecord { + async fn remove_record(&mut self, id: &String) -> Result<(), std::io::Error>; +} + +#[async_trait] +impl AsyncRemoveRecord for AppState { + async fn remove_record(&mut self, id: &String) -> Result<(), std::io::Error> { + let mut records = self.records.lock().await; + + if let Some(record) = records.get_mut(id) { + tokio::fs::remove_file(&record.file).await?; + records.remove(id); + cache::write_to_cache(&records).await?; + } + + Ok(()) + } +} + +#[async_trait] +impl AsyncRemoveRecord for HashMap { + async fn remove_record(&mut self, id: &String) -> Result<(), std::io::Error> { + if let Some(record) = self.get_mut(id) { + tokio::fs::remove_file(&record.file).await?; + self.remove(id); + cache::write_to_cache(&self).await?; + } + + Ok(()) + } +} diff --git a/src/views.rs b/src/views.rs index 75e56a3..c99fd12 100644 --- a/src/views.rs +++ b/src/views.rs @@ -1,5 +1,5 @@ use futures::TryFutureExt; -use leptos::{component, view, Children, IntoAttribute, IntoView, Scope}; +use leptos::{component, view, Children, IntoView, Scope}; use serde::Deserialize; use crate::state::UploadRecord; @@ -91,11 +91,11 @@ pub fn LinkView(cx: Scope, id: String, record: UploadRecord) -> impl IntoView { cx,
-