fix: ensure search paths are indexed (#896)

The file search extension relies on the OS's desktop search to work, and it
is possible that the desktop search indexer may not index the search paths
we specify.

This commit adds a hook that signals to the indexer and lets it index
the paths we need. This hook will be invoked when:

* initialing the extension
* enabling the extension
* upon every configuration change

to make our best effort to fix the issue.
This commit is contained in:
SteveLauC
2025-09-22 18:10:33 +08:00
committed by GitHub
parent bd0c9a740b
commit 450baccc92
15 changed files with 904 additions and 51 deletions

View File

@@ -32,6 +32,14 @@ jobs:
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils libtracker-sparql-3.0-dev
# On Windows, we need to generate bindings for 'searchapi.h' using bindgen.
# And bindgen relies on 'libclang'
# https://rust-lang.github.io/rust-bindgen/requirements.html#windows
- name: Install dependencies (Windows only)
if: startsWith(matrix.platform, 'windows-latest')
shell: bash
run: winget install LLVM.LLVM --silent --accept-package-agreements --accept-source-agreements
- name: Add pizza engine as a dependency
working-directory: src-tauri
shell: bash

View File

@@ -36,7 +36,7 @@ Information about release notes of Coco App is provided here.
- fix: resolve deeplink login issue #881
- fix: use kill_on_drop() to avoid zombie proc in error case #887
- fix: settings window rendering/loading issue 889
- fix: ensure search paths are indexed #896
### ✈️ Improvements

209
src-tauri/Cargo.lock generated
View File

@@ -432,6 +432,26 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.106",
]
[[package]]
name = "bit_field"
version = "0.10.3"
@@ -747,6 +767,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "cfb"
version = "0.7.3"
@@ -820,7 +849,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -833,6 +862,17 @@ dependencies = [
"inout",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading 0.8.8",
]
[[package]]
name = "coco"
version = "0.7.1"
@@ -842,19 +882,22 @@ dependencies = [
"async-recursion",
"async-trait",
"base64 0.13.1",
"bindgen",
"bitflags 2.9.4",
"borrowme",
"camino",
"cfg-if",
"chinese-number",
"chrono",
"configparser",
"derive_more 2.0.1",
"dirs 5.0.1",
"enigo",
"function_name",
"futures",
"futures-util",
"gio 0.20.12",
"gio 0.21.2",
"glib 0.21.2",
"hostname",
"http 1.3.1",
"hyper 0.14.32",
@@ -919,6 +962,7 @@ dependencies = [
"walkdir",
"which",
"windows 0.61.3",
"windows-sys 0.61.0",
"zip 4.6.1",
]
@@ -976,6 +1020,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "configparser"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b"
[[package]]
name = "const-random"
version = "0.1.18"
@@ -1394,7 +1444,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -2283,6 +2333,23 @@ dependencies = [
"smallvec",
]
[[package]]
name = "gio"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed68efc12b748a771be2dccc49480d8584004382967c98323245fc3c38b74a42"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"gio-sys 0.21.2",
"glib 0.21.2",
"libc",
"pin-project-lite",
"smallvec",
]
[[package]]
name = "gio-sys"
version = "0.18.1"
@@ -2309,6 +2376,19 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "gio-sys"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706"
dependencies = [
"glib-sys 0.21.2",
"gobject-sys 0.21.2",
"libc",
"system-deps 7.0.5",
"windows-sys 0.61.0",
]
[[package]]
name = "glib"
version = "0.18.5"
@@ -2353,6 +2433,27 @@ dependencies = [
"smallvec",
]
[[package]]
name = "glib"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8527f9a139b7ba936fe1cbb8710d7801d145a4e81534f2ae6e6be255b582f3b5"
dependencies = [
"bitflags 2.9.4",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys 0.21.2",
"glib-macros 0.21.2",
"glib-sys 0.21.2",
"gobject-sys 0.21.2",
"libc",
"memchr",
"smallvec",
]
[[package]]
name = "glib-macros"
version = "0.18.5"
@@ -2380,6 +2481,19 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "glib-macros"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55eda916eecdae426d78d274a17b48137acdca6fba89621bd3705f2835bc719f"
dependencies = [
"heck 0.5.0",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "glib-sys"
version = "0.18.1"
@@ -2400,6 +2514,16 @@ dependencies = [
"system-deps 7.0.5",
]
[[package]]
name = "glib-sys"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1"
dependencies = [
"libc",
"system-deps 7.0.5",
]
[[package]]
name = "glob"
version = "0.3.3"
@@ -2446,6 +2570,17 @@ dependencies = [
"system-deps 7.0.5",
]
[[package]]
name = "gobject-sys"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d"
dependencies = [
"glib-sys 0.21.2",
"libc",
"system-deps 7.0.5",
]
[[package]]
name = "gtk"
version = "0.18.2"
@@ -4799,6 +4934,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.106",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -7336,27 +7481,26 @@ dependencies = [
[[package]]
name = "tracker-rs"
version = "0.6.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc48e3b6e7a94b6c90b6905f157f2a875dfb5866cf19429476fd803c9c84eafc"
checksum = "4f6db4619caf27601f2ee31ce19fe9ddd6f7ebdedf6de3eef5399707f820b671"
dependencies = [
"bitflags 2.9.4",
"gio 0.20.12",
"glib 0.20.12",
"glib-sys 0.20.10",
"gio 0.21.2",
"glib 0.21.2",
"glib-sys 0.21.2",
"libc",
"tracker-sys",
]
[[package]]
name = "tracker-sys"
version = "0.6.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8814f2bd279c6fa3eb22921a5394df6109c6ec18f805e5973fa633a4e9a57785"
checksum = "b57f6d0182a3d2a149ca85cdea826849f2c3f329f39a8870432c65c8949b0d4f"
dependencies = [
"gio-sys 0.20.10",
"glib-sys 0.20.10",
"gobject-sys 0.20.10",
"gio-sys 0.21.2",
"glib-sys 0.21.2",
"gobject-sys 0.21.2",
"libc",
"system-deps 7.0.5",
]
@@ -8039,7 +8183,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-link 0.1.3",
"windows-numerics",
]
@@ -8095,7 +8239,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
@@ -8107,7 +8251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-link 0.1.3",
"windows-threading",
]
@@ -8207,6 +8351,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -8214,7 +8364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8223,7 +8373,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
@@ -8243,7 +8393,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8262,7 +8412,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8271,7 +8421,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8319,6 +8469,15 @@ dependencies = [
"windows-targets 0.53.3",
]
[[package]]
name = "windows-sys"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
dependencies = [
"windows-link 0.2.0",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -8371,7 +8530,7 @@ version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows-link 0.1.3",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@@ -8388,7 +8547,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8397,7 +8556,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]

View File

@@ -15,6 +15,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = ["default"] }
cfg-if = "1.0.1"
[features]
default = ["desktop"]
@@ -103,7 +104,6 @@ zip = "4.0.0"
url = "2.5.2"
camino = "1.1.10"
tokio-stream = { version = "0.1.17", features = ["io-util"] }
cfg-if = "1.0.1"
sysinfo = "0.35.2"
indexmap = { version = "2.10.0", features = ["serde"] }
strum = { version = "0.27.2", features = ["derive"] }
@@ -111,6 +111,7 @@ sys-locale = "0.3.2"
tauri-plugin-prevent-default = "1"
oneshot = "0.1.11"
bitflags = "2.9.3"
cfg-if = "1.0.1"
[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
@@ -121,9 +122,11 @@ objc2-application-services = { version = "0.3.1", features = ["HIServices"] }
objc2-core-graphics = { version = "=0.3.1", features = ["CGEvent"] }
[target."cfg(target_os = \"linux\")".dependencies]
gio = "0.20.12"
tracker-rs = "0.6.1"
gio = "0.21.2"
glib = "0.21.2"
tracker-rs = "0.7"
which = "8.0.0"
configparser = "3.1.0"
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
@@ -149,4 +152,8 @@ semver = { version = "1", features = ["serde"] }
[target."cfg(target_os = \"windows\")".dependencies]
enigo="0.3"
windows = { version = "0.61.3", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Search", "Win32_UI_Shell_PropertiesSystem", "Win32_Data"] }
windows = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Search", "Win32_UI_Shell_PropertiesSystem", "Win32_Data"] }
windows-sys = { version = "0.61", features = ["Win32", "Win32_System", "Win32_System_Com"] }
[target."cfg(target_os = \"windows\")".build-dependencies]
bindgen = "0.72.1"

View File

@@ -11,4 +11,32 @@ fn main() {
//
// unexpected condition name: `ci`
println!("cargo::rustc-check-cfg=cfg(ci)");
// Bindgen searchapi.h on Windows as the windows create does not provide
// bindings for it
cfg_if::cfg_if! {
if #[cfg(target_os = "windows")] {
use std::env;
use std::path::PathBuf;
let wrapper_header = r#"#include <windows.h>
#include <searchapi.h>"#;
let searchapi_bindings = bindgen::Builder::default()
.header_contents("wrapper.h", wrapper_header)
.generate()
.expect("failed to generate bindings for <searchapi.h>");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
searchapi_bindings
.write_to_file(out_path.join("searchapi_bindings.rs"))
.expect("couldn't write bindings to <OUT_DIR/searchapi_bindings.rs>")
// Looks like there is no need to link the library that contains the
// implementation of functions declared in 'searchapi.h' manually as
// the FFI bindings work (without doing that).
//
// This is wield, I do not expect the linker will link it automatically.
}
}
}

View File

@@ -1,5 +1,6 @@
//! File Search configuration entries definition and getter/setter functions.
use crate::extension::built_in::file_search::implementation::apply_config;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
@@ -23,7 +24,7 @@ static HOME_DIR: LazyLock<String> = LazyLock::new(|| {
.expect("User home directory should be encoded with UTF-8")
});
#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq)]
pub enum SearchBy {
Name,
NameAndContents,
@@ -197,13 +198,19 @@ pub async fn set_file_system_config(
.store(TAURI_STORE_FILE_SYSTEM_CONFIG)
.map_err(|e| e.to_string())?;
store.set(TAURI_STORE_KEY_SEARCH_PATHS, config.search_paths);
store.set(TAURI_STORE_KEY_EXCLUDE_PATHS, config.exclude_paths);
store.set(TAURI_STORE_KEY_FILE_TYPES, config.file_types);
store.set(TAURI_STORE_KEY_SEARCH_PATHS, config.search_paths.as_slice());
store.set(
TAURI_STORE_KEY_EXCLUDE_PATHS,
config.exclude_paths.as_slice(),
);
store.set(TAURI_STORE_KEY_FILE_TYPES, config.file_types.as_slice());
store.set(
TAURI_STORE_KEY_SEARCH_BY,
serde_json::to_value(config.search_by).unwrap(),
);
// Apply the config when we know that this set operation won't fail
apply_config(&config)?;
Ok(())
}

View File

@@ -10,7 +10,12 @@ use crate::{
common::document::{Document, OnOpened},
extension::built_in::file_search::config::SearchBy,
};
use camino::Utf8Path;
use gio::Cancellable;
use gio::Settings;
use gio::prelude::SettingsExtManual;
use glib::GString;
use glib::collections::strv::StrV;
use tracker::{SparqlConnection, SparqlCursor, prelude::SparqlCursorExtManual};
/// The service that we will connect to.
@@ -238,6 +243,103 @@ pub(crate) async fn hits(
Ok(result_hits)
}
fn ensure_path_in_recursive_indexing_scope(list: &mut StrV, path: &str) {
for item in list.iter() {
let item_path = Utf8Path::new(item.as_str());
let path = Utf8Path::new(path);
// It is already covered or listed
if path.starts_with(item_path) {
return;
}
}
list.push(
GString::from_utf8_checked(path.as_bytes().to_vec())
.expect("search_path_str contains an interior NUL"),
);
}
fn ensure_path_and_descendants_not_in_single_indexing_scope(list: &mut StrV, path: &str) {
// Indexes to the items that should be removed
let mut item_to_remove = Vec::new();
for (idx, item) in list.iter().enumerate() {
let item_path = Utf8Path::new(item.as_str());
let path = Utf8Path::new(path);
if item_path.starts_with(path) {
item_to_remove.push(idx);
}
}
// Reverse the indexes so that the remove operation won't invalidate them.
for idx in item_to_remove.into_iter().rev() {
list.remove(idx);
}
}
pub(crate) fn apply_config(config: &FileSearchConfig) -> Result<(), String> {
// Tracker provides the following configuration entries to allow users to
// tweak the indexing scope:
//
// 1. ignored-directories: A list of names, directories with such names will be ignored.
// ['po', 'CVS', 'core-dumps', 'lost+found']
// 2. ignored-directories-with-content: Avoid any directory containing a file blocklisted here
// ['.trackerignore', '.git', '.hg', '.nomedia']
// 3. ignored-files: List of file patterns to avoid
// ['*~', '*.o', '*.la', '*.lo', '*.loT', '*.in', '*.m4', '*.rej', ...]
// 4. index-recursive-directories: List of directories to index recursively
// ['&DESKTOP', '&DOCUMENTS', '&MUSIC', '&PICTURES', '&VIDEOS']
// 5. index-single-directories: List of directories to index without inspecting subfolders,
// ['$HOME', '&DOWNLOAD']
//
// The first 3 entries specify patterns, in order to use them, we have to walk
// through the whole directory tree listed in search paths, which is impractical.
// So we only use the last 2 entries.
//
//
// Just want to mention that setting search path to "/home" could break Tracker:
//
// ```text
// Unknown target graph for uri:'file:///home' and mime:'inode/directory'
// ```
//
// See the related bug reports:
//
// https://gitlab.gnome.org/GNOME/localsearch/-/issues/313
// https://bugs.launchpad.net/bugs/2077181
//
//
// There is nothing we can do.
const TRACKER_SETTINGS_SCHEMA: &str = "org.freedesktop.Tracker3.Miner.Files";
const KEY_INDEX_RECURSIVE_DIRECTORIES: &str = "index-recursive-directories";
const KEY_INDEX_SINGLE_DIRECTORIES: &str = "index-single-directories";
let search_paths = &config.search_paths;
let settings = Settings::new(TRACKER_SETTINGS_SCHEMA);
let mut recursive_list: StrV = settings.strv(KEY_INDEX_RECURSIVE_DIRECTORIES);
let mut single_list: StrV = settings.strv(KEY_INDEX_SINGLE_DIRECTORIES);
for search_path in search_paths {
// We want our search path to be included in the recursive directories or
// any directory within the list covers it.
ensure_path_in_recursive_indexing_scope(&mut recursive_list, search_path);
// We want our search path and its any descendants are not listed in
// the index directories list.
ensure_path_and_descendants_not_in_single_indexing_scope(&mut single_list, search_path);
}
settings
.set_strv(KEY_INDEX_RECURSIVE_DIRECTORIES, recursive_list)
.expect("key is not read-only");
settings
.set_strv(KEY_INDEX_SINGLE_DIRECTORIES, single_list)
.expect("key is not be read-only");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -8,6 +8,9 @@ use crate::common::document::{DataSourceReference, Document};
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
use crate::extension::OnOpened;
use crate::util::file::sync_get_file_icon;
use camino::Utf8Path;
use configparser::ini::Ini;
use configparser::ini::WriteOptions;
use futures::stream::Stream;
use futures::stream::StreamExt;
use std::os::fd::OwnedFd;
@@ -175,3 +178,131 @@ fn execute_baloosearch_query(
Ok((iter, child))
}
pub(crate) fn apply_config(config: &FileSearchConfig) -> Result<(), String> {
// Users can tweak Baloo via its configuration file, below are the fields that
// we need to modify:
//
// * Indexing-Enabled: turn indexing on or off
// * only basic indexing: If true, Baloo only indexes file names
// * folders: directories to index
// * exclude folders: directories to skip
//
// ```ini
// [Basic Settings]
// Indexing-Enabled=true
//
// [General]
// only basic indexing=true
// folders[$e]=$HOME/
// exclude folders[$e]=$HOME/FolderA/,$HOME/FolderB/
// ```
const SECTION_GENERAL: &str = "General";
const KEY_INCLUDE_FOLDERS: &str = "folders[$e]";
const KEY_EXCLUDE_FOLDERS: &str = "exclude folders[$e]";
const FOLDERS_SEPARATOR: &str = ",";
let rc_file_path = {
let mut home = dirs::home_dir()
.expect("cannot find the home directory, Coco should never run in such a environment");
home.push(".config/baloofilerc");
home
};
// Parse and load the rc file, it is in format INI
//
// Use `new_cs()`, the case-sensitive version of constructor as the config
// file contains uppercase letters, so it is case-sensitive.
let mut baloo_config = Ini::new_cs();
if rc_file_path.try_exists().map_err(|e| e.to_string())? {
let _ = baloo_config.load(rc_file_path.as_path())?;
}
// Ensure indexing is enabled
let _ = baloo_config.setstr("Basic Settings", "Indexing-Enabled", Some("true"));
// Let baloo index file content if we need that
if config.search_by == SearchBy::NameAndContents {
let _ = baloo_config.setstr(SECTION_GENERAL, "only basic indexing", Some("false"));
}
let mut include_folders = {
match baloo_config.get(SECTION_GENERAL, KEY_INCLUDE_FOLDERS) {
Some(str) => str
.split(FOLDERS_SEPARATOR)
.map(|str| str.to_string())
.collect::<Vec<String>>(),
None => Vec::new(),
}
};
let mut exclude_folders = {
match baloo_config.get(SECTION_GENERAL, KEY_EXCLUDE_FOLDERS) {
Some(str) => str
.split(FOLDERS_SEPARATOR)
.map(|str| str.to_string())
.collect::<Vec<String>>(),
None => Vec::new(),
}
};
fn ensure_path_included_include_folders(
include_folders: &mut Vec<String>,
search_path: &Utf8Path,
) {
for include_folder in include_folders.iter() {
let include_folder = Utf8Path::new(include_folder.as_str());
if search_path.starts_with(include_folder) {
return;
}
}
include_folders.push(search_path.as_str().to_string());
}
fn ensure_path_and_descendants_not_excluded(
exclude_folders: &mut Vec<String>,
search_path: &Utf8Path,
) {
let mut items_to_remove = Vec::new();
for (idx, exclude_folder) in exclude_folders.iter().enumerate() {
let exclude_folder = Utf8Path::new(exclude_folder);
if exclude_folder.starts_with(search_path) {
items_to_remove.push(idx);
}
}
for idx in items_to_remove.into_iter().rev() {
exclude_folders.remove(idx);
}
}
for search_path in config.search_paths.iter() {
let search_path = Utf8Path::new(search_path.as_str());
ensure_path_included_include_folders(&mut include_folders, search_path);
ensure_path_and_descendants_not_excluded(&mut exclude_folders, search_path);
}
let include_folders_str: String = include_folders.as_slice().join(FOLDERS_SEPARATOR);
let exclude_folders_str: String = exclude_folders.as_slice().join(FOLDERS_SEPARATOR);
let _ = baloo_config.set(
SECTION_GENERAL,
KEY_INCLUDE_FOLDERS,
Some(include_folders_str),
);
let _ = baloo_config.set(
SECTION_GENERAL,
KEY_EXCLUDE_FOLDERS,
Some(exclude_folders_str),
);
baloo_config
.pretty_write(rc_file_path.as_path(), &WriteOptions::new())
.map_err(|e| e.to_string())?;
Ok(())
}

View File

@@ -5,6 +5,11 @@ use super::super::config::FileSearchConfig;
use crate::common::document::Document;
use crate::util::LinuxDesktopEnvironment;
use crate::util::get_linux_desktop_environment;
use std::ops::Deref;
use std::sync::LazyLock;
static DESKTOP_ENVIRONMENT: LazyLock<Option<LinuxDesktopEnvironment>> =
LazyLock::new(|| get_linux_desktop_environment());
/// Dispatch to implementations powered by different backends.
pub(crate) async fn hits(
@@ -13,7 +18,7 @@ pub(crate) async fn hits(
size: usize,
config: &FileSearchConfig,
) -> Result<Vec<(Document, f64)>, String> {
let de = get_linux_desktop_environment();
let de = DESKTOP_ENVIRONMENT.deref();
match de {
Some(LinuxDesktopEnvironment::Gnome) => gnome::hits(query_string, from, size, config).await,
Some(LinuxDesktopEnvironment::Kde) => kde::hits(query_string, from, size, config).await,
@@ -27,3 +32,19 @@ pub(crate) async fn hits(
}
}
}
pub(crate) fn apply_config(config: &FileSearchConfig) -> Result<(), String> {
let de = DESKTOP_ENVIRONMENT.deref();
match de {
Some(LinuxDesktopEnvironment::Gnome) => gnome::apply_config(config),
Some(LinuxDesktopEnvironment::Kde) => kde::apply_config(config),
Some(LinuxDesktopEnvironment::Unsupported {
xdg_current_desktop: _,
}) => {
return Err("file search is not supported on this desktop environment".into());
}
None => {
return Err("could not determine Linux desktop environment".into());
}
}
}

View File

@@ -165,3 +165,26 @@ fn execute_mdfind_query(
Ok((iter, child))
}
pub(crate) fn apply_config(_: &FileSearchConfig) -> Result<(), String> {
// By default, macOS indexes all the files within a volume if indexing is
// enabled. So, to ensure our search paths are indexed by Spotlight,
// theoretically, we can do the following things:
//
// 1. Ensure indexing is enabled on the volumes where our search paths reside.
// However, we cannot do this as doing so requires `sudo`.
//
// 2. Ensure the search paths are not excluded from indexing scope. Users can
// stop Spotlight from indexing a directory by:
// 1. adding it to the "Privacy" list in 'System Settings'. Coco cannot
// modify this list, since the only way to change it is manually
// through System Settings.
// 2. Renaming directory name, adding a `.noindex` file extension to it.
// I don't want to use this trick, users won't feel comfortable and it
// could break at any time.
// 3. Creating a `.metadata_never_index` file within the directory (no longer works
// since macOS Mojave)
//
// There is nothing we can do.
Ok(())
}

View File

@@ -1,23 +1,36 @@
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
use cfg_if::cfg_if;
// `hits()` function is platform-specific, export the corresponding impl.
#[cfg(target_os = "linux")]
pub(crate) use linux::hits;
#[cfg(target_os = "macos")]
pub(crate) use macos::hits;
#[cfg(target_os = "windows")]
pub(crate) use windows::hits;
// * hits: the implementation of search
//
// * apply_config: Routines that should be performed to keep "other things"
// synchronous with the passed configuration.
// Currently, "other things" only include system indexer's setting entries.
cfg_if! {
if #[cfg(target_os = "linux")] {
mod linux;
pub(crate) use linux::hits;
pub(crate) use linux::apply_config;
} else if #[cfg(target_os = "macos")] {
mod macos;
pub(crate) use macos::hits;
pub(crate) use macos::apply_config;
} else if #[cfg(target_os = "windows")] {
mod windows;
pub(crate) use windows::hits;
pub(crate) use windows::apply_config;
}
}
use super::config::FileSearchConfig;
use camino::Utf8Path;
cfg_if! {
if #[cfg(not(target_os = "windows"))] {
use super::config::FileSearchConfig;
use camino::Utf8Path;
}
}
/// If `file_path` should be removed from the search results given the filter
/// conditions specified in `config`.
#[cfg(not(target_os = "windows"))] // Not used on Windows
pub(crate) fn should_be_filtered_out(
config: &FileSearchConfig,
file_path: &str,
@@ -74,7 +87,8 @@ pub(crate) fn should_be_filtered_out(
false
}
#[cfg(test)]
// should_be_filtered_out() is not defined for Windows
#[cfg(all(test, not(target_os = "windows")))]
mod tests {
use super::super::config::SearchBy;
use super::*;

View File

@@ -0,0 +1,234 @@
//! Wraps Windows `ISearchCrawlScopeManager`
mod searchapi_h_bindings;
use searchapi_h_bindings::CLSID_CSEARCH_MANAGER;
use searchapi_h_bindings::IID_ISEARCH_MANAGER;
use searchapi_h_bindings::{
HRESULT, ISearchCatalogManager, ISearchCatalogManagerVtbl, ISearchCrawlScopeManager,
ISearchCrawlScopeManagerVtbl, ISearchManager,
};
use std::ffi::OsStr;
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::path::PathBuf;
use std::ptr::null_mut;
use windows::core::w;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::{
CLSCTX_LOCAL_SERVER, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
};
#[derive(Debug, thiserror::Error)]
#[error("{msg}, function [{function}], HRESULT [{hresult}]")]
pub(crate) struct WindowSearchApiError {
function: &'static str,
hresult: HRESULT,
msg: String,
}
/// See doc of [`Rule`].
#[derive(Debug, PartialEq)]
pub(crate) enum RuleMode {
Inclusion,
Exclusion,
}
/// A rule adds or removes one or more paths to/from the Windows Search index.
#[derive(Debug)]
pub(crate) struct Rule {
/// A path or path pattern (wildcard supported, only for exclusion rule) that
/// specifies the paths that this rule applies to.
///
/// The rules used by Windows Search actually specify URLs rather than paths,
/// but we only care about paths, i.e., URLs with schema `file://`
pub(crate) paths: PathBuf,
/// Add or remove paths to/from the index.
pub(crate) mode: RuleMode,
}
/// A wrapper around Window's `ISearchCrawlScopeManager` type
pub(crate) struct CrawlScopeManager {
i_search_crawl_scope_manager: *mut ISearchCrawlScopeManager,
}
impl CrawlScopeManager {
fn vtable(&self) -> *mut ISearchCrawlScopeManagerVtbl {
unsafe { (*self.i_search_crawl_scope_manager).lpVtbl }
}
pub(crate) fn new() -> Result<Self, WindowSearchApiError> {
unsafe {
// 1. Initialize the COM library, use Apartment-threading as Self is not Send/Sync
let hr = CoInitializeEx(null_mut(), COINIT_APARTMENTTHREADED as u32);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "CoInitializeEx()",
hresult: hr,
msg: "failed to initialize the COM library".into(),
});
}
// 2. Create an instance of the CSearchManager.
let mut search_manager: *mut ISearchManager = null_mut();
let hr = CoCreateInstance(
&CLSID_CSEARCH_MANAGER, // CLSID of the object
null_mut(), // No outer unknown
CLSCTX_LOCAL_SERVER, // Server context
&IID_ISEARCH_MANAGER, // IID of the interface we want
&mut search_manager as *mut _ as *mut _, // Pointer to receive the interface
);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "CoCreateInstance()",
hresult: hr,
msg: "failed to initialize ISearchManager".into(),
});
}
assert!(!search_manager.is_null());
let search_manger_vtable = (*search_manager).lpVtbl;
let search_manager_fn_get_catalog = (*search_manger_vtable).GetCatalog.unwrap();
let mut search_catalog_manager: *mut ISearchCatalogManager = null_mut();
let string_literal_system_index = w!("SystemIndex");
let hr: HRESULT = search_manager_fn_get_catalog(
search_manager,
string_literal_system_index.0,
&mut search_catalog_manager as *mut *mut ISearchCatalogManager,
);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "ISearchManager::GetCatalog()",
hresult: hr,
msg: "failed to initialize ISearchCatalogManager".into(),
});
}
assert!(!search_catalog_manager.is_null());
let search_catalog_manager_vtable: *mut ISearchCatalogManagerVtbl =
(*search_catalog_manager).lpVtbl;
let fn_get_crawl_scope_manager = (*search_catalog_manager_vtable)
.GetCrawlScopeManager
.unwrap();
let mut search_crawl_scope_manager: *mut ISearchCrawlScopeManager = null_mut();
let hr =
fn_get_crawl_scope_manager(search_catalog_manager, &mut search_crawl_scope_manager);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "ISearchCatalogManager::GetCrawlScopeManager()",
hresult: hr,
msg: "failed to initialize ISearchCrawlScopeManager".into(),
});
}
assert!(!search_crawl_scope_manager.is_null());
Ok(Self {
i_search_crawl_scope_manager: search_crawl_scope_manager,
})
}
}
/// Does nothing unless you [`commit()`] the changes.
pub(crate) fn add_rule(&mut self, rule: Rule) -> Result<(), WindowSearchApiError> {
unsafe {
let vtable = self.vtable();
let fn_add_rule = (*vtable).AddUserScopeRule.unwrap();
let url: Vec<u16> = encode_path(&rule.paths);
let inclusion = (rule.mode == RuleMode::Inclusion) as i32;
let override_child_rules = true as i32;
let follow_flag = 0x1_u32; /* FF_INDEXCOMPLEXURLS */
let hr = fn_add_rule(
self.i_search_crawl_scope_manager,
url.as_ptr(),
inclusion,
override_child_rules,
follow_flag,
);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "ISearchCrawlScopeManager::AddUserScopeRule()",
hresult: hr,
msg: "failed to add scope rule".into(),
});
}
Ok(())
}
}
pub(crate) fn is_path_included<P: AsRef<Path> + ?Sized>(
&self,
path: &P,
) -> Result<bool, WindowSearchApiError> {
unsafe {
let vtable = self.vtable();
let fn_included_in_crawl_scope = (*vtable).IncludedInCrawlScope.unwrap();
let path: Vec<u16> = encode_path(path);
let mut included: i32 = 0 /* false */;
let hr = fn_included_in_crawl_scope(
self.i_search_crawl_scope_manager,
path.as_ptr(),
&mut included,
);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "ISearchCrawlScopeManager::IncludedInCrawlScope()",
hresult: hr,
msg: "failed to call IncludedInCrawlScope()".into(),
});
}
Ok(included == 1)
}
}
pub(crate) fn commit(&self) -> Result<(), WindowSearchApiError> {
unsafe {
let vtable = self.vtable();
let fn_commit = (*vtable).SaveAll.unwrap();
let hr = fn_commit(self.i_search_crawl_scope_manager);
if hr != S_OK {
return Err(WindowSearchApiError {
function: "ISearchCrawlScopeManager::SaveAll()",
hresult: hr,
msg: "failed to commit the changes".into(),
});
}
Ok(())
}
}
}
impl Drop for CrawlScopeManager {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
fn encode_path<P: AsRef<Path> + ?Sized>(path: &P) -> Vec<u16> {
let mut buffer = OsString::new();
// schema
buffer.push("file:///");
buffer.push(path.as_ref().as_os_str());
osstr_to_wstr(&buffer)
}
fn osstr_to_wstr<S: AsRef<OsStr> + ?Sized>(str: &S) -> Vec<u16> {
let os_str: &OsStr = str.as_ref();
let mut chars = os_str.encode_wide().collect::<Vec<u16>>();
chars.push(0 /* NUL */);
chars
}

View File

@@ -0,0 +1,30 @@
//! Rust binding of the types and functions declared in 'searchapi.h'
#![allow(unused)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(unnecessary_transmutes)]
include!(concat!(env!("OUT_DIR"), "/searchapi_bindings.rs"));
// The bindings.rs contains a GUID type as well, we use the one provided by
// the windows_sys crate here.
use windows_sys::core::GUID as WIN_SYS_GUID;
// https://github.com/search?q=CLSID_CSearchManager+language%3AC&type=code&l=C
pub(crate) static CLSID_CSEARCH_MANAGER: WIN_SYS_GUID = WIN_SYS_GUID {
data1: 0x7d096c5f,
data2: 0xac08,
data3: 0x4f1f,
data4: [0xbe, 0xb7, 0x5c, 0x22, 0xc5, 0x17, 0xce, 0x39],
};
// https://github.com/search?q=IID_ISearchManager+language%3AC&type=code
pub(crate) static IID_ISEARCH_MANAGER: WIN_SYS_GUID = WIN_SYS_GUID {
data1: 0xAB310581,
data2: 0xac80,
data3: 0x11d1,
data4: [0x8d, 0xf3, 0x00, 0xc0, 0x4f, 0xb6, 0xef, 0x69],
};

View File

@@ -3,6 +3,8 @@
//! https://github.com/IRONAGE-Park/rag-sample/blob/3f0ad8c8012026cd3a7e453d08f041609426cb91/src/native/windows.rs
//! is the starting point of this implementation.
mod crawl_scope_manager;
use super::super::EXTENSION_ID;
use super::super::config::FileSearchConfig;
use super::super::config::SearchBy;
@@ -10,6 +12,8 @@ use crate::common::document::{DataSourceReference, Document};
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
use crate::extension::OnOpened;
use crate::util::file::sync_get_file_icon;
use std::borrow::Borrow;
use std::path::PathBuf;
use windows::{
Win32::System::{
Com::{CLSCTX_INPROC_SERVER, CoCreateInstance},
@@ -468,6 +472,85 @@ pub(crate) async fn hits(
Ok(hits)
}
pub(crate) fn apply_config(config: &FileSearchConfig) -> Result<(), String> {
// To ensure Windows Search indexer index the paths we specified in the
// config, we will:
//
// 1. Add an inclusion rule for every search path to ensure indexer index
// them
// 2. For the exclude paths, we exclude them from the crawl scope if they
// were not included in the scope before we update the scope. Otherwise,
// we cannot exclude them as doing that could potentially break other
// apps (by removing the indexes they rely on).
//
// Windows APIs are pretty smart. They won't blindly add an inclusion rule if
// the path you are trying to include is already included. The same applies
// to exclusion rules as well. Since Windows APIs handle these checks for us,
// we don't need to worry about them.
use crawl_scope_manager::CrawlScopeManager;
use crawl_scope_manager::Rule;
use crawl_scope_manager::RuleMode;
use std::borrow::Cow;
/// Windows APIs need the path to contain a tailing '\'
fn add_tailing_backslash(path: &str) -> Cow<'_, str> {
if path.ends_with(r#"\"#) {
Cow::Borrowed(path)
} else {
let mut owned = path.to_string();
owned.push_str(r#"\"#);
Cow::Owned(owned)
}
}
let mut manager = CrawlScopeManager::new().map_err(|e| e.to_string())?;
let search_paths = &config.search_paths;
let exclude_paths = &config.exclude_paths;
// indexes to `exclude_paths` of the paths we need to exclude
let mut paths_to_exclude: Vec<usize> = Vec::new();
for (idx, exclude_path) in exclude_paths.into_iter().enumerate() {
let exclude_path = add_tailing_backslash(&exclude_path);
let exclude_path: &str = exclude_path.borrow();
if !manager
.is_path_included(exclude_path)
.map_err(|e| e.to_string())?
{
paths_to_exclude.push(idx);
}
}
for search_path in search_paths {
let inclusion_rule = Rule {
paths: PathBuf::from(add_tailing_backslash(&search_path).into_owned()),
mode: RuleMode::Inclusion,
};
manager
.add_rule(inclusion_rule)
.map_err(|e| e.to_string())?;
}
for idx in paths_to_exclude {
let exclusion_rule = Rule {
paths: PathBuf::from(add_tailing_backslash(&exclude_paths[idx]).into_owned()),
mode: RuleMode::Exclusion,
};
manager
.add_rule(exclusion_rule)
.map_err(|e| e.to_string())?;
}
manager.commit().map_err(|e| e.to_string())?;
Ok(())
}
// Skip these tests in our CI, they fail with the following error
// "SQL is invalid: "0x80041820""
//

View File

@@ -16,6 +16,8 @@ use crate::extension::{
ExtensionBundleIdBorrowed, PLUGIN_JSON_FILE_NAME, alter_extension_json_file,
};
use anyhow::Context;
use file_search::config::FileSearchConfig;
use file_search::implementation::apply_config as file_search_apply_config;
use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager};
@@ -227,6 +229,8 @@ pub(super) async fn init_built_in_extension(
search_source_registry
.register_source(file_system_search)
.await;
let file_search_config = FileSearchConfig::get(tauri_app_handle);
file_search_apply_config(&file_search_config)?;
log::debug!("built-in extension [{}] initialized", extension.id);
}
@@ -329,6 +333,8 @@ pub(crate) async fn enable_built_in_extension(
bundle_id,
update_extension,
)?;
let file_search_config = FileSearchConfig::get(tauri_app_handle);
file_search_apply_config(&file_search_config)?;
return Ok(());
}