pub mod chrome; pub mod firefox; pub mod gecko; pub mod zen; use crate::models::Bookmark; use rusqlite::{Connection, OpenFlags}; use std::path::{Path, PathBuf}; /// Trait for browser-specific importers. /// /// Implementations discover browser profiles and parse their bookmark stores /// and/or history into `Vec<Bookmark>`. /// /// # Example /// /// ```ignore /// use search_hub::importer::Importer; /// use search_hub::importer::firefox::FirefoxImporter; /// /// let importer = FirefoxImporter; /// let profiles = importer.discover_profiles(); /// if let Some(path) = profiles.first() { /// let bookmarks = importer.import(path).expect("import"); /// } /// ``` pub trait Importer { /// Human-readable name (e.g. "firefox", "zen"). fn name(&self) -> &'static str; /// Return paths to all detected browser profile directories. fn discover_profiles(&self) -> Vec<PathBuf>; /// Parse bookmarks from a profile directory. /// /// # Parameters /// /// * `profile_path` - Path to the browser profile directory. /// /// # Returns /// /// A `Vec<Bookmark>` with `source = "bookmark"`. fn import(&self, profile_path: &Path) -> anyhow::Result<Vec<Bookmark>>; /// Parse browser history from a profile directory. /// /// The default implementation returns an empty vec. Override this if the /// browser stores history (Firefox, Zen, Chrome/Chromium). /// /// # Parameters /// /// * `profile_path` - Path to the browser profile directory. /// /// # Returns /// /// A `Vec<Bookmark>` with `source = "history"`. fn import_history(&self, _profile_path: &Path) -> anyhow::Result<Vec<Bookmark>> { Ok(Vec::new()) } } /// Copy `places.sqlite` from a browser profile to a temp file and open it /// read-only. This avoids SQLITE_BUSY when the browser has the database open. /// /// # Example /// /// ```ignore /// use search_hub::importer::open_backup; /// use std::path::Path; /// /// let conn = open_backup(Path::new("/path/to/profile")) /// .expect("backup failed"); /// ``` /// /// # Parameters /// /// * `profile_path` - Path to the browser profile directory containing /// `places.sqlite`. /// /// # Returns /// /// A read-only `rusqlite::Connection` to the temporary copy. /// /// # Panics /// /// Returns an error (via `anyhow`) if `places.sqlite` is not found or /// cannot be copied. pub fn open_backup(profile_path: &Path) -> anyhow::Result<Connection> { open_backup_file(profile_path, "places.sqlite") } /// Copy an arbitrary SQLite file from a browser profile to a temp file and /// open it read-only. This avoids SQLITE_BUSY when the browser has the /// database open. /// /// # Example /// /// ```ignore /// use search_hub::importer::open_backup_file; /// use std::path::Path; /// /// let conn = open_backup_file(Path::new("/path/to/profile"), "History") /// .expect("backup failed"); /// ``` /// /// # Parameters /// /// * `profile_path` - Path to the browser profile directory. /// * `filename` - Name of the SQLite file inside the profile directory /// (e.g. `"History"`, `"places.sqlite"`). /// /// # Returns /// /// A read-only `rusqlite::Connection` to the temporary copy. /// /// # Panics /// /// Returns an error if the file is not found or cannot be copied. pub fn open_backup_file(profile_path: &Path, filename: &str) -> anyhow::Result<Connection> { let src = profile_path.join(filename); if !src.exists() { anyhow::bail!("{} not found in {:?}", filename, profile_path); } let tmp = tempfile::Builder::new() .suffix(&format!(".{}", filename)) .tempfile()?; let tmp_path = tmp.path().to_owned(); std::fs::copy(&src, &tmp_path)?; let conn = Connection::open_with_flags(&tmp_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?; std::mem::forget(tmp); Ok(conn) }