search_hub

at 27361b0 Raw

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)
}