mirror of
https://github.com/YGGverse/rssto.git
synced 2026-04-01 09:35:28 +00:00
draft initial http application
This commit is contained in:
parent
4c99208535
commit
353c78b2f0
12 changed files with 406 additions and 0 deletions
50
crates/http/src/config.rs
Normal file
50
crates/http/src/config.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use clap::Parser;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Config {
|
||||
/// Server name
|
||||
#[arg(long, default_value_t = String::from("rssto"))]
|
||||
pub title: String,
|
||||
|
||||
/// Server description
|
||||
#[arg(long)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Format timestamps (on the web view)
|
||||
///
|
||||
/// * tip: escape with `%%d/%%m/%%Y %%H:%%M` in the CLI/bash argument
|
||||
#[arg(long, default_value_t = String::from("%d/%m/%Y %H:%M"))]
|
||||
pub format_time: String,
|
||||
|
||||
/// Default listing limit
|
||||
#[arg(long, default_value_t = 20)]
|
||||
pub list_limit: usize,
|
||||
|
||||
/// Default capacity (estimated torrents in the `public` directory)
|
||||
#[arg(long, default_value_t = 1000)]
|
||||
pub capacity: usize,
|
||||
|
||||
/// Bind server on given host
|
||||
#[arg(long, default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
|
||||
pub host: IpAddr,
|
||||
|
||||
/// Bind server on given port
|
||||
#[arg(long, short, default_value_t = 8000)]
|
||||
pub port: u16,
|
||||
|
||||
/// Configure instance in the debug mode
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub debug: bool,
|
||||
|
||||
// Database
|
||||
#[arg(long, default_value_t = String::from("localhost"))]
|
||||
pub mysql_host: String,
|
||||
#[arg(long, default_value_t = 3306)]
|
||||
pub mysql_port: u16,
|
||||
|
||||
pub mysql_user: String,
|
||||
pub mysql_password: String,
|
||||
pub mysql_database: String,
|
||||
}
|
||||
58
crates/http/src/feed.rs
Normal file
58
crates/http/src/feed.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/// Export crawl index to the RSS file
|
||||
pub struct Feed {
|
||||
buffer: String,
|
||||
}
|
||||
|
||||
impl Feed {
|
||||
pub fn new(title: &str, description: Option<&str>, capacity: usize) -> Self {
|
||||
let t = chrono::Utc::now().to_rfc2822();
|
||||
let mut buffer = String::with_capacity(capacity);
|
||||
|
||||
buffer.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\"><channel>");
|
||||
|
||||
buffer.push_str(&format!("<pubDate>{t}</pubDate>"));
|
||||
buffer.push_str(&format!("<lastBuildDate>{t}</lastBuildDate>"));
|
||||
buffer.push_str(&format!("<title>{}</title>", escape(title)));
|
||||
|
||||
if let Some(d) = description {
|
||||
buffer.push_str(&format!("<description>{}</description>", escape(d)));
|
||||
}
|
||||
|
||||
Self { buffer }
|
||||
}
|
||||
|
||||
/// Append `item` to the feed `channel`
|
||||
pub fn push(
|
||||
&mut self,
|
||||
guid: u64,
|
||||
time: chrono::DateTime<chrono::Utc>,
|
||||
url: String,
|
||||
title: String,
|
||||
description: String,
|
||||
) {
|
||||
self.buffer.push_str(&format!(
|
||||
"<item><guid>{guid}</guid><title>{}</title><link>{url}</link><description>{}</description><pubDate>{}</pubDate></item>",
|
||||
escape(&title),
|
||||
escape(&description),
|
||||
time.to_rfc2822()
|
||||
))
|
||||
}
|
||||
|
||||
/// Write final bytes
|
||||
pub fn commit(mut self) -> String {
|
||||
self.buffer.push_str("</channel></rss>");
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO use tera filters?
|
||||
// https://keats.github.io/tera/docs/#built-in-filters
|
||||
|
||||
fn escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace("'", "'")
|
||||
}
|
||||
8
crates/http/src/global.rs
Normal file
8
crates/http/src/global.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use rocket::serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Global {
|
||||
pub list_limit: usize,
|
||||
pub format_time: String,
|
||||
}
|
||||
181
crates/http/src/main.rs
Normal file
181
crates/http/src/main.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
mod config;
|
||||
mod feed;
|
||||
mod global;
|
||||
mod meta;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use config::Config;
|
||||
use feed::Feed;
|
||||
use global::Global;
|
||||
use meta::Meta;
|
||||
use mysql::Mysql;
|
||||
use rocket::{State, http::Status, response::content::RawXml, serde::Serialize};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
|
||||
#[get("/?<search>&<page>")]
|
||||
fn index(
|
||||
search: Option<&str>,
|
||||
page: Option<usize>,
|
||||
db: &State<Mysql>,
|
||||
meta: &State<Meta>,
|
||||
global: &State<Global>,
|
||||
) -> Result<Template, Status> {
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct Content {
|
||||
content_id: u64,
|
||||
description: String,
|
||||
link: String,
|
||||
time: String,
|
||||
title: String,
|
||||
}
|
||||
let total = db.contents_total().map_err(|e| {
|
||||
error!("Could not get contents total: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
Ok(Template::render(
|
||||
"index",
|
||||
context! {
|
||||
title: {
|
||||
let mut t = String::new();
|
||||
if let Some(q) = search && !q.is_empty() {
|
||||
t.push_str(q);
|
||||
t.push_str(S);
|
||||
t.push_str("Search");
|
||||
t.push_str(S)
|
||||
}
|
||||
if let Some(p) = page && p > 1 {
|
||||
t.push_str(&format!("Page {p}"));
|
||||
t.push_str(S)
|
||||
}
|
||||
t.push_str(&meta.title);
|
||||
if let Some(ref description) = meta.description
|
||||
&& page.is_none_or(|p| p == 1) && search.is_none_or(|q| q.is_empty()) {
|
||||
t.push_str(S);
|
||||
t.push_str(description)
|
||||
}
|
||||
t
|
||||
},
|
||||
meta: meta.inner(),
|
||||
back: page.map(|p| uri!(index(search, if p > 2 { Some(p - 1) } else { None }))),
|
||||
next: if page.unwrap_or(1) * global.list_limit >= total { None }
|
||||
else { Some(uri!(index(search, Some(page.map_or(2, |p| p + 1))))) },
|
||||
rows: db.contents(Some(global.list_limit)).map_err(|e| {
|
||||
error!("Could not get contents: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let channel_item = db.channel_item(c.channel_item_id).unwrap().unwrap();
|
||||
Content {
|
||||
content_id: c.content_id,
|
||||
description: c.description,
|
||||
link: channel_item.link,
|
||||
time: time(channel_item.pub_date).format(&global.format_time).to_string(),
|
||||
title: c.title,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Content>>(),
|
||||
page: page.unwrap_or(1),
|
||||
pages: (total as f64 / global.list_limit as f64).ceil(),
|
||||
total,
|
||||
search
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/<content_id>")]
|
||||
fn info(
|
||||
content_id: u64,
|
||||
db: &State<Mysql>,
|
||||
meta: &State<Meta>,
|
||||
global: &State<Global>,
|
||||
) -> Result<Template, Status> {
|
||||
match db.content(content_id).map_err(|e| {
|
||||
error!("Could not get content `{content_id}`: `{e}`");
|
||||
Status::InternalServerError
|
||||
})? {
|
||||
Some(c) => {
|
||||
let i = db.channel_item(c.channel_item_id).unwrap().unwrap();
|
||||
Ok(Template::render(
|
||||
"info",
|
||||
context! {
|
||||
title: format!("{}{S}{}", c.title, meta.title),
|
||||
description: c.description,
|
||||
link: i.link,
|
||||
time: time(i.pub_date).format(&global.format_time).to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
None => Err(Status::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/rss")]
|
||||
fn rss(meta: &State<Meta>, db: &State<Mysql>) -> Result<RawXml<String>, Status> {
|
||||
let mut f = Feed::new(
|
||||
&meta.title,
|
||||
meta.description.as_deref(),
|
||||
1024, // @TODO
|
||||
);
|
||||
for c in db
|
||||
.contents(Some(20)) // @TODO
|
||||
.map_err(|e| {
|
||||
error!("Could not load channel item contents: `{e}`");
|
||||
Status::InternalServerError
|
||||
})?
|
||||
{
|
||||
let channel_item = db.channel_item(c.channel_item_id).unwrap().unwrap();
|
||||
f.push(
|
||||
c.channel_item_id,
|
||||
time(channel_item.pub_date),
|
||||
channel_item.link,
|
||||
c.title,
|
||||
c.description,
|
||||
)
|
||||
}
|
||||
Ok(RawXml(f.commit()))
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
use clap::Parser;
|
||||
let config = Config::parse();
|
||||
rocket::build()
|
||||
.attach(Template::fairing())
|
||||
.configure(rocket::Config {
|
||||
port: config.port,
|
||||
address: config.host,
|
||||
..if config.debug {
|
||||
rocket::Config::debug_default()
|
||||
} else {
|
||||
rocket::Config::release_default()
|
||||
}
|
||||
})
|
||||
.manage(Mysql::connect(
|
||||
&config.mysql_host,
|
||||
config.mysql_port,
|
||||
&config.mysql_user,
|
||||
&config.mysql_password,
|
||||
&config.mysql_database,
|
||||
))
|
||||
.manage(Global {
|
||||
format_time: config.format_time,
|
||||
list_limit: config.list_limit,
|
||||
})
|
||||
.manage(Meta {
|
||||
description: config.description,
|
||||
title: config.title,
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
})
|
||||
.mount("/", routes![index, rss, info])
|
||||
}
|
||||
|
||||
const S: &str = " • ";
|
||||
|
||||
fn time(timestamp: i64) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_timestamp(timestamp, 0).unwrap()
|
||||
}
|
||||
9
crates/http/src/meta.rs
Normal file
9
crates/http/src/meta.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use rocket::serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Meta {
|
||||
pub description: Option<String>,
|
||||
pub title: String,
|
||||
pub version: String,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue