Added some better error handling using anyhow
parent
e6b5457476
commit
41a47b42a5
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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)]
|
||||
|
|
58
src/main.rs
58
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<Connection> {
|
||||
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)
|
||||
}
|
||||
|
|
173
src/tags.rs
173
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<String> {
|
||||
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<Vec<String>> {
|
||||
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<String>) {
|
||||
let mut query = r#"DELETE FROM tag WHERE name IN ("#.to_string();
|
||||
fn remove_tags(conn: &mut Connection, tags: Vec<String>) -> 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<String>) {
|
||||
let tx = conn.transaction().unwrap();
|
||||
fn add_tags(conn: &mut Connection, tags: Vec<String>) -> 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<String>) {
|
|||
}
|
||||
}
|
||||
|
||||
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<String>) {
|
||||
let file = file.to_str().unwrap().to_string();
|
||||
fn tag_file(conn: &mut Connection, file: PathBuf, tags: Vec<String>) -> 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<i32> = 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<String>) {
|
|||
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<i32, rusqlite::Error> { row.get(0) })
|
||||
.unwrap()
|
||||
let mut stmt = tx.prepare(r#"INSERT INTO file(path) VALUES (?) RETURNING id"#)?;
|
||||
stmt.query_row([file], |row| -> Result<i32, rusqlite::Error> { row.get(0) })?
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -170,26 +201,26 @@ fn tag_file(conn: &mut Connection, file: PathBuf, tags: Vec<String>) {
|
|||
}
|
||||
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<i32, rusqlite::Error> { 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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue