summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2020-07-28 11:30:39 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2020-07-28 12:45:14 -0400
commit1870d9c750020c84ae9bc80cd61606d298d9b8d9 (patch)
tree2acdf2064edbcc218d961c6e48d6190f24dda5ad
parentb311644824bc0bff9603fea69d979fa6bb4b97e0 (diff)
Cleanup imports.
-rw-r--r--Cargo.toml4
-rw-r--r--src/kona-cli.rs17
-rw-r--r--src/kona-web.rs181
-rw-r--r--src/lib.rs (renamed from src/database.rs)16
4 files changed, 123 insertions, 95 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 143dca2..ff64959 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,10 @@ serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
+[lib]
+name = "kona"
+path = "src/lib.rs"
+
[[bin]]
name = "kona-cli"
path = "src/kona-cli.rs"
diff --git a/src/kona-cli.rs b/src/kona-cli.rs
index 04adfc8..74d7c7f 100644
--- a/src/kona-cli.rs
+++ b/src/kona-cli.rs
@@ -15,18 +15,11 @@
// You should have received a copy of the GNU Affero General Public
// License along with Kona. If not, see <http://www.gnu.org/licenses/>.
-#[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;
+use kona::TagDatabase;
+
fn main() {
let mut args: Vec<String> = env::args().collect();
if args.len() < 2 || args.iter().any(|arg| arg == "-h" || arg == "--help") {
@@ -50,7 +43,7 @@ fn main() {
}
args.remove(i);
- TagDatabase::new(args.remove(i + 1))
+ TagDatabase::new(args.remove(i))
} else if let Ok(path) = std::env::var("TB_PATH") {
TagDatabase::new(path)
} else {
@@ -100,9 +93,9 @@ fn main() {
}
"query" => {
let query = if args.len() < 3 {
- database::parse_query("").unwrap()
+ kona::parse_query("").unwrap()
} else {
- database::parse_query(&args[2]).unwrap()
+ kona::parse_query(&args[2]).unwrap()
};
for file in tb.query(&query, None).unwrap() {
println!("{}", file.path);
diff --git a/src/kona-web.rs b/src/kona-web.rs
index 2d63772..60fb88e 100644
--- a/src/kona-web.rs
+++ b/src/kona-web.rs
@@ -24,14 +24,16 @@ extern crate image;
extern crate rocket;
extern crate rocket_contrib;
#[macro_use]
-extern crate rusqlite;
-#[macro_use]
extern crate serde_derive;
-mod database;
+use std::convert::AsRef;
+use std::fs;
+use std::io::Write;
+use std::os::unix::fs::symlink;
+use std::path::Path;
+use std::sync::Mutex;
use anyhow::Result;
-use database::{Keyset, TagDatabase};
use image::ImageFormat;
use multipart::{MultipartFormData, MultipartFormDataField, MultipartFormDataOptions};
use rand::distributions::Alphanumeric;
@@ -43,13 +45,7 @@ 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;
+use kona::{Keyset, TagDatabase};
/// Maximum number of results returned by any API endpoint.
const RESULTS_PER_QUERY: i64 = 50;
@@ -57,7 +53,11 @@ const RESULTS_PER_QUERY: i64 = 50;
/// Shorthand for the state that is passed to all handlers.
type SiteState<'a> = State<'a, Mutex<DatabaseConnection>>;
-/// Everything necessary to serve files from the tag database.
+/// Bundle of everything pertaining to database access.
+///
+/// SQLite handles concurrent access reasonably well, but Kona is designed for
+/// the single-user use case. If this turns out to be a problem, the
+/// `TagDatabase` connection will be nixed.
struct DatabaseConnection {
tb: TagDatabase,
file_dir: String,
@@ -75,7 +75,7 @@ impl DatabaseConnection {
}
}
-/// Information about a file, as returned by the 'posts' endpoint.
+/// Serialization protocol for a file entry, used by the 'posts' endpoint.
#[derive(Debug, Serialize)]
struct FileResult {
id: i64,
@@ -85,24 +85,31 @@ struct FileResult {
tags: Vec<String>,
}
+/// Serialization protocol for a tag entry, used by the 'tags' endpoint.
+type TagResult = (i64, String);
+
/// Return whether or not `filename` ends with one of `extensions`.
-fn has_extension<T: AsRef<Path>>(filename: T, extensions: Vec<&str>) -> bool {
+fn has_extension<T, S>(filename: T, extensions: &[S]) -> bool
+where
+ T: AsRef<Path>,
+ S: AsRef<str>,
+{
filename
.as_ref()
.extension()
.and_then(|ext| ext.to_str())
- .map(|ext| extensions.contains(&ext))
+ .map(|ext| extensions.iter().any(|other| ext == other.as_ref()))
.unwrap_or(false)
}
/// Return whether or not `filename` ends with the extension of a known video file type.
fn is_video<T: AsRef<Path>>(filename: T) -> bool {
- has_extension(filename, vec!["mp4", "mkv", "webm"])
+ 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<T: AsRef<Path>>(filename: T) -> bool {
- has_extension(filename, vec!["png", "jpg", "jpeg", "gif"])
+ has_extension(filename, &vec!["png", "jpg", "jpeg", "gif"])
}
/// Return the store file name of `filename`.
@@ -133,13 +140,28 @@ fn thumb_filename<T: AsRef<Path>>(id: i64, path: T) -> String {
}
}
+/// Macro for obtaining a reference to the `tb` field of `SiteState`.
+///
+/// Ownership boundaries are quite difficult with mutexes, so I've opted to just
+/// expand the call for locking the database.
+macro_rules! lock_database {
+ ($conn:expr) => {{
+ $conn
+ .inner()
+ .lock()
+ .map_err(|_| anyhow!("Could not lock database."))?
+ }};
+}
+
+#[get("/tags")]
+fn get_tags(conn: SiteState) -> Result<Json<Vec<TagResult>>> {
+ let tb = &lock_database!(conn).tb;
+ Ok(Json(tb.top_tags(None)?))
+}
+
#[get("/posts?<query>&<last>")]
fn get_posts(conn: SiteState, query: String, last: Option<i64>) -> Result<Json<Vec<FileResult>>> {
- let tb = &conn
- .inner()
- .lock()
- .map_err(|_| anyhow!("Could not lock database."))?
- .tb;
+ let tb = &lock_database!(conn).tb;
let keyset = if let Some(id) = last {
Keyset {
@@ -152,10 +174,9 @@ fn get_posts(conn: SiteState, query: String, last: Option<i64>) -> Result<Json<V
how_many: RESULTS_PER_QUERY,
}
};
- let results = tb.query(&database::parse_query(query)?, Some(keyset))?;
Ok(Json(
- results
+ tb.query(&kona::parse_query(query)?, Some(keyset))?
.iter()
.map(|file| FileResult {
id: file.id,
@@ -170,11 +191,7 @@ fn get_posts(conn: SiteState, query: String, last: Option<i64>) -> Result<Json<V
#[get("/posts/<id>")]
fn get_post(conn: SiteState, id: i64) -> Result<Json<FileResult>> {
- let tb = &conn
- .inner()
- .lock()
- .map_err(|_| anyhow!("Could not lock database."))?
- .tb;
+ let tb = &lock_database!(conn).tb;
let file = tb.file_by_id(id)?;
Ok(Json(FileResult {
@@ -192,22 +209,34 @@ struct UpdateTags {
}
#[post("/posts/<id>", data = "<form>")]
-fn update_post(conn: SiteState, id: i64, form: Form<UpdateTags>) -> Result<Json<String>> {
- let tb = &conn
- .inner()
- .lock()
- .map_err(|_| anyhow!("Could not lock database."))?
- .tb;
- tb.tag_file(id, &form.tags.split(',').collect::<Vec<_>>()[..])?;
- Ok(Json(String::from("Updated!")))
+fn update_post(conn: SiteState, id: i64, form: Form<UpdateTags>) -> Result<()> {
+ let tb = &lock_database!(conn).tb;
+
+ let mut adding = Vec::new();
+ let mut removing = Vec::new();
+
+ for atom in form.tags.split(',') {
+ if atom.starts_with("-") {
+ removing.push(&atom[1..]);
+ } else {
+ adding.push(atom);
+ }
+ }
+
+ if !adding.is_empty() {
+ tb.tag_file(id, &adding[..])?;
+ }
+ if !removing.is_empty() {
+ tb.untag_file(id, &removing[..])?;
+ }
+
+ Ok(())
}
#[post("/posts", data = "<data>")]
-fn put_post(conn: SiteState, content_type: &ContentType, data: Data) -> Result<Json<String>> {
- let conn = conn
- .inner()
- .lock()
- .map_err(|_| anyhow!("Could not lock database."))?;
+fn put_post(conn: SiteState, content_type: &ContentType, data: Data) -> Result<()> {
+ let conn = lock_database!(conn);
+
let options = MultipartFormDataOptions::with_multipart_form_data_fields(vec![
MultipartFormDataField::text("tags"),
MultipartFormDataField::raw("image")
@@ -226,8 +255,7 @@ fn put_post(conn: SiteState, content_type: &ContentType, data: Data) -> Result<J
.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)?;
+ fs::File::create(&path)?.write_all(&raw.raw)?;
let tags = if let Some(mut vec) = data.texts.remove("tags") {
let text = vec.remove(0).text;
@@ -238,7 +266,7 @@ fn put_post(conn: SiteState, content_type: &ContentType, data: Data) -> Result<J
let file = conn.tb.add_file(&path, &tags)?;
add_to_store(&file, &conn.file_dir);
- Ok(Json("Uploaded!".into()))
+ Ok(())
}
#[get("/")]
@@ -266,17 +294,11 @@ fn display_posts(conn: SiteState, query: String, last: Option<i64>) -> Result<Te
next_page: String,
}
- let base_params = format!("/posts?query={}", query);
+ // This is done in a block because `tb` needs to go out of scope before
+ // calling out to `get_posts` to perform the query. Otherwise, we deadlock
+ // because the mutex is locked for the entirety of `display_posts`.
let tag_results = {
- // This is done in a block because `tb` needs to go out of scope before
- // calling out to `get_posts` to perform the query. Otherwise, we
- // deadlock because the mutex is locked for the entirety of
- // `display_posts`.
- let tb = &conn
- .inner()
- .lock()
- .map_err(|_| anyhow!("Could not lock database."))?
- .tb;
+ let tb = &lock_database!(conn).tb;
tb.top_tags(Some(20))?
.iter()
.map(|tup| TagResult {
@@ -285,18 +307,22 @@ fn display_posts(conn: SiteState, query: String, last: Option<i64>) -> Result<Te
})
.collect()
};
- let images = get_posts(conn, query, last)?.into_inner();
- let context = Context {
- tags: tag_results,
- next_page: if images.len() as i64 == RESULTS_PER_QUERY {
- let last_image = images.last().unwrap();
- format!("{}&last={}", base_params, last_image.id)
- } else {
- String::from("")
+
+ let images = get_posts(conn, query.clone(), last)?.into_inner();
+
+ Ok(Template::render(
+ "index",
+ Context {
+ tags: tag_results,
+ next_page: if images.len() as i64 == RESULTS_PER_QUERY {
+ let last_image = images.last().unwrap();
+ format!("/posts?query={}&last={}", query, last_image.id)
+ } else {
+ String::from("")
+ },
+ images,
},
- images,
- };
- Ok(Template::render("index", context))
+ ))
}
#[get("/posts/<id>")]
@@ -312,13 +338,12 @@ fn display_post(conn: SiteState, id: i64) -> Result<Template> {
is_unknown: bool,
}
- let tb = &conn
- .inner()
- .lock()
- .map_err(|_| anyhow!("Could not lock database."))?
- .tb;
+ let tb = &lock_database!(conn).tb;
+
let file = tb.file_by_id(id)?;
+ let is_image = is_image(&file.path);
+ let is_video = is_video(&file.path);
let orig_filename = String::from(
Path::new(&file.path)
.file_name()
@@ -327,9 +352,6 @@ fn display_post(conn: SiteState, id: i64) -> Result<Template> {
.ok_or_else(|| anyhow!("Could not parse filename"))?,
);
- let is_image = is_image(&file.path);
- let is_video = is_video(&file.path);
-
Ok(Template::render(
"image",
Context {
@@ -345,7 +367,7 @@ fn display_post(conn: SiteState, id: i64) -> Result<Template> {
}
/// Create a thumbnail for `file` in the store at `file_dir`.
-fn add_to_store(file: &database::File, file_dir: &str) {
+fn add_to_store(file: &kona::File, file_dir: &str) {
let store_path = store_filename(file.id, &file.path);
let store_path = Path::new(file_dir).join(store_path);
symlink(&file.path, &store_path).ok();
@@ -357,7 +379,7 @@ fn add_to_store(file: &database::File, file_dir: &str) {
let thumb_path = Path::new(file_dir).join(&thumb_path);
if !thumb_path.exists() {
if let Ok(im) = image::open(&store_path) {
- let out = &mut File::create(&thumb_path).unwrap();
+ let out = &mut fs::File::create(&thumb_path).unwrap();
im.thumbnail(100, 100)
.write_to(out, ImageFormat::Png)
.unwrap();
@@ -386,7 +408,7 @@ fn add_to_store(file: &database::File, file_dir: &str) {
/// Maybe initialize the directory for storing image symlinks and thumbnails.
fn create_image_directory(conn: &DatabaseConnection) {
fs::create_dir(&conn.file_dir).ok();
- let query = database::parse_query("").unwrap();
+ let query = kona::parse_query("").unwrap();
for file in conn.tb.query(&query, None).unwrap() {
add_to_store(&file, &conn.file_dir);
}
@@ -399,7 +421,10 @@ fn main() {
.mount("/public", StaticFiles::from("./static/"))
.mount("/image", StaticFiles::from(&conn.file_dir))
.mount("/", routes![index, upload, display_posts, display_post])
- .mount("/api", routes![get_posts, get_post, put_post, update_post])
+ .mount(
+ "/api",
+ routes![get_posts, get_tags, get_post, put_post, update_post],
+ )
.attach(Template::fairing())
.manage(Mutex::new(conn))
.launch();
diff --git a/src/database.rs b/src/lib.rs
index 0702ed9..6665eee 100644
--- a/src/database.rs
+++ b/src/lib.rs
@@ -15,12 +15,17 @@
// You should have received a copy of the GNU Affero General Public
// License along with Kona. If not, see <http://www.gnu.org/licenses/>.
-use anyhow::Result;
-use rusqlite::Connection;
+#[macro_use]
+extern crate anyhow;
+#[macro_use]
+extern crate rusqlite;
use std::convert::AsRef;
use std::path::Path;
+use anyhow::Result;
+use rusqlite::Connection;
+
/// Type of constraint on a column.
#[derive(Debug, PartialEq)]
pub enum Atom {
@@ -238,9 +243,10 @@ impl TagDatabase {
// Silently skip any tags which do not exist in the database.
if let Ok(tags) = self.tag_ids(tag.as_ref()) {
for tag in tags.iter() {
- self.0
- .prepare("DELETE FROM mapping WHERE file = ? AND tag = ?")?
- .insert(params![id, tag])?;
+ self.0.execute(
+ "DELETE FROM mapping WHERE file = ? AND tag = ?",
+ params![id, tag],
+ )?;
}
}
}