change history to clean

This commit is contained in:
Julian Freeman
2026-04-17 13:44:41 -04:00
parent b9f24e07cf
commit 6d2b117200
11 changed files with 696 additions and 351 deletions

View File

@@ -1,8 +1,15 @@
use std::{path::PathBuf, process::Command};
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use crate::{
config_store,
models::{BrowserConfigListResponse, CreateCustomBrowserConfigInput, ScanResponse},
models::{
BrowserConfigListResponse, CleanupHistoryInput, CleanupHistoryResponse,
CleanupHistoryResult, CreateCustomBrowserConfigInput, ScanResponse,
},
scanner,
};
use tauri::AppHandle;
@@ -61,6 +68,31 @@ pub fn open_browser_profile(
spawn_browser_process(executable_path, user_data_dir, profile_id)
}
#[tauri::command]
pub fn cleanup_history_files(
app: AppHandle,
input: CleanupHistoryInput,
) -> Result<CleanupHistoryResponse, String> {
let config = config_store::find_browser_config(&app, &input.browser_id)?;
let user_data_dir = PathBuf::from(&config.user_data_path);
if !user_data_dir.is_dir() {
return Err(format!(
"User data directory does not exist: {}",
user_data_dir.display()
));
}
let mut results = Vec::new();
for profile_id in input.profile_ids {
let profile_path = user_data_dir.join(&profile_id);
let result = cleanup_profile_history_files(&profile_path, &profile_id);
results.push(result);
}
Ok(CleanupHistoryResponse { results })
}
fn spawn_browser_process(
executable_path: PathBuf,
user_data_dir: PathBuf,
@@ -79,3 +111,56 @@ fn spawn_browser_process(
)
})
}
fn cleanup_profile_history_files(profile_path: &Path, profile_id: &str) -> CleanupHistoryResult {
if !profile_path.is_dir() {
return CleanupHistoryResult {
profile_id: profile_id.to_string(),
deleted_files: Vec::new(),
skipped_files: Vec::new(),
error: Some(format!(
"Profile directory does not exist: {}",
profile_path.display()
)),
};
}
let mut deleted_files = Vec::new();
let mut skipped_files = Vec::new();
for file_name in ["History", "Top Sites", "Visited Links"] {
let file_path = profile_path.join(file_name);
if !file_path.exists() {
skipped_files.push(file_name.to_string());
continue;
}
if let Err(error) = fs::remove_file(&file_path) {
return CleanupHistoryResult {
profile_id: profile_id.to_string(),
deleted_files,
skipped_files,
error: Some(format!("Failed to delete {}: {error}", file_path.display())),
};
}
deleted_files.push(file_name.to_string());
remove_sidecar_files(&file_path);
}
CleanupHistoryResult {
profile_id: profile_id.to_string(),
deleted_files,
skipped_files,
error: None,
}
}
fn remove_sidecar_files(path: &Path) {
for suffix in ["-journal", "-wal", "-shm"] {
let sidecar = PathBuf::from(format!("{}{}", path.display(), suffix));
if sidecar.is_file() {
let _ = fs::remove_file(sidecar);
}
}
}

View File

@@ -15,7 +15,8 @@ pub fn run() {
commands::list_browser_configs,
commands::create_custom_browser_config,
commands::delete_custom_browser_config,
commands::open_browser_profile
commands::open_browser_profile,
commands::cleanup_history_files
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -20,7 +20,6 @@ pub struct BrowserView {
pub extensions: Vec<ExtensionSummary>,
pub bookmarks: Vec<BookmarkSummary>,
pub password_sites: Vec<PasswordSiteSummary>,
pub history_domains: Vec<HistoryDomainSummary>,
pub stats: BrowserStats,
}
@@ -31,7 +30,7 @@ pub struct BrowserStats {
pub extension_count: usize,
pub bookmark_count: usize,
pub password_site_count: usize,
pub history_domain_count: usize,
pub history_cleanup_profile_count: usize,
}
#[derive(Serialize)]
@@ -46,6 +45,7 @@ pub struct ProfileSummary {
pub default_avatar_stroke_color: Option<i64>,
pub avatar_label: String,
pub path: String,
pub history_cleanup: HistoryCleanupSummary,
}
#[derive(Serialize)]
@@ -77,13 +77,41 @@ pub struct PasswordSiteSummary {
pub profiles: Vec<AssociatedProfileSummary>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HistoryCleanupSummary {
pub history: CleanupFileStatus,
pub top_sites: CleanupFileStatus,
pub visited_links: CleanupFileStatus,
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum CleanupFileStatus {
Found,
Missing,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CleanupHistoryInput {
pub browser_id: String,
pub profile_ids: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HistoryDomainSummary {
pub domain: String,
pub visit_count: i64,
pub profile_ids: Vec<String>,
pub profiles: Vec<AssociatedProfileSummary>,
pub struct CleanupHistoryResponse {
pub results: Vec<CleanupHistoryResult>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CleanupHistoryResult {
pub profile_id: String,
pub deleted_files: Vec<String>,
pub skipped_files: Vec<String>,
pub error: Option<String>,
}
#[derive(Serialize, Clone)]
@@ -194,10 +222,3 @@ pub struct TempPasswordSite {
pub profile_ids: BTreeSet<String>,
pub profiles: BTreeMap<String, AssociatedProfileSummary>,
}
pub struct TempHistoryDomain {
pub domain: String,
pub visit_count: i64,
pub profile_ids: BTreeSet<String>,
pub profiles: BTreeMap<String, AssociatedProfileSummary>,
}

View File

@@ -11,9 +11,9 @@ use crate::{
config_store,
models::{
AssociatedProfileSummary, BookmarkAssociatedProfileSummary, BookmarkSummary,
BrowserConfigEntry, BrowserStats, BrowserView, ExtensionSummary, HistoryDomainSummary,
PasswordSiteSummary, ProfileSummary, ScanResponse, TempBookmark, TempExtension,
TempHistoryDomain, TempPasswordSite,
BrowserConfigEntry, BrowserStats, BrowserView, CleanupFileStatus, ExtensionSummary,
HistoryCleanupSummary, PasswordSiteSummary, ProfileSummary, ScanResponse, TempBookmark,
TempExtension, TempPasswordSite,
},
utils::{
copy_sqlite_database_to_temp, first_non_empty, load_image_as_data_url, read_json_file,
@@ -48,7 +48,6 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
let mut extensions = BTreeMap::<String, TempExtension>::new();
let mut bookmarks = BTreeMap::<String, TempBookmark>::new();
let mut password_sites = BTreeMap::<String, TempPasswordSite>::new();
let mut history_domains = BTreeMap::<String, TempHistoryDomain>::new();
for profile_id in profile_ids {
let profile_path = root.join(&profile_id);
@@ -62,7 +61,6 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
scan_extensions_for_profile(&profile_path, &profile_summary, &mut extensions);
scan_bookmarks_for_profile(&profile_path, &profile_summary, &mut bookmarks);
scan_password_sites_for_profile(&profile_path, &profile_summary, &mut password_sites);
scan_history_domains_for_profile(&profile_path, &profile_summary, &mut history_domains);
profiles.push(profile_summary);
}
@@ -96,15 +94,15 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
profiles: entry.profiles.into_values().collect(),
})
.collect::<Vec<_>>();
let history_domains = history_domains
.into_values()
.map(|entry| HistoryDomainSummary {
domain: entry.domain,
visit_count: entry.visit_count,
profile_ids: entry.profile_ids.into_iter().collect(),
profiles: entry.profiles.into_values().collect(),
let history_cleanup_profile_count = profiles
.iter()
.filter(|profile| {
let cleanup = &profile.history_cleanup;
cleanup.history == CleanupFileStatus::Found
|| cleanup.top_sites == CleanupFileStatus::Found
|| cleanup.visited_links == CleanupFileStatus::Found
})
.collect::<Vec<_>>();
.count();
Some(BrowserView {
browser_id: config.id,
@@ -117,13 +115,12 @@ fn scan_browser(config: BrowserConfigEntry) -> Option<BrowserView> {
extension_count: extensions.len(),
bookmark_count: bookmarks.len(),
password_site_count: password_sites.len(),
history_domain_count: history_domains.len(),
history_cleanup_profile_count,
},
profiles,
extensions: sort_extensions(extensions),
bookmarks: sort_bookmarks(bookmarks),
password_sites: sort_password_sites(password_sites),
history_domains: sort_history_domains(history_domains),
})
}
@@ -189,6 +186,23 @@ fn build_profile_summary(
default_avatar_stroke_color,
avatar_label,
path: profile_path.display().to_string(),
history_cleanup: scan_history_cleanup_status(profile_path),
}
}
fn scan_history_cleanup_status(profile_path: &Path) -> HistoryCleanupSummary {
HistoryCleanupSummary {
history: cleanup_file_status(&profile_path.join("History")),
top_sites: cleanup_file_status(&profile_path.join("Top Sites")),
visited_links: cleanup_file_status(&profile_path.join("Visited Links")),
}
}
fn cleanup_file_status(path: &Path) -> CleanupFileStatus {
if path.is_file() {
CleanupFileStatus::Found
} else {
CleanupFileStatus::Missing
}
}
@@ -623,71 +637,6 @@ fn scan_password_sites_for_profile(
}
}
fn scan_history_domains_for_profile(
profile_path: &Path,
profile: &ProfileSummary,
history_domains: &mut BTreeMap<String, TempHistoryDomain>,
) {
let history_path = profile_path.join("History");
if !history_path.is_file() {
return;
}
let Some(temp_copy) = copy_sqlite_database_to_temp(&history_path) else {
return;
};
let Ok(connection) = Connection::open_with_flags(
temp_copy.path(),
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
) else {
return;
};
let Ok(mut statement) = connection
.prepare("SELECT url, visit_count FROM urls WHERE hidden = 0 AND visit_count > 0")
else {
return;
};
let Ok(rows) = statement.query_map([], |row| {
Ok((row.get::<_, Option<String>>(0)?, row.get::<_, i64>(1)?))
}) else {
return;
};
for row in rows.flatten() {
let Some(url) = row.0.as_deref() else {
continue;
};
let Some(domain) = domain_from_url(url) else {
continue;
};
let entry = history_domains
.entry(domain.clone())
.or_insert_with(|| TempHistoryDomain {
domain: domain.clone(),
visit_count: 0,
profile_ids: BTreeSet::new(),
profiles: BTreeMap::new(),
});
entry.visit_count += row.1.max(0);
entry.profile_ids.insert(profile.id.clone());
entry
.profiles
.entry(profile.id.clone())
.or_insert_with(|| AssociatedProfileSummary {
id: profile.id.clone(),
name: profile.name.clone(),
avatar_data_url: profile.avatar_data_url.clone(),
avatar_icon: profile.avatar_icon.clone(),
default_avatar_fill_color: profile.default_avatar_fill_color,
default_avatar_stroke_color: profile.default_avatar_stroke_color,
avatar_label: profile.avatar_label.clone(),
});
}
}
fn normalize_login_site(origin_url: Option<&str>, signon_realm: Option<&str>) -> Option<String> {
let candidate = [signon_realm, origin_url]
.into_iter()
@@ -719,16 +668,3 @@ fn sort_password_sites(mut password_sites: Vec<PasswordSiteSummary>) -> Vec<Pass
});
password_sites
}
fn sort_history_domains(
mut history_domains: Vec<HistoryDomainSummary>,
) -> Vec<HistoryDomainSummary> {
history_domains.sort_by(|left, right| {
right
.visit_count
.cmp(&left.visit_count)
.then_with(|| left.domain.to_lowercase().cmp(&right.domain.to_lowercase()))
.then_with(|| left.domain.cmp(&right.domain))
});
history_domains
}