Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.

This commit is contained in:
google-labs-jules[bot] 2025-06-12 05:56:43 +00:00
parent e5e812310c
commit 3913ecead1
3 changed files with 491 additions and 318 deletions

View File

@ -19,6 +19,72 @@ import {
TextNormal,
} from '~/components/ui'
// Define PathStatus enum to match backend
type PathStatusResponse = 'Empty' | 'NotEmpty' | 'IsPastebarDataAndNotEmpty' | 'HasPastebarDataSubfolder';
interface SelectPathResult {
status: 'selected' | 'cancelled' | 'error';
path?: string; // The final, potentially adjusted path
error?: string; // Error message specifically from the selection/path adjustment process
}
// Reusable function for path selection and initial processing logic
async function selectAndProcessPath(
t: ReturnType<typeof useTranslation>['t'],
): Promise<SelectPathResult> {
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
});
if (typeof selected === 'string') {
let finalPath = selected;
// Type assertion for status from invoke
const status: PathStatusResponse = await invoke('cmd_check_custom_data_path', { pathStr: selected });
if (status === 'HasPastebarDataSubfolder') {
finalPath = await join(selected, 'pastebar-data');
await dialog.message(
t('Found existing "pastebar-data" folder. The application will use this folder to store data.', { ns: 'settings' })
);
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t('The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?', { ns: 'settings' })
);
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data');
try {
await invoke('cmd_create_directory', { pathStr: finalPath });
} catch (dirError: any) {
console.error('Failed to create pastebar-data directory:', dirError);
return { status: 'error', error: t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' }) };
}
} else {
return { status: 'cancelled' }; // User cancelled subfolder creation
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t('This folder already contains PasteBar data. The application will use this existing data after restart.', { ns: 'settings' })
);
}
// `Empty` status requires no special handling here for path adjustment.
return { status: 'selected', path: finalPath };
} else {
// User cancelled the dialog
return { status: 'cancelled' };
}
} catch (err: any) {
console.error('Error during path selection and processing:', err);
// Check if error is a string or has a message property
const errorMessage = typeof err === 'string' ? err : err?.message || t('An error occurred during directory processing.', { ns: 'settings' });
return { status: 'error', error: errorMessage };
}
}
export default function CustomDatabaseLocationSettings() {
const { t } = useTranslation()
const {
@ -38,7 +104,7 @@ export default function CustomDatabaseLocationSettings() {
const [dbOperationForChangeDialog, setDbOperationForChangeDialog] = useState<
'copy' | 'none'
>('none')
const [isApplyingChange, setIsApplyingChange] = useState(false)
// Removed isApplyingChange, will use isProcessing for this purpose.
// isRevertingPath state is effectively handled by isProcessing when called from CardContent's revert button
// General states
@ -81,69 +147,19 @@ export default function CustomDatabaseLocationSettings() {
const handleBrowseForChangeDialog = async () => {
setOperationError(null)
setValidationErrorForChangeDialog(null)
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
})
if (typeof selected === 'string') {
const status: any = await invoke('cmd_check_custom_data_path', {
pathStr: selected,
})
let finalPath = selected
if (status === 'HasPastebarDataSubfolder') {
// Use existing pastebar-data subfolder
finalPath = await join(selected, 'pastebar-data')
await dialog.message(
t(
'Found existing "pastebar-data" folder. The application will use this folder to store data.',
{ ns: 'settings' }
)
)
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t(
'The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?',
{ ns: 'settings' }
)
)
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data')
// Create the directory after user confirmation
try {
await invoke('cmd_create_directory', { pathStr: finalPath })
} catch (error) {
console.error('Failed to create pastebar-data directory:', error)
setOperationError(
t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' })
)
return
}
} else {
return
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t(
'This folder already contains PasteBar data. The application will use this existing data after restart.',
{ ns: 'settings' }
)
)
}
const result = await selectAndProcessPath(t);
setSelectedPathForChangeDialog(finalPath)
if (finalPath !== customDbPath) {
await validateCustomDbPath(finalPath) // Validation result will be reflected in storeCustomDbPathError
}
if (result.status === 'selected' && result.path) {
setSelectedPathForChangeDialog(result.path);
if (result.path !== customDbPath) {
// Trigger validation, result will be reflected in storeCustomDbPathError via useEffect
await validateCustomDbPath(result.path);
}
} catch (error) {
console.error('Error handling directory selection for change:', error)
setOperationError(
t('An error occurred during directory processing.', { ns: 'settings' })
)
} else if (result.status === 'error') {
setOperationError(result.error || t('An error occurred during directory processing.', { ns: 'settings' }));
}
// If 'cancelled', do nothing
}
// Renamed from handleApply, used by "Apply and Restart" button in CardContent
@ -156,7 +172,7 @@ export default function CustomDatabaseLocationSettings() {
)
return
}
setIsApplyingChange(true)
// setIsApplyingChange(true) // Removed
setIsProcessing(true) // General processing state
setOperationError(null)
setValidationErrorForChangeDialog(null)
@ -170,7 +186,7 @@ export default function CustomDatabaseLocationSettings() {
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
)
setIsApplyingChange(false)
// setIsApplyingChange(false) // Removed
setIsProcessing(false)
return
}
@ -206,11 +222,11 @@ export default function CustomDatabaseLocationSettings() {
t('Failed to apply custom database location.', { ns: 'settings' })
)
} finally {
setIsApplyingChange(false)
// setIsApplyingChange(false) // Removed
setIsProcessing(false)
}
} else {
setIsApplyingChange(false)
// setIsApplyingChange(false) // Removed
setIsProcessing(false)
}
}
@ -251,67 +267,17 @@ export default function CustomDatabaseLocationSettings() {
const handleSetupPathSelection = async () => {
setOperationError(null)
setValidationErrorForChangeDialog(null)
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
})
if (typeof selected === 'string') {
const status: any = await invoke('cmd_check_custom_data_path', {
pathStr: selected,
})
let finalPath = selected
if (status === 'HasPastebarDataSubfolder') {
// Use existing pastebar-data subfolder
finalPath = await join(selected, 'pastebar-data')
await dialog.message(
t(
'Found existing "pastebar-data" folder. The application will use this folder to store data.',
{ ns: 'settings' }
)
)
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t(
'The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?',
{ ns: 'settings' }
)
)
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data')
// Create the directory after user confirmation
try {
await invoke('cmd_create_directory', { pathStr: finalPath })
} catch (error) {
console.error('Failed to create pastebar-data directory:', error)
setOperationError(
t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' })
)
return
}
} else {
return
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t(
'This folder already contains PasteBar data. The application will use this existing data after restart.',
{ ns: 'settings' }
)
)
}
const result = await selectAndProcessPath(t);
setSelectedPathForChangeDialog(finalPath)
await validateCustomDbPath(finalPath) // Validation result will be reflected in storeCustomDbPathError
}
} catch (error) {
console.error('Error handling directory selection for setup:', error)
setOperationError(
t('An error occurred during directory processing.', { ns: 'settings' })
)
if (result.status === 'selected' && result.path) {
setSelectedPathForChangeDialog(result.path);
// Trigger validation, result will be reflected in storeCustomDbPathError via useEffect
await validateCustomDbPath(result.path);
} else if (result.status === 'error') {
setOperationError(result.error || t('An error occurred during directory processing.', { ns: 'settings' }));
}
// If 'cancelled', do nothing
}
// Handle applying the initial setup
@ -322,7 +288,7 @@ export default function CustomDatabaseLocationSettings() {
)
return
}
setIsApplyingChange(true)
// setIsApplyingChange(true) // Removed
setIsProcessing(true)
setOperationError(null)
setValidationErrorForChangeDialog(null)
@ -336,7 +302,7 @@ export default function CustomDatabaseLocationSettings() {
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
)
setIsApplyingChange(false)
// setIsApplyingChange(false) // Removed
setIsProcessing(false)
return
}
@ -372,11 +338,11 @@ export default function CustomDatabaseLocationSettings() {
t('Failed to apply custom database location.', { ns: 'settings' })
)
} finally {
setIsApplyingChange(false)
// setIsApplyingChange(false) // Removed
setIsProcessing(false)
}
} else {
setIsApplyingChange(false)
// setIsApplyingChange(false) // Removed
setIsProcessing(false)
}
}
@ -385,99 +351,55 @@ export default function CustomDatabaseLocationSettings() {
const chooseAndSetCustomPath = async () => {
setOperationError(null)
setIsProcessing(true)
let pathSuccessfullySet = false
let pathSuccessfullySet = false // This seems to be for the toggle's return value
setIsProcessing(true) // Keep this for the overall operation
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
})
const selectionResult = await selectAndProcessPath(t);
if (typeof selected === 'string') {
let finalPath = selected
const status: any = await invoke('cmd_check_custom_data_path', {
pathStr: selected,
})
if (selectionResult.status === 'selected' && selectionResult.path) {
const finalPath = selectionResult.path;
// Path selected and processed, now proceed with validation and confirmation specific to this flow
await validateCustomDbPath(finalPath);
const currentStoreState = settingsStore.getState();
if (status === 'HasPastebarDataSubfolder') {
// Use existing pastebar-data subfolder
finalPath = await join(selected, 'pastebar-data')
await dialog.message(
t(
'Found existing "pastebar-data" folder. The application will use this folder to store data.',
{ ns: 'settings' }
)
)
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t(
'The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?',
{ ns: 'settings' }
)
)
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data')
// Create the directory after user confirmation
try {
await invoke('cmd_create_directory', { pathStr: finalPath })
} catch (error) {
console.error('Failed to create pastebar-data directory:', error)
setOperationError(
t('Failed to create directory. Please check permissions and try again.', { ns: 'settings' })
)
setIsProcessing(false)
return false
}
} else {
setIsProcessing(false)
return false // User cancelled subfolder creation
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t(
'This folder already contains PasteBar data. The application will use this existing data after restart.',
{ ns: 'settings' }
)
)
}
await validateCustomDbPath(finalPath)
const currentStoreState = settingsStore.getState()
if (!currentStoreState.isCustomDbPathValid) {
setOperationError(
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
)
setIsProcessing(false)
return false // Validation failed
}
const confirmed = await dialog.confirm(
t(
'Are you sure you want to set "{{path}}" as the new data folder? The application will restart.',
{ ns: 'settings', path: finalPath }
)
)
if (confirmed) {
await applyCustomDbPath(finalPath, 'none') // 'none' for initial setup
relaunchApp()
pathSuccessfullySet = true // Path will be set by store, app restarts
}
if (!currentStoreState.isCustomDbPathValid) {
setOperationError(
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
);
setIsProcessing(false); // Stop processing as validation failed
return false;
}
} catch (error) {
console.error('Error choosing custom DB path:', error)
setOperationError(
t('An error occurred during directory selection or setup.', {
ns: 'settings',
})
)
} finally {
setIsProcessing(false)
const confirmed = await dialog.confirm(
t(
'Are you sure you want to set "{{path}}" as the new data folder? The application will restart.',
{ ns: 'settings', path: finalPath }
)
);
if (confirmed) {
try {
await applyCustomDbPath(finalPath, 'none'); // 'none' for initial setup
relaunchApp();
pathSuccessfullySet = true;
} catch (applyError: any) {
setOperationError(
applyError.message ||
t('Failed to apply custom database location.', { ns: 'settings' })
);
// Path selection was ok, but apply failed.
}
} else {
// User cancelled confirmation
}
} else if (selectionResult.status === 'error') {
setOperationError(selectionResult.error || t('An error occurred during directory processing.', { ns: 'settings' }));
}
return pathSuccessfullySet // This return might not be directly used if app restarts
// If 'cancelled', do nothing more for path selection part
setIsProcessing(false); // End processing for the whole operation
return pathSuccessfullySet;
}
const handleToggle = async (checked: boolean) => {
@ -494,7 +416,7 @@ export default function CustomDatabaseLocationSettings() {
// If customDbPath is already set, the toggle is disabled so this won't be called
}
const isLoading = dbRelocationInProgress || isProcessing || isApplyingChange
const isLoading = dbRelocationInProgress || isProcessing // Simplified isLoading
const currentPathDisplay = customDbPath || t('Default', { ns: 'settings' })
const isPathUnchangedForChangeDialog = selectedPathForChangeDialog === customDbPath
@ -564,9 +486,7 @@ export default function CustomDatabaseLocationSettings() {
variant="outline"
className="flex-1 h-10"
>
{dbRelocationInProgress && !isApplyingChange && !isProcessing ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{/* Spinner removed from here, button will be disabled by isLoading if another operation is in progress */}
{t('Change Custom Data Folder...', { ns: 'settings' })}
</Button>
@ -576,7 +496,7 @@ export default function CustomDatabaseLocationSettings() {
variant="secondary"
className="flex-1 h-10 bg-yellow-500 hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white border-yellow-500 dark:border-yellow-600"
>
{isProcessing && !isApplyingChange ? (
{isProcessing ? ( // Simplified spinner condition
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Revert to Default', { ns: 'settings' })}
@ -634,7 +554,7 @@ export default function CustomDatabaseLocationSettings() {
}
className="w-full h-10"
>
{isApplyingChange ? (
{isProcessing ? ( // Simplified spinner condition
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Apply and Restart', { ns: 'settings' })}
@ -682,9 +602,7 @@ export default function CustomDatabaseLocationSettings() {
variant="outline"
className="w-full h-10"
>
{isProcessing && !isApplyingChange ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{/* Spinner removed from here, button will be disabled by isLoading if another operation is in progress */}
{selectedPathForChangeDialog
? t('Change Selected Folder...', { ns: 'settings' })
: t('Select Data Folder...', { ns: 'settings' })
@ -741,7 +659,7 @@ export default function CustomDatabaseLocationSettings() {
}
className="w-full h-10"
>
{isApplyingChange ? (
{isProcessing ? ( // Simplified spinner condition
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Apply and Restart', { ns: 'settings' })}

View File

@ -90,53 +90,131 @@ pub fn cmd_create_directory(path_str: String) -> Result<(), String> {
Ok(())
}
/// Validates if the provided path is a writable directory.
/// Validates if the provided path is suitable for a custom database location.
/// Checks for path traversal, parent directory writability (if path doesn't exist),
/// or target directory writability (if path exists).
#[command]
pub fn cmd_validate_custom_db_path(path_str: String) -> Result<bool, String> {
let path = Path::new(&path_str);
if !path.exists() {
// Attempt to create it if it doesn't exist, to check writability of parent
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
format!(
"Failed to create parent directory {}: {}",
parent.display(),
e
)
})?;
}
let input_path_buf = PathBuf::from(&path_str);
// Initial security check for '..' components in the raw path string.
for component in input_path_buf.components() {
if component == std::path::Component::ParentDir {
return Err(format!(
"Path {} contains '..' and is considered unsafe.",
path_str
));
}
if component.as_os_str() == ".." {
return Err(format!(
"Path {} contains '..' component name and is considered unsafe.",
path_str
));
}
// Check if we can create the directory itself (simulates future db file creation in this dir)
fs::create_dir_all(&path).map_err(|e| {
format!(
"Path {} is not a valid directory or cannot be created: {}",
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
let temp_file_path = path.join(".tmp_pastebar_writable_check");
let path_to_check_writability: PathBuf;
let type_of_check: &str; // "target directory" or "parent directory"
if input_path_buf.exists() {
if !input_path_buf.is_dir() {
return Err(format!(
"Path {} exists but is not a directory.",
path_str
));
}
path_to_check_writability = input_path_buf.clone();
type_of_check = "target directory";
} else {
// Path does not exist, check parent directory.
let parent_dir = input_path_buf.parent().ok_or_else(|| {
format!(
"Cannot get parent directory for path {}. Please provide a valid path.",
path_str
)
})?;
if !parent_dir.exists() {
return Err(format!(
"Parent directory {} does not exist. Please create it first.",
parent_dir.display()
));
}
if !parent_dir.is_dir() {
return Err(format!(
"Parent path {} is not a directory.",
parent_dir.display()
));
}
path_to_check_writability = parent_dir.to_path_buf();
type_of_check = "parent directory";
}
// Perform writability check by creating a temporary file in path_to_check_writability
let temp_file_name = format!(".tmp_pastebar_writable_check_{}", std::process::id());
let temp_file_path = path_to_check_writability.join(&temp_file_name);
match fs::File::create(&temp_file_path) {
Ok(_) => {
fs::remove_file(&temp_file_path)
.map_err(|e| format!("Failed to remove temporary check file: {}", e))?;
Ok(true)
.map_err(|e| format!("Failed to remove temporary check file {}: {}", temp_file_path.display(), e))?;
}
Err(e) => {
return Err(format!(
"The {} {} is not writable: {}",
type_of_check,
path_to_check_writability.display(),
e
));
}
Err(e) => Err(format!("Directory {} is not writable: {}", path_str, e)),
}
// Canonicalize the path
// If input_path_buf exists, canonicalize it directly.
// If not, canonicalize its existing parent and append the intended filename/dirname.
let canonical_path: PathBuf;
if input_path_buf.exists() {
canonical_path = fs::canonicalize(&input_path_buf).map_err(|e| {
format!(
"Failed to canonicalize existing path {}: {}",
input_path_buf.display(),
e
)
})?;
} else {
// Parent directory is confirmed to exist and be a directory from checks above.
let parent_dir = input_path_buf.parent().unwrap(); // Safe due to earlier checks
let file_name = input_path_buf.file_name().ok_or_else(|| {
format!("Path {} does not have a file/directory name.", path_str)
})?;
let canonical_parent = fs::canonicalize(parent_dir).map_err(|e| {
format!(
"Failed to canonicalize parent directory {}: {}",
parent_dir.display(),
e
)
})?;
canonical_path = canonical_parent.join(file_name);
}
// After canonicalization, re-check for '..' components.
for component in canonical_path.components() {
if component == std::path::Component::ParentDir {
return Err(format!(
"Canonicalized path {} still contains '..' component, which is unsafe.",
canonical_path.display()
));
}
if component.as_os_str() == ".." {
return Err(format!(
"Canonicalized path {} still contains '..' name component, which is unsafe.",
canonical_path.display()
));
}
}
Ok(true)
}
/// Sets the custom data path, and moves/copies the data directory.
@ -146,55 +224,123 @@ pub fn cmd_set_and_relocate_data(
operation: String,
) -> Result<String, String> {
let current_data_dir = get_data_dir();
// new_data_dir is the new PARENT directory where items like 'pastebar-db.data' will reside.
let new_data_dir = PathBuf::from(&new_parent_dir_path);
fs::create_dir_all(&new_data_dir)
.map_err(|e| format!("Failed to create new data directory: {}", e))?;
.map_err(|e| format!("Failed to create new data directory parent {}: {}", new_data_dir.display(), e))?;
let items_to_relocate = vec!["pastebar-db.data", "clip-images", "clipboard-images"];
let mut backup_dir_path_opt: Option<PathBuf> = None;
for item_name in items_to_relocate {
if operation == "move" {
let backup_base_name = current_data_dir.file_name().unwrap_or_default().to_str().unwrap_or("pastebar-data");
// Using a fixed suffix for simplicity, a timestamp would be better for multiple quick retries.
let backup_folder_name_str = format!("{}-backup-migration", backup_base_name);
let backup_dir = current_data_dir.with_file_name(backup_folder_name_str);
if backup_dir.exists() {
fs::remove_dir_all(&backup_dir)
.map_err(|e| format!("Failed to remove existing backup directory {}: {}", backup_dir.display(), e))?;
}
fs::create_dir_all(&backup_dir)
.map_err(|e| format!("Failed to create backup directory {}: {}", backup_dir.display(), e))?;
backup_dir_path_opt = Some(backup_dir.clone()); // Save for later use
println!("Backing up data to {}", backup_dir.display());
for item_name in &items_to_relocate {
let source_item_path = current_data_dir.join(item_name);
if source_item_path.exists() {
let backup_item_path = backup_dir.join(item_name);
let copy_options = CopyOptions { overwrite: true, ..Default::default() };
if source_item_path.is_dir() {
copy(&source_item_path, &backup_item_path, &copy_options).map_err(|e| {
// Attempt to clean up partial backup before erroring
let _ = fs::remove_dir_all(&backup_dir);
format!("Backup failed for directory {}: {}", source_item_path.display(), e)
})?;
} else {
fs::copy(&source_item_path, &backup_item_path).map_err(|e| {
// Attempt to clean up partial backup before erroring
let _ = fs::remove_dir_all(&backup_dir);
format!("Backup failed for file {}: {}", source_item_path.display(), e)
})?;
}
}
}
println!("Backup completed to {}", backup_dir.display());
}
let mut moved_items_dest_paths: Vec<PathBuf> = Vec::new();
for item_name_str in items_to_relocate {
let item_name = Path::new(item_name_str);
let source_path = current_data_dir.join(item_name);
let dest_path = new_data_dir.join(item_name);
if !source_path.exists() {
println!(
"Source item {} does not exist, skipping.",
source_path.display()
);
println!("Source item {} does not exist, skipping.", source_path.display());
continue;
}
match operation.as_str() {
"move" => {
if source_path.is_dir() {
fs::rename(&source_path, &dest_path).map_err(|e| {
format!(
"Failed to move directory {} to {}: {}",
source_path.display(),
dest_path.display(),
e
)
})?;
} else {
fs::rename(&source_path, &dest_path).map_err(|e| {
format!(
"Failed to move file {} to {}: {}",
source_path.display(),
dest_path.display(),
e
)
})?;
let current_backup_dir = backup_dir_path_opt.as_ref().expect("Backup path should exist for move operation");
println!("Moving {} to {}", source_path.display(), dest_path.display());
if let Err(move_err) = fs::rename(&source_path, &dest_path) {
eprintln!("Error moving {}: {}. Attempting rollback.", source_path.display(), move_err);
// 1. Move back already moved items from dest_path to source_path
for moved_dest_path in moved_items_dest_paths.iter().rev() {
let original_item_name = moved_dest_path.file_name().unwrap();
let original_source_path = current_data_dir.join(original_item_name);
if moved_dest_path.exists() { // Ensure it's there before trying to move
if let Err(e) = fs::rename(moved_dest_path, &original_source_path) {
eprintln!("Rollback error: Failed to move {} back to {}: {}", moved_dest_path.display(), original_source_path.display(), e);
}
}
}
moved_items_dest_paths.clear();
// 2. Restore from backup
println!("Restoring from backup directory: {}", current_backup_dir.display());
for bk_item_name_str in &items_to_relocate {
let bk_item_name = Path::new(bk_item_name_str);
let backup_item_path = current_backup_dir.join(bk_item_name);
if backup_item_path.exists() {
let target_restore_path = current_data_dir.join(bk_item_name);
if target_restore_path.exists() {
if target_restore_path.is_dir() {
fs::remove_dir_all(&target_restore_path).unwrap_or_else(|e| eprintln!("Rollback: Failed to remove dir during restore {}: {}", target_restore_path.display(), e));
} else {
fs::remove_file(&target_restore_path).unwrap_or_else(|e| eprintln!("Rollback: Failed to remove file during restore {}: {}", target_restore_path.display(), e));
}
}
let copy_options = CopyOptions { overwrite: true, ..Default::default() };
if backup_item_path.is_dir() {
copy(&backup_item_path, &target_restore_path, &copy_options).unwrap_or_else(|e| {eprintln!("Rollback: Failed to copy dir from backup {}: {}", backup_item_path.display(), e); 0});
} else {
fs::copy(&backup_item_path, &target_restore_path).unwrap_or_else(|e| {eprintln!("Rollback: Failed to copy file from backup {}: {}", backup_item_path.display(), e); 0});
}
}
}
// 3. Clean up backup directory as restore was attempted
fs::remove_dir_all(&current_backup_dir).unwrap_or_else(|e| eprintln!("Rollback: Failed to remove backup dir {}: {}", current_backup_dir.display(), e));
return Err(format!("Failed to move {}: {}. Operation rolled back using backup.", source_path.display(), move_err));
}
moved_items_dest_paths.push(dest_path.clone());
}
"copy" => {
let copy_options = CopyOptions { overwrite: true, ..Default::default() };
if source_path.is_dir() {
let mut options = CopyOptions::new();
options.overwrite = true;
copy(&source_path, &dest_path, &options)
.map_err(|e| format!("Failed to copy directory: {}", e))?;
copy(&source_path, &dest_path, &copy_options)
.map_err(|e| format!("Failed to copy directory {} to {}: {}", source_path.display(), dest_path.display(), e))?;
} else {
fs::copy(&source_path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
fs::copy(&source_path, &dest_path)
.map_err(|e| format!("Failed to copy file {} to {}: {}", source_path.display(), dest_path.display(), e))?;
}
}
"none" => {
@ -204,12 +350,66 @@ pub fn cmd_set_and_relocate_data(
}
}
user_settings_service::set_custom_db_path(&new_parent_dir_path)?;
if let Err(settings_err) = user_settings_service::set_custom_db_path(&new_parent_dir_path) {
if operation == "move" {
if let Some(ref bk_dir) = backup_dir_path_opt {
eprintln!("CRITICAL: Failed to update settings ({}) after moving data. Attempting to roll back data move.", settings_err);
let mut rollback_successful = true;
for item_name_str in &items_to_relocate {
let item_name = Path::new(item_name_str);
let moved_item_path = new_data_dir.join(item_name);
if moved_item_path.exists() {
let original_source_path = current_data_dir.join(item_name);
// Ensure target for rollback is clean or doesn't exist if it's a file
if original_source_path.exists() && !original_source_path.is_dir() {
fs::remove_file(&original_source_path).unwrap_or_else(|e| eprintln!("Settings rollback: Failed to remove file at original source {}: {}", original_source_path.display(), e));
} else if original_source_path.exists() && original_source_path.is_dir() {
// For directories, rename expects target not to exist or to be empty on some platforms.
// If original source path (directory) exists and is not empty, rename might fail.
// This part of rollback might need to be more robust (e.g. clean restore from backup)
}
if let Err(e) = fs::rename(&moved_item_path, &original_source_path) {
eprintln!("Settings rollback: Could not move {} back to {}: {}", moved_item_path.display(), original_source_path.display(), e);
rollback_successful = false;
}
}
}
if rollback_successful {
println!("Settings rollback: Data relocation was successfully rolled back to {}.", current_data_dir.display());
fs::remove_dir_all(&bk_dir).unwrap_or_else(|e| eprintln!("Settings rollback: Failed to remove backup dir {}: {}", bk_dir.display(), e));
return Err(format!("Failed to update settings: {}. Data relocation was rolled back. Original data location is active.", settings_err));
} else {
return Err(format!(
"CRITICAL: Failed to update settings ({}) after moving data. Also failed to automatically roll back the data move. Data is currently in {}. A backup is available at {}. Please manually restore data to {} or update settings to point to the new location if appropriate.",
settings_err,
new_data_dir.display(),
bk_dir.display(),
current_data_dir.display()
));
}
}
}
// For "copy" or "none" operation, or if backup_dir_path_opt was None for "move" (which shouldn't happen)
return Err(format!("Data {} was successful to {}, but failed to update settings: {}. If data was copied, it resides in both locations. If data was moved, it is in the new location but the application is not yet configured to use it. Backup (if applicable) may still be present.", operation, new_data_dir.display(), settings_err));
}
// Success: Clean up backup (if "move")
if operation == "move" {
if let Some(bk_dir) = backup_dir_path_opt {
if bk_dir.exists() {
println!("Operation successful, removing backup directory: {}", bk_dir.display());
fs::remove_dir_all(&bk_dir)
.map_err(|e| format!("Move successful and settings updated, but failed to remove backup directory {}: {}", bk_dir.display(), e))?;
}
}
}
Ok(format!(
"Data successfully {} to {}. Please restart the application.",
operation,
new_data_dir.display()
new_data_dir.display() // This is the new parent directory
))
}

View File

@ -1,6 +1,6 @@
use lazy_static::lazy_static;
use once_cell::sync::OnceCell;
use serde::Serialize;
use std::sync::RwLock;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
@ -38,9 +38,8 @@ pub struct ConnectionOptions {
pub busy_timeout: Option<Duration>,
}
lazy_static! {
pub static ref DB_POOL_CONNECTION: Pool = init_connection_pool();
}
// Replaced lazy_static with RwLock for dynamic pool reinitialization
pub static DB_POOL_STATE: RwLock<Option<(String, Pool)>> = RwLock::new(None);
impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
for ConnectionOptions
@ -72,24 +71,74 @@ pub fn adjust_canonicalization<P: AsRef<Path>>(p: P) -> String {
}
}
fn init_connection_pool() -> Pool {
// debug only with simple sql logger set_default_instrumentation suports only on diesel master
// diesel::connection::set_default_instrumentation(simple_sql_logger);
let db_path = get_db_path();
// Renamed and modified to take db_path as argument
fn do_init_connection_pool(db_path_for_pool: &str) -> Pool {
debug_output(|| {
println!("Init pool database connection to: {}", db_path);
println!("Initializing new connection pool for: {}", db_path_for_pool);
});
let manager = diesel_r2d2::ConnectionManager::<SqliteConnection>::new(db_path);
let manager = diesel_r2d2::ConnectionManager::<SqliteConnection>::new(db_path_for_pool);
r2d2::Pool::builder()
.connection_customizer(Box::new(ConnectionOptions {
enable_wal: false,
enable_wal: false, // Consider making these configurable or consistent
enable_foreign_keys: false,
busy_timeout: Some(Duration::from_secs(3)),
}))
.build(manager)
.expect("Failed to create db pool.")
.expect("Failed to create db pool.") // Consider returning Result in future
}
// New function to explicitly reinitialize (or initialize for the first time)
pub fn reinitialize_db_pool() -> Result<(), String> {
let current_db_path = get_db_path();
let mut write_guard = DB_POOL_STATE.write().map_err(|e| format!("Failed to acquire write lock on DB_POOL_STATE: {}", e))?;
// If a pool exists and is for the current path, nothing to do.
if let Some((pool_path, _)) = &*write_guard {
if pool_path == &current_db_path {
debug_output(|| println!("DB pool already initialized for path: {}", current_db_path));
return Ok(());
}
}
println!("Reinitializing DB pool for path: {}", current_db_path);
let new_pool = do_init_connection_pool(&current_db_path);
*write_guard = Some((current_db_path, new_pool));
Ok(())
}
// New function to get a clone of the current pool
pub fn get_db_pool_cloned() -> Result<Pool, String> {
let read_guard = DB_POOL_STATE.read().map_err(|e| format!("Failed to acquire read lock on DB_POOL_STATE: {}", e))?;
if let Some((path, pool)) = &*read_guard {
// Check if the path of the current pool is still the active db path.
// This is a safeguard. `reinitialize_db_pool` should be the primary mechanism for updates.
let current_db_path = get_db_path();
if path == &current_db_path {
debug_output(|| println!("Cloning existing DB pool for path: {}", path));
return Ok(pool.clone());
} else {
// Path mismatch, means pool is stale. Needs reinitialization.
// This situation should ideally be avoided by calling reinitialize_db_pool proactively.
debug_output(|| println!("Pool path {} is stale (current is {}). Forcing reinitialization.", path, current_db_path));
// Fall through to reinitialize logic by releasing read lock and proceeding.
}
}
// Release the current read lock before attempting to acquire a write lock for reinitialization
drop(read_guard);
debug_output(|| println!("DB pool not initialized or stale. Attempting to reinitialize now."));
reinitialize_db_pool()?; // Initialize or reinitialize it
let read_guard_after_init = DB_POOL_STATE.read().map_err(|e| format!("Failed to acquire read lock after init: {}", e))?;
if let Some((_, pool)) = &*read_guard_after_init {
Ok(pool.clone())
} else {
// This should not happen if reinitialize_db_pool succeeded.
Err("Failed to get DB pool even after reinitialization attempt.".to_string())
}
}
pub fn init(app: &mut tauri::App) {
@ -160,6 +209,9 @@ pub fn init(app: &mut tauri::App) {
create_db_file();
}
// Initialize the DB pool after constants are set and db file potentially created
reinitialize_db_pool().expect("Failed to initialize DB pool at startup");
run_migrations();
}
@ -178,12 +230,10 @@ pub fn ensure_dir_exists(path: &PathBuf) {
pub fn establish_pool_db_connection(
) -> diesel_r2d2::PooledConnection<diesel_r2d2::ConnectionManager<SqliteConnection>> {
debug_output(|| {
println!("Connecting to db pool");
println!("Establishing connection from DB pool.");
});
DB_POOL_CONNECTION
.get()
.unwrap_or_else(|_| panic!("Error connecting to db pool"))
let pool = get_db_pool_cloned().unwrap_or_else(|e| panic!("Failed to get DB pool: {}", e));
pool.get().unwrap_or_else(|e| panic!("Error getting connection from pool: {}", e))
}
pub fn _establish_direct_db_connection() -> SqliteConnection {
@ -197,6 +247,9 @@ pub fn _establish_direct_db_connection() -> SqliteConnection {
}
fn run_migrations() {
// It's important that reinitialize_db_pool() is called before this if the path might have changed.
// Or, ensure establish_pool_db_connection always provides a connection to the *correct* current DB.
// With get_db_pool_cloned() attempting reinitialization if stale, this should be safer.
let mut connection = establish_pool_db_connection();
connection.run_pending_migrations(MIGRATIONS).unwrap();
}
@ -244,6 +297,8 @@ pub fn get_db_path() -> String {
"pastebar-db.data"
};
// It's crucial that APP_CONSTANTS and user_config (for custom_db_path) are readable here.
// get_data_dir() handles this.
let db_path = get_data_dir().join(filename);
db_path.to_string_lossy().into_owned()
}
@ -268,13 +323,12 @@ pub fn get_default_db_path_string() -> String {
pub fn to_relative_image_path(absolute_path: &str) -> String {
let data_dir = get_data_dir();
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
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)
.trim_start_matches('/')
.trim_start_matches('\\');
.trim_start_matches(|c| c == '/' || c == '\\'); // handles both path separators
format!("{{{{base_folder}}}}/{}", relative_path)
} else {
// If path doesn't start with data dir, return as is
@ -288,9 +342,8 @@ pub fn to_absolute_image_path(relative_path: &str) -> String {
let data_dir = get_data_dir();
let path_without_placeholder = relative_path
.strip_prefix("{{base_folder}}")
.unwrap_or(relative_path)
.trim_start_matches('/')
.trim_start_matches('\\');
.unwrap_or(relative_path) // Should not happen if prefix matches
.trim_start_matches(|c| c == '/' || c == '\\'); // handles both path separators
data_dir.join(path_without_placeholder).to_string_lossy().into_owned()
} else {
// If path doesn't have placeholder, return as is
@ -298,6 +351,8 @@ pub fn to_absolute_image_path(relative_path: &str) -> String {
}
}
// This function seems unused, can_access_or_create. Keeping it for now.
#[allow(dead_code)]
fn can_access_or_create(db_path: &str) -> bool {
let path = std::path::Path::new(db_path);