summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2020-06-06 18:59:03 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2020-06-06 19:03:31 -0400
commit8e89d43bdf35b2a1f69f3f7e3d87c57fb1e3f58a (patch)
treee7152a060dce2ef8a1dbf41cc6202339c5337a97 /src
parent0f0d4477a4eab8ec0b99c134154ce0fedb4d6400 (diff)
Implement file uploading.
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))