From b311644824bc0bff9603fea69d979fa6bb4b97e0 Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Sun, 26 Jul 2020 19:23:37 -0400 Subject: Rebrand (again). --- Cargo.lock | 38 ++--- Cargo.toml | 10 +- README.md | 42 +++--- src/birka-cli.rs | 121 --------------- src/birka-web.rs | 406 --------------------------------------------------- src/database.rs | 20 +-- src/kona-cli.rs | 121 +++++++++++++++ src/kona-web.rs | 406 +++++++++++++++++++++++++++++++++++++++++++++++++++ templates/image.hbs | 4 +- templates/index.hbs | 4 +- templates/upload.hbs | 4 +- 11 files changed, 587 insertions(+), 589 deletions(-) delete mode 100644 src/birka-cli.rs delete mode 100644 src/birka-web.rs create mode 100644 src/kona-cli.rs create mode 100644 src/kona-web.rs diff --git a/Cargo.lock b/Cargo.lock index 27a846a..7cb742b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,25 +87,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" -[[package]] -name = "birka" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64 0.12.1", - "blake2", - "dirs", - "image", - "rand 0.7.3", - "rocket", - "rocket-multipart-form-data", - "rocket_contrib", - "rusqlite", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "bitflags" version = "1.2.1" @@ -645,6 +626,25 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kona" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.12.1", + "blake2", + "dirs", + "image", + "rand 0.7.3", + "rocket", + "rocket-multipart-form-data", + "rocket_contrib", + "rusqlite", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "language-tags" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index a4a707f..143dca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "birka" +name = "kona" version = "0.1.0" authors = ["Jakob L. Kreuze "] edition = "2018" @@ -25,9 +25,9 @@ serde_json = "1.0" serde_derive = "1.0" [[bin]] -name = "birka-cli" -path = "src/birka-cli.rs" +name = "kona-cli" +path = "src/kona-cli.rs" [[bin]] -name = "birka-web" -path = "src/birka-web.rs" +name = "kona-web" +path = "src/kona-web.rs" diff --git a/README.md b/README.md index 9d24c59..f756061 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ -# бирка-тян: электронная доска объявлений +# Kona: An Imageboard Engine -_В вашем деле нет пометки об этом!_ - -бирка-тян (birka-tyan) is a lightweight Danbooru-style imageboard engine for the -purposes of self-hosting a tagged image gallery. It lacks many of the features -one would expect of such an application (particularly use rauthentication), so -if you have a use case other than being able to search through your pictures -when you are not at home, you would be better off looking at an established -solution such as [Danbooru](https://github.com/danbooru/danbooru). +Kona is a lightweight Danbooru-style imageboard engine for the purposes of +self-hosting a tagged image gallery. It lacks many of the features one would +expect of such an application (particularly use rauthentication), so if you have +a use case other than being able to search through your pictures when you are +not at home, you would be better off looking at an established solution such as +[Danbooru](https://github.com/danbooru/danbooru). This is a _very_ early release. Perhaps pre-alpha, if there is no stage earlier in the software release life cycle. Use at your own risk. @@ -26,7 +24,7 @@ the general public. ### Command-Line -There are two ways of interacting with бирка-тян: through the `birka-cli` +There are two ways of interacting with бирка-тян: through the `kona-cli` command-line tool (i), and through the web interface (ii). The inclusion of (i) is in consideration for this being, essentially, a personal @@ -40,26 +38,26 @@ or more tags following the path of an image. The path is automatically canonicalized, so relative names are perfectly acceptable. ```sh -$ birka-cli add Cat.jpeg animal cat cute +$ kona-cli add Cat.jpeg animal cat cute ``` Ah, drat. We should have tagged that image with "photograph", too. ```sh -$ birka-cli add_tags Cat.jpeg photograph +$ kona-cli add_tags Cat.jpeg photograph ``` On second thought, that wasn't a particularly cute picture. ```sh -$ birka-cli remove_tags Cat.jpeg cute +$ kona-cli remove_tags Cat.jpeg cute ``` Now, let's see all of the photographs in the tag database tagged with "photograph". ```sh -$ birka-cli query photograph +$ kona-cli query photograph /home/jakob/Camera/Cat.jpeg ``` @@ -70,29 +68,29 @@ convenience, the 'query' command can be invoked without a query string, to list all paths indexed by the database. ```sh -$ birka-cli query +$ kona-cli query ... ``` The tag database is stored in a file, and the path of that file is dependent on how the program is invoked. If, when parsing the command-line arguments, -`birka-cli` sees a `-o` or `--output` argument, the path following that argument +`kona-cli` sees a `-o` or `--output` argument, the path following that argument will be used. Alternatively, the `TB_PATH` environment variable can be specified, but `-o` takes a higher precedence. If neither are specified, -"birka.db" in the current directory is used. +"kona.db" in the current directory is used. A few other things that can be done from the command-line: Removing an image can be done with 'remove'. ```sh -$ birka-cli remove Cat.jpeg +$ kona-cli remove Cat.jpeg ``` Getting the tags in the database: ```sh -$ birka-cli tags +$ kona-cli tags ... ``` @@ -152,7 +150,7 @@ POST /api/posts/[filename] There are a few things I'd like to push off 'till later, since this is working for me right now. -- [birka-cli] Improve the `birka-cli add` interface, allowing multiple files to +- [kona-cli] Improve the `kona-cli add` interface, allowing multiple files to be indexed and tagged in the same invocation. -- [birka-web js] Implement a dynamic "endless scroll" view. -- [birka-web js] Implement a mass-tagger tool in the web interface. +- [kona-web js] Implement a dynamic "endless scroll" view. +- [kona-web js] Implement a mass-tagger tool in the web interface. diff --git a/src/birka-cli.rs b/src/birka-cli.rs deleted file mode 100644 index 4a8681f..0000000 --- a/src/birka-cli.rs +++ /dev/null @@ -1,121 +0,0 @@ -// 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 . - -#[macro_use] -extern crate anyhow; -extern crate dirs; -#[macro_use] -extern crate rusqlite; - -mod database; - -use database::TagDatabase; -use std::env; -use std::path::Path; - -fn main() { - let mut args: Vec = env::args().collect(); - if args.len() < 2 || args.iter().any(|arg| arg == "-h" || arg == "--help") { - println!("usage: {} ACTION [ARGS ...]", args[0]); - println!(); - println!("\t-o, --output [PATH]: Path to the tag database."); - println!(); - println!("\tACTION: add [PATH] [TAGS ...]: index an image, optionally tagged."); - println!("\tACTION: remove [PATH]: remove an image from the database."); - println!("\tACTION: add_tags [PATH] [TAGS ...]: add tags to an indexed image."); - println!("\tACTION: remove_tags [PATH] [TAGS ...]: remove tags from an indexed image."); - println!("\tACTION: query [QUERY_STRING]: list images in the database."); - std::process::exit(1); - } - - let tb = if let Some(i) = args.iter().position(|x| x == "-o" || x == "--output") { - // Ensure that a path follows the "-o" argument. - if i + 1 >= args.len() { - eprintln!("{} requires an argument", args[i]); - std::process::exit(1); - } - - args.remove(i); - TagDatabase::new(args.remove(i + 1)) - } else if let Ok(path) = std::env::var("TB_PATH") { - TagDatabase::new(path) - } else { - TagDatabase::new("./birka.db") - } - .unwrap(); - - let action = &args[1]; - match action.to_lowercase().as_str() { - "add" => { - if args.len() < 3 { - eprintln!("usage: {} add PATH [TAGS ...]", args[0]); - std::process::exit(1); - } - - let path = Path::new(&args[2]).canonicalize().unwrap(); - if path.is_dir() { - eprintln!("'{}' is a directory", args[2]); - } - - tb.add_file(&path, &args[3..].to_vec()).unwrap(); - } - "remove" => { - if args.len() < 3 { - eprintln!("usage: {} remove PATH", args[0]); - std::process::exit(1); - } - - let file = tb.file_by_path(&args[2]).unwrap(); - tb.remove_file(file.id).unwrap(); - } - "add_tags" => { - if args.len() < 3 { - eprintln!("usage: {} add_tags PATH [TAGS ...]", args[0]); - std::process::exit(1); - } - let file = tb.file_by_path(&args[2]).unwrap(); - tb.tag_file(file.id, &args[3..].to_vec()).unwrap(); - } - "remove_tags" => { - if args.len() < 3 { - eprintln!("usage: {} remove_tags PATH [TAGS ...]", args[0]); - std::process::exit(1); - } - let file = tb.file_by_path(&args[2]).unwrap(); - tb.untag_file(file.id, &args[3..].to_vec()).unwrap(); - } - "query" => { - let query = if args.len() < 3 { - database::parse_query("").unwrap() - } else { - database::parse_query(&args[2]).unwrap() - }; - for file in tb.query(&query, None).unwrap() { - println!("{}", file.path); - } - } - "tags" => { - for (count, tag) in tb.top_tags(None).unwrap() { - println!("{}: {}", count, tag); - } - } - _ => { - eprintln!("Unknown action '{}'", action); - std::process::exit(1); - } - } -} diff --git a/src/birka-web.rs b/src/birka-web.rs deleted file mode 100644 index cd68923..0000000 --- a/src/birka-web.rs +++ /dev/null @@ -1,406 +0,0 @@ -// 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 rusqlite; -#[macro_use] -extern crate serde_derive; - -mod database; - -use anyhow::Result; -use database::{Keyset, TagDatabase}; -use image::ImageFormat; -use multipart::{MultipartFormData, MultipartFormDataField, MultipartFormDataOptions}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; -use rocket::http::ContentType; -use rocket::request::Form; -use rocket::{Data, State}; -use rocket_contrib::json::Json; -use rocket_contrib::serve::StaticFiles; -use rocket_contrib::templates::Template; - -use std::convert::AsRef; -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; - -/// Maximum number of results returned by any API endpoint. -const RESULTS_PER_QUERY: i64 = 50; - -/// Shorthand for the state that is passed to all handlers. -type SiteState<'a> = State<'a, Mutex>; - -/// Everything necessary to serve files from the tag database. -struct DatabaseConnection { - tb: TagDatabase, - file_dir: String, -} - -impl DatabaseConnection { - fn new() -> Result { - // 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 file_dir = bin_dir.join("files/").to_str().unwrap().to_string(); - let tb = TagDatabase::new(&bin_dir.join("birka.db"))?; - Ok(DatabaseConnection { tb, file_dir }) - } -} - -/// Information about a file, as returned by the 'posts' endpoint. -#[derive(Debug, Serialize)] -struct FileResult { - id: i64, - filename: String, - thumb_filename: String, - orig_filename: String, - tags: Vec, -} - -/// Return whether or not `filename` ends with one of `extensions`. -fn has_extension>(filename: T, extensions: Vec<&str>) -> bool { - filename - .as_ref() - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| extensions.contains(&ext)) - .unwrap_or(false) -} - -/// Return whether or not `filename` ends with the extension of a known video file type. -fn is_video>(filename: T) -> bool { - has_extension(filename, vec!["mp4", "mkv", "webm"]) -} - -/// Return whether or not `filename` ends with the extension of a known image file type. -fn is_image>(filename: T) -> bool { - has_extension(filename, vec!["png", "jpg", "jpeg", "gif"]) -} - -/// Return the store file name of `filename`. -fn store_filename>(id: i64, path: T) -> String { - if let Some(filename) = path.as_ref().file_name() { - let filename = filename.to_string_lossy(); - if let Some(n) = filename.rfind('.') { - format!("{id}.{ext}", id = id, ext = &filename[n + 1..]) - } else { - id.to_string() - } - } else { - id.to_string() - } -} - -/// Return the store file name of the thumbnail of `filename`. -fn thumb_filename>(id: i64, path: T) -> String { - if let Some(ext) = path.as_ref().extension() { - if let Some(ext) = ext.to_str() { - let ext = if is_video(path.as_ref()) { "png" } else { ext }; - format!("{id}_thumb.{ext}", id = id, ext = ext) - } else { - format!("{}_thumb", id) - } - } else { - format!("{}_thumb", id) - } -} - -#[get("/posts?&")] -fn get_posts(conn: SiteState, query: String, last: Option) -> Result>> { - let tb = &conn - .inner() - .lock() - .map_err(|_| anyhow!("Could not lock database."))? - .tb; - - let keyset = if let Some(id) = last { - Keyset { - last_id: id, - how_many: RESULTS_PER_QUERY, - } - } else { - Keyset { - last_id: i64::MAX, - how_many: RESULTS_PER_QUERY, - } - }; - let results = tb.query(&database::parse_query(query)?, Some(keyset))?; - - Ok(Json( - results - .iter() - .map(|file| FileResult { - id: file.id, - filename: store_filename(file.id, &file.path), - thumb_filename: thumb_filename(file.id, &file.path), - orig_filename: file.path.clone(), - tags: file.tags.clone(), - }) - .collect(), - )) -} - -#[get("/posts/")] -fn get_post(conn: SiteState, id: i64) -> Result> { - let tb = &conn - .inner() - .lock() - .map_err(|_| anyhow!("Could not lock database."))? - .tb; - let file = tb.file_by_id(id)?; - - Ok(Json(FileResult { - id: file.id, - filename: store_filename(file.id, &file.path), - thumb_filename: thumb_filename(file.id, &file.path), - orig_filename: file.path.clone(), - tags: file.tags, - })) -} - -#[derive(FromForm)] -struct UpdateTags { - tags: String, -} - -#[post("/posts/", data = "
")] -fn update_post(conn: SiteState, id: i64, form: Form) -> Result> { - let tb = &conn - .inner() - .lock() - .map_err(|_| anyhow!("Could not lock database."))? - .tb; - tb.tag_file(id, &form.tags.split(',').collect::>()[..])?; - Ok(Json(String::from("Updated!"))) -} - -#[post("/posts", data = "")] -fn put_post(conn: SiteState, content_type: &ContentType, data: Data) -> Result> { - let conn = conn - .inner() - .lock() - .map_err(|_| anyhow!("Could not lock database."))?; - 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))?, - ]); - - let mut data = MultipartFormData::parse(content_type, data, options)?; - let raw = if let Some(mut data) = data.raw.remove("image") { - data.remove(0) - } else { - bail!("No `image` field.") - }; - - let filename = raw - .file_name - .unwrap_or_else(|| thread_rng().sample_iter(&Alphanumeric).take(30).collect()); - let path = Path::new(&conn.file_dir).join(&filename); - let mut f = File::create(&path)?; - f.write_all(&raw.raw)?; - - let tags = if let Some(mut vec) = data.texts.remove("tags") { - let text = vec.remove(0).text; - text.split(',').map(String::from).collect() - } else { - vec![] - }; - let file = conn.tb.add_file(&path, &tags)?; - add_to_store(&file, &conn.file_dir); - - Ok(Json("Uploaded!".into())) -} - -#[get("/")] -fn index(conn: SiteState) -> Result