// Copyright © 2020 Jakob L. Kreuze // // This file is part of бирка-тян. // // бирка-тян is free software; you can redistribute it and/or modify it // under the terms of the GNU Affero General Public License as published // by the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // бирка-тян is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public // License along with бирка-тян. If not, see . #![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate anyhow; extern crate image; #[macro_use] extern crate rocket; extern crate rocket_contrib; #[macro_use] extern crate serde_derive; mod database; use anyhow::Result; use database::{hash_file, Query, TagDatabase}; use image::ImageFormat; use rocket::{Data, State}; use rocket_contrib::json::Json; use rocket_contrib::serve::StaticFiles; use rocket_contrib::templates::Template; use std::env; use std::fs; use std::fs::File; use std::os::unix::fs::symlink; use std::path::Path; use std::sync::Mutex; type SiteState<'a> = State<'a, Mutex>; struct DatabaseConnection { tb: TagDatabase, image_dir: String, } impl DatabaseConnection { fn new() -> Result { let bin = env::current_exe()?.canonicalize()?; let bin_dir = bin.parent().unwrap(); let image_dir = String::from(bin_dir.join("image/").to_str().unwrap()); let tb_path = bin_dir.join("birka.db"); let tb = TagDatabase::new(tb_path.to_str().unwrap())?; Ok(DatabaseConnection { tb, image_dir }) } } #[get("/")] fn index() -> Template { let context = HashMap::<&str, &str>::new(); Template::render("index", &context) } #[get("/posts?")] fn display_posts(conn: SiteState, tags: String) -> Template { #[derive(Serialize)] struct TagResult { name: String, count: i64, } #[derive(Serialize)] struct Asdf { tags: Vec, images: Vec, } let tag_results = { let tb = &conn.inner().lock().unwrap().tb; tb.top_tags(20) .unwrap() .iter() .map(|tup| TagResult { name: tup.1.clone(), count: tup.0, }) .collect() }; let images = get_posts(conn, tags).into_inner(); let context = Asdf { tags: tag_results, images, }; Template::render("index", context) } #[derive(Serialize)] struct ImageResult { filename: String, thumb_filename: String, tags: Vec, } #[get("/posts?")] fn get_posts(conn: SiteState, tags: String) -> Json> { let tb = &conn.inner().lock().unwrap().tb; let tags = tags.split(",").collect::>(); let results = tb .query(Query { tags: &tags[..], last_id: Some(0), limit: Some(50), }) .unwrap(); Json( results .iter() .map(|image| ImageResult { filename: image.filename.clone(), thumb_filename: thumb_filename(&image.filename), tags: tb.tags_for_image(image.id).unwrap(), }) .collect(), ) } #[post("/posts/", data = "")] fn put_posts(conn: SiteState, filename: String, data: Data) -> Json<&str> { let conn = conn.inner().lock().unwrap(); let path = Path::new(&conn.image_dir).join(filename); data.stream_to_file(&path).unwrap(); conn.tb .import_image(&path, &hash_file(&path).unwrap()) .unwrap(); Json("uploaded!") } fn thumb_filename(filename: &str) -> String { String::from(format!( "{}_thumb.png", // Strip extension. if let Some(n) = filename.rfind('.') { &filename[..n] } else { &filename[..] } )) } /// Maybe initialize the directory for storing image symlinks and thumbnails. fn create_image_directory(conn: &DatabaseConnection) { fs::create_dir(&conn.image_dir).ok(); for image in conn.tb.images_by_tags(&vec![][..]).unwrap() { let src = format!("{}/{}", image.orig_dir, image.filename); let dst = format!("{}/{}", conn.image_dir, image.filename); symlink(&src, &dst).ok(); let stem = Path::new(&dst).file_stem().unwrap().to_str().unwrap(); let thumb_path = format!("{}/{}", conn.image_dir, thumb_filename(&stem)); let thumb_path = Path::new(&thumb_path); if !thumb_path.exists() { let im = image::open(&Path::new(&src)).unwrap(); let fout = &mut File::create(&thumb_path).unwrap(); im.thumbnail(100, 100) .write_to(fout, ImageFormat::Png) .unwrap(); } } } fn main() { let conn = DatabaseConnection::new().unwrap(); let image_dir = conn.image_dir.clone(); create_image_directory(&conn); rocket::ignite() .attach(Template::fairing()) .manage(Mutex::new(conn)) .mount("/public", StaticFiles::from("./static/")) .mount("/image", StaticFiles::from(image_dir)) .mount("/", routes![index, display_posts]) .mount("/api", routes![get_posts, put_posts]) .launch(); }