prehtmx refactoring

This commit is contained in:
Zynh0722 2023-07-27 08:40:01 -07:00
parent 2f52fe4ced
commit b93a3950d0
5 changed files with 138 additions and 26 deletions

View file

@ -8,7 +8,7 @@ edition = "2021"
[dependencies] [dependencies]
async-bincode = { version = "0.7.0", features = ["tokio"] } async-bincode = { version = "0.7.0", features = ["tokio"] }
async_zip = { version = "0.0.13", features = ["deflate", "tokio", "tokio-fs", "async-compression"] } async_zip = { version = "0.0.13", features = ["deflate", "tokio", "tokio-fs", "async-compression"] }
axum = { version = "0.6.12", features = ["multipart", "http2", "headers", "macros"] } axum = { version = "0.6.12", features = ["multipart", "http2", "headers", "macros", "original-uri"] }
bincode = "1.3.3" bincode = "1.3.3"
chrono = { version = "0.4.24", features = ["serde"] } chrono = { version = "0.4.24", features = ["serde"] }
futures = "0.3.28" futures = "0.3.28"

21
dist/scripts/link.js vendored Normal file
View file

@ -0,0 +1,21 @@
document.addEventListener("DOMContentLoaded", () => {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params.link !== null) {
let link = `${window.location.origin}/download/${params.link}`;
let link_el = document.getElementById("link");
link_el.href = link;
link_el.innerHTML = link;
}
});
function clipboard() {
let copyText = document.getElementById("link");
navigator.clipboard?.writeText(copyText.href).then(() => alert("Copied: " + copyText.href));
}

View file

@ -7,16 +7,17 @@ use axum::{
middleware::{self, Next}, middleware::{self, Next},
response::{Html, IntoResponse, Redirect, Response}, response::{Html, IntoResponse, Redirect, Response},
routing::{get, post}, routing::{get, post},
Router, TypedHeader, Json, Router, TypedHeader,
}; };
use futures::TryStreamExt; use futures::TryStreamExt;
use leptos::IntoView;
use nyazoom_headers::ForwardedFor; use nyazoom_headers::ForwardedFor;
use sanitize_filename_reader_friendly::sanitize; use sanitize_filename_reader_friendly::sanitize;
use std::{io, net::SocketAddr, path::Path}; use std::{io, net::SocketAddr, path::Path, time::Duration};
use tokio_util::{ use tokio_util::{
compat::FuturesAsyncWriteCompatExt, compat::FuturesAsyncWriteCompatExt,
@ -35,7 +36,7 @@ mod views;
use state::{AppState, UploadRecord}; use state::{AppState, UploadRecord};
use crate::views::Welcome; use crate::views::{DownloadLink, Welcome};
pub mod error { pub mod error {
use std::io::{Error, ErrorKind}; use std::io::{Error, ErrorKind};
@ -56,7 +57,13 @@ async fn main() -> io::Result<()> {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
// tracing::info!("{}", get_cat_fact().await); // Spawn a repeating task that will clean files periodically
tokio::spawn(async {
loop {
tracing::info!("Cleaning Sweep!");
tokio::time::sleep(Duration::from_secs(15 * 60)).await
}
});
// uses create_dir_all to create both .cache and serve inside it in one go // uses create_dir_all to create both .cache and serve inside it in one go
util::make_dir(".cache/serve").await?; util::make_dir(".cache/serve").await?;
@ -67,13 +74,16 @@ async fn main() -> io::Result<()> {
let app = Router::new() let app = Router::new()
.route("/", get(welcome)) .route("/", get(welcome))
.route("/upload", post(upload_to_zip)) .route("/upload", post(upload_to_zip))
.route("/records", get(records))
.route("/records/links", get(records_links))
.route("/download/:id", get(download)) .route("/download/:id", get(download))
.route("/link/:id", get(link))
.layer(DefaultBodyLimit::disable()) .layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new( .layer(RequestBodyLimitLayer::new(
10 * 1024 * 1024 * 1024, // 10GiB 10 * 1024 * 1024 * 1024, // 10GiB
)) ))
.with_state(state) .with_state(state)
.nest_service("/dist", ServeDir::new("dist")) .fallback_service(ServeDir::new("dist"))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.layer(middleware::from_fn(log_source)); .layer(middleware::from_fn(log_source));
@ -95,6 +105,49 @@ async fn welcome() -> impl IntoResponse {
})) }))
} }
async fn records(State(state): State<AppState>) -> impl IntoResponse {
Json(state.records.lock().await.clone())
}
async fn records_links(State(state): State<AppState>) -> impl IntoResponse {
let records = state.records.lock().await.clone();
Html(leptos::ssr::render_to_string(move |cx| {
leptos::view! { cx,
<ul>
{records
.iter()
.map(|(key, _)|
leptos::view! { cx, <li><a href="/link/{key}">{key}</a></li> })
.collect::<Vec<_>>()}
</ul>
}
}))
}
async fn link(
axum::extract::Path(id): axum::extract::Path<String>,
State(state): State<AppState>,
) -> Result<Html<String>, Redirect> {
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, <DownloadLink id=id record=record /> }
}
})));
} else {
let _ = tokio::fs::remove_file(&record.file).await;
records.remove(&id);
cache::write_to_cache(&records).await.unwrap();
}
}
Err(Redirect::to(&format!("/404.html")))
}
async fn log_source<B>( async fn log_source<B>(
ConnectInfo(addr): ConnectInfo<SocketAddr>, ConnectInfo(addr): ConnectInfo<SocketAddr>,
forwarded_for: Option<TypedHeader<ForwardedFor>>, forwarded_for: Option<TypedHeader<ForwardedFor>>,
@ -162,10 +215,7 @@ async fn upload_to_zip(
writer.close().await.unwrap(); writer.close().await.unwrap();
Ok(Redirect::to(&format!( Ok(Redirect::to(&format!("/link/{}", cache_name)))
"/dist/link.html?link={}",
cache_name
)))
} }
async fn download( async fn download(

View file

@ -4,12 +4,12 @@ use std::{
sync::Arc, sync::Arc,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::Mutex; use tokio::sync::Mutex;
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadRecord { pub struct UploadRecord {
pub uploaded: DateTime<Utc>, pub uploaded: DateTime<Utc>,
pub file: PathBuf, pub file: PathBuf,
@ -26,7 +26,10 @@ impl UploadRecord {
} }
pub fn can_be_downloaded(&self) -> bool { pub fn can_be_downloaded(&self) -> bool {
self.downloads < self.max_downloads let now = Utc::now();
let dur_since_upload = now.signed_duration_since(self.uploaded);
dur_since_upload < Duration::days(3) && self.downloads < self.max_downloads
} }
} }
@ -36,7 +39,7 @@ impl Default for UploadRecord {
uploaded: Utc::now(), uploaded: Utc::now(),
file: Path::new("").to_owned(), file: Path::new("").to_owned(),
downloads: 0, downloads: 0,
max_downloads: 1, max_downloads: 5,
} }
} }
} }

