fix: address review issues for custom db path

This commit is contained in:
Sergey 2025-06-12 01:46:54 -04:00
parent e5e812310c
commit 88cbd4458a
4 changed files with 118 additions and 76 deletions

View File

@ -117,7 +117,9 @@ export default function CustomDatabaseLocationSettings() {
} catch (error) { } catch (error) {
console.error('Failed to create pastebar-data directory:', error) console.error('Failed to create pastebar-data directory:', error)
setOperationError( setOperationError(
t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' }) t('Failed to create directory. Please check permissions and try again.', {
ns: 'settings',
})
) )
return return
} }
@ -201,6 +203,12 @@ export default function CustomDatabaseLocationSettings() {
await applyCustomDbPath(selectedPathForChangeDialog, dbOperationForChangeDialog) await applyCustomDbPath(selectedPathForChangeDialog, dbOperationForChangeDialog)
relaunchApp() relaunchApp()
} catch (error: any) { } catch (error: any) {
// Attempt rollback on failure
try {
await revertToDefaultDbPath()
} catch (_) {
// Ignore rollback errors
}
setOperationError( setOperationError(
error.message || error.message ||
t('Failed to apply custom database location.', { ns: 'settings' }) t('Failed to apply custom database location.', { ns: 'settings' })
@ -287,7 +295,9 @@ export default function CustomDatabaseLocationSettings() {
} catch (error) { } catch (error) {
console.error('Failed to create pastebar-data directory:', error) console.error('Failed to create pastebar-data directory:', error)
setOperationError( setOperationError(
t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' }) t('Failed to create directory. Please check permissions and try again.', {
ns: 'settings',
})
) )
return return
} }
@ -317,9 +327,7 @@ export default function CustomDatabaseLocationSettings() {
// Handle applying the initial setup // Handle applying the initial setup
const handleApplySetup = async () => { const handleApplySetup = async () => {
if (!selectedPathForChangeDialog) { if (!selectedPathForChangeDialog) {
setOperationError( setOperationError(t('Please select a directory first.', { ns: 'settings' }))
t('Please select a directory first.', { ns: 'settings' })
)
return return
} }
setIsApplyingChange(true) setIsApplyingChange(true)
@ -367,6 +375,9 @@ export default function CustomDatabaseLocationSettings() {
await applyCustomDbPath(selectedPathForChangeDialog, dbOperationForChangeDialog) await applyCustomDbPath(selectedPathForChangeDialog, dbOperationForChangeDialog)
relaunchApp() relaunchApp()
} catch (error: any) { } catch (error: any) {
try {
await revertToDefaultDbPath()
} catch (_) {}
setOperationError( setOperationError(
error.message || error.message ||
t('Failed to apply custom database location.', { ns: 'settings' }) t('Failed to apply custom database location.', { ns: 'settings' })
@ -424,7 +435,9 @@ export default function CustomDatabaseLocationSettings() {
} catch (error) { } catch (error) {
console.error('Failed to create pastebar-data directory:', error) console.error('Failed to create pastebar-data directory:', error)
setOperationError( setOperationError(
t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' }) t('Failed to create directory. Please check permissions and try again.', {
ns: 'settings',
})
) )
setIsProcessing(false) setIsProcessing(false)
return false return false
@ -462,9 +475,19 @@ export default function CustomDatabaseLocationSettings() {
) )
if (confirmed) { if (confirmed) {
await applyCustomDbPath(finalPath, 'none') // 'none' for initial setup try {
relaunchApp() await applyCustomDbPath(finalPath, 'none') // 'none' for initial setup
pathSuccessfullySet = true // Path will be set by store, app restarts relaunchApp()
pathSuccessfullySet = true // Path will be set by store, app restarts
} catch (error) {
try {
await revertToDefaultDbPath()
} catch (_) {}
setOperationError(
(error as any).message ||
t('Failed to apply custom database location.', { ns: 'settings' })
)
}
} }
} }
} catch (error) { } catch (error) {
@ -506,7 +529,11 @@ export default function CustomDatabaseLocationSettings() {
return ( return (
<Box className="animate-in fade-in max-w-xl mt-4"> <Box className="animate-in fade-in max-w-xl mt-4">
<Card <Card
className={`${!customDbPath && !isSetupSectionExpanded ? 'opacity-80 bg-gray-100 dark:bg-gray-900/80' : ''}`} className={`${
!customDbPath && !isSetupSectionExpanded
? 'opacity-80 bg-gray-100 dark:bg-gray-900/80'
: ''
}`}
> >
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
<CardTitle className="animate-in fade-in text-md font-medium"> <CardTitle className="animate-in fade-in text-md font-medium">
@ -569,7 +596,7 @@ export default function CustomDatabaseLocationSettings() {
) : null} ) : null}
{t('Change Custom Data Folder...', { ns: 'settings' })} {t('Change Custom Data Folder...', { ns: 'settings' })}
</Button> </Button>
<Button <Button
onClick={handleRevertFromContent} onClick={handleRevertFromContent}
disabled={isLoading} disabled={isLoading}
@ -685,10 +712,9 @@ export default function CustomDatabaseLocationSettings() {
{isProcessing && !isApplyingChange ? ( {isProcessing && !isApplyingChange ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null} ) : null}
{selectedPathForChangeDialog {selectedPathForChangeDialog
? t('Change Selected Folder...', { ns: 'settings' }) ? t('Change Selected Folder...', { ns: 'settings' })
: t('Select Data Folder...', { ns: 'settings' }) : t('Select Data Folder...', { ns: 'settings' })}
}
</Button> </Button>
{selectedPathForChangeDialog && ( {selectedPathForChangeDialog && (

View File

@ -15,6 +15,22 @@ use crate::services::user_settings_service::{
use fs_extra::dir::{copy, CopyOptions}; use fs_extra::dir::{copy, CopyOptions};
use std::path::PathBuf; use std::path::PathBuf;
fn rollback_moves(items: &[(PathBuf, PathBuf)]) {
for (src, dest) in items.iter().rev() {
if dest.exists() {
if dest.is_dir() {
let mut options = CopyOptions::new();
options.overwrite = true;
let _ = copy(dest, src, &options);
let _ = fs::remove_dir_all(dest);
} else {
let _ = fs::copy(dest, src);
let _ = fs::remove_file(dest);
}
}
}
}
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub enum PathStatus { pub enum PathStatus {
Empty, Empty,
@ -50,9 +66,11 @@ pub fn cmd_check_custom_data_path(path_str: String) -> Result<PathStatus, String
let pastebar_data_subfolder = path.join("pastebar-data"); let pastebar_data_subfolder = path.join("pastebar-data");
if pastebar_data_subfolder.exists() && pastebar_data_subfolder.is_dir() { if pastebar_data_subfolder.exists() && pastebar_data_subfolder.is_dir() {
// Check if the pastebar-data subfolder contains database files // Check if the pastebar-data subfolder contains database files
let has_dev_db_in_subfolder = pastebar_data_subfolder.join("local.pastebar-db.data").exists(); let has_dev_db_in_subfolder = pastebar_data_subfolder
.join("local.pastebar-db.data")
.exists();
let has_prod_db_in_subfolder = pastebar_data_subfolder.join("pastebar-db.data").exists(); let has_prod_db_in_subfolder = pastebar_data_subfolder.join("pastebar-db.data").exists();
if has_dev_db_in_subfolder || has_prod_db_in_subfolder { if has_dev_db_in_subfolder || has_prod_db_in_subfolder {
return Ok(PathStatus::IsPastebarDataAndNotEmpty); return Ok(PathStatus::IsPastebarDataAndNotEmpty);
} else { } else {
@ -80,51 +98,33 @@ pub fn cmd_get_custom_db_path() -> Option<String> {
#[command] #[command]
pub fn cmd_create_directory(path_str: String) -> Result<(), String> { pub fn cmd_create_directory(path_str: String) -> Result<(), String> {
let path = Path::new(&path_str); let path = Path::new(&path_str);
fs::create_dir_all(&path).map_err(|e| { fs::create_dir_all(&path)
format!( .map_err(|e| format!("Failed to create directory {}: {}", path.display(), e))?;
"Failed to create directory {}: {}",
path.display(),
e
)
})?;
Ok(()) Ok(())
} }
/// Validates if the provided path is a writable directory. /// Validates if the provided path is a writable directory.
#[command] #[command]
pub fn cmd_validate_custom_db_path(path_str: String) -> Result<bool, String> { pub fn cmd_validate_custom_db_path(path_str: String) -> Result<bool, String> {
let path = Path::new(&path_str); let input_path = PathBuf::from(&path_str);
if !path.exists() {
// Attempt to create it if it doesn't exist, to check writability of parent if input_path
if let Some(parent) = path.parent() { .components()
if !parent.exists() { .any(|c| matches!(c, std::path::Component::ParentDir))
fs::create_dir_all(parent).map_err(|e| { {
format!( return Err("Path traversal not allowed".to_string());
"Failed to create parent directory {}: {}", }
parent.display(),
e let path = if input_path.exists() {
) input_path
})?; .canonicalize()
} .map_err(|e| format!("Invalid path: {}", e))?
} } else {
// Check if we can create the directory itself (simulates future db file creation in this dir) input_path
fs::create_dir_all(&path).map_err(|e| { };
format!(
"Path {} is not a valid directory or cannot be created: {}", if path.exists() && !path.is_dir() {
path.display(), return Err(format!("Path {} is not a directory.", path.display()));
e
)
})?;
// Clean up by removing the directory if we created it for validation
fs::remove_dir(&path).map_err(|e| {
format!(
"Failed to clean up validation directory {}: {}",
path.display(),
e
)
})?;
} else if !path.is_dir() {
return Err(format!("Path {} is not a directory.", path_str));
} }
// Check writability by trying to create a temporary file // Check writability by trying to create a temporary file
@ -153,6 +153,8 @@ pub fn cmd_set_and_relocate_data(
let items_to_relocate = vec!["pastebar-db.data", "clip-images", "clipboard-images"]; let items_to_relocate = vec!["pastebar-db.data", "clip-images", "clipboard-images"];
let mut moved_items: Vec<(PathBuf, PathBuf)> = Vec::new();
for item_name in items_to_relocate { for item_name in items_to_relocate {
let source_path = current_data_dir.join(item_name); let source_path = current_data_dir.join(item_name);
let dest_path = new_data_dir.join(item_name); let dest_path = new_data_dir.join(item_name);
@ -168,24 +170,24 @@ pub fn cmd_set_and_relocate_data(
match operation.as_str() { match operation.as_str() {
"move" => { "move" => {
if source_path.is_dir() { if source_path.is_dir() {
fs::rename(&source_path, &dest_path).map_err(|e| { let mut options = CopyOptions::new();
format!( options.overwrite = true;
"Failed to move directory {} to {}: {}", copy(&source_path, &dest_path, &options)
source_path.display(), .map_err(|e| format!("Failed to copy directory: {}", e))?;
dest_path.display(), if let Err(e) = fs::remove_dir_all(&source_path) {
e let _ = fs_extra::dir::remove(&dest_path);
) rollback_moves(&moved_items);
})?; return Err(format!("Failed to remove original directory: {}", e));
}
} else { } else {
fs::rename(&source_path, &dest_path).map_err(|e| { fs::copy(&source_path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
format!( if let Err(e) = fs::remove_file(&source_path) {
"Failed to move file {} to {}: {}", let _ = fs::remove_file(&dest_path);
source_path.display(), rollback_moves(&moved_items);
dest_path.display(), return Err(format!("Failed to remove original file: {}", e));
e }
)
})?;
} }
moved_items.push((source_path.clone(), dest_path.clone()));
} }
"copy" => { "copy" => {
if source_path.is_dir() { if source_path.is_dir() {
@ -205,6 +207,7 @@ pub fn cmd_set_and_relocate_data(
} }
user_settings_service::set_custom_db_path(&new_parent_dir_path)?; user_settings_service::set_custom_db_path(&new_parent_dir_path)?;
crate::db::reinitialize_connection_pool();
Ok(format!( Ok(format!(
"Data successfully {} to {}. Please restart the application.", "Data successfully {} to {}. Please restart the application.",
@ -218,6 +221,7 @@ pub fn cmd_set_and_relocate_data(
pub fn cmd_revert_to_default_data_location() -> Result<String, String> { pub fn cmd_revert_to_default_data_location() -> Result<String, String> {
// Simply remove the custom database path setting // Simply remove the custom database path setting
remove_custom_db_path()?; remove_custom_db_path()?;
crate::db::reinitialize_connection_pool();
Ok("Custom database location setting removed successfully.".to_string()) Ok("Custom database location setting removed successfully.".to_string())
} }

View File

@ -4,6 +4,7 @@ use serde::Serialize;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::RwLock;
use std::time::Duration; use std::time::Duration;
use diesel::connection::SimpleConnection; use diesel::connection::SimpleConnection;
@ -39,7 +40,7 @@ pub struct ConnectionOptions {
} }
lazy_static! { lazy_static! {
pub static ref DB_POOL_CONNECTION: Pool = init_connection_pool(); pub static ref DB_POOL_CONNECTION: RwLock<Pool> = RwLock::new(init_connection_pool());
} }
impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error> impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
@ -92,6 +93,12 @@ fn init_connection_pool() -> Pool {
.expect("Failed to create db pool.") .expect("Failed to create db pool.")
} }
pub fn reinitialize_connection_pool() {
let new_pool = init_connection_pool();
let mut pool_lock = DB_POOL_CONNECTION.write().unwrap();
*pool_lock = new_pool;
}
pub fn init(app: &mut tauri::App) { pub fn init(app: &mut tauri::App) {
let config = app.config().clone(); let config = app.config().clone();
@ -182,6 +189,7 @@ pub fn establish_pool_db_connection(
}); });
DB_POOL_CONNECTION DB_POOL_CONNECTION
.read()
.get() .get()
.unwrap_or_else(|_| panic!("Error connecting to db pool")) .unwrap_or_else(|_| panic!("Error connecting to db pool"))
} }
@ -268,10 +276,11 @@ pub fn get_default_db_path_string() -> String {
pub fn to_relative_image_path(absolute_path: &str) -> String { pub fn to_relative_image_path(absolute_path: &str) -> String {
let data_dir = get_data_dir(); let data_dir = get_data_dir();
let data_dir_str = data_dir.to_string_lossy(); let data_dir_str = data_dir.to_string_lossy();
if absolute_path.starts_with(&data_dir_str.as_ref()) { if absolute_path.starts_with(&data_dir_str.as_ref()) {
// Remove the data directory prefix and replace with placeholder // Remove the data directory prefix and replace with placeholder
let relative_path = absolute_path.strip_prefix(&data_dir_str.as_ref()) let relative_path = absolute_path
.strip_prefix(&data_dir_str.as_ref())
.unwrap_or(absolute_path) .unwrap_or(absolute_path)
.trim_start_matches('/') .trim_start_matches('/')
.trim_start_matches('\\'); .trim_start_matches('\\');
@ -291,7 +300,10 @@ pub fn to_absolute_image_path(relative_path: &str) -> String {
.unwrap_or(relative_path) .unwrap_or(relative_path)
.trim_start_matches('/') .trim_start_matches('/')
.trim_start_matches('\\'); .trim_start_matches('\\');
data_dir.join(path_without_placeholder).to_string_lossy().into_owned() data_dir
.join(path_without_placeholder)
.to_string_lossy()
.into_owned()
} else { } else {
// If path doesn't have placeholder, return as is // If path doesn't have placeholder, return as is
relative_path.to_string() relative_path.to_string()

View File

@ -39,7 +39,7 @@ pub fn insert_or_update_setting_by_name(
setting: &Setting, setting: &Setting,
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
) -> Result<String, Error> { ) -> Result<String, Error> {
let connection = &mut DB_POOL_CONNECTION.get().unwrap(); let connection = &mut DB_POOL_CONNECTION.read().unwrap().get().unwrap();
match settings match settings
.filter(name.eq(&setting.name)) .filter(name.eq(&setting.name))