Initial commit

This commit is contained in:
2020-11-13 22:41:03 +01:00
commit bbfb050279
8 changed files with 2309 additions and 0 deletions

199
src/main.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::PathBuf;
use actix_web::{get, post, web, http, App, HttpResponse, HttpServer, Responder};
use askama_actix::{Template, TemplateIntoResponse};
use chrono::{DateTime, FixedOffset};
use serde::{Serialize, Deserialize};
use simple_logger::SimpleLogger;
const DATA_DIR: &'static str = "/home/manuel/wolke/Projects/tinypod/data";
const DB_DIR: &'static str = "data";
const DB_FILE_EXT: &'static str = ".json";
mod date_format {
use chrono::{DateTime, FixedOffset};
use serde::{Serializer, Deserialize, Deserializer};
pub fn serialize<S>(date: &DateTime<FixedOffset>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
serializer.serialize_str(&date.to_rfc3339())
}
pub fn deserialize<'de, D> (deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?;
let mut res = DateTime::parse_from_rfc2822(&s).map_err(serde::de::Error::custom);
if res.is_err() {
res = DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom);
}
res
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Episode {
title: String,
video_url: String,
guid: String,
#[serde(with="date_format")]
pub_date: DateTime<FixedOffset>
}
impl Episode {
fn new() -> Episode {
Episode{title: String::new(), video_url: String::new(), guid: String::new(), pub_date: chrono::Utc::now().into()}
}
}
#[derive(Serialize, Deserialize)]
struct Podcast {
title: String,
logo_url: Option<String>,
homepage: String,
episodes: Vec<Episode>
}
impl Podcast {
fn build_path<S: AsRef<str>>(name: S) -> PathBuf {
let filename = String::from(name.as_ref()) + DB_FILE_EXT;
let mut path = PathBuf::from(DATA_DIR);
path.push(DB_DIR);
path.push(filename);
path
}
fn db_load<S: AsRef<str>>(name: S) -> Result<Podcast, std::io::Error> {
let file = BufReader::new(File::open(Self::build_path(name))?);
let podcast = Podcast::deserialize(serde_json::from_reader::<_, serde_json::Value>(file)?)?;
Ok(podcast)
}
fn db_store<S: AsRef<str>>(&self, name: S) -> Result<(), std::io::Error> {
let file = BufWriter::new(File::create(Self::build_path(name))?);
serde_json::to_writer_pretty(file, &self)?;
Ok(())
}
}
#[derive(Template)]
#[template(path = "feed.xml", escape="xml")]
// TODO Change ext to rss
struct FeedTemplate<'a> {
podcast: &'a Podcast
}
#[get("/feed/{name}")]
async fn feed(web::Path(name): web::Path<String>) -> impl Responder {
let podcast = match Podcast::db_load(&name) {
Ok(podcast) => podcast,
Err(e) => {
// TODO Log error
return HttpResponse::InternalServerError().body(e.to_string());
}
};
FeedTemplate{podcast: &podcast}.into_response().unwrap()
}
#[derive(Template)]
#[template(path = "edit_podcast.html")]
struct EditPodcastTemplate<'a> {
podcast: &'a Podcast
}
#[get("/edit/{name}/")]
async fn edit_podcast(web::Path(name): web::Path<String>) -> impl Responder {
let podcast = match Podcast::db_load(&name) {
Ok(podcast) => podcast,
Err(e) => {
// TODO Log error
return HttpResponse::InternalServerError().body(e.to_string());
}
};
EditPodcastTemplate{podcast: &podcast}.into_response().unwrap()
}
#[derive(Template)]
#[template(path = "edit_episode.html")]
struct EditEpisodeTemplate<'a> {
episode: &'a Episode
}
#[get("/edit/{podcast_name}/edit/{episode_number}")]
async fn edit_episode(web::Path((podcast_name, episode_number)): web::Path<(String, usize)>) -> impl Responder {
let podcast = match Podcast::db_load(&podcast_name) {
Ok(podcast) => podcast,
Err(e) => {
// TODO Log error
return HttpResponse::InternalServerError().body(e.to_string());
}
};
// TODO Check episode_number out of bounds
EditEpisodeTemplate{episode: &podcast.episodes[episode_number]}.into_response().unwrap()
}
#[post("/edit/{podcast_name}/edit/{episode_number}")]
async fn post_edit_episode(web::Path((podcast_name, episode_number)): web::Path<(String, usize)>, form: web::Form<Episode>) -> impl Responder {
let mut podcast = match Podcast::db_load(&podcast_name) {
Ok(podcast) => podcast,
Err(e) => {
// TODO Log error
return HttpResponse::InternalServerError().body(e.to_string());
}
};
podcast.episodes[episode_number] = form.into_inner();
match podcast.db_store(&podcast_name) {
Ok(_) => {},
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
}
HttpResponse::Found().header(http::header::LOCATION, format!("/edit/{}/", &podcast_name)).finish()
}
#[get("/edit/{podcast_name}/new")]
async fn new_episode(web::Path(podcast_name): web::Path<String>) -> impl Responder {
// TODO This checks if the podcast exists. Build a slimmer test
match Podcast::db_load(&podcast_name) {
Ok(_) => {},
Err(e) => {
// TODO Log error
return HttpResponse::InternalServerError().body(e.to_string());
}
};
// TODO Check episode_number out of bounds
EditEpisodeTemplate{episode: &Episode::new()}.into_response().unwrap()
}
#[post("/edit/{podcast_name}/new")]
async fn post_new_episode(web::Path(podcast_name): web::Path<String>, form: web::Form<Episode>) -> impl Responder {
let mut podcast = match Podcast::db_load(&podcast_name) {
Ok(podcast) => podcast,
Err(e) => {
// TODO Log error
return HttpResponse::InternalServerError().body(e.to_string());
}
};
podcast.episodes.insert(0, form.into_inner());
match podcast.db_store(&podcast_name) {
Ok(_) => {},
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
}
HttpResponse::Found().header(http::header::LOCATION, format!("/edit/{}/", &podcast_name)).finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
SimpleLogger::new().init().unwrap();
HttpServer::new(|| {
App::new()
.service(feed)
.service(edit_podcast)
.service(edit_episode)
.service(post_edit_episode)
.service(new_episode)
.service(post_new_episode)
})
.bind("0.0.0.0:8080")?
.run()
.await
}