feat: integrate Google semantic search and branded desktop launcher
Core CI / core (ubuntu-latest) (push) Failing after 3m27s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m45s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Failing after 3m27s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m45s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
a90df15503
commit
caf6a2f82e
80 files changed
+14971
-837
No files matched your search
@@ -0,0 +1,201 @@
|
||||
//! Versioned local settings. Writes replace the destination atomically; failed
|
||||
//! saves leave both the previous file and in-memory settings intact.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const SETTINGS_VERSION: u32 = 1;
|
||||
const MAX_SETTINGS_BYTES: u64 = 128 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub version: u32,
|
||||
pub roots: Vec<PathBuf>,
|
||||
pub cpu_threads: usize,
|
||||
/// Managed local Google EmbeddingGemma2 artifacts. Older settings default
|
||||
/// to unconfigured; model acquisition is an explicit desktop action.
|
||||
#[serde(default)]
|
||||
pub model_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: SETTINGS_VERSION,
|
||||
roots: Vec::new(),
|
||||
cpu_threads: 1,
|
||||
model_directory: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.version != SETTINGS_VERSION {
|
||||
return Err(format!("Unsupported settings version {}", self.version));
|
||||
}
|
||||
if !(1..=8).contains(&self.cpu_threads) {
|
||||
return Err("CPU threads must be between 1 and 8.".into());
|
||||
}
|
||||
if self.roots.len() > 64 {
|
||||
return Err("At most 64 indexed folders are supported.".into());
|
||||
}
|
||||
if let Some(directory) = &self.model_directory {
|
||||
if !directory.is_absolute() || directory.to_string_lossy().len() > 32768 {
|
||||
return Err("Model directory must be an absolute bounded path.".into());
|
||||
}
|
||||
}
|
||||
for (i, root) in self.roots.iter().enumerate() {
|
||||
if !root.is_absolute() {
|
||||
return Err("Indexed folder paths must be absolute.".into());
|
||||
}
|
||||
if root.to_string_lossy().len() > 32768 {
|
||||
return Err("Indexed folder path is too long.".into());
|
||||
}
|
||||
if self.roots[..i].contains(root) {
|
||||
return Err("Indexed folder paths must be unique.".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsStore {
|
||||
path: PathBuf,
|
||||
pub value: Settings,
|
||||
}
|
||||
|
||||
impl SettingsStore {
|
||||
pub fn load(path: PathBuf) -> Result<Self, String> {
|
||||
let value = match fs::metadata(&path) {
|
||||
Ok(metadata) => {
|
||||
if metadata.len() > MAX_SETTINGS_BYTES {
|
||||
return Err("Settings file exceeds the size limit.".into());
|
||||
}
|
||||
serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| format!("Invalid settings file: {e}"))?
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Settings::default(),
|
||||
Err(error) => return Err(error.to_string()),
|
||||
};
|
||||
let store = Self { path, value };
|
||||
store.value.validate()?;
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
pub fn save(&mut self, next: Settings) -> Result<(), String> {
|
||||
next.validate()?;
|
||||
let parent = self.path.parent().ok_or("Settings path has no parent.")?;
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
let mut temporary = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
|
||||
let bytes = serde_json::to_vec_pretty(&next).map_err(|e| e.to_string())?;
|
||||
if bytes.len() as u64 > MAX_SETTINGS_BYTES {
|
||||
return Err("Settings exceed the size limit.".into());
|
||||
}
|
||||
temporary.write_all(&bytes).map_err(|e| e.to_string())?;
|
||||
temporary.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
temporary.persist(&self.path).map_err(|e| e.to_string())?;
|
||||
self.value = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_root(&mut self, root: &Path) -> Result<(), String> {
|
||||
let canonical = fs::canonicalize(root).map_err(|e| e.to_string())?;
|
||||
if !canonical.is_dir() {
|
||||
return Err("Indexed folder must be a directory.".into());
|
||||
}
|
||||
let mut next = self.value.clone();
|
||||
if !next.roots.contains(&canonical) {
|
||||
next.roots.push(canonical);
|
||||
self.save(next)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn settings_round_trip_and_atomic_replacement() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("settings.json");
|
||||
let mut store = SettingsStore::load(path.clone()).unwrap();
|
||||
assert_eq!(store.value.cpu_threads, 1);
|
||||
store.add_root(directory.path()).unwrap();
|
||||
store.add_root(directory.path()).unwrap();
|
||||
let mut next = store.value.clone();
|
||||
next.cpu_threads = 2;
|
||||
store.save(next.clone()).unwrap();
|
||||
assert_eq!(SettingsStore::load(path).unwrap().value, next);
|
||||
assert_eq!(store.value.roots.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_save_preserves_previous_file_and_memory() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("settings.json");
|
||||
let mut store = SettingsStore::load(path.clone()).unwrap();
|
||||
store.save(Settings::default()).unwrap();
|
||||
let original = fs::read(&path).unwrap();
|
||||
let mut invalid = store.value.clone();
|
||||
invalid.cpu_threads = 0;
|
||||
assert!(store.save(invalid).is_err());
|
||||
assert_eq!(store.value, Settings::default());
|
||||
assert_eq!(fs::read(&path).unwrap(), original);
|
||||
let mut invalid = store.value.clone();
|
||||
invalid.roots.push(PathBuf::from("relative"));
|
||||
assert!(store.save(invalid).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_corruption_unknown_version_and_excessive_threads() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("settings.json");
|
||||
fs::write(&path, "broken").unwrap();
|
||||
assert!(SettingsStore::load(path.clone()).is_err());
|
||||
fs::write(&path, r#"{"version":99,"roots":[],"cpu_threads":1}"#).unwrap();
|
||||
assert!(SettingsStore::load(path).is_err());
|
||||
let invalid = Settings {
|
||||
cpu_threads: 9,
|
||||
..Settings::default()
|
||||
};
|
||||
assert!(invalid.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_older_settings_with_unconfigured_google_model() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("settings.json");
|
||||
fs::write(&path, r#"{"version":1,"roots":[],"cpu_threads":1}"#).unwrap();
|
||||
let store = SettingsStore::load(path).unwrap();
|
||||
assert!(store.value.model_directory.is_none());
|
||||
let invalid = Settings {
|
||||
model_directory: Some(PathBuf::from("relative")),
|
||||
..Settings::default()
|
||||
};
|
||||
assert!(invalid.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_save_leaves_existing_settings_readable() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("settings.json");
|
||||
let mut store = SettingsStore::load(path.clone()).unwrap();
|
||||
store.save(Settings::default()).unwrap();
|
||||
let mut oversized = store.value.clone();
|
||||
oversized.roots = (0..64)
|
||||
.map(|i| directory.path().join(format!("{i}{}", "x".repeat(3000))))
|
||||
.collect();
|
||||
assert!(store.save(oversized).is_err());
|
||||
assert_eq!(
|
||||
SettingsStore::load(path).unwrap().value,
|
||||
Settings::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user