diff --git a/Cargo.lock b/Cargo.lock index 0a8be4b..221fbd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "bitflags" version = "2.9.0" @@ -249,6 +255,7 @@ dependencies = [ name = "taggo" version = "0.1.0" dependencies = [ + "anyhow", "clap", "rusqlite", ] diff --git a/Cargo.toml b/Cargo.toml index b82d2a0..2cc0897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ unsafe_code = { level = "forbid" } [dependencies] clap = { version = "4.5", features = ["derive"] } rusqlite = { version = "0.35", features = ["bundled"] } +anyhow = { version = "1.0" } diff --git a/src/cli.rs b/src/cli.rs index 9bd8b92..bc44fa0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{Args, Parser, Subcommand}; #[derive(Debug, Parser, PartialEq, Eq)] #[command(version, about, author, long_about = None, args_override_self = true)] diff --git a/src/main.rs b/src/main.rs index 9ca41be..c8976da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,69 +1,65 @@ -use std::{path, process::exit}; - +use anyhow::anyhow; use clap::Parser; use cli::CliArgs; -use rusqlite::Connection; +use rusqlite::{Connection, OpenFlags}; const DB_PATH: &str = ".tags"; mod cli; mod tags; -fn main() { +fn main() -> anyhow::Result<()> { let args = CliArgs::parse(); match args.commands { - cli::Commands::Init => { - if !has_database() { - let conn = Connection::open(DB_PATH).unwrap(); - init_db(&conn); - } else { - panic!("Database is already initialized in this folder"); - } - exit(0); - } + cli::Commands::Init => init_db().map_err(|_| anyhow!("Database already exists!")), cli::Commands::Tag(args) => tags::handle_tag(args), cli::Commands::Tags(args) => tags::handle_tags(args), cli::Commands::Files(args) => tags::handle_files(args), } } -fn init_db(conn: &Connection) { +fn init_db() -> anyhow::Result<()> { + let conn = Connection::open(DB_PATH)?; + conn.execute( - r#"CREATE TABLE IF NOT EXISTS tag( + r#"CREATE TABLE tag( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL UNIQUE );"#, (), - ) - .unwrap(); + )?; conn.execute( - r#"CREATE TABLE IF NOT EXISTS file( + r#"CREATE TABLE file( id INTEGER PRIMARY KEY AUTOINCREMENT, path VARCHAR(255) NOT NULL UNIQUE );"#, (), - ) - .unwrap(); + )?; conn.execute( - r#"CREATE TABLE IF NOT EXISTS file_tag( + r#"CREATE TABLE file_tag( file_id INT REFERENCES file(id), tag_id INT REFERENCES tag(id), PRIMARY KEY (file_id, tag_id) );"#, (), + )?; + + Ok(()) +} + +pub fn try_database() -> anyhow::Result { + let conn = Connection::open_with_flags( + DB_PATH, + OpenFlags::SQLITE_OPEN_READ_WRITE + | OpenFlags::SQLITE_OPEN_URI + | OpenFlags::SQLITE_OPEN_NO_MUTEX, ) - .unwrap(); -} + .map_err(|_| { + anyhow!("Database does not exist! Run `taggo init` first to generate a new database.") + })?; -pub fn try_database() { - if !has_database() { - panic!("No database found! Use taggo init to create one first."); - } -} - -pub fn has_database() -> bool { - path::Path::new(DB_PATH).exists() + Ok(conn) } diff --git a/src/tags.rs b/src/tags.rs index 759b596..684076a 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -4,18 +4,18 @@ use std::{ str::FromStr, }; +use anyhow::{Result, anyhow}; + use rusqlite::{Connection, params_from_iter}; use crate::{ - DB_PATH, cli::{self, FilesArgs, TagArgs, TagsArgs}, try_database, }; -pub fn handle_files(args: FilesArgs) { - try_database(); +pub fn handle_files(args: FilesArgs) -> Result<()> { + let conn = try_database()?; - let conn = Connection::open(DB_PATH).unwrap(); let mut sql = r#"SELECT path, group_concat(name) as tags FROM file_tag JOIN file ON file.id = file_tag.file_id JOIN tag ON tag.id = file_tag.tag_id"# @@ -38,90 +38,111 @@ pub fn handle_files(args: FilesArgs) { sql.push_str(" GROUP BY path"); - let mut stmt = conn.prepare(&sql).unwrap(); - let result = stmt - .query_map([], |row| -> Result<(String, String), rusqlite::Error> { - Ok((row.get(0).unwrap(), row.get(1).unwrap())) - }) - .unwrap() - .flatten(); + let mut stmt = conn.prepare(&sql)?; + let result: Vec<(String, String)> = stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + .flatten() + .collect(); let mut w = io::stdout(); - for (file, tags) in result { - if show_tags { - writeln!(&mut w, "{}: {}", file, tags).unwrap(); - } else { - writeln!(&mut w, "{}", file).unwrap(); + if !result.is_empty() { + for (file, tags) in result { + if show_tags { + writeln!(&mut w, "{}: {}", file, tags)?; + } else { + writeln!(&mut w, "{}", file)?; + } } + } else { + writeln!(&mut w, "No files registered!")?; } - w.flush().unwrap(); + w.flush()?; + + Ok(()) } -pub fn handle_tag(args: TagArgs) { - try_database(); +pub fn handle_tag(args: TagArgs) -> Result<()> { + let mut conn = try_database()?; - let mut conn = Connection::open(DB_PATH).unwrap(); + tag_file(&mut conn, args.file, args.tags)?; - tag_file(&mut conn, args.file, args.tags); + Ok(()) } -pub fn handle_tags(args: TagsArgs) { - try_database(); - - let mut conn = Connection::open(DB_PATH).unwrap(); +pub fn handle_tags(args: TagsArgs) -> Result<()> { + let mut conn = try_database()?; match args.commands { Some(cli::TagsCommands::List) | None => { let mut w = io::stdout(); - let tags = list_tags(&conn); - for tag in tags { - writeln!(&mut w, "{}", tag).unwrap(); + let tags = list_tags(&conn)?; + if !tags.is_empty() { + for tag in tags { + writeln!(&mut w, "{}", tag)?; + } + } else { + writeln!(&mut w, "No tags registered!")?; } - w.flush().unwrap(); + w.flush()?; + Ok(()) } Some(cli::TagsCommands::Add { add }) => add_tags(&mut conn, add), - Some(cli::TagsCommands::Remove { remove }) => remove_tags(&conn, remove), + Some(cli::TagsCommands::Remove { remove }) => remove_tags(&mut conn, remove), Some(cli::TagsCommands::Rename { old, new }) => rename_tag(&conn, old, new), - }; + } } -fn rename_tag(conn: &Connection, old: String, new: String) { - let mut stmt = conn - .prepare("UPDATE tag SET name = ? WHERE name = ?") - .unwrap(); +fn rename_tag(conn: &Connection, old: String, new: String) -> Result<()> { + let mut stmt = conn.prepare("UPDATE tag SET name = ? WHERE name = ?")?; - stmt.execute([&new, &old]).unwrap(); + stmt.execute([&new, &old])?; + + Ok(()) } -fn list_tags(conn: &Connection) -> Vec { - let mut stmt = conn.prepare("SELECT name FROM tag").unwrap(); - let result = stmt.query_map([], |row| row.get(0)).unwrap(); +fn list_tags(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT name FROM tag")?; + let result = stmt.query_map([], |row| row.get(0))?.flatten(); let mut tags = Vec::new(); for name in result { - tags.push(name.unwrap()); + tags.push(name); } - tags + Ok(tags) } -fn remove_tags(conn: &Connection, tags: Vec) { - let mut query = r#"DELETE FROM tag WHERE name IN ("#.to_string(); +fn remove_tags(conn: &mut Connection, tags: Vec) -> Result<()> { + let tx = conn.transaction()?; + { + let mut query = r#"DELETE FROM tag WHERE name IN ("#.to_string(); - for (i, _tag) in tags.iter().enumerate() { - query.push('?'); - if i < tags.len() - 1 { - query.push(','); + for (i, _tag) in tags.iter().enumerate() { + query.push('?'); + if i < tags.len() - 1 { + query.push(','); + } + } + + query.push(')'); + + tx.execute(&query, params_from_iter(&tags))?; + + if tx.changes() > 0 { + let mut w = io::stdout(); + writeln!(&mut w, "Removed Tags:")?; + for tag in tags { + writeln!(&mut w, " {tag}")?; + } } } + tx.commit()?; - query.push(')'); - - conn.execute(&query, params_from_iter(tags)).unwrap(); + Ok(()) } -fn add_tags(conn: &mut Connection, tags: Vec) { - let tx = conn.transaction().unwrap(); +fn add_tags(conn: &mut Connection, tags: Vec) -> Result<()> { + let tx = conn.transaction()?; { let mut query = r#"INSERT INTO tag(name) VALUES"#.to_string(); @@ -132,15 +153,28 @@ fn add_tags(conn: &mut Connection, tags: Vec) { } } - tx.execute(&query, params_from_iter(tags)).unwrap(); + tx.execute(&query, params_from_iter(&tags))?; + + if tx.changes() > 0 { + let mut w = io::stdout(); + writeln!(&mut w, "Added Tags:")?; + for tag in tags { + writeln!(&mut w, " {tag}")?; + } + } } - tx.commit().unwrap(); + tx.commit()?; + + Ok(()) } -fn tag_file(conn: &mut Connection, file: PathBuf, tags: Vec) { - let file = file.to_str().unwrap().to_string(); +fn tag_file(conn: &mut Connection, file: PathBuf, tags: Vec) -> Result<()> { + let file = file + .to_str() + .ok_or(anyhow!("Unable to find path {:?}", file))? + .to_string(); - let tx = conn.transaction().unwrap(); + let tx = conn.transaction()?; { let file_id: Option = tx .query_row(r#"SELECT id FROM file WHERE path = ?"#, [&file], |row| { @@ -151,11 +185,8 @@ fn tag_file(conn: &mut Connection, file: PathBuf, tags: Vec) { let file_id = match file_id { Some(id) => id, None => { - let mut stmt = tx - .prepare(r#"INSERT INTO file(path) VALUES (?) RETURNING id"#) - .unwrap(); - stmt.query_row([file], |row| -> Result { row.get(0) }) - .unwrap() + let mut stmt = tx.prepare(r#"INSERT INTO file(path) VALUES (?) RETURNING id"#)?; + stmt.query_row([file], |row| -> Result { row.get(0) })? } }; @@ -170,26 +201,26 @@ fn tag_file(conn: &mut Connection, file: PathBuf, tags: Vec) { } sql.push(')'); - let mut stmt = tx.prepare(&sql).unwrap(); + let mut stmt = tx.prepare(&sql)?; let result = stmt .query_map( params_from_iter(tags), |row| -> Result { row.get(0) }, - ) - .unwrap(); + )? + .flatten(); let mut tags_ids = Vec::new(); - for id in result.flatten() { + for id in result { tags_ids.push(id); } - let mut stmt = tx - .prepare(r#"INSERT INTO file_tag(file_id, tag_id) VALUES (?, ?)"#) - .unwrap(); + let mut stmt = tx.prepare(r#"INSERT INTO file_tag(file_id, tag_id) VALUES (?, ?)"#)?; for tag_id in tags_ids { - stmt.execute((file_id, tag_id)).unwrap(); + stmt.execute((file_id, tag_id))?; } } - tx.commit().unwrap(); + tx.commit()?; + + Ok(()) }