View file

@ -1,7 +1,9 @@
use futures::TryFutureExt; use futures::TryFutureExt;
use leptos::IntoView; use leptos::{Children, IntoView};
use serde::Deserialize; use serde::Deserialize;
use crate::state::UploadRecord;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CatFact { pub struct CatFact {
pub fact: String, pub fact: String,
@ -15,32 +17,68 @@ pub async fn get_cat_fact() -> String {
.unwrap_or_else(|_| String::from("The cat fact goddess has failed me :<")) .unwrap_or_else(|_| String::from("The cat fact goddess has failed me :<"))
} }
// {https://api.thecatapi.com/v1/images/search?size=small&format=src}
// {https://cataas.com/cat?width=250&height=250}
#[leptos::component] #[leptos::component]
pub fn Welcome(cx: leptos::Scope, fact: String) -> impl IntoView { pub fn Welcome(cx: leptos::Scope, fact: String) -> impl IntoView {
leptos::view! { cx, leptos::view! { cx,
<head> <HtmxPage>
<title>NyaZoom</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="dist/css/main.css" rel="stylesheet" />
<script src="dist/scripts/file_label.js" />
</head>
<body>
<h1>NyaZoom<sup>2</sup></h1> <h1>NyaZoom<sup>2</sup></h1>
<div class="form-wrapper"> <div class="form-wrapper">
<form action="/upload" method="post" enctype="multipart/form-data" class="main-form"> <form action="/upload" method="post" enctype="multipart/form-data" class="main-form">
<div class="cat-img-wrapper"> <div class="cat-img-wrapper">
<img class="cat-img" src="https://cataas.com/cat?width=250&height=250" /> <img class="cat-img" src="https://api.thecatapi.com/v1/images/search?size=small&format=src" />
</div> </div>
<input type="file" id="file" name="file" data-multiple-caption="{{count}} files selected" multiple /> <input type="file" id="file" name="file" data-multiple-caption="{count} files selected" multiple />
<label for="file">Select Files</label> <label for="file">Select Files</label>
<input type="submit" value="Get Link~" /> <input type="submit" value="Get Link~" />
<p id="cat-fact">{fact}</p> <p id="cat-fact">{fact}</p>
</form> </form>
</div> </div>
</HtmxPage>
}
}
// <link href="../dist/css/link.css" rel="stylesheet" />
// #TODO: Handle pushing cleaner
#[leptos::component]
pub fn DownloadLink(cx: leptos::Scope, id: String, record: UploadRecord) -> impl IntoView {
let downloads_remaining = record.max_downloads - record.downloads;
let plural = if downloads_remaining > 1 { "s" } else { "" };
leptos::view! { cx,
<HtmxPage>
<div class="link-wrapper">
<a id="link" href=format!("/download/{id}")>Download Now!</a>
</div>
<div class="link-wrapper">
You have {record.max_downloads - record.downloads} download{plural} remaining!
</div>
<button class="return-button" onclick="clipboard()">Copy to Clipboard</button>
<a href="/" class="return-button">Return to home</a>
</HtmxPage>
}
}
#[leptos::component]
pub fn HtmxPage(cx: leptos::Scope, children: Children) -> impl IntoView {
leptos::view! { cx,
<head>
<title>Nyazoom</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/css/main.css" rel="stylesheet" />
<link href="/css/link.css" rel="stylesheet" />
<script src="/scripts/file_label.js" />
<script src="/scripts/link.js" />
</head>
<body>
{children(cx)}
</body> </body>
} }
} }