change history to clean
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user