Files
booksman/backend/api/src/lib.rs
2022-11-13 17:47:53 +05:30

546 lines
18 KiB
Rust

use axum::{
http::{HeaderValue, Method, StatusCode},
response::IntoResponse,
routing::{get,post,get_service},
Json, Router,
extract::{Extension, Path},
};
use axum_extra::routing::SpaRouter;
use clap::Parser;
use image;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::io;
use tower::ServiceBuilder;
use tower_http::services::ServeDir;
use tower_http::cors::{Any,CorsLayer};
use tower_http::trace::TraceLayer;
use booksman_orm::{
sea_orm::{Database, DatabaseConnection},
Mutation as MutationCore, Query as QueryCore,
BookAndMeta,
};
use itertools::Itertools;
use ::entity::entities::{book,book_author,book_person,book_place,book_subject,book_time,book_isbn};
use std::env;
#[derive(Deserialize, Serialize, Debug, Clone)]
struct BookUI {
id: i32,
open_library_key: Option<String>,
title: String,
edition_count: Option<i32>,
first_publish_year: Option<i32>,
median_page_count: Option<i32>,
goodread_id: Option<String>,
description: Option<String>,
cover: Option<String>,
location: Option<String>,
time_added: Option<String>,
rating: Option<i32>,
comments: Option<String>,
author_name: Option<Vec<String>>,
person: Option<Vec<String>>,
place: Option<Vec<String>>,
subject: Option<Vec<String>>,
time: Option<Vec<String>>,
isbn: Option<Vec<String>>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct Docs {
key: String,
title: String,
edition_count: Option<i32>,
first_publish_year: Option<i32>,
number_of_pages_median: Option<i32>,
goodread_id: Option<String>,
description: Option<String>,
isbn: Option<Vec<String>>,
cover_i: Option<i32>,
cover_url: Option<String>,
author_name: Option<Vec<String>>,
person: Option<Vec<String>>,
place: Option<Vec<String>>,
subject: Option<Vec<String>>,
time: Option<Vec<String>>,
}
#[derive(Deserialize, Serialize, Debug)]
struct Books {
num_found: u32,
docs: Vec<Docs>,
}
#[derive(Deserialize, Serialize, Debug)]
struct PaginatedBookUIList {
num_pages: u32,
books: Vec<BookUI>,
}
#[derive(Deserialize, Serialize, Debug)]
struct BookPageOL {
description: Option<DescriptionOL>,
}
#[derive(Deserialize, Serialize, Debug)]
struct DescriptionOL {
value: Option<String>,
}
impl Books {
fn set_all_cover_urls(&mut self) {
for book in self.docs.iter_mut() {
match book.cover_i {
Some(cover_i) => {
book.cover_url = Some(format!(
"https://covers.openlibrary.org/b/id/{}-L.jpg",
cover_i
))
}
None => (),
}
}
}
async fn set_all_descriptions(&mut self) {
for book in self.docs.iter_mut() {
let query = format!("https://openlibrary.org/{}.json", book.key);
let res = reqwest::get(query).await.expect("Unable to request");
let resjson = res.json::<BookPageOL>().await.expect("Unable to return value");
let description = resjson.description;
match description {
Some(description) => {
match description.value {
Some(value) => book.description = Some(value),
None => (),
}
}
None => (),
}
}
}
}
async fn handle_error(_err: io::Error) -> impl IntoResponse {
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
}
// Setup the command line interface with clap.
#[derive(Parser, Debug)]
#[clap(name = "server", about = "A server for our wasm project!")]
struct Opt {
/// set the log level
#[clap(short = 'l', long = "log", default_value = "debug")]
log_level: String,
/// set the listen addr
#[clap(short = 'a', long = "addr", default_value = "::1")]
addr: String,
/// set the listen port
#[clap(short = 'p', long = "port", default_value = "8081")]
port: u16,
/// set the directory where static files are to be found
#[clap(long = "static-dir", default_value = "../dist")]
static_dir: String,
}
#[tokio::main]
pub async fn main() {
let opt = Opt::parse();
// Setup logging & RUST_LOG from args
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", format!("{},hyper=info,mio=info", opt.log_level))
}
// enable console logging
tracing_subscriber::fmt::init();
dotenvy::dotenv().ok();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
let conn = Database::connect(db_url)
.await
.expect("Database connection failed");
//Migrator::up(&conn, None).await.unwrap();
let app = Router::new()
.route("/api/search_openlibrary", get(search_openlibrary))
.route("/api/delete/:id", get(delete_book))
.route("/api/list", get(list_book))
.route("/api/create", post(create_book))
.route("/api/update", post(update_book))
.nest("/images", get_service(ServeDir::new("../images")).handle_error(handle_error))
.merge(SpaRouter::new("/assets", opt.static_dir))
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.layer(Extension(conn))
.layer(
// see https://docs.rs/tower-http/latest/tower_http/cors/index.html
// for more details
//
// pay attention that for some request types like posting content-type: application/json
// it is required to add ".allow_headers([http::header::CONTENT_TYPE])"
// or see this issue https://github.com/tokio-rs/axum/issues/849
CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_origin("http://localhost:8080".parse::<HeaderValue>().unwrap())
.allow_headers(Any),
);
let sock_addr = SocketAddr::from((
IpAddr::from_str(opt.addr.as_str()).unwrap_or(IpAddr::V6(Ipv6Addr::LOCALHOST)),
opt.port,
));
log::info!("listening on http://{}", sock_addr);
axum::Server::bind(&sock_addr)
.serve(app.into_make_service())
.await
.expect("Unable to start server");
}
async fn search_openlibrary(
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> impl IntoResponse {
print!("Get items with query params: {:?}\n", params);
let search = params.get("search").unwrap();
let query = format!("https://openlibrary.org/search.json?q={}", search);
let res = reqwest::get(query).await.expect("Unable to request");
let mut resjson = res.json::<Books>().await.expect("Unable to return value");
resjson.docs.truncate(12);
resjson.set_all_cover_urls();
// resjson.set_all_descriptions().await;
print!("Search token {:?}\n", search);
let mut vec = Vec::with_capacity(12);
for i in 0..12 as usize {
let doc = resjson.docs[i].clone();
vec.push(
BookUI{
id: -((i+1) as i32),
open_library_key: Some(doc.key),
title: doc.title,
edition_count: doc.edition_count,
first_publish_year: doc.first_publish_year,
median_page_count: doc.number_of_pages_median,
goodread_id: doc.goodread_id,
description: doc.description,
cover: doc.cover_url,
location: None,
time_added: None,
rating: None,
comments: None,
author_name: doc.author_name,
person: doc.person,
place: doc.place,
subject: doc.subject,
time: doc.time,
isbn: doc.isbn
}
);
}
return Json(vec);
}
async fn delete_book(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
) -> impl IntoResponse {
MutationCore::delete_book(conn, id)
.await
.expect("could not delete book");
"success"
}
async fn list_book(
Extension(ref conn): Extension<DatabaseConnection>,
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> impl IntoResponse {
let page: usize = params.get("page").unwrap().parse().unwrap();
let books = QueryCore::find_books_plus_meta_in_page(conn,page,5)
.await
.expect("could not list books");
let mut resbooks: Vec<BookUI> = Vec::with_capacity(5);
for bookandmeta in books.0.into_iter() {
let mut cover = bookandmeta.clone().book.cover;
if !cover.is_none() {
cover = Some(format!("http://localhost:8081/images/{}",cover.unwrap()));
}
let bookui = BookUI{
id: bookandmeta.clone().book.id,
title: bookandmeta.clone().book.title,
open_library_key: bookandmeta.clone().book.open_library_key,
edition_count: bookandmeta.clone().book.edition_count,
first_publish_year: bookandmeta.clone().book.first_publish_year,
median_page_count: bookandmeta.clone().book.median_page_count,
goodread_id: bookandmeta.clone().book.goodread_id,
description: bookandmeta.clone().book.description,
cover: cover.clone(),
location: bookandmeta.clone().book.location,
time_added: bookandmeta.clone().book.time_added,
rating: bookandmeta.clone().book.rating,
comments: bookandmeta.clone().book.comments,
author_name: Some(bookandmeta.clone().authors.into_iter().map(|u| u.author_name).collect() ),
person: Some(bookandmeta.clone().persons.into_iter().map(|u| u.person).collect()),
place: Some(bookandmeta.clone().places.into_iter().map(|u| u.place).collect()),
subject: Some(bookandmeta.clone().subjects.into_iter().map(|u| u.subject).collect()),
time: Some(bookandmeta.clone().times.into_iter().map(|u| u.time).collect()),
isbn: Some(bookandmeta.clone().isbns.into_iter().map(|u| u.isbn).collect()),
};
resbooks.push(bookui);
}
let res = PaginatedBookUIList{
num_pages: books.1 as u32,
books: resbooks
};
return Json(res);
// "success"
}
async fn create_book(
Extension(ref conn): Extension<DatabaseConnection>,
Json(doc_sent): Json<BookUI>,
) -> impl IntoResponse {
println!("Creating book");
let mut cover = doc_sent.cover.clone();
if !doc_sent.cover.is_none() {
let img_bytes = reqwest::get(cover.unwrap()).await.unwrap().bytes().await.unwrap();
//.expect("Could not fetch image");
//let img_bytes = img_resp.unwrap().bytes();
let image = image::load_from_memory(&img_bytes).unwrap();
let temp_cover = doc_sent.cover.clone().unwrap();
let img_id = temp_cover.split("/").last().unwrap();
image.save(format!("../images/{}",img_id)).expect("Failed to save image");
cover = Some(img_id.to_string());
}
let book: book::Model = book::Model{
open_library_key: doc_sent.open_library_key.to_owned(),
title: (doc_sent.title.to_owned()),
edition_count: doc_sent.edition_count.to_owned(),
first_publish_year: (doc_sent.first_publish_year.to_owned()),
median_page_count: (doc_sent.median_page_count.to_owned()),
goodread_id: doc_sent.goodread_id.to_owned(),
description: doc_sent.description.to_owned(),
comments: doc_sent.comments.to_owned(),
cover: cover.to_owned(),
rating: doc_sent.rating.to_owned(),
time_added: doc_sent.time_added.to_owned(),
id: 1,
location: doc_sent.location.to_owned(),
};
let created_book = MutationCore::create_book(conn, book)
.await
.expect("could not create book");
for author_name in doc_sent.author_name.as_ref().unwrap().iter() {
let record : book_author::Model = book_author::Model{
id: 1,
book_id: (created_book.last_insert_id),
author_name: author_name.to_owned(),
};
MutationCore::create_book_author(conn, record)
.await
.expect("could not create book");
}
for person in doc_sent.person.as_ref().unwrap().iter() {
let record : book_person::Model = book_person::Model{
id: 1,
book_id: (created_book.last_insert_id),
person: person.to_owned(),
};
MutationCore::create_book_person(conn, record)
.await
.expect("could not create book");
}
for place in doc_sent.place.as_ref().unwrap().iter() {
let record : book_place::Model = book_place::Model{
id: 1,
book_id: (created_book.last_insert_id),
place: place.to_owned(),
};
MutationCore::create_book_place(conn, record)
.await
.expect("could not create book");
}
for subject in doc_sent.subject.as_ref().unwrap().iter() {
let record : book_subject::Model = book_subject::Model{
id: 1,
book_id: (created_book.last_insert_id),
subject: subject.to_owned(),
};
MutationCore::create_book_subject(conn, record)
.await
.expect("could not create book");
}
for time in doc_sent.time.as_ref().unwrap().iter() {
let record : book_time::Model = book_time::Model{
id: 1,
book_id: (created_book.last_insert_id),
time: time.to_owned(),
};
MutationCore::create_book_time(conn, record)
.await
.expect("could not create book");
}
for isbn in doc_sent.isbn.as_ref().unwrap().iter() {
let record : book_isbn::Model = book_isbn::Model{
id: 1,
book_id: (created_book.last_insert_id),
isbn: isbn.to_owned(),
};
MutationCore::create_book_isbn(conn, record)
.await
.expect("could not create book");
}
"success"
}
async fn update_book(
Extension(ref conn): Extension<DatabaseConnection>,
Json(doc_sent): Json<BookUI>,
) -> impl IntoResponse {
println!("Updating book");
let mut cover = doc_sent.cover.clone();
if !doc_sent.cover.is_none() {
let img_bytes = reqwest::get(cover.unwrap()).await.unwrap().bytes().await.unwrap();
//.expect("Could not fetch image");
//let img_bytes = img_resp.unwrap().bytes();
let image = image::load_from_memory(&img_bytes).unwrap();
let temp_cover = doc_sent.cover.clone().unwrap();
let img_id = temp_cover.split("/").last().unwrap();
image.save(format!("../images/{}",img_id)).expect("Failed to save image");
cover = Some(img_id.to_string());
}
let book: book::Model = book::Model{
open_library_key: doc_sent.open_library_key.to_owned(),
title: (doc_sent.title.to_owned()),
edition_count: doc_sent.edition_count.to_owned(),
first_publish_year: (doc_sent.first_publish_year.to_owned()),
median_page_count: (doc_sent.median_page_count.to_owned()),
goodread_id: doc_sent.goodread_id.to_owned(),
description: doc_sent.description.to_owned(),
comments: doc_sent.comments.to_owned(),
cover: cover.to_owned(),
rating: doc_sent.rating.to_owned(),
time_added: doc_sent.time_added.to_owned(),
id: doc_sent.id.to_owned(),
location: doc_sent.location.to_owned(),
};
let created_book = MutationCore::update_book_by_id(conn, book.id, book)
.await
.expect("could not update book");
MutationCore::delete_book_author(conn, doc_sent.id)
.await
.expect("could not delete book authors while updating");
MutationCore::delete_book_person(conn, doc_sent.id)
.await
.expect("could not delete book persons while updating");
MutationCore::delete_book_place(conn, doc_sent.id)
.await
.expect("could not delete book places while updating");
MutationCore::delete_book_subject(conn, doc_sent.id)
.await
.expect("could not delete book subjects while updating");
MutationCore::delete_book_time(conn, doc_sent.id)
.await
.expect("could not delete book times while updating");
MutationCore::delete_book_isbn(conn, doc_sent.id)
.await
.expect("could not delete book isbns while updating");
for author_name in doc_sent.author_name.as_ref().unwrap().iter() {
let record : book_author::Model = book_author::Model{
id: 1,
book_id: doc_sent.id,
author_name: author_name.to_owned(),
};
MutationCore::create_book_author(conn, record)
.await
.expect("could not create book");
}
for person in doc_sent.person.as_ref().unwrap().iter() {
let record : book_person::Model = book_person::Model{
id: 1,
book_id: doc_sent.id,
person: person.to_owned(),
};
MutationCore::create_book_person(conn, record)
.await
.expect("could not create book");
}
for place in doc_sent.place.as_ref().unwrap().iter() {
let record : book_place::Model = book_place::Model{
id: 1,
book_id: doc_sent.id,
place: place.to_owned(),
};
MutationCore::create_book_place(conn, record)
.await
.expect("could not create book");
}
for subject in doc_sent.subject.as_ref().unwrap().iter() {
let record : book_subject::Model = book_subject::Model{
id: 1,
book_id: doc_sent.id,
subject: subject.to_owned(),
};
MutationCore::create_book_subject(conn, record)
.await
.expect("could not create book");
}
for time in doc_sent.time.as_ref().unwrap().iter() {
let record : book_time::Model = book_time::Model{
id: 1,
book_id: doc_sent.id,
time: time.to_owned(),
};
MutationCore::create_book_time(conn, record)
.await
.expect("could not create book");
}
for isbn in doc_sent.isbn.as_ref().unwrap().iter() {
let record : book_isbn::Model = book_isbn::Model{
id: 1,
book_id: doc_sent.id,
isbn: isbn.to_owned(),
};
MutationCore::create_book_isbn(conn, record)
.await
.expect("could not create book");
}
"success"
}