diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/birka-web.rs (renamed from src/main.rs) | 139 |
1 files changed, 100 insertions, 39 deletions
diff --git a/src/main.rs b/src/birka-web.rs index 2a5b440..22ace80 100644 --- a/src/main.rs +++ b/src/birka-web.rs @@ -24,20 +24,27 @@ extern crate image; extern crate rocket; extern crate rocket_contrib; #[macro_use] +extern crate rusqlite; +#[macro_use] extern crate serde_derive; mod database; use anyhow::Result; -use database::{hash_file, Query, TagDatabase}; +use database::{Image, Query, TagDatabase}; use image::ImageFormat; +use multipart::{MultipartFormData, MultipartFormDataField, MultipartFormDataOptions}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use rocket::http::ContentType; 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::io::Write; use std::os::unix::fs::symlink; use std::path::Path; use std::sync::Mutex; @@ -48,7 +55,7 @@ const RESULTS_PER_QUERY: i64 = 50; /// Shorthand for the state that is passed to all handlers. type SiteState<'a> = State<'a, Mutex<DatabaseConnection>>; -/// Collection of data necessary to serve images from the tag database. +/// Everything necessary to serve images from the tag database. struct DatabaseConnection { tb: TagDatabase, image_dir: String, @@ -56,17 +63,18 @@ struct DatabaseConnection { impl DatabaseConnection { fn new() -> Result<Self> { - let bin = env::current_exe()?.canonicalize()?; + // FIXME: It is somewhat of an arbitrary choice that the directory + // containing symbolic links and thumbnails be relative to the binary. + let bin = std::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())?; + let image_dir = bin_dir.join("image/").to_str().unwrap().to_string(); + let tb = TagDatabase::new(&bin_dir.join("birka.db"))?; Ok(DatabaseConnection { tb, image_dir }) } } /// Information about an image, as returned by the 'posts' endpoint. -#[derive(Serialize)] +#[derive(Debug, Serialize)] struct ImageResult { id: i64, filename: String, @@ -75,6 +83,7 @@ struct ImageResult { tags: Vec<String>, } +/// Return the store file name of the image at `filename`. fn image_store_filename(id: i64, filename: &str) -> String { if let Some(n) = filename.rfind('.') { format!("{id}.{ext}", id = id, ext = &filename[n + 1..]) @@ -83,7 +92,7 @@ fn image_store_filename(id: i64, filename: &str) -> String { } } -/// Return the file name of the thumbnail for the image at `filename`. +/// Return the store file name of the thumbnail for the image at `filename`. fn thumb_filename(id: i64, filename: &str) -> String { if let Some(n) = filename.rfind('.') { format!("{id}_thumb.{ext}", id = id, ext = &filename[n + 1..]) @@ -103,12 +112,9 @@ fn get_posts(conn: SiteState, tags: String, last: Option<i64>) -> Json<Vec<Image } else { vec![] }; + let results = tb - .query(Query { - tags: &tags[..], - last_id: last, - limit: Some(RESULTS_PER_QUERY), - }) + .query(Query::new(&tags[..], last, Some(RESULTS_PER_QUERY))) .unwrap(); Json( @@ -139,16 +145,46 @@ fn get_post(conn: SiteState, id: i64) -> Json<ImageResult> { }) } -#[post("/posts/<filename>", data = "<data>")] -fn put_post(conn: SiteState, filename: String, data: Data) -> Json<&str> { +#[post("/posts", data = "<data>")] +fn put_post(conn: SiteState, content_type: &ContentType, data: Data) -> Json<String> { 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(); + let options = MultipartFormDataOptions::with_multipart_form_data_fields(vec![ + MultipartFormDataField::text("tags"), + MultipartFormDataField::raw("image") + .size_limit(32 * 1024 * 1024) + .content_type_by_string(Some(multipart::mime::IMAGE_STAR)) + .unwrap(), + ]); - Json("uploaded!") + let mut data = MultipartFormData::parse(content_type, data, options).unwrap(); + let raw = data.raw.remove("image").unwrap().remove(0); + + let filename = raw + .file_name + .unwrap_or(thread_rng().sample_iter(&Alphanumeric).take(30).collect()); + let path = Path::new(&conn.image_dir).join(&filename); + let mut f = File::create(&path).unwrap(); + f.write_all(&raw.raw).unwrap(); + + let hash = database::hash_file(&path).unwrap(); + let id = conn.tb.import_image(&path, &hash).unwrap(); + add_to_store( + &Image { + id, + blake2: hash, + filename: filename.clone(), + orig_dir: conn.image_dir.clone(), + }, + &conn.image_dir, + ); + + let tags = data.texts.remove("tags").unwrap().remove(0).text; + for tag in tags.split(",") { + conn.tb.add_tag(tag).unwrap(); + conn.tb.tag_image(id, tag).unwrap(); + } + + Json("Uploaded!".into()) } #[get("/")] @@ -156,6 +192,11 @@ fn index(conn: SiteState) -> Template { display_posts(conn, String::from(""), None) } +#[get("/upload")] +fn upload() -> Template { + Template::render("upload", None::<()>) +} + #[get("/posts?<tags>&<last>")] fn display_posts(conn: SiteState, tags: String, last: Option<i64>) -> Template { #[derive(Serialize)] @@ -203,28 +244,48 @@ fn display_posts(conn: SiteState, tags: String, last: Option<i64>) -> Template { #[get("/posts/<id>")] fn display_post(conn: SiteState, id: i64) -> Template { - Template::render("image", get_post(conn, id).into_inner()) + #[derive(Serialize)] + struct Context { + tags: Vec<String>, + filename: String, + orig_filename: String, + } + + let tb = &conn.inner().lock().unwrap().tb; + let image = tb.image_by_id(id).unwrap(); + + Template::render( + "image", + Context { + tags: tb.tags_for_image(image.id).unwrap(), + filename: image_store_filename(image.id, &image.filename), + orig_filename: image.filename, + }, + ) +} + +/// Create a thumbnail for `image` in the store at `image_dir`. +fn add_to_store(image: &Image, image_dir: &str) { + let orig_path = Path::new(&image.orig_dir).join(&image.filename); + let store_path = image_store_filename(image.id, &image.filename); + let store_path = Path::new(image_dir).join(store_path); + symlink(orig_path, &store_path).ok(); + let thumb_path = thumb_filename(image.id, &image.filename); + let thumb_path = Path::new(image_dir).join(&thumb_path); + if !thumb_path.exists() { + let im = image::open(store_path).unwrap(); + let out = &mut File::create(&thumb_path).unwrap(); + im.thumbnail(100, 100) + .write_to(out, ImageFormat::Png) + .unwrap(); + } } /// 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 = Path::new(&image.orig_dir).join(&image.filename); - let dst = image_store_filename(image.id, &image.filename); - let dst = Path::new(&conn.image_dir).join(dst); - symlink(&src, &dst).ok(); - - let thumb_path = thumb_filename(image.id, &image.filename); - let thumb_path = Path::new(&conn.image_dir).join(&thumb_path); - if !thumb_path.exists() { - let im = image::open(src).unwrap(); - let out = &mut File::create(&thumb_path).unwrap(); - im.thumbnail(100, 100) - .write_to(out, ImageFormat::Png) - .unwrap(); - } + add_to_store(&image, &conn.image_dir); } } @@ -234,7 +295,7 @@ fn main() { rocket::ignite() .mount("/public", StaticFiles::from("./static/")) .mount("/image", StaticFiles::from(&conn.image_dir)) - .mount("/", routes![index, display_posts, display_post]) + .mount("/", routes![index, upload, display_posts, display_post]) .mount("/api", routes![get_posts, get_post, put_post]) .attach(Template::fairing()) .manage(Mutex::new(conn)) |