support delete bookmarks

This commit is contained in:
Julian Freeman
2026-04-17 16:27:57 -04:00
parent 42905bf6d3
commit 45662dc642
10 changed files with 946 additions and 24 deletions

View File

@@ -7,9 +7,11 @@ use std::{
use crate::{
config_store,
models::{
BrowserConfigListResponse, CleanupHistoryInput, CleanupHistoryResponse,
CleanupHistoryResult, CreateCustomBrowserConfigInput, ExtensionInstallSourceSummary,
RemoveExtensionResult, RemoveExtensionsInput, RemoveExtensionsResponse, ScanResponse,
BookmarkRemovalRequest, BrowserConfigListResponse, CleanupHistoryInput,
CleanupHistoryResponse, CleanupHistoryResult, CreateCustomBrowserConfigInput,
ExtensionInstallSourceSummary, RemoveBookmarkResult, RemoveBookmarksInput,
RemoveBookmarksResponse, RemoveExtensionResult, RemoveExtensionsInput,
RemoveExtensionsResponse, ScanResponse,
},
scanner,
};
@@ -124,6 +126,35 @@ pub fn remove_extensions(
Ok(RemoveExtensionsResponse { results })
}
#[tauri::command]
pub fn remove_bookmarks(
app: AppHandle,
input: RemoveBookmarksInput,
) -> Result<RemoveBookmarksResponse, 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 removal in input.removals {
for profile_id in &removal.profile_ids {
results.push(remove_bookmark_from_profile(
&user_data_dir.join(profile_id),
&removal,
profile_id,
));
}
}
Ok(RemoveBookmarksResponse { results })
}
fn spawn_browser_process(
executable_path: PathBuf,
user_data_dir: PathBuf,
@@ -298,6 +329,106 @@ fn remove_extension_from_profile(
}
}
fn remove_bookmark_from_profile(
profile_path: &Path,
removal: &BookmarkRemovalRequest,
profile_id: &str,
) -> RemoveBookmarkResult {
if !profile_path.is_dir() {
return RemoveBookmarkResult {
url: removal.url.clone(),
profile_id: profile_id.to_string(),
removed_count: 0,
removed_files: Vec::new(),
skipped_files: Vec::new(),
error: Some(format!(
"Profile directory does not exist: {}",
profile_path.display()
)),
};
}
let mut removed_files = Vec::new();
let mut skipped_files = Vec::new();
let removed_backup = remove_bookmark_backups(profile_path).map_err(|error| RemoveBookmarkResult {
url: removal.url.clone(),
profile_id: profile_id.to_string(),
removed_count: 0,
removed_files: removed_files.clone(),
skipped_files: skipped_files.clone(),
error: Some(error),
});
let removed_backup = match removed_backup {
Ok(value) => value,
Err(result) => return result,
};
if removed_backup {
removed_files.push("Bookmarks.bak".to_string());
} else {
skipped_files.push("Bookmarks.bak".to_string());
}
let Some(bookmarks_path) = resolve_bookmarks_path(profile_path) else {
return RemoveBookmarkResult {
url: removal.url.clone(),
profile_id: profile_id.to_string(),
removed_count: 0,
removed_files,
skipped_files,
error: Some(format!(
"Bookmarks file does not exist in {}",
profile_path.display()
)),
};
};
let mut document = match read_json_document(&bookmarks_path) {
Ok(document) => document,
Err(error) => {
return RemoveBookmarkResult {
url: removal.url.clone(),
profile_id: profile_id.to_string(),
removed_count: 0,
removed_files,
skipped_files,
error: Some(error),
};
}
};
let checksum_removed = document
.as_object_mut()
.and_then(|object| object.remove("checksum"))
.is_some();
let removed_count = remove_matching_bookmarks(&mut document, &removal.url);
if checksum_removed || removed_count > 0 {
if let Err(error) = write_json_document(&bookmarks_path, &document) {
return RemoveBookmarkResult {
url: removal.url.clone(),
profile_id: profile_id.to_string(),
removed_count: 0,
removed_files,
skipped_files,
error: Some(error),
};
}
removed_files.push("Bookmarks".to_string());
} else {
skipped_files.push("Bookmarks".to_string());
}
RemoveBookmarkResult {
url: removal.url.clone(),
profile_id: profile_id.to_string(),
removed_count,
removed_files,
skipped_files,
error: None,
}
}
fn remove_extension_from_secure_preferences(
path: &Path,
extension_id: &str,
@@ -427,3 +558,69 @@ fn cleanup_sessions_directory(path: &Path) -> Result<bool, std::io::Error> {
Ok(deleted_any)
}
fn remove_bookmark_backups(profile_path: &Path) -> Result<bool, String> {
let mut deleted_any = false;
for backup_name in ["Bookmarks.bak", "Bookmark.bak"] {
let backup_path = profile_path.join(backup_name);
if !backup_path.is_file() {
continue;
}
fs::remove_file(&backup_path)
.map_err(|error| format!("Failed to delete {}: {error}", backup_path.display()))?;
deleted_any = true;
}
Ok(deleted_any)
}
fn resolve_bookmarks_path(profile_path: &Path) -> Option<PathBuf> {
["Bookmarks", "Bookmark"]
.into_iter()
.map(|name| profile_path.join(name))
.find(|path| path.is_file())
}
fn remove_matching_bookmarks(value: &mut Value, target_url: &str) -> usize {
match value {
Value::Object(object) => {
let mut removed_count = 0;
if let Some(children) = object.get_mut("children").and_then(Value::as_array_mut) {
let mut index = 0;
while index < children.len() {
let matches_url = children[index]
.as_object()
.map(|child| {
child.get("type").and_then(Value::as_str) == Some("url")
&& child.get("url").and_then(Value::as_str) == Some(target_url)
})
.unwrap_or(false);
if matches_url {
children.remove(index);
removed_count += 1;
continue;
}
removed_count += remove_matching_bookmarks(&mut children[index], target_url);
index += 1;
}
}
for (key, child) in object.iter_mut() {
if key == "children" {
continue;
}
removed_count += remove_matching_bookmarks(child, target_url);
}
removed_count
}
Value::Array(array) => array
.iter_mut()
.map(|item| remove_matching_bookmarks(item, target_url))
.sum(),
_ => 0,
}
}

View File

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

View File

@@ -122,6 +122,13 @@ pub struct RemoveExtensionsInput {
pub removals: Vec<ExtensionRemovalRequest>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveBookmarksInput {
pub browser_id: String,
pub removals: Vec<BookmarkRemovalRequest>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExtensionRemovalRequest {
@@ -129,12 +136,25 @@ pub struct ExtensionRemovalRequest {
pub profile_ids: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BookmarkRemovalRequest {
pub url: String,
pub profile_ids: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveExtensionsResponse {
pub results: Vec<RemoveExtensionResult>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveBookmarksResponse {
pub results: Vec<RemoveBookmarkResult>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveExtensionResult {
@@ -145,6 +165,17 @@ pub struct RemoveExtensionResult {
pub error: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveBookmarkResult {
pub url: String,
pub profile_id: String,
pub removed_count: usize,
pub removed_files: Vec<String>,
pub skipped_files: Vec<String>,
pub error: Option<String>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AssociatedProfileSummary {