diff --git a/Cargo.lock b/Cargo.lock index 221fbd6..5359d71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "taggo", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -256,7 +265,6 @@ name = "taggo" version = "0.1.0" dependencies = [ "anyhow", - "clap", "rusqlite", ] diff --git a/Cargo.toml b/Cargo.toml index 2cc0897..968ee86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ name = "taggo" version = "0.1.0" edition = "2024" authors = ["Wynd "] -description = "application for file tagging" license = "GPL-3.0-or-later" [lib] @@ -11,10 +10,16 @@ name = "taggo" path = "src/lib.rs" bench = false +[workspace] +members = ["cli"] +default-members = ["cli"] + + [workspace.dependencies] + anyhow = { version = "1.0" } + [lints.rust] unsafe_code = { level = "forbid" } [dependencies] -clap = { version = "4.5", features = ["derive"] } rusqlite = { version = "0.35", features = ["bundled"] } anyhow = { version = "1.0" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..b1d9e15 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2024" +description = "cli application for file tagging" + +[dependencies] +taggo = { path = "../" } +clap = { version = "4.5", features = ["derive"] } +anyhow = { workspace = true } diff --git a/src/cli.rs b/cli/src/cli.rs similarity index 100% rename from src/cli.rs rename to cli/src/cli.rs diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..5d7be78 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,157 @@ +use std::{ + fs, + io::{self, Write}, + path::PathBuf, + str::FromStr, +}; + +use clap::Parser; +use cli::{CliArgs, FilesArgs, TagArgs, TagsArgs, UntagArgs}; +use taggo::{init_db, try_database}; + +use anyhow::{Result, anyhow}; + +mod cli; + +pub const TERM_RESET: &str = "\x1b[0m"; +pub const TERM_BOLD: &str = "\x1b[1m"; + +fn main() -> anyhow::Result<()> { + let args = CliArgs::parse(); + + match args.commands { + cli::Commands::Init => init_db().map_err(|_| anyhow!("Database already exists!")), + cli::Commands::Tag { + args, + files, + file_tags, + } => handle_tag(args, files, file_tags), + cli::Commands::Untag(args) => handle_untag(args), + cli::Commands::Tags(args) => handle_tags(args), + cli::Commands::Files(args) => handle_files(args), + } +} + +fn handle_files(args: FilesArgs) -> Result<()> { + let conn = try_database()?; + + 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"# + .to_string(); + + let mut show_tags = true; + + let mut paths: Vec = vec![]; + if args.all { + paths = fs::read_dir(".")? + .filter_map(|f| f.ok()) + .filter(|f| f.metadata().is_ok() && f.metadata().unwrap().is_file()) + .map(|f| f.path()) + .collect(); + } else if let Some(query) = args.query { + let path = PathBuf::from_str(&query).unwrap_or_default(); + match path.exists() { + true => { + sql.push_str(&format!(" WHERE path IN ('{}')", query)); + paths = vec![path]; + } + false => { + sql.push_str(&format!(" WHERE tag.name LIKE '%{}%'", query)); + show_tags = false; + } + }; + } + + let mut all_paths: Vec = paths + .iter_mut() + .map(|f| f.to_string_lossy().replace("./", "").to_string()) + .collect(); + + sql.push_str(" GROUP BY path"); + + let mut stmt = conn.prepare(&sql)?; + let tagged_files: Vec<(String, String)> = stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + .flatten() + .collect(); + + let mut w = io::stdout(); + if !tagged_files.is_empty() { + if args.all { + writeln!(&mut w, "{TERM_BOLD}Tagged Files: {TERM_RESET}")?; + } + for (file, tags) in tagged_files { + // Remove already tagged files + if let Some(idx) = all_paths.iter().position(|v| *v == file) { + all_paths.swap_remove(idx); + } + + if show_tags { + writeln!(&mut w, "{}: {}", file, tags)?; + } else { + writeln!(&mut w, "{}", file)?; + } + } + } else { + writeln!(&mut w, "No files registered!")?; + } + + if args.all && !all_paths.is_empty() { + writeln!(&mut w, "\n{TERM_BOLD}Untagged Files: {TERM_RESET}")?; + for path in all_paths { + let path_str = path; + writeln!(&mut w, "{}", path_str)?; + } + } + + w.flush()?; + + Ok(()) +} + +pub fn handle_tag(args: TagArgs, files: Vec, file_tags: Vec) -> Result<()> { + let mut conn = try_database()?; + + if let Some(file) = args.file { + taggo::tag_file(&mut conn, &file, &args.tags)?; + } else { + // TODO: Maybe rework this at some point so its more optimized for batch insertion ? + for file in &files { + taggo::tag_file(&mut conn, file, &file_tags)?; + } + } + + Ok(()) +} + +pub fn handle_untag(args: UntagArgs) -> Result<()> { + let mut conn = try_database()?; + + taggo::untag_file(&mut conn, args.file, args.tags)?; + + Ok(()) +} + +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 = taggo::list_tags(&conn)?; + if !tags.is_empty() { + for tag in tags { + writeln!(&mut w, "{} - {}", tag.0, tag.1)?; + } + } else { + writeln!(&mut w, "No tags registered!")?; + } + w.flush()?; + Ok(()) + } + Some(cli::TagsCommands::Add { add }) => taggo::add_tags(&mut conn, add), + Some(cli::TagsCommands::Remove { remove }) => taggo::remove_tags(&mut conn, remove), + Some(cli::TagsCommands::Rename { renames }) => taggo::rename_tag(&conn, renames), + } +} diff --git a/src/lib.rs b/src/lib.rs index e69de29..70bd57e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1,252 @@ +use std::io::Write; +use std::{io, path::PathBuf}; + +use anyhow::{Result, anyhow}; +use rusqlite::{Connection, OpenFlags, params_from_iter, types::Value}; + +const DB_PATH: &str = ".tags"; + +pub fn init_db() -> anyhow::Result<()> { + let conn = Connection::open(DB_PATH)?; + + conn.execute( + r#"CREATE TABLE tag( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL UNIQUE + );"#, + (), + )?; + + conn.execute( + r#"CREATE TABLE file( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path VARCHAR(255) NOT NULL UNIQUE + );"#, + (), + )?; + + conn.execute( + r#"CREATE TABLE file_tag( + file_id INTEGER REFERENCES file(id) ON DELETE CASCADE, + tag_id INTEGER REFERENCES tag(id) ON DELETE CASCADE, + 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, + ) + .map_err(|_| { + anyhow!("Database does not exist! Run `taggo init` first to generate a new database.") + })?; + + Ok(conn) +} + +pub fn rename_tag(conn: &Connection, renames: Vec) -> Result<()> { + if renames.is_empty() { + return Err(anyhow!("No input found!")); + } else if renames.len() % 2 != 0 { + return Err(anyhow!( + "Attempting to rename an uneven amount of entries: {}", + renames.len() + )); + } + + let mut stmt = conn.prepare("UPDATE tag SET name = ? WHERE name = ?")?; + + let mut i = 0; + while i < renames.len() { + // TODO: Merge this when let chaining is stabilized in 1.88 + if let Some(old) = renames.get(i) { + if let Some(new) = renames.get(i + 1) { + stmt.execute([&new, &old])?; + } + } + i += 2; + } + + Ok(()) +} + +pub fn list_tags(conn: &Connection) -> Result> { + let mut stmt = conn.prepare( + r#"SELECT name, COUNT(file_id) as count FROM tag + LEFT JOIN file_tag ON file_tag.tag_id = tag.id + GROUP BY name + ORDER BY count DESC"#, + )?; + let result = stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + .flatten(); + + let mut tags: Vec<(String, i32)> = Vec::new(); + for entry in result { + tags.push(entry); + } + + Ok(tags) +} + +pub 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(','); + } + } + + 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()?; + + Ok(()) +} + +pub fn add_tags(conn: &mut Connection, tags: Vec) -> Result<()> { + let tx = conn.transaction()?; + { + let mut query = r#"INSERT INTO tag(name) VALUES"#.to_string(); + + for (i, _tag) in tags.iter().enumerate() { + query.push_str("(?)"); + if i < tags.len() - 1 { + query.push(','); + } + } + + 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()?; + + Ok(()) +} + +pub 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()?; + { + let file_id: Option = tx + .query_row(r#"SELECT id FROM file WHERE path = ?"#, [&file], |row| { + row.get(0) + }) + .ok(); + + let file_id = match file_id { + Some(id) => id, + None => { + let mut stmt = tx.prepare(r#"INSERT INTO file(path) VALUES (?) RETURNING id"#)?; + stmt.query_row([file], |row| -> Result { row.get(0) })? + } + }; + + let mut sql = "SELECT id FROM tag WHERE name IN ".to_string(); + sql.push('('); + let size = tags.len(); + for (i, _tag) in tags.iter().enumerate() { + sql.push('?'); + if i < size - 1 { + sql.push(','); + } + } + sql.push(')'); + + let mut stmt = tx.prepare(&sql)?; + let result = stmt + .query_map( + params_from_iter(tags), + |row| -> Result { row.get(0) }, + )? + .flatten(); + + let mut tags_ids = Vec::new(); + for id in result { + tags_ids.push(id); + } + + 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))?; + } + } + tx.commit()?; + + Ok(()) +} + +pub fn untag_file(conn: &mut Connection, file: PathBuf, tags: Option>) -> Result<()> { + let file = file + .to_str() + .ok_or(anyhow!("Unable to find path {:?}", file))? + .to_string(); + + let tx = conn.transaction()?; + { + let _file_id: i32 = tx + .query_row(r#"SELECT id FROM file WHERE path = ?"#, [&file], |row| { + row.get(0) + }) + .map_err(|_| anyhow!("No {} file registered!", file))?; + + let mut sql = r#" + DELETE FROM file_tag + WHERE ROWID IN ( + SELECT file_tag.ROWID FROM file_tag + INNER JOIN file ON file_tag.file_id = file.id + INNER JOIN tag ON file_tag.tag_id = tag.id"# + .to_string(); + + let params = match tags { + Some(tags) => { + let tags = tags.join(","); + sql.push_str(" WHERE file.path = ?1 AND tag.name IN (?2))"); + vec![Value::Text(file), Value::Text(tags)] + } + None => { + sql.push_str(" WHERE file.path = ?1)"); + vec![Value::Text(file)] + } + }; + + let mut stmt = tx.prepare(&sql)?; + stmt.execute(params_from_iter(params))?; + } + tx.commit()?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 28a26ac..0000000 --- a/src/main.rs +++ /dev/null @@ -1,70 +0,0 @@ -use anyhow::anyhow; -use clap::Parser; -use cli::CliArgs; -use rusqlite::{Connection, OpenFlags}; - -const DB_PATH: &str = ".tags"; - -mod cli; -mod tags; - -fn main() -> anyhow::Result<()> { - let args = CliArgs::parse(); - - match args.commands { - cli::Commands::Init => init_db().map_err(|_| anyhow!("Database already exists!")), - cli::Commands::Tag { - args, - files, - file_tags, - } => tags::handle_tag(args, files, file_tags), - cli::Commands::Untag(args) => tags::handle_untag(args), - cli::Commands::Tags(args) => tags::handle_tags(args), - cli::Commands::Files(args) => tags::handle_files(args), - } -} - -fn init_db() -> anyhow::Result<()> { - let conn = Connection::open(DB_PATH)?; - - conn.execute( - r#"CREATE TABLE tag( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL UNIQUE - );"#, - (), - )?; - - conn.execute( - r#"CREATE TABLE file( - id INTEGER PRIMARY KEY AUTOINCREMENT, - path VARCHAR(255) NOT NULL UNIQUE - );"#, - (), - )?; - - conn.execute( - r#"CREATE TABLE file_tag( - file_id INTEGER REFERENCES file(id) ON DELETE CASCADE, - tag_id INTEGER REFERENCES tag(id) ON DELETE CASCADE, - 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, - ) - .map_err(|_| { - anyhow!("Database does not exist! Run `taggo init` first to generate a new database.") - })?; - - Ok(conn) -} diff --git a/src/tags.rs b/src/tags.rs deleted file mode 100644 index c732015..0000000 --- a/src/tags.rs +++ /dev/null @@ -1,342 +0,0 @@ -use std::{ - fs, - io::{self, Write}, - path::PathBuf, - str::FromStr, -}; - -use anyhow::{Result, anyhow}; - -use rusqlite::{Connection, params_from_iter, types::Value}; - -pub const TERM_RESET: &str = "\x1b[0m"; -pub const TERM_BOLD: &str = "\x1b[1m"; - -use crate::{ - cli::{self, FilesArgs, TagArgs, TagsArgs, UntagArgs}, - try_database, -}; - -pub fn handle_files(args: FilesArgs) -> Result<()> { - let conn = try_database()?; - - 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"# - .to_string(); - - let mut show_tags = true; - - let mut paths: Vec = vec![]; - if args.all { - paths = fs::read_dir(".")? - .filter_map(|f| f.ok()) - .filter(|f| f.metadata().is_ok() && f.metadata().unwrap().is_file()) - .map(|f| f.path()) - .collect(); - } else if let Some(query) = args.query { - let path = PathBuf::from_str(&query).unwrap_or_default(); - match path.exists() { - true => { - sql.push_str(&format!(" WHERE path IN ('{}')", query)); - paths = vec![path]; - } - false => { - sql.push_str(&format!(" WHERE tag.name LIKE '%{}%'", query)); - show_tags = false; - } - }; - } - - let mut all_paths: Vec = paths - .iter_mut() - .map(|f| f.to_string_lossy().replace("./", "").to_string()) - .collect(); - - sql.push_str(" GROUP BY path"); - - let mut stmt = conn.prepare(&sql)?; - let tagged_files: Vec<(String, String)> = stmt - .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? - .flatten() - .collect(); - - let mut w = io::stdout(); - if !tagged_files.is_empty() { - if args.all { - writeln!(&mut w, "{TERM_BOLD}Tagged Files: {TERM_RESET}")?; - } - for (file, tags) in tagged_files { - // Remove already tagged files - if let Some(idx) = all_paths.iter().position(|v| *v == file) { - all_paths.swap_remove(idx); - } - - if show_tags { - writeln!(&mut w, "{}: {}", file, tags)?; - } else { - writeln!(&mut w, "{}", file)?; - } - } - } else { - writeln!(&mut w, "No files registered!")?; - } - - if args.all && !all_paths.is_empty() { - writeln!(&mut w, "\n{TERM_BOLD}Untagged Files: {TERM_RESET}")?; - for path in all_paths { - let path_str = path; - writeln!(&mut w, "{}", path_str)?; - } - } - - w.flush()?; - - Ok(()) -} - -pub fn handle_tag(args: TagArgs, files: Vec, file_tags: Vec) -> Result<()> { - let mut conn = try_database()?; - - if let Some(file) = args.file { - tag_file(&mut conn, &file, &args.tags)?; - } else { - // TODO: Maybe rework this at some point so its more optimized for batch insertion ? - for file in &files { - tag_file(&mut conn, file, &file_tags)?; - } - } - - Ok(()) -} - -pub fn handle_untag(args: UntagArgs) -> Result<()> { - let mut conn = try_database()?; - - untag_file(&mut conn, args.file, args.tags)?; - - Ok(()) -} - -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)?; - if !tags.is_empty() { - for tag in tags { - writeln!(&mut w, "{} - {}", tag.0, tag.1)?; - } - } else { - writeln!(&mut w, "No tags registered!")?; - } - w.flush()?; - Ok(()) - } - Some(cli::TagsCommands::Add { add }) => add_tags(&mut conn, add), - Some(cli::TagsCommands::Remove { remove }) => remove_tags(&mut conn, remove), - Some(cli::TagsCommands::Rename { renames }) => rename_tag(&conn, renames), - } -} - -fn rename_tag(conn: &Connection, renames: Vec) -> Result<()> { - if renames.is_empty() { - return Err(anyhow!("No input found!")); - } else if renames.len() % 2 != 0 { - return Err(anyhow!( - "Attempting to rename an uneven amount of entries: {}", - renames.len() - )); - } - - let mut stmt = conn.prepare("UPDATE tag SET name = ? WHERE name = ?")?; - - let mut i = 0; - while i < renames.len() { - // TODO: Merge this when let chaining is stabilized in 1.88 - if let Some(old) = renames.get(i) { - if let Some(new) = renames.get(i + 1) { - stmt.execute([&new, &old])?; - } - } - i += 2; - } - - Ok(()) -} - -fn list_tags(conn: &Connection) -> Result> { - let mut stmt = conn.prepare( - r#"SELECT name, COUNT(file_id) as count FROM tag - LEFT JOIN file_tag ON file_tag.tag_id = tag.id - GROUP BY name - ORDER BY count DESC"#, - )?; - let result = stmt - .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? - .flatten(); - - let mut tags: Vec<(String, i32)> = Vec::new(); - for entry in result { - tags.push(entry); - } - - Ok(tags) -} - -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(','); - } - } - - 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()?; - - Ok(()) -} - -fn add_tags(conn: &mut Connection, tags: Vec) -> Result<()> { - let tx = conn.transaction()?; - { - let mut query = r#"INSERT INTO tag(name) VALUES"#.to_string(); - - for (i, _tag) in tags.iter().enumerate() { - query.push_str("(?)"); - if i < tags.len() - 1 { - query.push(','); - } - } - - 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()?; - - Ok(()) -} - -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()?; - { - let file_id: Option = tx - .query_row(r#"SELECT id FROM file WHERE path = ?"#, [&file], |row| { - row.get(0) - }) - .ok(); - - let file_id = match file_id { - Some(id) => id, - None => { - let mut stmt = tx.prepare(r#"INSERT INTO file(path) VALUES (?) RETURNING id"#)?; - stmt.query_row([file], |row| -> Result { row.get(0) })? - } - }; - - let mut sql = "SELECT id FROM tag WHERE name IN ".to_string(); - sql.push('('); - let size = tags.len(); - for (i, _tag) in tags.iter().enumerate() { - sql.push('?'); - if i < size - 1 { - sql.push(','); - } - } - sql.push(')'); - - let mut stmt = tx.prepare(&sql)?; - let result = stmt - .query_map( - params_from_iter(tags), - |row| -> Result { row.get(0) }, - )? - .flatten(); - - let mut tags_ids = Vec::new(); - for id in result { - tags_ids.push(id); - } - - 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))?; - } - } - tx.commit()?; - - Ok(()) -} - -fn untag_file(conn: &mut Connection, file: PathBuf, tags: Option>) -> Result<()> { - let file = file - .to_str() - .ok_or(anyhow!("Unable to find path {:?}", file))? - .to_string(); - - let tx = conn.transaction()?; - { - let _file_id: i32 = tx - .query_row(r#"SELECT id FROM file WHERE path = ?"#, [&file], |row| { - row.get(0) - }) - .map_err(|_| anyhow!("No {} file registered!", file))?; - - let mut sql = r#" - DELETE FROM file_tag - WHERE ROWID IN ( - SELECT file_tag.ROWID FROM file_tag - INNER JOIN file ON file_tag.file_id = file.id - INNER JOIN tag ON file_tag.tag_id = tag.id"# - .to_string(); - - let params = match tags { - Some(tags) => { - let tags = tags.join(","); - sql.push_str(" WHERE file.path = ?1 AND tag.name IN (?2))"); - vec![Value::Text(file), Value::Text(tags)] - } - None => { - sql.push_str(" WHERE file.path = ?1)"); - vec![Value::Text(file)] - } - }; - - let mut stmt = tx.prepare(&sql)?; - stmt.execute(params_from_iter(params))?; - } - tx.commit()?; - - Ok(()) -}