working v1
This commit is contained in:
parent
cc21efdea9
commit
ca8c12f56e
4
server/.gitignore → .gitignore
vendored
4
server/.gitignore → .gitignore
vendored
@ -8,3 +8,7 @@ Cargo.lock
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Videos & audio downloaded from testing
|
||||
*.mp3
|
||||
*.mp4
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "site/themes/lugo"]
|
||||
path = site/themes/lugo
|
||||
url = https://github.com/Rushmore75/lugo.git
|
@ -1,21 +1,15 @@
|
||||
[package]
|
||||
name = "poem_website_template"
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5.3"
|
||||
futures-util = "0.3"
|
||||
password-hash = "0.5.0"
|
||||
poem = { version="3", features = ["static-files", "websocket", "session"]}
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uuid = "1.8.0"
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
csv = "1.3.0"
|
||||
url = "2.5.0"
|
@ -1,2 +0,0 @@
|
||||
username,hashed_password,salt
|
||||
admin,MMEq3XimAWJ3OKTDHl5cPFEC45ghVW4c2G3/fE9mfwY,k8omIbCJkNyoLa3QihqyQw
|
|
@ -1,240 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
use argon2::Argon2;
|
||||
use password_hash::rand_core::OsRng;
|
||||
use password_hash::{PasswordHash, SaltString};
|
||||
use poem::http::StatusCode;
|
||||
use poem::{
|
||||
web::cookie::SameSite,
|
||||
middleware::CookieJarManager,
|
||||
session::CookieConfig,
|
||||
IntoResponse, Request, Endpoint, Middleware
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
use crate::FILESYSTEM_ROOT;
|
||||
|
||||
pub static COOKIE_JAR_NAME: &str = "clocks";
|
||||
|
||||
|
||||
pub mod simple_storage {
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, warn};
|
||||
use super::{Account, AccountDB};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FilesystemDB {
|
||||
data: Arc<RwLock<HashMap<String, Account>>>,
|
||||
}
|
||||
|
||||
impl AccountDB for FilesystemDB {
|
||||
|
||||
|
||||
async fn account_with_name(&self, name: &str) -> Option<Account> {
|
||||
let data = self.data.read().await;
|
||||
data.get(name).cloned()
|
||||
}
|
||||
|
||||
async fn init() -> Self {
|
||||
let path = "./import.csv";
|
||||
let db: Arc<RwLock<HashMap<String, Account>>> = Default::default();
|
||||
let csv = csv::Reader::from_path(path);
|
||||
|
||||
let mut lock = db.write().await;
|
||||
|
||||
match csv {
|
||||
Ok(mut r) => {
|
||||
for i in r.deserialize::<Account>() {
|
||||
match i {
|
||||
Ok(acct) => {
|
||||
debug!("Importing: {:?}", acct.username);
|
||||
lock.insert(acct.username.clone(), acct);
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error whilst importing account. {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// TODO, if the file isn't found create random admin credentials and print them out.
|
||||
error!("Could not read csv: \"{:?}\". {}", path, e);
|
||||
warn!("Continuing without loading any accounts!");
|
||||
}
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
Self { data: db }
|
||||
}
|
||||
|
||||
type Output = Self;
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods for interacting with a database
|
||||
pub trait AccountDB {
|
||||
type Output;
|
||||
|
||||
fn account_with_name(&self, name: &str) -> impl std::future::Future<Output = Option<Account>> + Send;
|
||||
|
||||
async fn init() -> Self::Output;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Account {
|
||||
username: String,
|
||||
hashed_password: String,
|
||||
salt: String,
|
||||
}
|
||||
|
||||
impl Display for Account {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{},{},{}",self.username,self.hashed_password,self.salt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Account {
|
||||
pub fn try_new(username: &str, password: &str) -> Result<Self, password_hash::Error> {
|
||||
let salt = SaltString::generate(OsRng);
|
||||
match PasswordHash::generate(Argon2::default(), password, &salt) {
|
||||
Ok(hash) => {
|
||||
return Ok(Self {
|
||||
username: username.to_owned(),
|
||||
hashed_password: hash.to_string(),
|
||||
salt: salt.as_str().to_string(),
|
||||
});
|
||||
},
|
||||
Err(e) => {
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_valid_creds<A>(
|
||||
accounts: &A,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> bool where A: AccountDB,
|
||||
{
|
||||
// find account with same username
|
||||
if let Some(account) = accounts.account_with_name(username).await {
|
||||
// retrieve the salt
|
||||
if let Ok(salt) = SaltString::from_b64(&account.salt) {
|
||||
// hash the incoming password to see if it is the same as the saved one
|
||||
if let Ok(hash) = PasswordHash::generate(Argon2::default(), password, &salt) {
|
||||
if hash.to_string() == account.hashed_password {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
type SessionDB = Arc<RwLock<HashSet<String>>>;
|
||||
#[derive(Clone)]
|
||||
pub struct AppState<A>
|
||||
where A: AccountDB
|
||||
{
|
||||
active_sessions: SessionDB,
|
||||
cookie_config: Arc<CookieConfig>,
|
||||
accounts: A,
|
||||
}
|
||||
|
||||
impl<A> AppState<A>
|
||||
where A: AccountDB<Output = A>
|
||||
{
|
||||
pub async fn new() -> Self {
|
||||
let cc = CookieConfig::default()
|
||||
.name(COOKIE_JAR_NAME)
|
||||
.same_site(SameSite::Strict)
|
||||
.max_age(None);
|
||||
|
||||
Self {
|
||||
cookie_config: Arc::new(cc),
|
||||
active_sessions: SessionDB::default(),
|
||||
accounts: A::init().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AccountDB + Clone + Send + Sync, E: Endpoint> Middleware<E> for AppState<A> {
|
||||
type Output = poem::middleware::CookieJarManagerEndpoint<ServerSessionEndpoint<E, A>>;
|
||||
|
||||
fn transform(&self, ep: E) -> Self::Output {
|
||||
CookieJarManager::new().transform(ServerSessionEndpoint {
|
||||
// These are both Arc, thus cloning is cheap
|
||||
config: self.cookie_config.clone(),
|
||||
sessions: self.active_sessions.clone(),
|
||||
endpoint: ep,
|
||||
accounts: self.accounts.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerSessionEndpoint<E, A> {
|
||||
endpoint: E,
|
||||
config: Arc<CookieConfig>,
|
||||
sessions: SessionDB,
|
||||
accounts: A
|
||||
}
|
||||
|
||||
impl<E, A> Endpoint for ServerSessionEndpoint<E, A>
|
||||
where E: Endpoint,
|
||||
A: AccountDB + Clone + Send+ Sync,
|
||||
{
|
||||
type Output = poem::Response;
|
||||
|
||||
async fn call(&self, req: Request) -> Result<Self::Output, poem::Error> {
|
||||
let cookie_jar = req.cookie().clone();
|
||||
// the config holds the keys to the cookie jar if it's locked. That's why we need self.config
|
||||
let client_session_id = self.config.get_cookie_value(&cookie_jar);
|
||||
|
||||
// Up next:
|
||||
// check if the client's session id is valid
|
||||
// if it doesn't exist try to sign in
|
||||
// if the session is invalid and signing in doesn't work - reject request
|
||||
|
||||
if let Some(id) = client_session_id {
|
||||
let server_sessions = self.sessions.read().await;
|
||||
if server_sessions.get(&id).is_some() {
|
||||
// Client's session is found on the server.
|
||||
// Go ahead and call the endpoint.
|
||||
let res = self.endpoint.call(req).await?;
|
||||
return Ok(res.into_response());
|
||||
}
|
||||
}
|
||||
// couldn't validate req via the cookie
|
||||
// try to login the user
|
||||
|
||||
// Try to log the user in
|
||||
if let Some(username) = req.header("username") { // These are case-insensitive keys
|
||||
if let Some(password) = req.header("password") {
|
||||
if Account::is_valid_creds(&self.accounts, username, password).await {
|
||||
// set session id
|
||||
let uuid = Uuid::default().to_string();
|
||||
/* server */ self.sessions.write().await.insert(uuid.clone());
|
||||
/* client */ self.config.set_cookie_value(&cookie_jar, &uuid);
|
||||
let res = self.endpoint.call(req).await?;
|
||||
return Ok(res.into_response());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// this request couldn't be validated in any way.
|
||||
// tell them to login
|
||||
let x = poem::endpoint::StaticFileEndpoint::new(FILESYSTEM_ROOT.to_owned() + "/login.html");
|
||||
let mut res = x.call(req).await?;
|
||||
res.set_status(StatusCode::UNAUTHORIZED);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
@ -1,95 +0,0 @@
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![deny(clippy::todo)]
|
||||
#![deny(clippy::unimplemented)]
|
||||
#![feature(ascii_char)]
|
||||
#![feature(addr_parse_ascii)]
|
||||
#![feature(async_closure)]
|
||||
|
||||
use std::future::IntoFuture;
|
||||
|
||||
use account::simple_storage::FilesystemDB;
|
||||
use clap::Parser;
|
||||
use poem::{
|
||||
EndpointExt, endpoint::StaticFilesEndpoint, listener::TcpListener, Route, Server
|
||||
};
|
||||
use tracing::{Level, instrument};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::account::AppState;
|
||||
|
||||
mod account;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
new: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let filter = EnvFilter::builder()
|
||||
.parse("room_clocks=trace,poem=debug,tokio=warn")
|
||||
.expect("Could not create env filter.")
|
||||
;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
tracing_subscriber::fmt::fmt()
|
||||
.with_max_level(Level::TRACE)
|
||||
.with_target(true)
|
||||
.with_env_filter(filter)
|
||||
.with_thread_ids(false)
|
||||
.with_file(false)
|
||||
.without_time()
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
match args.new {
|
||||
Some(a) => {
|
||||
let mut x = a.split_ascii_whitespace();
|
||||
let username = x.next().expect("You passed an empty string as credentials...");
|
||||
let password = x.next().expect("You need to pass a password along with that username.");
|
||||
// let acct = Account::new(&AccountLogin::new(password, username)).expect("Internal error whilst building account. Most likely during the password hashing process.");
|
||||
if let Ok(act) = account::Account::try_new(username, password) {
|
||||
println!("{}", act);
|
||||
}
|
||||
return Ok(())
|
||||
},
|
||||
None => {/*continue*/},
|
||||
}
|
||||
|
||||
// Tick thru messages
|
||||
run_server().into_future().await
|
||||
}
|
||||
|
||||
|
||||
pub static FILESYSTEM_ROOT: &str = "./www/";
|
||||
#[instrument(level="trace", name="web" skip_all)]
|
||||
async fn run_server() -> Result<(), std::io::Error> {
|
||||
let appstate = AppState::<FilesystemDB>::new().await;
|
||||
|
||||
// Wow, poem routing gets a bit messy
|
||||
let app = Route::new()
|
||||
.nest("/", StaticFilesEndpoint::new(FILESYSTEM_ROOT)
|
||||
.index_file("index.html")
|
||||
.fallback_to_index()
|
||||
)
|
||||
|
||||
.nest("/protected", Route::new()
|
||||
.nest("/", StaticFilesEndpoint::new(FILESYSTEM_ROOT.to_owned()+"protected/")
|
||||
.index_file("account.html")
|
||||
.fallback_to_index()
|
||||
)
|
||||
.with(appstate)
|
||||
)
|
||||
.with(poem::middleware::Tracing)
|
||||
;
|
||||
|
||||
Server::new(TcpListener::bind("0.0.0.0:3000"))
|
||||
.run(app)
|
||||
.await
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
this is some html text
|
||||
|
||||
or - would be, if it was more than text
|
@ -1 +0,0 @@
|
||||
login
|
@ -1 +0,0 @@
|
||||
account
|
@ -1 +0,0 @@
|
||||
this or that
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
@ -1,4 +0,0 @@
|
||||
baseURL = 'http://example.org/'
|
||||
languageCode = 'en-us'
|
||||
title = 'My New Hugo Site'
|
||||
theme = 'lugo'
|
@ -1 +0,0 @@
|
||||
Subproject commit ca92c2dddeee70d4627931d2c214a299143b6735
|
256
src/main.rs
Normal file
256
src/main.rs
Normal file
@ -0,0 +1,256 @@
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![deny(clippy::todo)]
|
||||
#![deny(clippy::unimplemented)]
|
||||
#![feature(iter_collect_into)]
|
||||
|
||||
use std::{collections::HashMap, process::Command, sync::Arc};
|
||||
|
||||
use poem::{
|
||||
endpoint::StaticFilesEndpoint, get, handler, http::StatusCode, listener::TcpListener, middleware::AddData, post, web::{Data, Form, Path, StaticFileRequest}, Body, EndpointExt, FromRequest, IntoResponse, Request, Route, Server
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info, instrument, trace, Level};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use url::Url;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let filter = EnvFilter::builder()
|
||||
.parse("server=trace,poem=debug,tokio=warn")
|
||||
.expect("Could not create env filter.")
|
||||
;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
tracing_subscriber::fmt::fmt()
|
||||
.with_max_level(Level::TRACE)
|
||||
.with_target(true)
|
||||
.with_env_filter(filter)
|
||||
.with_thread_ids(false)
|
||||
.with_file(false)
|
||||
.without_time()
|
||||
.init();
|
||||
|
||||
if !(check_cmd("ffmpeg", "-version") && check_cmd("yt-dlp", "--version")) {
|
||||
error!("One or more commands errored out, continuing is pointless.");
|
||||
return Ok(());
|
||||
}
|
||||
trace!("Loading up server...");
|
||||
run_server().await
|
||||
}
|
||||
|
||||
fn check_cmd(cmd: &str, ver_arg: &str) -> bool {
|
||||
match Command::new(cmd).arg(ver_arg).output()
|
||||
{
|
||||
Ok(e) => {
|
||||
let stderr = e.stderr.iter().map(|f| *f as char).collect::<String>();
|
||||
let stdout = e.stdout.iter().map(|f| *f as char).collect::<String>();
|
||||
|
||||
if !e.status.success() {
|
||||
error!("Failed with to run {}!", cmd);
|
||||
error!("{}", stderr);
|
||||
false
|
||||
} else {
|
||||
info!("{} version: {}", cmd, stdout);
|
||||
true
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
error!("Couldn't run commands at all!");
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AppData {
|
||||
mapping: RwLock<HashMap<String, std::path::PathBuf>>,
|
||||
}
|
||||
|
||||
pub static FILESYSTEM_ROOT: &str = "./www/";
|
||||
#[instrument(level="trace", name="web" skip_all)]
|
||||
async fn run_server() -> Result<(), std::io::Error> {
|
||||
|
||||
// Wow, poem routing gets a bit messy
|
||||
let app = Route::new()
|
||||
.nest("/", StaticFilesEndpoint::new(FILESYSTEM_ROOT)
|
||||
.index_file("index.html")
|
||||
.fallback_to_index()
|
||||
)
|
||||
.at("/download", post(download_url))
|
||||
.at("/download/:path", get(get_file))
|
||||
.with(AddData::new(Arc::new(AppData::default())))
|
||||
.with(poem::middleware::Tracing)
|
||||
;
|
||||
|
||||
Server::new(TcpListener::bind("0.0.0.0:3000"))
|
||||
.run(app)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Input {
|
||||
url: String,
|
||||
output: OutputFormat,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum OutputFormat {
|
||||
Video,
|
||||
Audio,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
fn to_format(&self) -> &str {
|
||||
match self {
|
||||
OutputFormat::Video => "mp4",
|
||||
OutputFormat::Audio => "mp3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct YtDlpOutput {
|
||||
filename: String,
|
||||
key: String,
|
||||
}
|
||||
|
||||
impl YtDlpOutput {
|
||||
fn from(value: String) -> Self {
|
||||
let x = value.lines();
|
||||
let mut v = Vec::new();
|
||||
x.collect_into(&mut v);
|
||||
Self { filename: v[0].to_owned(), key: v[1].to_owned() }
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
#[instrument(level="trace", skip_all)]
|
||||
async fn download_url(Form(data): Form<Input>, state: Data<&Arc<AppData>>) -> poem::Response {
|
||||
trace!("Download attempt...");
|
||||
// validate url
|
||||
match Url::parse(&data.url) {
|
||||
Ok(url) => {
|
||||
match url.domain() {
|
||||
Some(domain) => {
|
||||
let mut domain: String = domain.to_string();
|
||||
|
||||
let splits: Vec<&str> = domain.split('.').collect();
|
||||
if splits.len() > 2 {
|
||||
domain = [
|
||||
splits[splits.len()-2],
|
||||
".",
|
||||
splits[splits.len()-1]
|
||||
]
|
||||
.iter()
|
||||
.map(|f| *f)
|
||||
.collect();
|
||||
}
|
||||
|
||||
if domain == "youtube.com" || domain == "yout.be" {
|
||||
debug!("Downloading {} to {} format.", data.url, data.output.to_format());
|
||||
|
||||
match Command::new("yt-dlp")
|
||||
.arg("--quiet") // shut up
|
||||
.arg("--no-warnings") // shut up again
|
||||
.arg("--windows-filenames") // windows compatible file name
|
||||
.arg("--no-playlist") // if video or playlist, choose video
|
||||
.args(["--print","after_move:filename,id"]) // output filename
|
||||
.args(["-f", OutputFormat::Video.to_format()]) // format selector
|
||||
.arg(data.url.clone())
|
||||
.output()
|
||||
{
|
||||
Ok(o) => {
|
||||
if let Some(code) = o.status.code() {
|
||||
|
||||
let stderr = o.stderr.iter().map(|f| *f as char).collect::<String>();
|
||||
let stdout = o.stdout.iter().map(|f| *f as char).collect::<String>();
|
||||
|
||||
match code {
|
||||
0 => {
|
||||
info!("Download done.");
|
||||
let mut out = YtDlpOutput::from(stdout);
|
||||
|
||||
// You can potentially do post-processing directly in yt-dlp
|
||||
match data.output {
|
||||
OutputFormat::Video => out.key = format!("video_{}", out.key),
|
||||
OutputFormat::Audio => {
|
||||
trace!("Converting to audio file.");
|
||||
let audio_file = format!("{}.mp3", out.filename);
|
||||
match Command::new("ffmpeg")
|
||||
.args(["-i", &out.filename]) // ffmpeg input
|
||||
.arg(audio_file.clone()) // ffmpeg output
|
||||
.output()
|
||||
{
|
||||
Ok(_) => {
|
||||
out = YtDlpOutput {
|
||||
filename: audio_file,
|
||||
key: format!("audio_{}", out.key),
|
||||
};
|
||||
},
|
||||
Err(_) => error!("Couldn't create shell for ffmpeg."),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
trace!("Setting state...");
|
||||
state.mapping.write().await.insert(out.key.clone(), out.filename.into());
|
||||
|
||||
let response = poem::Response::builder()
|
||||
.content_type("text/html")
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body(Body::from_string(format!("<script>window.location = '/download/{}'</script>", out.key)))
|
||||
;
|
||||
|
||||
return response;
|
||||
},
|
||||
2 => {
|
||||
error!("{} STDERR: {}", o.status, stderr);
|
||||
|
||||
}
|
||||
_ => {
|
||||
error!("{}", o.status);
|
||||
error!("{}", stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
error!("Failed to create shell for yt-dlp.");
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
None => error!("Url didn't have a domain."),
|
||||
}
|
||||
},
|
||||
Err(_) => error!("Couldn't parse url from string."),
|
||||
}
|
||||
poem::Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body("Internal error")
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[handler]
|
||||
#[instrument(level="trace", skip(state, r))]
|
||||
async fn get_file(Path(request_key): Path<String>,r: &Request, state: Data<&Arc<AppData>>) -> Result<poem::Response, poem::Error> {
|
||||
trace!("Getting: {}", request_key);
|
||||
|
||||
match state.mapping.read().await.get(&request_key) {
|
||||
Some(real_path) => {
|
||||
trace!("Found real path: {:?}", real_path);
|
||||
Ok(StaticFileRequest::from_request_without_body(r)
|
||||
.await?
|
||||
.create_response(real_path, true)?
|
||||
.into_response())
|
||||
},
|
||||
None => {
|
||||
error!("Tried to get {} from the state, but failed.", request_key);
|
||||
Err(poem::Error::from_status(StatusCode::NOT_FOUND))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
49
www/form.css
Normal file
49
www/form.css
Normal file
@ -0,0 +1,49 @@
|
||||
input {
|
||||
color: #ffffff;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
input, label, button {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
input[type="submit"]:focus {
|
||||
outline-color: #344a70;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
background-color: #344a70;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type=radio] {
|
||||
width: 1em;
|
||||
height: 0.75em;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="password"], [type="text"] {
|
||||
background-color: #ffffff;
|
||||
color: #222222;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
input[type="text"]::placeholder {
|
||||
text-align: center;
|
||||
color: #dddddd;
|
||||
}
|
||||
|
||||
textarea, input {
|
||||
border-radius: 0.5rem;
|
||||
border-style: hidden;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
47
www/index.html
Normal file
47
www/index.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Download Youtube Videos</title>
|
||||
<link rel="canonical" href="http://localhost:1313/" />
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="./style.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./lugo.css"/>
|
||||
<meta name="description" content="" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
<a href="./">Download Youtube Videos</a>
|
||||
</h1>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<div class="article-header">
|
||||
<h3 class="spaced"></h3>
|
||||
</div>
|
||||
<article>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="./form.css" />
|
||||
<form action="/download" enctype="application/x-www-form-urlencoded" method="post">
|
||||
<input type="text" name="url" placeholder="youtube url" />
|
||||
<br/>
|
||||
|
||||
<div class="line">
|
||||
<input id="audio" type="radio" name="output" value="Audio" />
|
||||
<label for="audio">Audio</label>
|
||||
</div>
|
||||
|
||||
<div class="line">
|
||||
<input id="video" type="radio" name="output" value="Video" />
|
||||
<label for="video">Video</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
67
www/lugo.css
Normal file
67
www/lugo.css
Normal file
@ -0,0 +1,67 @@
|
||||
/* Shortcode css*/
|
||||
/* For NEXTPREV.HTML */
|
||||
#nextprev {
|
||||
/* The container for both the previous and next articles. */
|
||||
color: black;
|
||||
}
|
||||
|
||||
#nextprev a:any-link {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#nextart {
|
||||
float: left ;
|
||||
text-align: left ;
|
||||
}
|
||||
|
||||
#prevart {
|
||||
float: right ;
|
||||
text-align: right ;
|
||||
}
|
||||
|
||||
#nextart,#prevart {
|
||||
max-width: 33% ;
|
||||
}
|
||||
|
||||
/* For TAGLIST.HTML */
|
||||
.taglist {
|
||||
text-align: center;
|
||||
clear: both;
|
||||
color: var(--bg-text);
|
||||
}
|
||||
|
||||
/* For putting right / left boxes next to each other */
|
||||
.left {
|
||||
width: 45%;
|
||||
padding-right: 5%;
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 45%;
|
||||
padding-left: 5%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
figure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
color: var(--bg-text);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 50vh;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.rss {
|
||||
border-radius: 0.25rem;
|
||||
}
|
141
www/style.css
Normal file
141
www/style.css
Normal file
@ -0,0 +1,141 @@
|
||||
:root {
|
||||
--bg: #222222;
|
||||
--bg-text: #606060;
|
||||
--text: #ffffff;
|
||||
--header: #454545;
|
||||
--header-text: #707070;
|
||||
--content-bg: #303030;
|
||||
--accent: #344a70;
|
||||
--link: #3e765d;
|
||||
--radius: 0.75rem;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background-color: var(--header);
|
||||
/* border-radius: var(--radius); */
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* blockquote~blockquote {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
blockquote+blockquote {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
} */
|
||||
|
||||
pre {
|
||||
background-color: var(--bg);
|
||||
padding: 1rem;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.copy:hover {
|
||||
color: var(--accent);
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: var(--accent);
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.article-header {
|
||||
background-color: var(--header);
|
||||
color: var(--header-text);
|
||||
}
|
||||
|
||||
section {
|
||||
border-radius: var(--radius);
|
||||
margin: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
article {
|
||||
background-color: var(--content-bg);
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 2rem;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--header);
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
footer {
|
||||
color: var(--bg-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
margin: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--accent);
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
header a {
|
||||
color: inherit;
|
||||
text-decoration-line: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
header a:hover {
|
||||
font-weight: bold;
|
||||
color: inherit
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1500px) {
|
||||
main {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user